您正在查看: EOS-优秀转载 分类下的文章

EOS源码分析之二网络

# eos源码分析之二网络

一、网络的初始化和启动



P2P网络是区块链的运行的基础模块,在EOS中,主要就是net_plugin,http_plugin,net_pai_plugin,当然在这个过程中网络也会引用到其它的一些模块的接口,但为了清晰,重点介绍网络相关部分,其它略过。


首先看一下网络插件生成的时候的代码:

net_plugin::net_plugin()
   :my( new net_plugin_impl ) {
   my_impl = my.get();//此处写得不是太好,从智能指针又退化回到 普通指针
}



可以看到,它生成了一个net_plugin_impl的实例,真正的网络操作相关的代码其实在这个类中,看名字也可以明白,JAVA接口经常这么干。然后接着按Main函数中的初始化来看:

void net_plugin::plugin_initialize( const variables_map& options ) {
......//日志相关忽略

   //初始化相关参数,版本,是否发送完整块,交易周期等
   my->network_version = static_cast<uint16_t>(app().version());
   my->network_version_match = options.at("network-version-match").as<bool>();
   my->send_whole_blocks = def_send_whole_blocks;

   my->sync_master.reset( new sync_manager(options.at("sync-fetch-span").as<uint32_t>() ) );
   my->big_msg_master.reset( new big_msg_manager );

   my->connector_period = std::chrono::seconds(options.at("connection-cleanup-period").as<int>());
   my->txn_exp_period = def_txn_expire_wait;
   my->resp_expected_period = def_resp_expected_wait;
   my->big_msg_master->just_send_it_max = def_max_just_send;
   my->max_client_count = options.at("max-clients").as<int>();

   my->num_clients = 0;
   my->started_sessions = 0;

   //使用BOOST的resolver来处理与网络相关的数据格式的转换
   my->resolver = std::make_shared<tcp::resolver>( std::ref( app().get_io_service() ) );

   //根据options设置来设置相关配置
   if(options.count("p2p-listen-endpoint")) {
      my->p2p_address = options.at("p2p-listen-endpoint").as< string >();
      auto host = my->p2p_address.substr( 0, my->p2p_address.find(':') );
      auto port = my->p2p_address.substr( host.size()+1, my->p2p_address.size() );
      idump((host)(port));
      tcp::resolver::query query( tcp::v4(), host.c_str(), port.c_str() );
      // Note: need to add support for IPv6 too?
      //得到监听地址
      my->listen_endpoint = \*my->resolver->resolve( query);
      //重置boost socket网络接收器
      my->acceptor.reset( new tcp::acceptor( app().get_io_service() ) );
   }
   if(options.count("p2p-server-address")) {
      my->p2p_address = options.at("p2p-server-address").as< string >();
   }
   else {
      if(my->listen_endpoint.address().to_v4() == address_v4::any()) {
         boost::system::error_code ec;
         auto host = host_name(ec);
         if( ec.value() != boost::system::errc::success) {

            FC_THROW_EXCEPTION( fc::invalid_arg_exception,
                                "Unable to retrieve host_name. ${msg}",( "msg",ec.message()));

         }
         auto port = my->p2p_address.substr( my->p2p_address.find(':'), my->p2p_address.size());
         my->p2p_address = host + port;
      }
   }
   ......
   //处理连接设置
   if(options.count("allowed-connection")) {
      const std::vector<std::string> allowed_remotes = options["allowed-connection"].as<std::vector<std::string>>();
      for(const std::string& allowed_remote : allowed_remotes)
         {
            if(allowed_remote == "any")
               my->allowed_connections |= net_plugin_impl::Any;
            else if(allowed_remote == "producers")
               my->allowed_connections |= net_plugin_impl::Producers;
            else if(allowed_remote == "specified")
               my->allowed_connections |= net_plugin_impl::Specified;
            else if(allowed_remote == "none")
               my->allowed_connections = net_plugin_impl::None;
         }
   }
......
   //查找依赖的链插件
   my->chain_plug = app().find_plugin<chain_plugin>();//插件已经在上一篇中讲过的宏中注册
   my->chain_plug->get_chain_id(my->chain_id);
   fc::rand_pseudo_bytes(my->node_id.data(), my->node_id.data_size());
   ilog("my node_id is ${id}",("id",my->node_id));
   //重置心跳定时器
   my->keepalive_timer.reset(new boost::asio::steady_timer(app().get_io_service()));
   my->ticker();
}



初始化完成后,看一下启动的代码

void net_plugin::plugin_startup() {
   if( my->acceptor ) {
      常见的网络服务操作,打开监听服务,设置选项,绑定地址,启动监听
      my->acceptor->open(my->listen_endpoint.protocol());
      my->acceptor->set_option(tcp::acceptor::reuse_address(true));
      my->acceptor->bind(my->listen_endpoint);
      my->acceptor->listen();
      ilog("starting listener, max clients is ${mc}",("mc",my->max_client_count));
      my->start_listen_loop();//循环接收连接
   }

   //绑定等待交易信号
   my->chain_plug->chain().on_pending_transaction.connect( &net_plugin_impl::transaction_ready);
   my->start_monitors();//启动连接和交易到期的监视(一个自循环)

   for( auto seed_node : my->supplied_peers ) {
      connect( seed_node );//连接种子节点,接入P2P网络
   }
}



代码看上去很少,其实信息量真的不小。下面分别来说明。

二、网络的监听和接收



先看一下循环监听,写得跟别人不一样,但是目的达到的是一样。

void net_plugin_impl::start_listen_loop( ) {
   auto socket = std::make_shared<tcp::socket>( std::ref( app().get_io_service() ) );
    //异步监听的lambada表达式
   acceptor->async_accept( *socket, [socket,this]( boost::system::error_code ec ) {
         if( !ec ) {
            uint32_t visitors = 0;
            for (auto &conn : connections) {
               if(conn->current() && conn->peer_addr.empty()) {
                  visitors++;
               }
            }
            //判断新连接并增加计数
            if (num_clients != visitors) {
               ilog ("checking max client, visitors = ${v} num clients ${n}",("v",visitors)("n",num_clients));
               num_clients = visitors;
            }
            if( max_client_count == 0 || num_clients < max_client_count ) {
               ++num_clients;
               connection_ptr c = std::make_shared<connection>( socket );
               connections.insert( c );//保存新连接的指针
               start_session( c );
            } else {
               elog( "Error max_client_count ${m} exceeded",
                     ( "m", max_client_count) );
               socket->close( );
            }
            start_listen_loop();//继续监听
         } else {
            elog( "Error accepting connection: ${m}",( "m", ec.message() ) );
         }
      });
}
void net_plugin_impl::start_session( connection_ptr con ) {
   boost::asio::ip::tcp::no_delay nodelay( true );
   con->socket->set_option( nodelay );
   start_read_message( con );//开始读取连接的消息
   ++started_sessions;

   // for now, we can just use the application main loop.
   //     con->readloop_complete  = bf::async( [=](){ read_loop( con ); } );
   //     con->writeloop_complete = bf::async( [=](){ write_loop con ); } );
}

其实上面的代码没什么特殊的,只是引用了BOOST的库,可能得熟悉一下,接着看如何读取消息,真正的数据交互在这里:

void net_plugin_impl::start_read_message( connection_ptr conn ) {

   try {
      if(!conn->socket) {
         return;
      }
      //真正的数据异步读取
      conn->socket->async_read_some
         (conn->pending_message_buffer.get_buffer_sequence_for_boost_async_read(),
          [this,conn]( boost::system::error_code ec, std::size_t bytes_transferred ) {
            try {
               if( !ec ) {
                 //判断是否超大小读取数据
                  if (bytes_transferred > conn->pending_message_buffer.bytes_to_write()) {
                     elog("async_read_some callback: bytes_transfered = ${bt}, buffer.bytes_to_write = ${btw}",
                          ("bt",bytes_transferred)("btw",conn->pending_message_buffer.bytes_to_write()));
                  }
                  //判断是不是符合情况
                  FC_ASSERT(bytes_transferred <= conn->pending_message_buffer.bytes_to_write());
                  conn->pending_message_buffer.advance_write_ptr(bytes_transferred);
                  //处理数据
                  while (conn->pending_message_buffer.bytes_to_read() > 0) {
                     uint32_t bytes_in_buffer = conn->pending_message_buffer.bytes_to_read();

                     if (bytes_in_buffer < message_header_size) {
                        break;
                     } else {
                        uint32_t message_length;
                        auto index = conn->pending_message_buffer.read_index();
                        conn->pending_message_buffer.peek(&message_length, sizeof(message_length), index);
                        if(message_length > def_send_buffer_size*2) {
                           elog("incoming message length unexpected (${i})", ("i", message_length));
                           close(conn);
                           return;
                        }
                        if (bytes_in_buffer >= message_length + message_header_size) {
                           conn->pending_message_buffer.advance_read_ptr(message_header_size);
                           if (!conn->process_next_message(*this, message_length)) {
                              return;
                           }
                        } else {
                           conn->pending_message_buffer.add_space(message_length + message_header_size - bytes_in_buffer);
                           break;
                        }
                     }
                  }
                  start_read_message(conn);//继续读取
               } else {
                  auto pname = conn->peer_name();
                  if (ec.value() != boost::asio::error::eof) {
                     elog( "Error reading message from ${p}: ${m}",("p",pname)( "m", ec.message() ) );
                  } else {
                     ilog( "Peer ${p} closed connection",("p",pname) );
                  }
                  close( conn );
               }
            }
            catch(const std::exception &ex) {
......
            }
......
         } );
   } catch (...) {
......
   }
}

/*
 *  创建一个数据接收的缓冲区
 *  Creates and returns a vector of boost mutable_buffers that can
 *  be passed to boost async_read() and async_read_some() functions.
 *  The beginning of the vector will be the write pointer, which
 *  should be advanced the number of bytes read after the read returns.
 */
std::vector<boost::asio::mutable_buffer> get_buffer_sequence_for_boost_async_read() {
  std::vector<boost::asio::mutable_buffer> seq;
  FC_ASSERT(write_ind.first < buffers.size());
  seq.push_back(boost::asio::buffer(&buffers[write_ind.first]->at(write_ind.second),
                                            buffer_len - write_ind.second));
  for (std::size_t i = write_ind.first + 1; i < buffers.size(); i++) {
    seq.push_back(boost::asio::buffer(&buffers[i]->at(0), buffer_len));
  }
  return seq;
}


三、网络的连接



处理完成监听和接收,来看一下主动连接:

void net_plugin_impl::start_monitors() {
   connector_check.reset(new boost::asio::steady_timer( app().get_io_service()));
   transaction_check.reset(new boost::asio::steady_timer( app().get_io_service()));
   start_conn_timer();//调用两个函数
   start_txn_timer();
}
//调用的start_conn_timer
void net_plugin_impl::start_conn_timer( ) {
   connector_check->expires_from_now( connector_period);// 设置定时器
   connector_check->async_wait( [&](boost::system::error_code ec) {
         if( !ec) {
            connection_monitor( );//调用连接监控
         }
         else {
            elog( "Error from connection check monitor: ${m}",( "m", ec.message()));
            start_conn_timer( );
         }
      });
}
void net_plugin_impl::connection_monitor( ) {
   start_conn_timer();//循环调用
   vector <connection_ptr> discards;
   num_clients = 0;
   for( auto &c : connections ) {
      if( !c->socket->is_open() && !c->connecting) {
         if( c->peer_addr.length() > 0) {
            connect(c);//连接指定的点。
         }
         else {
            discards.push_back( c);
         }
      } else {
         if( c->peer_addr.empty()) {
            num_clients++;
         }
      }
   }
   //处理断开的连接
   if( discards.size( ) ) {
      for( auto &c : discards) {
         connections.erase( c );
         c.reset();
      }
   }
}
//交易的定时器监视
void net_plugin_impl::start_txn_timer() {
   transaction_check->expires_from_now( txn_exp_period);
   transaction_check->async_wait( [&](boost::system::error_code ec) {
         if( !ec) {
            expire_txns( );//处理到期交易的情况
         }
         else {
            elog( "Error from transaction check monitor: ${m}",( "m", ec.message()));
            start_txn_timer( );
         }
      });
}
void net_plugin_impl::expire_txns() {
   start_txn_timer( );
   auto &old = local_txns.get<by_expiry>();
   auto ex_up = old.upper_bound( time_point::now());
   auto ex_lo = old.lower_bound( fc::time_point_sec( 0));
   old.erase( ex_lo, ex_up);

   auto &stale = local_txns.get<by_block_num>();
   chain_controller &cc = chain_plug->chain();
   uint32_t bn = cc.last_irreversible_block_num();
   auto bn_up = stale.upper_bound(bn);
   auto bn_lo = stale.lower_bound(1);
   stale.erase( bn_lo, bn_up);
}



最后看一看连接的代码:


/**
 *  Used to trigger a new connection from RPC API
 */
string net_plugin::connect( const string& host ) {
   if( my->find_connection( host ) )
      return "already connected";

   connection_ptr c = std::make_shared<connection>(host);
   fc_dlog(my->logger,"adding new connection to the list");
   my->connections.insert( c );
   fc_dlog(my->logger,"calling active connector");
   my->connect( c );
   return "added connection";
}
//两个连接的重载,其实都很简单,第个Connect负责解析,第二个Connect负责真正连接
void net_plugin_impl::connect( connection_ptr c ) {
   if( c->no_retry != go_away_reason::no_reason) {
      fc_dlog( logger, "Skipping connect due to go_away reason ${r}",("r", reason_str( c->no_retry )));
      return;
   }

   auto colon = c->peer_addr.find(':');

   if (colon == std::string::npos || colon == 0) {
      elog ("Invalid peer address. must be \"host:port\": ${p}", ("p",c->peer_addr));
      return;
   }

   auto host = c->peer_addr.substr( 0, colon );
   auto port = c->peer_addr.substr( colon + 1);
   idump((host)(port));
   tcp::resolver::query query( tcp::v4(), host.c_str(), port.c_str() );
   // Note: need to add support for IPv6 too

   resolver->async_resolve( query,
                            [c, this]( const boost::system::error_code& err,
                                       tcp::resolver::iterator endpoint_itr ){
                               if( !err ) {
                                  connect( c, endpoint_itr );
                               } else {
                                  elog( "Unable to resolve ${peer_addr}: ${error}",
                                        (  "peer_addr", c->peer_name() )("error", err.message() ) );
                               }
                            });
}

void net_plugin_impl::connect( connection_ptr c, tcp::resolver::iterator endpoint_itr ) {
   if( c->no_retry != go_away_reason::no_reason) {
      string rsn = reason_str(c->no_retry);
      return;
   }
   auto current_endpoint = \*endpoint_itr;
   ++endpoint_itr;
   c->connecting = true;
   c->socket->async_connect( current_endpoint, [c, endpoint_itr, this] ( const boost::system::error_code& err ) {
         if( !err ) {
            start_session( c );//读取数据
            c->send_handshake ();//发送握手
         } else {
            if( endpoint_itr != tcp::resolver::iterator() ) {
               c->close();
               connect( c, endpoint_itr );
            }
            else {
               elog( "connection failed to ${peer}: ${error}",
                     ( "peer", c->peer_name())("error",err.message()));
               c->connecting = false;
               my_impl->close(c);
            }
         }
      } );
}


四、网络的数据同步



在前面看了start_read_message,对内部没有怎么做细节的分析,网络也启动了,节点也发现了,那么P2P的职责开始实现了,首先就是同步数据,和比特币类似,也有一个中心的消息处理系统,名字都有点像。

bool connection::process_next_message(net_plugin_impl& impl, uint32_t message_length) {
   try {
      // If it is a signed_block, then save the raw message for the cache
      // This must be done before we unpack the message.
      // This code is copied from fc::io::unpack(..., unsigned_int)
      auto index = pending_message_buffer.read_index();
      uint64_t which = 0; char b = 0; uint8_t by = 0;
      do {
         pending_message_buffer.peek(&b, 1, index);
         which |= uint32_t(uint8_t(b) & 0x7f) << by;
         by += 7;
      } while( uint8_t(b) & 0x80 );

      if (which == uint64_t(net_message::tag<signed_block>::value)) {
         blk_buffer.resize(message_length);
         auto index = pending_message_buffer.read_index();
         pending_message_buffer.peek(blk_buffer.data(), message_length, index);
      }
      auto ds = pending_message_buffer.create_datastream();
      net_message msg;
      fc::raw::unpack(ds, msg);
      msgHandler m(impl, shared_from_this() );//impl是net_plugin_impl
      msg.visit(m);//注意这里最终是调用一个仿函数,static_variant.hpp中
   } catch(  const fc::exception& e ) {
      edump((e.to_detail_string() ));
      impl.close( shared_from_this() );
      return false;
   }
   return true;
}

//仿函数实现是通过重载了小括号
struct msgHandler : public fc::visitor<void> {
   net_plugin_impl &impl;
   connection_ptr c;
   msgHandler( net_plugin_impl &imp, connection_ptr conn) : impl(imp), c(conn) {}

   template <typename T>
   void operator()(const T &msg) const
   {
      impl.handle_message( c, msg); //这里会调用net_plugin_impl中的handle_message
   }
};



下面开始调用分发函数:


void net_plugin_impl::handle_message( connection_ptr c, const handshake_message &msg) {
   fc_dlog( logger, "got a handshake_message from ${p} ${h}", ("p",c->peer_addr)("h",msg.p2p_address));
   if (!is_valid(msg)) {
      elog( "Invalid handshake message received from ${p} ${h}", ("p",c->peer_addr)("h",msg.p2p_address));
      c->enqueue( go_away_message( fatal_other ));
      return;
   }
   chain_controller& cc = chain_plug->chain();
   uint32_t lib_num = cc.last_irreversible_block_num( );
   uint32_t peer_lib = msg.last_irreversible_block_num;
   if( c->connecting ) {
      c->connecting = false;
   }
   if (msg.generation == 1) {
      if( msg.node_id == node_id) {
         elog( "Self connection detected. Closing connection");
         c->enqueue( go_away_message( self ) );
         return;
      }

      if( c->peer_addr.empty() || c->last_handshake_recv.node_id == fc::sha256()) {
         fc_dlog(logger, "checking for duplicate" );
         for(const auto &check : connections) {
            if(check == c)
               continue;
            if(check->connected() && check->peer_name() == msg.p2p_address) {
               // It's possible that both peers could arrive here at relatively the same time, so
               // we need to avoid the case where they would both tell a different connection to go away.
               // Using the sum of the initial handshake times of the two connections, we will
               // arbitrarily (but consistently between the two peers) keep one of them.
               if (msg.time + c->last_handshake_sent.time <= check->last_handshake_sent.time + check->last_handshake_recv.time)
                  continue;

               fc_dlog( logger, "sending go_away duplicate to ${ep}", ("ep",msg.p2p_address) );
               go_away_message gam(duplicate);
               gam.node_id = node_id;
               c->enqueue(gam);
               c->no_retry = duplicate;
               return;
            }
         }
      }
      else {
         fc_dlog(logger, "skipping duplicate check, addr == ${pa}, id = ${ni}",("pa",c->peer_addr)("ni",c->last_handshake_recv.node_id));
      }

      if( msg.chain_id != chain_id) {
         elog( "Peer on a different chain. Closing connection");
         c->enqueue( go_away_message(go_away_reason::wrong_chain) );
         return;
      }
      if( msg.network_version != network_version) {
         if (network_version_match) {
            elog("Peer network version does not match expected ${nv} but got ${mnv}",
                 ("nv", network_version)("mnv", msg.network_version));
            c->enqueue(go_away_message(wrong_version));
            return;
         } else {
            wlog("Peer network version does not match expected ${nv} but got ${mnv}",
                 ("nv", network_version)("mnv", msg.network_version));
         }
      }

      if(  c->node_id != msg.node_id) {
         c->node_id = msg.node_id;
      }

      if(!authenticate_peer(msg)) {
         elog("Peer not authenticated.  Closing connection.");
         c->enqueue(go_away_message(authentication));
         return;
      }

      bool on_fork = false;
      fc_dlog(logger, "lib_num = ${ln} peer_lib = ${pl}",("ln",lib_num)("pl",peer_lib));

      if( peer_lib <= lib_num && peer_lib > 0) {
         try {
            block_id_type peer_lib_id =  cc.get_block_id_for_num( peer_lib);
            on_fork =( msg.last_irreversible_block_id != peer_lib_id);
         }
         catch( const unknown_block_exception &ex) {
            wlog( "peer last irreversible block ${pl} is unknown", ("pl", peer_lib));
            on_fork = true;
         }
         catch( ...) {
            wlog( "caught an exception getting block id for ${pl}",("pl",peer_lib));
            on_fork = true;
         }
         if( on_fork) {
            elog( "Peer chain is forked");
            c->enqueue( go_away_message( forked ));
            return;
         }
      }

      if (c->sent_handshake_count == 0) {
         c->send_handshake();
      }
   }

   c->last_handshake_recv = msg;
   sync_master->recv_handshake(c,msg);//这里开始同步
}
void sync_manager::recv_handshake (connection_ptr c, const handshake_message &msg) {
   chain_controller& cc = chain_plug->chain();
......
   //--------------------------------
   // sync need checkz; (lib == last irreversible block)
   //
   // 0. my head block id == peer head id means we are all caugnt up block wise
   // 1. my head block num < peer lib - start sync locally
   // 2. my lib > peer head num - send an last_irr_catch_up notice if not the first generation
   //
   // 3  my head block num <= peer head block num - update sync state and send a catchup request
   // 4  my head block num > peer block num ssend a notice catchup if this is not the first generation
   //
   //-----------------------------

   uint32_t head = cc.head_block_num( );
   block_id_type head_id = cc.head_block_id();
   if (head_id == msg.head_id) {
      fc_dlog(logger, "sync check state 0");
      // notify peer of our pending transactions
      notice_message note;
      note.known_blocks.mode = none;
      note.known_trx.mode = catch_up;
      note.known_trx.pending = my_impl->local_txns.size();
      c->enqueue( note );
      return;
   }
   if (head < peer_lib) {
      fc_dlog(logger, "sync check state 1");
      start_sync( c, peer_lib);//同步
      return;
   }
......
}




再深入的细节就不再分析了,就是基本的数据交互通信。

转载自:https://github.com/XChainLab/documentation/edit/master/eos/eos%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8B%E4%BA%8C%E7%BD%91%E7%BB%9C.md

EOS源码分析之一整体介绍

EOS整体介绍


一、EOS的插件式设计



EOS中,虽然编程的复杂度和设计较比特币大幅提高,但其核心的思想其实并没有多大改变,目前来看,仍然以BOOST的signal,boost::asio的信号消息机制来完成模块间的解耦。相比比特币来言,做得更优雅,封装也更良好。


先看一下插件设计的整体类图:




从上面的类图可以清楚的看到,整个插件的依赖和传导机制。然后在下面的流程分析中会详细说明一下它的具体的应用。

二、EOS的整体流程



EOS的版本做了一次比较大的更迭,至少从形式上看是,它的生成路径下,完成了以下几个目标:


cleos:客户端,用来处理和区块链通信。帐户钱包等的管理。


eosio-abigen:二进制ABI的生成程序。


eosio-launcher:简化了eosd节点跨局域网或者跨更宽泛的网络的分布。


keosd:钱包和帐户的实现控制程序


nodeos:核心的节点程序,这个和老版本不一样了,至少名字不一样了。



一般情况下会启动cleos调用keosd来创建帐户和钱包。然后通过启动nodeos来产生节点,进行通信并根据配置生成区块和验证。进入重点,直接看一下 nodeos的创建代码:

int main(int argc, char** argv)
{
   try {
      app().set_version(eosio::nodeos::config::version);
      auto root = fc::app_path();
      app().set_default_data_dir(root / "eosio/nodeos/data" );
      app().set_default_config_dir(root / "eosio/nodeos/config" );
      //这里直接初始化了四个插件
      if(!app().initialize<chain_plugin, http_plugin, net_plugin, producer_plugin>(argc, argv))
         return -1;
      initialize_logging();
      ilog("nodeos version ${ver}", ("ver", eosio::nodeos::config::itoh(static_cast<uint32_t>(app().version()))));
      ilog("eosio root is ${root}", ("root", root.string()));
      app().startup();
      app().exec();
   } catch (const fc::exception& e) {
      elog("${e}", ("e",e.to_detail_string()));
   } catch (const boost::exception& e) {
      elog("${e}", ("e",boost::diagnostic_information(e)));
   } catch (const std::exception& e) {
      elog("${e}", ("e",e.what()));
   } catch (...) {
      elog("unknown exception");
   }
   return 0;
}


代码看上去并不多,当然,比之比特币最新中的几行代码来看,还是要稍有复杂的感觉,但是还可以承受,不过,随后可能c++技能的消耗水平会极剧增加。忽略开前几行的相关文件配置直接进行初始化代码看看去。


template<typename... Plugin>
bool                 initialize(int argc, char** argv) {
   return initialize_impl(argc, argv, {find_plugin<Plugin>()...});
}


没啥,一个向量的初始化。不过有一个变参模板,如果想深入学习的得去看看相关资料。

bool application::initialize_impl(int argc, char** argv, vector<abstract_plugin*> autostart_plugins) {
   set_program_options();//设置命令选项

   bpo::variables_map options;//声明保存结果变量
   bpo::store(bpo::parse_command_line(argc, argv, my->_app_options), options);//分析参数并保存

   if( options.count( "help" ) ) {
      cout << my->_app_options << std::endl;
      return false;
   }

   ......

   //分析配置文件
   bpo::store(bpo::parse_config_file<char>(config_file_name.make_preferred().string().c_str(),
                                           my->_cfg_options, true), options);

   if(options.count("plugin") > 0)
   {
      auto plugins = options.at("plugin").as<std::vector<std::string>>();
      for(auto& arg : plugins)
      {
         vector<string> names;
         boost::split(names, arg, boost::is_any_of(" \t,"));
         for(const std::string& name : names)
            get_plugin(name).initialize(options);//分步初始化第一步,获取指定名插件并初始化,其它类同
      }
   }
   //下面是注册插件,并查寻依赖的相关插件,然后调用,并初始化
   for (auto plugin : autostart_plugins)
      if (plugin != nullptr && plugin->get_state() == abstract_plugin::registered)
         plugin->initialize(options);//分步初始化第一步,获取指定名插件并初始化,其它类同

   bpo::notify(options);//更新最新参数至options

   return true;
}


里面反复的参数控制代码略过了。里面主要是使用了BOOST的参数解析和更新机制



这里的调用很简单,其实就是从map里查找相关的插件,用类名和字符串,这里面用到了BOOST中的一些库boost::core::demangle(typeid(Plugin).name()),用来返回类型的名字。然后再用名字的字符串查找出插件。这里面有一个问题,为什么从plugins这个map中可以查找出对象,仔细看一下有些插件的CPP文件中会有类似的代码:

static appbase::abstract_plugin& _net_plugin = app().register_plugin<net_plugin>();



静态注册了啊。但是有一些插件里没有啊,怎么回事儿?其实接着看代码就发现了问题所在。如下:

virtual void initialize(const variables_map& options) override {
   if(\_state == registered) {
      \_state = initialized;
      //分步初始化,第二步
      static_cast<Impl*>(this)->plugin_requires([&](auto& plug){ plug.initialize(options); });//初始化此插件依赖的插件,并递归调用依赖插件
      static_cast<Impl*>(this)->plugin_initialize(options);  //初始化插件
      //ilog( "initializing plugin ${name}", ("name",name()) );
      app().plugin_initialized(*this);//保存启动的插件
   }
   assert(\_state == initialized); /// if initial state was not registered, final state cannot be initiaized
}



plugin_requires,这个函数的定义就通过宏来产生了。

//先看一个调用实现
class chain_plugin : public plugin<chain_plugin> {
public:
   APPBASE_PLUGIN_REQUIRES()
......
};
#define APPBASE_PLUGIN_REQUIRES_VISIT( r, visitor, elem ) \
  visitor( appbase::app().register_plugin<elem>() );

#define APPBASE_PLUGIN_REQUIRES( PLUGINS )                               \
   template<typename Lambda>                                           \
   void plugin_requires( Lambda&& l ) {                                \
      BOOST_PP_SEQ_FOR_EACH( APPBASE_PLUGIN_REQUIRES_VISIT, l, PLUGINS ) \
   }
//再看另外一个调用实现
class producer_plugin : public appbase::plugin<producer_plugin> {
public:
   APPBASE_PLUGIN_REQUIRES((chain_plugin))
......
};



就这样,基础的插件和基础插件依赖的插件,就这么被一一加载初始化。


三、EOS的程序技术特点


1、使用了较多的宏,并配合BOOST库。


在EOS的代码中,可以隐约看到类似MFC的代码实现机制,举一个例子:

#define FC_CAPTURE_AND_RETHROW( ... ) \
   catch( fc::exception& er ) { \
      FC_RETHROW_EXCEPTION( er, warn, "", FC_FORMAT_ARG_PARAMS(__VA_ARGS__) ); \
   } catch( const std::exception& e ) {  \
      fc::exception fce( \
                FC_LOG_MESSAGE( warn, "${what}: ",FC_FORMAT_ARG_PARAMS(__VA_ARGS__)("what",e.what())), \
                fc::std_exception_code,\
                BOOST_CORE_TYPEID(decltype(e)).name(), \
                e.what() ) ; throw fce;\
   } catch( ... ) {  \
      throw fc::unhandled_exception( \
                FC_LOG_MESSAGE( warn, "",FC_FORMAT_ARG_PARAMS(__VA_ARGS__)), \
                std::current_exception() ); \
   }

FC_CAPTURE_AND_RETHROW( (t) )


包括前面提到的递归调用插件化的宏定义,再通过上面的调用实现对比,基本上是以动态生成代码为主,在比特币也有类似的实现,但规模和应用要小得多。

2、模板的使用普及化


在工程代码上广泛使用了模板,看一下插件的例子:

template<typename Impl>
class plugin : public abstract_plugin {
   public:
      plugin():\_name(boost::core::demangle(typeid(Impl).name())){}
      virtual ~plugin(){}

      virtual state get_state()const override         { ... }
      virtual const std::string& name()const override { ... }

      virtual void register_dependencies() {
.......
      }

      virtual void initialize(const variables_map& options) override {
......
      }

      virtual void startup() override {
......
      }

      virtual void shutdown() override {
......
      }

......
};


3、更深入的绑定使用了c++1X和BOOST



这个就非常明显了,试举一个简单的例子:

//c++11语法糖
for (const auto& at: trx_trace.action_traces) {
   for (const auto& auth: at.act.authorization) {
      result.emplace_back(auth.actor);
   }

   result.emplace_back(at.receiver);
}
//BOOST的网络通信
using boost::asio::ip::tcp;
unique_ptr<tcp::acceptor>        acceptor;
std::unique_ptr<class net_plugin_impl> my;
void net_plugin::plugin_startup() {
   if( my->acceptor ) {
      my->acceptor->open(my->listen_endpoint.protocol());
      my->acceptor->set_option(tcp::acceptor::reuse_address(true));
      my->acceptor->bind(my->listen_endpoint);
      my->acceptor->listen();
      ilog("starting listener, max clients is ${mc}",("mc",my->max_client_count));
      my->start_listen_loop();
   }

   my->chain_plug->chain().on_pending_transaction.connect( &net_plugin_impl::transaction_ready);
   my->start_monitors();

   for( auto seed_node : my->supplied_peers ) {
      connect( seed_node );
   }
}


目前初步看来,EOS对BOOST和c++14的依赖更深。

转载自:https://github.com/XChainLab/documentation/blob/master/eos/eos%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8B%E4%B8%80%E6%95%B4%E4%BD%93%E4%BB%8B%E7%BB%8D.md

从源代码上理解REX

期待了这么长时间,REX 终于上线了!现在已经出现了很多怎么使用 REX 的教程,但是我们还没有看到有人解释在与 REX 交互的各种操作。所以 EOS Canada 希望深入地解析 REX 代码,给 EOS 社区做一下科普。
首先我们要定义八个术语,保证我们理解它们,它们对 REX 讨论都非常重要。

成熟期

当你购买 REX 代币时,在四天内将无法把它们换回 EOS。在此期间,这些代币被称为在成熟期。在同一天的不同时间购买的 REX 都从第二天0点开始计时,所以你最多可以有四个不同的到期日。

4天

如上所述,到期日的计算均从 UTC 时间的第二天00:00开始。因此,如果用户在今天16:00 UTC 购买 REX 代币,那么他们将只能在4天8小时后卖回 EOS(成熟后)。所以每当人们说“四天”时,它实际上是从购买后的 00:00 UTC 计算的四天。

30天

每次借用的 CPU 或网络带宽的有效期为30天。只有借来这些资源的用户才需要注意这个30天的期限。如果你是借出你的代币资源的用户,你只需要注意四天的成熟期。

储蓄桶

你可以选择把 REX 代币放到储蓄桶里,放到储蓄桶里的代不会自动进入成熟期的,直到你提出要取出里面的代币,它才会开始四天的成熟期,成熟后才能移动它们。这是为了让你能保证你的代币安全的一个选项。举一个具体的用例,如果你的活跃权限的密钥被盗,有你的密钥的人就能动用你已经成熟了的 REX 代币,换回 EOS 然后盗走。但是如果你的 REX 在储蓄桶里,你就有时间用拥有者权限更改你的活跃权限,然后取消储蓄桶里代币的成熟期。

REX 基金

要与 REX 交互,你先要在把 EOS 代币存入你的 REX 基金中 ,REX 基金中存的是 EOS 而不是 REX 代币。

投票前提

想用 REX 出租自己资源,有一个前提,他们必须至少投票给21个BP节点或者代理投票。

流动性紧缩

这种情况出现的几率很小,但是我们还是应该有所了解。如果在池中没有足够的EOS 代币来满足提款的数量,这种现象就叫“流动性紧缩”。这意味着所有提款订单会被排队,等有新的 EOS 代币进入 REX 池之后,或者有借用的资源到期。用户赎回 EOS 是没有风险的,他们可能最多需要等待30天,但再强调,这个现象是非常罕见的。

市场价

REX 由 Bancor 支持的,就是说价格不是由用户自己竞价决定的,而是由系统根据池中 EOS 和 REX 代币的比率去计算的。这就是为什么收益率或续约价格是不确定的,因为一切都是在购买时确定的,取决于买卖时间和当时的状况。

我们还想强调一下两件值得注意的事情。EOS:REX 比值的确定方式决定了你卖出REX收回EOS数量不是高于就是等于你投入的EOS数量。这意味着你永远不会因为持有 REX 而失去任何 EOS,你只会获益。

另一点是,在获取帐户快照时,空投可以选择是否考虑你的 REX 余额。就是说你在 REX 中的代币会不会被包含在内取决于该空投的开发者。
我们现在来看一下在与 REX 交互时可调用的所有操作,并作出相关解释。

转载自:https://mp.weixin.qq.com/s/zOmJA4R8JOHEedmHqhaoBw (by EOS Canada)

基于PBFT提升EOS共识速度的算法

基于PBFT提升EOS共识速度的算法

1 背景介绍

1.1 现象

当前主网的链高度和共识高度之间有325+个块的差距,相当于~3分钟左右的时间差。也就是说,当下提交的trx需要等到~3分钟后才能确认是否被记在链上。这样的表现对于很多DApp来说是不可承受的,尤其是那些需要即时确认的应用。

1.2 原因阐述

  • 造成主网上这种现象的原因,是EOS基于DPOS的共识算法中,所有块同步和确认信息都只通过出块的时候才能发出。也就是说,在BP1出块(所出块为BLK)、BP1~BP21轮流出块的情况下,BP2~BP21会陆续收到并验证BLK,但所有BP只能等到自己出块的时候才能发出对BLK的确认信息。这也是为什么我们看到nodes的log中,每个BP在schedule中第一次出块的时候,confirmed总是240。DPOS+Pipeline BFT理论上共识的最快速度(即head和LIB之间的最小差距)为325。

  • 240 = (21-1)*12
    这其实是(在网络情况良好的情况下)上一轮所有块数之和。每个节点在block_header中维护着一个长度最长为240,初始值为14的vector confirm_count,对应所有收到但是未达成共识的块以及尚需的确认数。每当收到多一个BP对这些块的确认,对应块的数值-1,直到某一块所需的确认数减到0,此块及之前的所有块便进入共识(相关代码)。

  • 325 = 12*(13+1+13) + 1
    整个网络需要15个人确认才能达成共识。每个人默认会对自己出的块进行确认,所以每个块需要14个人的implicit confirm和(explicit)confirm。第14个BP在出块时由于包括自己在内确认人数已经达到15人,所以它会同时发出implicit confirm和(explicit)confirm。那么理想情况下,一个块从它产生后,要到之后的第28个BP所产出的第一个块时才能得到全网共识,进入LIB。因此有以上计算。

  • 我们认为,所有BP不需要等到出块的时候才对其他块进行确认,用PBFT(Practical Byzantine Fault Tolerance[1]来替代Pipeline BFT,让BP之间实时地对当前正在生产的区块进行确认,能够使整个系统最终达到接近实时的共识速度。

2 算法核心

  • 保留DPOS的BP Schedule机制,和EOS一样对synchronized clock和BP Schedule进行强约束。

  • 去掉EOS中的Pipeline BFT部分共识(即去掉原本EOS中出块时的implicit confirm和(explict) confirm部分),因为在极端情况下可能与PBFT的共识结果有冲突。

  • 共识的通讯机制使用现有p2p网络进行通信,但增加通信成本,使用PBFT机制广播prepare和commit信息。

  • 通过batch方式优化(替换掉PBFT中对每个块进行共识的要求), 能够达成批量共识,以此来逼近实时BFT的理想状态并减轻网络负载。

3 基础概念

3.1 DPOS中BP变更的具体实现

  • 当前代码中,每60s(120个块)刷新一次投票排名(相关代码),如果前21名发生变化,会在下一次刷新排名的时候发出promoting proposed schedule相关代码

  • 当包含promoting proposed schedule的块进入LIB后,BP会陆续更新自己block header中的pending_schedule

  • 等到2/3 +1个BP节点都已经更新block header后,pending schedule达成共识。BP会陆续将active schedule更新为此时pending schedule的值,并按照新的BP组合开始出块,整个过程需要至少经过两轮完整的出块。

  • 每一次新的BP组合,一定要能够达成共识才能真正生效。换句话说,如果网络中7个或更多节点无法正常通信,那么无论如何不能通过投票的方式产生新的BP。网络的LIB会一直停留在节点崩溃的那个共识点。

  • DPOS这样的做法可以有效的避免一部分分叉问题,所以仍会沿用DPOS关于BP选举部分的共识机制,即所有的BP变动,需要等到propose schedule进入LIB后才真实生效。

3.2 PBFT的前提

  • 如果网络中的拜占庭节点为f个,那么要求总节点数n满足n≥3f+1。拜占庭节点是指对外状态表现不一致的节点,包括主动作恶的节点和因为网络原因导致失效或部分失效的节点。

  • 所有信息最终可达: 所有通信信息可能会被延迟/乱序/丢弃, 但通过重试的方式可以保证信息最终会被送达。

3.3 PBFT中的关键概念对应DPOS

pre-prepare,指primary节点收到请求,广播给网络里的所有replica。可以类比为DPOS中BP出块并广播至全网。

prepare,指replica收到请求后向全网广播将要对此请求进行执行。可类比为DPOS重所有节点收到块并验证成功后广播已收到的信息。

commit,指replica收到足够多的对同一请求的prepare消息,向全网广播执行此请求。可以类比为DPOS中节点收到足够多对同一个块的prepare消息, 提出proposed lib消息

committed-local, 指replica收到足够多对同一请求的commit消息, 完成了验证工作. 可以类比为DPOS中的LIB提升.

view change,指primary节点因为各种原因失去replica信任,整个系统更改primary的过程。由于EOS采用了DPOS的算法,所有BP是通过投票的方式提前确定的,在同一个BP schedule下整个系统的出块顺序是完全不变的,当网络情况良好并且BP schedule不变的时候可以认为不存在view change。
当引入PBFT后,为了避免分叉导致共识不前进的情况,加入view change机制,抛弃所有未达成共识的块进行replay,不断重试直到继续共识。

checkpoint, 指在某一个块高度记录共识证据, 以此来提供安全性证明. 当足够多的replica的checkpoint相同时, 这个checkpoint被认为是stable的. checkpoint的生成包括两大类,一类是固定k个块生成; 另一类是特殊的需要提供安全性证明的点,例如BP schedule发生变更的块.

4 未优化版本概述

术语:

  • v: view version
  • i: BP的名字
  • BLKn: 第n个块
  • dn: 对应第n个块的共识消息摘要digest
  • σi: 名为i的BP的签名
  • n: 区块的高度

所有BP针对每一个块按顺序进行共识, 采用PBFT机制. 以下分情况进行描述:

4.1 在正常的情况下(不涉及BP变更也没有分叉,且网络状况良好)

pre-prepare阶段,与现行逻辑没有区别,即BP广播其签名的块。

prepare阶段,BPi收到当前BP签名的块BLKn,经过验证后发出 (PREPARE,v,n,dn,i)σi 消息,等待共识。当BPi收到了2/3的节点发出view v下对BLKn的PREPARE消息,认为网络中对此块的prepare已达成共识。已发出的PREPARE消息,不可更改。

commit阶段,当BLKn标记为 prepared 后,BPi发出(COMMIT,v,n,dn,i)σi。需要注意的是,PBFT是通过保证严格顺序来实现安全性的,所以对所有节点对块的共识也是严格的按照顺序进行,也就是说,(PREPARE,v,n,dn,i)σi发出的前提条件是在同一个view下,BLKn-1至少已经处于 committed 状态。

全网角度下LIB提升,当BPi收到了2/3的节点发出v下对BLKn的COMMIT消息,BPi认为网络中对此块的commit已达成共识,即此块已达成共识,此块标记为 committed 状态,并将LIB提升到当前高度n,然后开始对下一个块进行prepare。若此区块高度为Hi,所有BP的LIB高度进行降序排列后得到长度为L的向量Vc, 从全网角度来看Vc[2/3L]及以下的LIB可以被认为 stable ,Vc[2/3L]即此时全网的LIB高度。

对于同一个块而言,只有收集足够的PREPARE消息,才会进入commit阶段。同理,只有收集足够的COMMIT消息,才会开始对下一个块开始prepare,否则就一直重发直到消息数满足要求或进行view change(见后文)。

4.2 当BP产生变化的时候

pre-prepare阶段,与4.1无区别。

preparecommit阶段,由于不同BP间对于BP变动的信息达成共识的时间有先后,此时便会出现BP之间对于schedule的不一致状态。
以BPi为例,BPi收到了当前BPc签名的块BLKn,如果此时多数BP的active schedule已改为S',而BPi仍是S,那么BPi便会持续等待S中的BP发送的PREPARE信息,从而无法进入commit阶段。
但此时网络中的多数节点仍会相互达成共识,致使全网的LIB提升。如果BPi收到足够的同一个view下的commit信息, BPi会进入commit-local状态,提升自己的LIB。

4.3 当产生分叉的时候

pre-prepare阶段,与4.1无区别。

preparecommit阶段,当BPi 在timeout=T内没有收集足够的PREPARE或COMMIT消息,即共识没有在这个时间段内提升,此时发出VIEW-CHANGE消息,发起view change 并不再接收除VIEW-CHANGE、NEW-VIEW和CHECKPOINT外的任何消息。

view change阶段,BPi 发出 (VIEW-CHANGE,v+1,hlib,n,i)σi消息。当收集到 2/3 +1 个v'=v+1的VIEW-CHANGE消息后,由schedule中的下一个BP发出 (NEW-VIEW,v+1,n,VC,O)σbp消息,其中VC是所有包括BP签名的VIEW-CHANGE消息,O是所有未达成共识的PRE-PREPARE消息(介于hlib和nmax之间)。当其它BP收到并验证NEW-VIEW消息合法后,丢弃掉所有当前未达成共识的块,基于所有的PRE-PREPARE消息重新进行prepare和commit阶段。
若view change未能在timeout=T内达成共识(没有正确的NEW-VIEW消息发出),即发起新一轮v+2的view change,等待时间timeout=2T, 依次类推不断重试,直到网络状态收敛,共识开始提升。

备注: 原始的PBFT不存在分叉的问题, 因为PBFT只有在一个请求达成共识后才会开始处理下一个请求。

5 未优化版本存在的问题:

5.1 共识速度

当对一个块的共识速度小于500ms,即两轮消息的发送可以在500ms内收到足够的确认数,head和LIB的差距稳定后可以趋近于1个块,即实时共识。而当对一个块的平均共识速度大于等于500ms或网络状态极差导致重试次数过多,本算法表现可能慢于DPOS+Pipeline BFT。

5.2 网络开销

假设网络中的节点为N,消息传播使用gossip算法,块大小为B,那么DPOS需要传播的消息为N2,所需带宽为BN2
假设PREPARE和COMMIT消息大小分别为p和c,PBFT+DPOS所需要传播的消息数为 (1+rp+rc)N2,其中1 是pre-prepare的传输,rm,rc为prepare和commit的重试次数,所需带宽为(B+prp+crc)N2。当p、c优化的足够小后,额外的带宽开销主要取决于重试次数。

6 优化后的版本概述

6.1 通过自适应粒度调整,实现批量共识

6.1.1 batch 策略

LIB的高度为hLIB
fork中最高点的块的高度为 hHEAD
涉及到BP schedule变动的块高度为 hs
批量共识batch:

  • batchmin = 1
  • batchmax = min(default_batch_max, hHEAD - hLIB)

当batchmax中不包含BP Schedule变动时, batch = batchmax
当batchmax中包含BP Schedule变动且hLIB < hs 时, batch = hs - 1
当batchmax中包含BP Schedule变动且hLIB == hs 时, batch = batchmax

6.1.2 批量共识原理

当未出现分叉情况时, 以上构筑可类比PBFT中view不变情况下的共识. 并且基于Merkle Tree的基本结构,当多数节点可以对BLKn的Hash达成共识,那么之前的所有块都应该是共识的. 此处保证了块的total order.

当出现分叉情况时, PREPARE 信息不能变动,否则可能对外表现为拜占庭错误。此时需要不断重发当前的PREPARE消息直到网络达成共识或触发timeout 后发起view change。

6.1.3 实现方法
  • 每当收到新的块时, BP 通过batch的策略生成PREPARE信息, 进行缓存及广播

  • 每个BP为block_header维护一个最低水位h,和最高水位H,分别对应自己还没有达成共识的最低点和最高点。

  • 同时维护两个长度为(H-h)的向量 Vp & Vc,包括水位间每一个块所需要的PREPARE消息数和COMMIT消息数。

  • 每收到一个高度为n的PREPARE消息(或COMMIT消息),通过消息的签名和digest进行验证并确认他与自己处于相同的fork后,依次将Vp(Vc)中(h ≤ n)的所有数值-1。

  • 不断重发同一个fork上高度为H的PREPARE消息(或COMMIT消息),直到达成共识或超时后触发View Change(基于New View重新开始PBFT共识,此时v' = v+1)。

  • 当某一个处于高度x(h ≤ x ≤ H)的块收集超过2/3 +1个PREPARE消息,依次执行从h~x的块内容并标记所有(h ≤ x)的块为 prepared,然后自动发出高度为x的COMMIT消息。

  • 当某一个处于高度y(y ≤ H)的块收集超过2/3 +1个COMMIT消息,依次执行从h~y的块内容并标记所有(h ≤ y)的块为 committed。此时认为≤y的所有块已达成共识,将自己的LIB高度提升至y。

  • 每隔若干块生成checkpoint以提高性能。当网络内超过2/3 +1的最新的checkpoint 都达到某一高度c,并且处于同一fork上,则认为此checkpoint稳定。

6.1.4 view change策略
  • BP依据出块的schedule依次成为前一人的backup,确保每一次view change后的primary只可能有一人。

  • 当网络开始进入view change后,NEW-VIEW应该重新对2/3 +1人看到的最低点h和最高点H之间的块进行重新共识。

  • 发出NEW-VIEW的BP应该在消息内包括所有VIEW-CHANGE消息,并根据所有的VIEW-CHANGE消息计算出h和H,并将[h, H]区间内超过(2/3 +1)的人选择的fork一并发出。

  • 当BP收到NEW-VIEW消息并进行验证后,基于NEW-VIEW的内容重新进行prepare。

  • 若在timeout=T内无法完成view change,便开始发起v+2的新一轮view change,直到网络对fork的选择达成共识。

6.2 通过始终prepare最长链并结合view change,避免分叉风险

  • 当BP收到多个fork的时候,应该对当前所能看到的最长链进行prepare, 采取longest-live-fork原则.

  • BP在进行prepare的时候,应该错开BP切换的时间点,从而避免选择少数人支持的fork。

  • BP一旦对某个fork进行prepare,就不能再对prepare消息进行更改,否则可能成为拜占庭错误, BP需要:
    1)不断重发之前的PREPARE消息,等待最终达成共识。即使这个fork不是最长链, 因为有更多人支持,也应该选择这个fork;
    2)或等待timeout=T后,发起view change,所有BP基于NEW-VIEW发出的fork开始新的BPFT共识;
    3)收到超过(2/3 +1)同一fork的COMMIT消息或checkpoint,抛弃当前状态同步至多数人达成共识的高度。

6.3 通过Checkpoint机制实现GC并提升同步性能

  • BP不断网络内广播自己当前的checkpoint状态,并且接收来自其他人的checkpoint。

  • 当同一分支上有超过(2/3 +1)人的checkpoint已经高于c,认为CHECKPOINTc已经stable,删除高度低于c以前所有PREPARE、COMMIT消息等cache。

  • 通过验证checkpoint的正确性,可以大幅提升节点的同步速度。

7 FAQ

DPOS相关问题(见1.2)

  1. 简单说明DPOS是如何工作的
    暂略
  2. 为什么DPOS的lib是12个12个的涨
    暂略
  3. 为什么DPOS的HEAD和LIB差距这么大
    暂略
  4. 当BP变动时, DPOS是如何工作的
    暂略
  5. 目前节点间的数据是如何同步的
    暂略

PBFT相关问题

  1. 简单说明PBFT
    暂略

DPOS-PBFT相关问题

  1. 简单说明DPOS-PBFT是如何工作的
    见5

  2. 为什么不能只广播一次prepare的信息
    当网络出现分叉(或BP变动)的时候,如果只有PREPARE信息,所有节点是无法对其它节点的view change进行响应的,会导致硬分叉。 举例说明: 因为分布式网络的特性, 信息会被延迟或打乱。假设现在有三个连续出块的BP A,B,C 如果B没有收到A的最后一个块, 那么他会继续从倒数第二个块开始出块。这样造成了两个fork选择F1 F2. 假定A的最后一个块里包含了BP变动的信息(该块在F1里), 那么选择了F1的节点需要一个新的BP S1来进行共识, 而F2的节点需要原有的BP S2 进行共识。 共识的群体发生了变化, 很有可能会两边最终都进入共识状态, 进而导致整体网络发生分叉。

  3. prepare和commit重发机制是如何工作的
    当超过给定的timeout T后仍然没有对某一个处于 prepared 或者 committed 的块收集到足够多的确认,就对同一个消息进行多一次的重发,直到收集到足够多的确认或发生view change。

  4. 当BP集合变动的时候,是否存在分叉风险
    见4.2

  5. 是否需要等待共识完成才能继续出块
    出块可以持续进行,共识只影响LIB的高度

  6. 如果第N个块未满足BFT共识个数,但第N+1个块收到了足够多的confirm,该如何处理
    对于优化后的算法,可以直接开始基于N+2个块开始收集共识消息

  7. 持续出块是否会因为共识未迅速达成而分叉
    不会,至少表现为DPOS的状态,最终会共识在最长链上

  8. BFT的commit信息是否需要写入块中
    所有消息(发出的和收到的)都只存在本地. 但需要保留一段时间, 用以为peer提供共识的证据

  9. 额外增加的开销有多少
    见5.2

  10. 共识的速度真的能提升吗,如果BFT共识平均时间>500ms,BFT的高度是低于DPOS的
    见5.1

8 参考

[1] http://pmg.csail.mit.edu/papers/osdi99.pdf

转载自:https://github.com/eosiosg/dpos-pbft/blob/master/documentation/%E5%9F%BA%E4%BA%8EPBFT%E6%8F%90%E5%8D%87EOS%E5%85%B1%E8%AF%86%E9%80%9F%E5%BA%A6%E7%9A%84%E7%AE%97%E6%B3%95.md

EOS虚拟机与智能合约详解与分析

EOS虚拟机同经典的EVM,是EOS中运行智能合约的容器,但是从设计上讲它与EOS.IO是分离的。进
一步脚本语言和虚拟机的技术设计与EOS.IO分离。从宏观来讲任何语言或者虚拟机,只要满足条件适
合沙盒模式运行,同时满足一定的运行效率,都可以通过满足EOS.IO提供的API来加入到EOS.IO的消
息传递过程中。以下为github上官方的说明:

The EOS.IO software will be first and foremost a platform for coordinating
the delivery of authenticated messages (called Actions) to accounts. The details 
of scripting language and virtual machine are implementation specific details
that are mostly independent from the design of the EOS.IO technology. Any 
language or virtual machine that is deterministic and properly sandboxed with
sufficient performance can be integrated with the EOS.IO software API.

本文就EOSIO中的智能合约和虚拟机进行分析来从更加全面的角度来看EOS是如何构建和实现。

相关背景知识

LLVM相关内容

LLVM相关技术的理解对于我们深入理解EOS虚拟机的运行机制至关重要,所以必要的LLVM的相关知
识在这里是需要的。同时LLVM作为一个成熟的编译器后端实现,无论从架构还是相关设计思想以及相
关的工具的实现都是值得学习的。

LLVM架构概述

概括来讲LLVM项目是一系列分模块、可重用的编译工具链。它提供了一种代码良好的中间表示(IR),
LLVM实现上可以作为多种语言的后端,还可以提供与语言无关的优化和针对多种CPU的代码生成功能。
最初UIUC的Chris Lattner主持开发了一套称为LLVM(Low Level Virtual Machine)的编译器工具库套
件,但是后来随着LLVM的范围的不断扩大,则这个简写并不代表底层虚拟机的含义,而作为整个项目
的正式名称使用,并一直延续至今。所以现在的LLVM并不代表Low Level Virtual Machine。

The LLVM Project is a collection of modular and reusable compiler and toolchain
technologies. Despite its name, LLVM has little to do with traditional virtual machines.
The name "LLVM" itself is not an acronym; it is the full name of the project.

LLVM不同于传统的我们熟知的编译器。传统的静态编译器(如gcc)通常将编译分为三个阶段,分别
由三个组件来完成具体工作,分别为前端、优化器和后端,如下图所示。

LLVM项目在整体上也分为三个部分,同传统编译器一致,如下图所示,不同的语言的前端,统一的
优化器,以及针对不同平台的机器码生成。从图2我们也可以得到启发,如果想实现一门自定义的
语言,目前主要的工作可以集中在如何实现一个LLVM的前端上来。

LLVM的架构相对于传统编译器更加的灵活,有其他编译器不具备的优势,从LLVM整体的流程中我
们就可以看到这一点,如下图所示为LLVM整体的流程,编译前端将源码编译成LLVM中间格式的文
件,然后使用LLVM Linker进行链接。Linker执行大量的链接时优化,特别是过程间优化。链接得
到的LLVM code最终会被翻译成特定平台的机器码,另外LLVM支持JIT。本地代码生成器会在代码
生成过程中插入一些轻量级的操作指令来收集运行时的一些信息,例如识别hot region。运行时收
集到的信息可以用于离线优化,执行一些更为激进的profile-driven的优化策略,调整native code
以适应特定的架构。

从图中我们也可以得出LLVM突出的几个优势:

  • 持续的程序信息,每个阶段都可以获得程序的信息内容
  • 离线代码生成,产生较高的可执行程序
  • 便捷profiling及优化,方便优化的实施
  • 透明的运行时模型
  • 统一,全程序编译

LLVM IR介绍与分析

根据编译原理可知,编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,即
"IR"。之后再由中间语言,利用后端程序翻译为目标平台的汇编语言。由于中间语言相当于一款编
译器前端和后端的“桥梁”,不同编译器的中间语言IR是不一样的,IR语言的设计直接会影响到编
译器后端的优化工作。LLVM IR官方介绍见:http://llvm.org/docs/LangRef.html

LLVM IR格式

The LLVM code representation is designed to be used in three different forms: as an 
in-memory compiler IR, as an on-disk bitcode representation (suitable for fast loading
by a Just-In-Time compiler), and as a human readable assembly language representation.
This allows LLVM to provide a powerful intermediate representation for efficient compiler
transformations and analysis, while providing a natural means to debug and visualize the
transformations.

由上诉的引用得知目前LLVM IR提供三种格式,分别是内存里面的IR模型,存储在磁盘上的二进制
格式,存储在磁盘上的文本可读格式。三者本质上没有区别,其中二进制格式以bc为文件扩展名,
文本格式以ll为文件扩展名。除了以上两个格式文件外,和IR相关的文件格式还有s和out文件,这
两种一个是由IR生成汇编的格式文件,一个是生成的可执行文件格式(linux下如ELF格式),

  • bc结尾,LLVM IR文件,二进制格式,可以通过lli执行
  • ll结尾,LLVM IR文件,文本格式,可以通过lli执行
  • s结尾,本地汇编文件
  • out, 本地可执行文件

以上几种不同文件的转化图如下所示,整体上我们可以看一下这几种格式的转化关系,同时从中
我们也可以看出工具clang、llvm-dis、llvm-as等工具的作用和使用。

中间语言IR的表示,一般是按照如下的结构进行组织的由外到内分别是:

  • 模块(Module)
  • 函数(Function)
  • 代码块(BasicBlock)
  • 指令(Instruction)

模块包含了函数,函数又包含了代码块,后者又是由指令组成。除了模块以外,所有结构都是从
值产生而来的。如下为一个ll文件的片段,从中可以简单的看出这种组织关系。


; ModuleID = 'main.ll'
source_filename = "main.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
 
; Function Attrs: noinline nounwind uwtable
define i32 @add(i32, i32) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %3, align 4
  %6 = load i32, i32* %4, align 4
  %7 = add nsw i32 %5, %6
  ret i32 %7
}

LLVM IR指令集

指令集的分类大致可以分为基于栈的,基于运算器的还有基于寄存器的,基于栈的和基于寄存器
的虚拟机目前是比较常见的,两种不同之处主要在运行效率,指令集大小和性能三个方面。LLVM
IR采用的是基于寄存器的满足RISC架构以及load/store模式,也就是说只能通过将load和store
指令来进行CPU和内存间的数据交换。LLVM IR指令集拥有普通CPU一些关键的操作,屏蔽掉了
一些和机器相关的一些约束。LLVM提供了足够多的寄存器来存储基本类型值,寄存器是为SSA形
式(静态单态赋值),这种形式的UD链(use-define chain, 赋值代表define, 使用变量代表use)
便于优化。LLVM指令集仅包含31条操作码。LLVM中的内存地址没有使用SSA形式,因为内存地
址有可能会存在别名或指针指向,这样就很难构造出来一个紧凑可靠的SSA表示。在LLVM中一个
function就是一组基本块的组合,一个基本块就是一组连续执行的指令并以中指指令结束
(包括branch, return, unwind, 或者invoke等),中止指令指明了欲跳转的目的地址。

LLVM IR类型系统

LLVM的类型系统为语言无关。每一个SSA寄存器或者显示的内存对象都有其对应的类型。这些类
型和操作码一起表明这个操作的语义,这些类型信息让LLVM能够在低层次code的基础上进行一
些高层次的分析与转换,LLVM IR包含了一些语言共有的基本类型,并给他们一些预定义的大小,
从8bytes到64bytes不等,基本类型的定义保证了LLVM IR的移植性。同时LLVM又包含了四种复杂
类型,pointer,arrays, structures和functions。这四种类型足够表示现有的所有语言类型。为
了支持类型转换,LLVM提供了一个cast操作来实现类型的转换,同时为了支持地址运算,LLVM
提供了getelementptr的命令。LLVM中的许多优化都是基于地址做的(后续的总结再分析)。

LLVM IR内存模型

LLVM提供特定类型的内存分配,可以使用malloc指令在堆上分配一个或多个同一类型的内存对象,
free指令用来释放malloc分配的内存(和C语言中的内存分配类似)。另外提供了alloca指令用于
在栈上分配内存对象,该内存对象在通常在函数结尾会被释放。统一内存模型,所有能够取地址的
对象都必须显示分配。局部变量也要使用alloca来显示分配,没有隐式地手段来获取内存地址,这就
简化了关于内存的分析。

LLVM IR函数调用

LLVM中对普通函数调用,LLVM提供了call指令来调用附带类型信息的函数指针。这种抽象屏蔽了
机器相关的调用惯例。还有一个不能忽略的就是异常处理,在LLVM中,LLVM提供了invoke和
unwind指令。invoke指令指定在栈展开的过程中必须要执行的代码,例如栈展开的时候需要析构
局部对象等。而unwind指令用于抛出异常并执行栈展开的操作。栈展开的过程会被invoke指令停
下来,执行catch块中的行为或者执行在跳出当前活动记录之前需的操作。执行完成后继续代码执
行或者继续栈展开操作。注意像C++的RTTI则由C++自己的库处理,LLVM并不负责。

LLVM IR示例

下面我们编写一个简短的程序并编译成LLVM IR的形式来看LLVM的IR的具体格式和结构如下为一
段程序,保存为main.c

#include <stdio.h>
int add(int a, int b)
{
    return (a + b);
}
int main(int argc, char** argv)
{
    add(3, 5);
    return 0;
}

我们使用命令clang -o0 -emit-llvm main.c -S -o main.ll编译生成ll文件,ll文件为文本可见
文件,内容如下:

; ModuleID = 'main.c'
source_filename = "main.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
//函数特征如inline
; Function Attrs: noinline nounwind uwtable
define i32 @add(i32, i32) #0 {    //@代表是全局属性 i32为数据类型
%3 = alloca i32, align 4          //申请空间存放变量,%为局部属性
%4 = alloca i32, align 4          //3,4用来存放传入的参数,aling为位宽
store i32 %0, i32* %3, align 4    //将传入的参数放到是对应的存储位置
store i32 %1, i32* %4, align 4
%5 = load i32, i32* %3, align 4   //将参数存到待运算的临时变量中
%6 = load i32, i32* %4, align 4
%7 = add nsw i32 %5, %6           //执行具体的相加操作
ret i32 %7                        //最后返回结果
}
; Function Attrs: noinline nounwind uwtable
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = call i32 @add(i32 3, i32 5)
ret i32 0
}

以上代码不难发现函数add的展开中有部分临时变量的浪费,更为简洁的表达可以如下,当然
际的优化到什么程度要看后续的具体的实现。

%3 = add nsw i32 %1, %0
ret i32 %3

LLVM JIT介绍与分析

JIT技术Just-In-Time Compiler,是一种动态编译中间代码的方式,根据需要,在程序中编
译并执行生成的机器码,能够大幅提升动态语言的执行速度。LLVM设计上考虑了解释执行
的功能,这使它的IR可以跨平台去使用,代码可以方便地跨平台运行,同时又具有编译型语言
的优势,非常的方便。像Java语言,.NET平台等,广泛使用JIT技术,使得程序达到了非常
高的执行效率,逐渐接近原生机器语言代码的性能。

LLVM JIT实现原理

JIT引擎的工作原理并没有那么复杂,本质上是将原来编译器要生成机器码的部分要直接写
入到当前的内存中,然后通过函数指针的转换,找到对应的机器码并进行执行。实际编写
过程中往往需要处理例如内存的管理,符号的重定向,处理外部符号等问题。实现一个LLVM
的字节码(bc)的解释器其实并不复杂最好的实例就是LLVM自身的解释器lli,其总共不超过
800行代码实现了一个LLVM的字节码解释器,其源代码的github地址为:
https://github.com/llvm-mirror/llvm/blob/master/tools/lli/lli.cpp

LLVM JIT代码示例

下面就以LLVM源代码中的例子来解释LLVM-JIT是如何使用和运行的,在这之前,我们需
要明确llvm中常用的语句表达结构为module-->function-->basicblock-->instruction
-->operator
我们主要分析源代码example/HowToUseJIT部分的代码,主要代码片段如下:
该例子中在内存中创建了一个LLVM的module,这个module包含如下两个function:

int add1(int x) {
  return x+1;
}
int foo() {
  return add1(10);
}

针对以上两个函数,创建LLVM内存中IR中间格式的代码如下:

//首先包含llvm JIT需要的相关头文件


#include "llvm/ADT/STLExtras.h"
#include "llvm/ExecutionEngine/ExecutionEngine.h"
#include "llvm/ExecutionEngine/GenericValue.h"
...............
...............
#include "llvm/Support/raw_ostream.h"
#include <algorithm>
#include <cassert>
#include <memory>
#include <vector>
using namespace llvm;
 
int main() {
  InitializeNativeTarget(); //初始化本地执行环境,和具体的机器相关
  LLVMContext Context;      //定义一个LLVM的上下文变量
  //创建一个module对象,以便后续我们可以把function放入其中
  //这里这个module对象的名字是text,关联的上下文为上面声明
  std::unique_ptr<Module> Owner = make_unique<Module>("test", Context);
  Module* M = Owner.get();
  //创建add1函数对象,并把该对象加入到module中,
  Function* Add1F = cast<Function>(M->getOrInsertFunction(
                                  "add1",  //函数的名字为add1
                                  Type::getInt32Ty(Context),//函数的参数为int32
                                  Type::getInt32Ty(Context))); //函数的返回值为int32
  //创建一个块,并把块关联到add1函数上,注意函数的最后一个参数
  BasicBlock* BB = BasicBlock::Create(Context, "EntryBlock", Add1F);
  //创建一个basic block的builder,这个builder的工作就是将instructions添加到
  //basic block中去
  IRBuilder<> builder(BB);
  //获得一个指向常量数字1的指针
  Value* One = builder.getInt32(1);
  //获得指向函数add1第一个参数的指针
  assert(Add1F->arg_begin() != Add1F->arg_end()); // 确保有参数
  Argument* ArgX = &* Add1F->arg_begin();          // 获得参数指针
  ArgX->setName("AnArg");        
      // 设置参数名称,便于后续的查找
  //创建加1的指令,并把指令放入到块的尾部
  Value* Add = builder.CreateAdd(One, ArgX);
  //创建返回指令, 至此add1的函数已经创建完毕
  builder.CreateRet(Add);
  //创建函数foo
  Function* FooF = cast<Function>(M->getOrInsertFunction(
                                  "foo", Type::getInt32Ty(Context)));
  BB = BasicBlock::Create(Context, "EntryBlock", FooF);
  //通知builder关联到一个新的block上
  builder.SetInsertPoint(BB);
  Value* Ten = builder.getInt32(10);
  //创建一个函数的调用,并把参数传递进去
  CallInst* Add1CallRes = builder.CreateCall(Add1F, Ten);
  Add1CallRes->setTailCall(true);
  //创建返回结果
  builder.CreateRet(Add1CallRes);
  // 创建JIT引擎,创建参数为上下文
  ExecutionEngine* EE = EngineBuilder(std::move(Owner)).create();
  outs() << "We just constructed this LLVM module:\n\n" << * M;
  outs() << "\n\nRunning foo: ";
  outs().flush();
  //调用函数foo
  std::vector<GenericValue> noargs;
  GenericValue gv = EE->runFunction(FooF, noargs);
  //获得函数返回值
  outs() << "Result: " << gv.IntVal << "\n";
  delete EE;
  //关闭LLVM虚拟机
  llvm_shutdown();
  return 0;
}

以上代码在内存中创建了LLVM IR,并调用LLVM JIT的执行引擎运行代码,从中我们得到启
发是如果我们借助LLVM JIT运行我们的合约代码,我们就需要将合约代码最终转化为LLVM
能识别的中间代码IR上,下面将一步一步的分析EOS中是如何利用LLVM-JIT技术实现的虚
拟机运行。

WebAssembly相关内容

WebAssembly概述

WASM在浏览器中运行的效果和Java语言在浏览器上的表现几近相同的时候,但是WASM
不是一种语言,确切的说WASM是一种技术方案,该技术方案允许应用诸如C、C++这种
编程语言编写运行在web浏览其中的程序。更加细节的去讲,WASM是一种新的字节码格
式,是一种全新的底层二进制语法。突出的特点就是精简,加载时间短以及高速的执行模
型。还有一点比较重要,那就是它设计为web多语言编程的目标文件格式。具体可见官网
相关介绍:https://webassembly.org/

WebAssembly格式介绍与分析

WebAssembly同LLVM的IR类似,提供两种格式,分别为可读的文本格式wast和二进
制格式wasm,两者最终是等价的,可以通过工具wast2wasm完成wast到wasm的格式转
而工具wasm2wast则执行这一过程的返作用。

WebAssembly WAST格式介绍

为了能够让人阅读和编辑WebAssembly,wasm二进制格式提供了相应的文本表示。这
是一种用来在文本编辑器、浏览器开发者工具等工具中显示的中间形式。下面将用基本
语法的方式解释了这种文本表示是如何工作的以及它是如何与它表示的底层字节码。

无论是二进制还是文本格式,WebAssembly代码中的基本单元是一个模块。在文本格式
中,一个模块被表示为一个S-表达式。S-表达式是一个非常古老和非常简单的用来表示树
的文本格式。具体介绍:https://en.wikipedia.org/wiki/S-expression 因此,我们可以
把一个模块想象为一棵由描述了模块结构和代码的节点组成的树。与编程语言的抽象语
法树不同的是,WebAssembly的树是平坦的,也就是大部分包含了指令列表。树上的
每个一个节点都有一对括号包围。括号内的第一个标签表示该节点的类型,其后跟随的
是由空格分隔的属性或孩子节点列表。因此WebAssembly的S表达式结构大概如下所示:

(module (memory 1) (func))

上面的表达式的含义是模块module包含两个孩子节点,分别是属性为1的内存节点,和
函数func节点。从上面我们知道一个空的模块定义为module,那将一个空的模块转化为
wasm将是什么格式,如下所示:

0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0d00 0000 ; WASM_BINARY_VERSION

WebAssembly模块中的所有代码都是包含函数里面。函数的结构如下所示:

( func [signature] [locals] [body] )
  • signature 函数的签名声明函数的参数和返回值
  • local 局部变量,声明了具体的类型
  • body 为函数体,一个低级的的指令的线性列表
    关于数据类型这里简单说明一下,wasm目前有四种可用的数据类型,分别为i32 i64 f32 f64
    关于签名我们来看一个签名的具体例子,如下所示表示函数需要两个参数,均为i32类型,
    返回值是一个f64类型,参数可以看成是函数调用过程中传递过来的实参初始化后的局部变量。
    (func (param i32) (param i32) (result f64) ... )

    关于局部变量这里需要注意两个操作:get_local和set_local,先看下面的例子:

    (func (param i32) (param f32) (local f64) get_local 0 get_local 1 get_local 2)
  • get_local 0会得到i32类型的参数
  • get_local 1会得到f32类型的参数
  • get_local 2会得到f64类型的局部变量
    为了便于识记,可以定义变量名的方式来取代索引的方式,具体如下:
    (func (param $p1 i32) (param $p2 f32) (local $loc i32) …)

    关于函数体,在具体介绍函数体之前,我们要明确的一点是,虽然wasm被设计成高效执行
    的代码,但是最后wasm的执行依然是一个栈式机器定义的,下面我们参考如下代码:

    (func (param $p i32) ..get_local $p get_local $p i32.add)

    上面函数的功能概括为i+i,即计算表达是$p+$p的结果,结果将放在最后运行的栈的顶部。
    现在我们完整的写出一个module,该module就包含上述的功能,具体的S表达式如下:

    (module
    (func (param $lhs i32) (param $rhs i32) (result i32)
      get_local $lhs
      get_local $rhs
      i32.ad
    )
    )

    上面的描述似乎缺少了什么,那就我们如何才能使用这个函数,于是涉及到函数的导出和调用。
    wasm中是通过export来完成导出的,通过call关键字来完成函数调用的,如下一个更加复杂
    的例子:

    (module
    (func $getNum (result i32)
      i32.const 42)
    (func (export "getPlus") (result i32)
      call $getNum
      i32.const 1
      i32.add
    )
    )

    函数运行最后的结果在栈顶保存43这个元素,注意其中的(export "getPlus")也可以通过如下的
    方式(export "getPlus" (func $getPlus))的方式导出。最后一个问题wasm如何导入函数?
    下面我们看一个具体的例子 :

    (module</br>
    (import "console" "log" (func $log (param i32)))
    (func (export "logIt")
      i32.const 13
      call $log))

    WebAssembly使用了两级命名空间,这里的导入语句是说我们要求从console模块导入log函
    数。导出的logIt函数使用call指令调用了导入的函数。
    小结: 到目前为止我们熟悉了wast的具体格式,关于wast中的外部内存使用,表格等高级内容
    可以单独去了解。

    WebAssembly WASM格式介绍

    wasm为WebAssembly的二进制格式,可以通过工具wast2wasm将wast转化为wasm格式,下
    面将如下wast转化为wasm, 命令为wat2wasm simple.wast -o simple.wasm
    上述工具的地址为:https://github.com/WebAssembly/wabt/

    (module
    (func $getNum (result i32)
      i32.const 42)
    (func (export "getPlus") (result i32)
      call $getNum
      i32.const 1
      i32.add
    )
    )

    虽然编译好的二进制文件没有办法进行直观的读取,但是可以借助wat2wasm工具进行查看其
    verbose的输出,命令为:./wat2wasm test.wat -v输出结果为如下,通过对如下字节流的理
    我们可以清晰看到wasm的二进制流格式是什么样的,以及它是如何运行的。基于以下的代码我
    可以自己构建一个wasm的解析引擎,引擎需要使用寄存器的设计加上栈的运行控制。

    0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
    0000004: 0100 0000                                 ; WASM_BINARY_VERSION
    ; section "Type" (1)
    0000008: 01                                        ; section code
    0000009: 00                                        ; section size (guess)
    000000a: 01                                        ; num types
    ; type 0
    000000b: 60                                        ; func
    000000c: 00                                        ; num params
    000000d: 01                                        ; num results
    000000e: 7f                                        ; i32
    0000009: 05                                        ; FIXUP section size
    ; section "Function" (3)
    000000f: 03                                        ; section code
    0000010: 00                                        ; section size (guess)
    0000011: 02                                        ; num functions
    0000012: 00                                        ; function 0 signature index
    0000013: 00                                        ; function 1 signature index
    0000010: 03                                        ; FIXUP section size
    ; section "Export" (7)
    0000014: 07                                        ; section code
    0000015: 00                                        ; section size (guess)
    0000016: 01                                        ; num exports
    0000017: 07                                        ; string length
    0000018: 6765 7450 6c75 73                        getPlus  ; export name
    000001f: 00                                        ; export kind
    0000020: 01                                        ; export func index
    0000015: 0b                                        ; FIXUP section size
    ; section "Code" (10)
    0000021: 0a                                        ; section code
    0000022: 00                                        ; section size (guess)
    0000023: 02                                        ; num functions
    ; 上面的代码基本上都声明和签名,如下代码才是真正的函数体代码
    ; function body 0
    0000024: 00                                        ; func body size (guess)
    0000025: 00                                        ; local decl count
    0000026: 41                                        ; i32.const
    0000027: 2a                                        ; i32 literal
    0000028: 0b                                        ; end
    0000024: 04                                        ; FIXUP func body size
    ; function body 1
    0000029: 00                                        ; func body size (guess)
    000002a: 00                                        ; local decl count
    000002b: 10                                        ; call
    000002c: 00                                        ; function index
    000002d: 41                                        ; i32.const
    000002e: 01                                        ; i32 literal
    000002f: 6a                                        ; i32.add
    0000030: 0b                                        ; end
    0000029: 07                                        ; FIXUP func body size
    0000022: 0e                                        ; FIXUP section size

    这里我们要注意一点是wasm中不同section是有一定的排序的,具体的顺序如下

    user                       0
    type                       1
    import                     2
    functionDeclarations       3  
    table                      4
    memory                     5
    global                     6
    export                     7
    start                      8
    elem                       9
    functionDefinitions        10
    data                       11

    WASM运行介绍与分析

    wasm目前主要的应用领域在于web应用,对于EOS其将作为智能合约的最终格式,其目前运行
    在WAVM上,其机制不同于目前浏览的运行和调用方式。首先我们先简单了解一下wasm是如
    在浏览器中运行,而WAVM的运行时分析将在EOS虚拟机中进行。
    浏览器运行的示例:https://webassembly.org/getting-started/developers-guide/ 
    这里可以看到利用emcc的工具生成的最终代码,其中主要有wasm文件,js胶水文件和html 
    调用文件。

EOS智能合约分析

EOS智能合约概览

EOS中的智能合约概括的来讲就是对多个输入来组织商议输出的过程,EOS中的合约不仅仅
可以实现例如转账的这种经济行为,也可以描述游戏规则。EOS中的合约作为注册在EOS区
块链上的应用程序并最终运行在EOS的节点上。EOS的智能合约定义了相关的接口,这些接
口包含action,数据结构和相关的参数,同时智能合约实现这些接口,最后被编译成二进制格
式,在EOS中为wasm,节点负责解析字节码来执行对应的智能合约。对于区块链而言,最
终存储的是智能合约的交易(transactions)。

EOS智能合约模型和执行流程

EOS中的智能合约由两个部分组成分别为action集合和类型的定义:

  • action集合,定义和实现了智能合约的行为和功能
  • 类型定义,定义了合约需要的内容和数据结构

    EOS智能合约与Action

    EOS中的action操作构建与一个消息架构之上,客户端通过发送消息来触发action的执行,
    我们知道智能合约最终的存储形式是一个transaction,那transaction和action之间是什么关
    系,在这里一个transaction包含至少一个action,而一个action代表的是大一的操作。如下为
    一个包含多个action的transaction。对于如下的transaction,当其中所有的action都成功的
    时候,这个transaction才算成功。如果一个transaction成功后,则其receipt生成,但是此时
    并不代表transaction已经确认,只是说明确认的概率大一些

    {
      "expiration": "...",
      "region": 0,
      "ref_block_num": ...,
      "ref_block_prefix": ...,
      "net_usage_words": ..,
      "kcpu_usage": ..,
      "delay_sec": 0,
      "context_free_actions": [],
      "actions": [{
          "account": "...",
          "name": "...",
          "authorization": [{
              "actor": "...",
              "permission": "..."
            }
          ],
          "data": "..."
        }, {
          "account": "...",
          "name": "...",
          "authorization": [{
              "actor": "...",
              "permission": "..."
            }
          ],
          "data": "..."
        }
      ],
      "signatures": [
        ""
      ],
      "context_free_data": []
    }

    EOS的智能合约提供一个action handler来完成对action的请求,每次一个action执行在实现
    上通过调用apply方法,EOSIO通过创建一个apply的上下文来辅助action的执行,如下的图
    说明一个apply上下文的关键元素。

从全局的角度看,EOS区块链中的每个节点将获得智能合约中每个action的一个拷贝,在
所有节点的运行状态中,一些节点在执行智能合约的实际工作,而一些节点在做交易的验
证,因此对于一个合约来说能比较重要的一点就是知道当前运行的实际的上下文是什么,
也就是说目前处在哪个阶段,在这里上下文的标识被记录在action的上下文中来完成上面
的工作,如上面图所示这个上下文标识包括三个部分,分别是reciver,code和action。
receiver表示当前处理这个action的账户,code代表授权了这个合约账户,而action是
当前运行的action的ID。
根据上面我们知道transaction和action的关系,如果一个transaction失败,所有在这个
transaction中的action的计算结果都需要被回滚,在一个action上下文中一个关键的数据
成员就是当前的transaction数据,它包含以下几个部分:

  • transaction的头
  • 包含transaction中所有的原始的action的容器,容器已经排好序
  • 包含transaction中的上下文无关的action的容器
  • 一个可以删节的上下文无关的数据,这部分数据是被合约定义的,以一个二进制长
  • 对象集合提供
  • 对上述二进制长对象的索引

在EOS中每个action执行的时候都会重新的申请一块新的内存,每个action上下文中的变量是
私有的,即使在同一个transaction中的action,他们的变量也是不可以共享,唯一的一种方式
来共享变量就是通过持久化数据到EOS的数据库中,这些可以通过EOSIO的持久化API来实现。

EOS智能合约执行流程

EOS中的智能合约彼此可以进行通讯,例如一个合约调用另外的合约来完成相关操作来完成当
前的transaction,或者去触发一个当前transaction的scope外的一个外来的transaction。
EOS中支持两种不基本的通讯模型,分别是inline和deferred两种,典型的在当前transaction
中的操作是inline的方式的action的实例,而被触发的一个将要执行的transaction则是一个deferred
action的实例。在智能合约之间的通讯我们可以看做是异步的。

inline Communication

Inline的通讯模式主要体现在对需要执行的action的请求过程直接采用调用的方式,Inline方式
下的action在同一transaction的scope和认证下,同时action被组织起来用于执行当前的transaction
Inline action可以被看做是transaction的嵌套,如果transaction的任何一个部分执行失败,那么
inline action也只会在transaction的剩下部分展开, 调用inline action不会产生任何对外的通知
无论其中是成功还是失败,综上也就是说inline action的作用范围是在一个transaction中的。

Deferred Communication

Deferred的通讯模式采用的是通过通知另一个节点transaction的方式来实现的。一个Deferred
actions一般会稍后调用,对于出块生产者来说并不保证其执行。对于创造Deferred action的
transaction来说它只能保证是否创建和提交成功,对于是否执行成功与否无法保证。对于一个
Deferred action来说其携带合约的验证信息进行传递。特殊的一个transaction可以取消一个
deferred的transaction。

执行流程示例

如下如未EOS wiki上给出的一个包含inline action的transaction的执行流程。

从图中我们可以看到,这个transaction中有两个inline action,分别是

  • employer::runpayroll
  • employer::dootherstuff

由上面的图,我们可以很清晰的知道,action通过调用inline action并递归的调用最后来完成
整个transactio的执行。同上对于上面的一个转账发薪酬的场景也可以通过Deferred的方式
来完成,如下图所示:

EOS智能合约示例说明

EOS智能合约一般用c++语言实现,可以通过工具来进行编译成最后的智能合约二进制码,一
段典型的智能合约代码如下:


#include <eosiolib/eosio.hpp>
 
using namespace eosio;
 
class hello : public eosio::contract {
  public:
      using contract::contract;
      /// @abi action
      void hi( account_name user ) {
         print( "Hello, ", name{user} );
      }
};
 
EOSIO_ABI( hello, (hi) )

对于每一个智能合约而言,其必须提供一个apply的接口,这个接口函数需要监听所有输入的aciton
并作出对应的动作,apply用recevier,code和action来过来输入并执行特定的操作。形式如下:

if (code == N(${contract_name}) {
   // your handler to respond to particular action
}

EOS中的的宏EOSIO_ABI屏蔽了底层实现的细节,宏展开如下所示:

#define EOSIO_ABI( TYPE, MEMBERS ) \
extern "C" { \
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
      auto self = receiver; \
      if( action == N(onerror)) { \
         eosio_assert(code == N(eosio), \
         "onerror action's are only valid from the \"eosio\" system account"); \
      } \
      if( code == self || action == N(onerror) ) { \
         TYPE thiscontract( self ); \
         switch( action ) { \
            EOSIO_API( TYPE, MEMBERS ) \
         } \
         /* does not allow destructor of thiscontract to run: eosio_exit(0); * / \
      } \
   } \
} \

其中EOSIO_ABI的宏定义如下:

#define EOSIO_API( TYPE,  MEMBERS ) \
   BOOST_PP_SEQ_FOR_EACH( EOSIO_API_CALL, TYPE, MEMBERS )

我们继续展开宏EOSIO_API_CALL如下:

#define EOSIO_API_CALL( r, OP, elem ) \
   case ::eosio::string_to_name( BOOST_PP_STRINGIZE(elem) ): \
      eosio::execute_action( &thiscontract, &OP::elem ); \
      break;

这样我们就明确一个只能合约被调用的时候最后是如何反应到代码层面进行路由调用的。

EOS智能合约相关工具

由上文我们知道一个智能合约源文件大概的样子,现在我们来看一下如何生成EOS虚拟机支持的格
式。EOS虚拟机目前支持加载wast和wasm两种格式的智能合约。现在看下EOS中智能合约是如何
构建的,如下代码为tools/eosiocpp.in中关于合约的编译脚本,其中省略部分非关键代码:

function build_contract {    
($PRINT_CMDS; @WASM_CLANG@ -emit-llvm -O3 --std=c++14 --target=wasm32 -nostdinc \
  -nostdlib -nostdlibinc -ffreestanding -nostdlib -fno-threadsafe-statics -fno-rtti \
  -fno-exceptions -I ${EOSIO_INSTALL_DIR}/include \
  -I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \
  -I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \
  -I${BOOST_INCLUDE_DIR} \
  -I $filePath \
  -c $file -o $workdir/built/$name)
 
  ($PRINT_CMDS; @WASM_LLVM_LINK@ -only-needed -o $workdir/linked.bc $workdir/built/* \
    ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/eosiolib.bc \
    ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc++.bc \
    ${EOSIO_INSTALL_DIR}/usr/share/eosio/contractsdk/lib/libc.bc
  )
  ($PRINT_CMDS; @WASM_LLC@ -thread-model=single --asm-verbose=false -o \
    $workdir/assembly.s $workdir/linked.bc)
  ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-s2wasm -o $outname -s \
    16384 $workdir/assembly.s)
  ($PRINT_CMDS; ${EOSIO_INSTALL_DIR}/bin/eosio-wast2wasm $outname \
    ${outname%.*}.wasm -n)
}

由上述的代码可知,智能合约的编译主要过程如下:

  • 利用clang以wasm32为目标,生成中间文件bc
  • 利用LLVM-link链接上一个步骤生成bc文件和标准库bc文件生成link.bc文件
  • 利用LLVM的llc生成s汇编文件assembly.s
  • 应用eosio-s2wasm工具讲s文件转化为wast文件
  • 应用eosio-wast2wasm工具将wast文件转化为最终的wast文件

通过以上的步骤我们就生成了一个以wasm为格式的智能合约,上面一共经历了5个步骤才将我们的

源文件变异成wasm,其实还可以应用开源工具emcc来编译,但是该工具并不是针对智能合约设计
工具比较庞大,我们把没有应用emcc的wasm的生成方案统一称为wasm without emcc。
由于上述的编译过程很复杂,这里需要分析说明一下为什么采用这种方式?

The Runtime is the primary consumer of the byte code. It provides an API for 
instantiating WebAssembly modules and calling functions exported from them. 
To instantiate a module, it initializes the module's runtime environment 
(globals, memory objects, and table objects), translates the byte code into LLVM
IR, and uses LLVM to generate machine code for the module's functions.

由上文我们得知,WAVM是将wasm或者wast文件转化为LLVM的IR表示,然后通过LLVM运行代码来实现
最后的程序运行,那么问题来了,对于智能合约,为什么我们不直接用clang生成bc文件,然后修改
lli(前文介绍过代码不超过800行)来实现虚拟机呢? 个人分析主要有以下几个原因:

  • 如果EOS定义智能合约二进制格式为bc,文本方式为ll,也就是对标wasm和wast个人觉得利用lli
    没有问题,关键受限于LLVM。
  • 出于对未来的考虑,毕竟对wasm支持的解释容器比较多,方便多种虚拟机的接入,但是目前看大多数
    容器都是浏览器js引擎,因此解决js胶水代码仍然是个问题,所以寻求一个wasm的虚拟机目前看WAVM
    比较合适
  • WAVM实现了wasm的虚拟机,而且EOS也声称不提供虚拟机,也就是说wasm的选型限制了以上的工具链

这里还有个重要的文件生成,那就是abi的文件的构建,这个的实现也在eosiocpp.in中,abi这里的
作用是什么?就是它会描述一个合约对外暴露的接口,具体为JSON格式,用户可以通过eosc工具构建
合适的message来调用对应的接口。eosiocpp中generate_abi的部分代码如下:
${ABIGEN} -extra-arg=-c -extra-arg=--std=c++14 -extra-arg=--target=wasm32 \
  -extra-arg=-nostdinc -extra-arg=-nostdinc++ -extra-arg=-DABIGEN \
  -extra-arg=-I${EOSIO_INSTALL_DIR}/include/libc++/upstream/include \
  -extra-arg=-I${EOSIO_INSTALL_DIR}/include/musl/upstream/include \
  -extra-arg=-I${BOOST_INCLUDE_DIR} \
  -extra-arg=-I${EOSIO_INSTALL_DIR}/include -extra-arg=-I$context_folder \
  -extra-arg=-fparse-all-comments -destination-file=${outname} -verbose=0 \
  -context=$context_folder $1 --}}

最后通过二进制工具cleos来部署智能合约,例如:

cleos set contract eosio build/contracts/eosio.bios -j -p eosio
  • 第一个eosio为账户
  • 第二个eosio为权限
  • -j 以json输出结果
  • build/contracts/eosio.bios 为智能合约所在目录

同时可以通过cleos工具来推送action测试contract,例如如下命令:

cleos push action eosio.token create '{"issuer":"eosio", "maximum_supply":"
1000000000.0000 EOS", "can_freeze":0, "can_recall":0, "can_whitelist":0}'
-j -p eosio.token
  • esoio.token为contract
  • create为action
  • 后面json格式的为具体的数据
  • -p为指定权限

小结:EOS智能合约通过复杂的工具链最后生成wasm或者wast,并配合abi文件最后进行分发到EOS
系统中去。

EOS虚拟机分析

EOS在技术白皮书中指明并不提供具体的虚拟机实现,任何满足沙盒机制的虚拟机都可以运行在EOSIO
中,源代码层面,EOS提供了一种虚拟机的实现,虚拟机以wasm为输入,利用相关的技术完成代码的
快速执行。

EOS虚拟机概览

EOS虚拟机代码实现来自WAVM,参见具体的文件发现其基本上都是wasm-jit目录下的内容从项目信
息可以看出其是fork AndrewScheidecker/WAVM的实现,这个也是为啥很多人瞧不起EOS虚拟机的
原因,但是Andrew Scheidecker本人主要在提交代码,所以对他下结论为时尚早,作者
Andrew Scheidecker是虚幻引擎的主要贡献者,代码质量至少能有所保障。
首先是EOS虚拟机的代码,在github上有两个地方可以查看到EOS中虚拟机的代码分别为:

https://github.com/EOSIO/eos
https://github.com/EOSIO/WAVM
其中eos目录下为这个EOS的代码其中虚拟机部分主要存在于如下几个关键的目录下:

  • libraries/chain,主要是定义虚拟机相关接口

  • libraries/wasm-jit,主要是智能合约执行的实现

  • contracts目录下,为相关的ABI辅助源代码

    This is a standalone VM for WebAssembly. It can load both the standard binary
    format, and the text format defined by the WebAssembly reference interpreter.
    For the text format, it can load both the standard stack machine syntax and 
    the old-fashioned AST syntax used by the reference interpreter, and all of the
    testing commands

由上述的描述我们可以知道WAVM支持两种的输入分别是二进制的输入和文本格式的输入,对应的具
体的格式是wasm和wast。参见WAVM使用说明如下:

The primary executable is wavm:
Usage: wavm [switches] [programfile] [--] [arguments]
in.wast|in.wasm Specify program file (.wast/.wasm)
-f|--function name Specify function name to run in module rather than main
-c|--check Exit after checking that the program is valid
-d|--debug Write additional debug information to stdout
-- Stop parsing arguments

由上我们得知EOS的智能合约支持两种格式分别就是上文描述的wasm和wast。

EOS虚拟机实现思路分析

EOS在智能合约目标格式选择上应该做过一定的考虑,对于wasm的选择可能出于社区支持和实现上
的双重考虑,这点在采用LLVM-JIT技术就有所体现。EOS在选择如何实现虚拟机的方案上采用的是
开放的态度,即如白皮书所讲。EOS为了使项目完整,需要提供一个的虚拟机。首先选定wasm不仅
仅是因为支持的大厂比较多,还有出于多语言支持的考虑,敲定wasm目标格式后痛苦的事情就来了
目前需要一个能执行他的虚拟机容器,目前都是浏览器支持,落地就是JS的解析引起支持,如果用JS
解析引擎,工程量大,发布还要附带js胶水代码加麻烦的还有结果如何安全获取。于是需要的是一个
wasm的执行的轻量级虚拟机,WAVM成了首选,多亏AndrewScheidecker之前写过一个这样的项
目,于是直接Fork,加些接口就完成了implementation。从另外一个角度看 如果不考虑生态的问题,
LLVM中的bc也可以作为智能合约的语言,通过修改lli来完成虚拟机的实现,而且工程实践更加简单,
但是问题就是和LLVM绑定了,虚拟机只能和LLVM混,这个限制太大。

EOS虚拟机架构概述

EOS虚拟机面对的编译的智能合约格式为wasm或者wast两种格式,这两种格式本质上没有区别,那么
如何解析这两种格式并执行内部的相关指令就称为虚拟机主要考虑的问题,EOS的实现思路如下:

将wasm转化为LLVM能识别的IR中间语言。
借助LLVM-JIT技术来实现IR的语言的运行。
这里有两个关键点,一个是如何将wasm格式文件转化为IR中间文件,第二个就是如何保证IR的相关
运行时环境的维护。以下几个章节将解释相关的问题。

EOS虚拟机实现与分析

EOS虚拟机核心接口

我们先High Level的看一下EOS虚拟机是如何响应外部执行需求的,这个主要体现在对外接
层面EOS虚拟机接口对外暴露虚拟机实例创建和合约执行入口,具体声明在如下路径文件中
libraries/chain/inlcude/eosio/chain/webassembly/runtime_interface.hpp
文件中主要对外暴露了两个接口,分别为instantiate_module和apply,分别声明在两个不同
的类中,如下为接口的具体声明:


class wasm_instantiated_module_interface {
public:
  virtual void apply(apply_context& context) = 0;
  virtual ~wasm_instantiated_module_interface();
};
class wasm_runtime_interface {
public:
  virtual std::unique_ptr<wasm_instantiated_module_interface>
  instantiate_module(const char* code_bytes,
                     size_t code_size,
                     std::vector<uint8_t> initial_memory) = 0;
  virtual ~wasm_runtime_interface();
};

接口apply实现在文件\libraries\chain\include\eosio\chain\webassembly\wavm.hpp
接口instantiate_module实现在\libraries\chain\webassembly\wavm.hpp
接口apply的实现如下代码所示:

void apply(apply_context& context) override {
  //组织参数列表
  //这里需要说明一下每个被分发的action通过scope就是account和
  //function就是name来定义的
  vector<Value> args = {
    Value(uint64_t(context.receiver)),//当前运行的代码
    Value(uint64_t(context.act.account)),//action中的账户
    Value(uint64_t(context.act.name))};//action的名称
  call("apply", args, context);
}

下面来看call具体执行的逻辑功能,这里我们将看到运行在虚拟机上的代码是如何启动的。
这里我们一行一行来进行分析:

void call(
  const string &entry_point, //函数入口点,例如:main是一个exe的入口
  const vector <Value> &args, //函数参数列表
  apply_context &context) {//需要执行的具体的内容
  try {
    //首先根据entry_point(这里为apply)获取到传入的代码中是否有名字为
    //entry_point的object,通俗的讲就是根据函数名找到函数指针
    FunctionInstance* call = asFunctionNullable(
                      getInstanceExport(_ instance,entry_point));
    if( !call )//如果没有找到函数的入口在直接返回,注意此处无异常
      return;
    //检查传入的参数个数和函数需要的个数是否相等,注意为什么没有检查类型
    //因为由上述函数apply得知类型均为uint_64,内部对应类型IR::ValuType::i64
    FC_ASSERT( getFunctionType(call)->parameters.size() == args.size() );
 
    //获得内存实例,在一个wavm_instantiated_modules中,内存实例是被重用的,
    //但是在wasm的实例中将不会看到getDefaultMemeory()
    MemoryInstance* default_mem = getDefaultMemory(_ instance);
    if(default_mem) {
      //重置memory的大小为初始化的大小,然后清零内存
      resetMemory(default_mem, _ module->memories.defs[0].type);
      char* memstart = &memoryRef<char>(getDefaultMemory(_ instance), 0);
      memcpy(memstart, _ initial_memory.data(), _ initial_memory.size());
    }
    //设置运行上下文的内存和执行的上下文信息
    the_running_instance_context.memory = default_mem;
    the_running_instance_context.apply_ctx = &context;
    //重置全局变量
    resetGlobalInstances(_ instance);
    //调用module的起始函数,这个函数做一些环境的初始化工作
    //其在instantiateModule函数中被设置
    runInstanceStartFunc(_ instance);
    //invoke call(上面已经指向apply函数的地址了)
    Runtime::invokeFunction(call,args);
  } catch( const wasm_exit& e ) {
  } catch( const Runtime::Exception& e ) {
    FC_THROW_EXCEPTION(wasm_execution_error,"cause: ${cause}\n${callstack}",
    ("cause", string(describeExceptionCause(e.cause)))
    ("callstack", e.callStack));
  } FC_CAPTURE_AND_RETHROW()
}

上述代码中通过call寻找entry_point名字的函数,这里为apply,注意上一个主题中EOSIO_ABI
的展开中为apply函数的实现,如下:


#define EOSIO_ABI( TYPE, MEMBERS ) \
extern "C" { \
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
      auto self = receiver; \
      if( action == N(onerror)) { \
         eosio_assert(code == N(eosio), \

总结:上面通过接口的了解和代码的阅读分析快速的从比较高的视角看到EOS虚拟机执行
的大体过程,下面我们就从细节上来了解EOS虚拟的采用的技术和最后是如何应用在EOS系统中的。

EOS虚拟机架构应用层

我们这里将EOS虚拟机关于智能合约部署以及虚拟机外层调用逻辑统一称为虚拟机应用层,现在分别进行
说明,先从虚拟机的外围了解其整体的工作流程。
EOS虚拟机客户端合约部署
首先看命令行工具cleos是如何将智能合约发送给EOSIO程序的,具体的代码见文件:
eosio\eos\programs\cleos\main.cpp。如下代码片段为添加命令行参数。

auto contractSubcommand = setSubcommand->add_subcommand(
  "contract",
  localized("Create or update the contract on an account"));
contractSubcommand->add_option(
  "account",
  account,
  localized("The account to publish a contract for"))
  ->required();
contractSubcommand->add_option(
  "contract-dir",
  contractPath,
  localized("The path containing the .wast and .abi"))
  ->required();
contractSubcommand->add_option(
  "wast-file",
  wastPath,
  localized("The file containing the contract WAST or WASM relative to contract-dir"));
auto abi = contractSubcommand->add_option(
  "abi-file,-a,--abi",
  abiPath,
  localized("The ABI for the contract relative to contract-dir"));

上述的命令为set命令的子命令,现在看一下命令是如何发送出去的,主要在如下两个回调函数

  • set_code_callback
  • set_abi_callback

两个回调函数,我们以set_code_callback分析是如何运行的,关键代码如下:

actions.emplace_back( create_setcode(account, bytes(wasm.begin(), wasm.end()) ) );
if ( shouldSend ) {
  std::cout << localized("Setting Code...") << std::endl;
  send_actions(std::move(actions), 10000, packed_transaction::zlib);
}

如上代码知道其是调用send_actions将智能合约的相关信息已一个action的形式发送出去,
而send_actions将调用push_action函数,最后push_action将调用关键函数call代码如下:

fc::variant call( const std::string& url,
  const std::string& path,
  const T& v ) {
try {
  eosio::client::http::connection_param * cp =
  new eosio::client::http::connection_param((std::string&)url, (std::string&)path,
  no_verify ? false : true, headers);
  return eosio::client::http::do_http_call( *cp, fc::variant(v) );
}
}

由上可知客户端最后通过http的方式将部署智能合约的代码发送到了EOSIO上。注意其中的url具体为

const string chain_func_base = "/v1/chain";
const string push_txn_func = chain_func_base + "/push_transaction";
EOS虚拟机服务端合约部署

上面我们了解了合约是如何通过客户端传递到服务端的,现在我们重点分析一下服务端是如何部署
或者更加准确的说存储合约的。我们重点分析一下nodeos(eosio\eos\programs\nodeos)是如何
处理push_transaction的,先看其主函数关键片段:


if(!app().initialize<chain_plugin, http_plugin, net_plugin, producer_plugin>
  (argc, argv))
return INITIALIZE_FAIL;
initialize_logging();
ilog("nodeos version ${ver}", \
  ("ver", eosio::utilities::common::itoh(static_cast<uint32_t>(app().version()))));
ilog("eosio root is ${root}", ("root", root.string()));
app().startup();
app().exec();

主要注册了chain,http,net和producer几个插件,我们先看chain_api_plugin的关键实现:

auto ro_api = app().get_plugin<chain_plugin>().get_read_only_api();
auto rw_api = app().get_plugin<chain_plugin>().get_read_write_api();
 
app().get_plugin<http_plugin>().add_api(
CHAIN_RO_CALL(get_info, 200l),
CHAIN_RO_CALL(get_block, 200),
CHAIN_RO_CALL(get_account, 200),
CHAIN_RO_CALL(get_code, 200),
CHAIN_RO_CALL(get_table_rows, 200),
CHAIN_RO_CALL(get_currency_balance, 200),
CHAIN_RO_CALL(get_currency_stats, 200),
CHAIN_RO_CALL(get_producers, 200),
CHAIN_RO_CALL(abi_json_to_bin, 200),
CHAIN_RO_CALL(abi_bin_to_json, 200),
CHAIN_RO_CALL(get_required_keys, 200),
CHAIN_RW_CALL_ASYNC(push_block, chain_apis::read_write::push_block_results, 202),
CHAIN_RW_CALL_ASYNC(push_transaction, \
  chain_apis::read_write::push_transaction_results, 202),
CHAIN_RW_CALL_ASYNC(push_transactions, \
  chain_apis::read_write::push_transactions_results, 202) \
)

我们下一步具体详细的看一下http_plugin中的add_api的具体实现代码如下:

void add_api(const api_description& api) {
  for (const auto& call : api)
    add_handler(call.first, call.second);
}
void http_plugin::add_handler(const string& url, const url_handler& handler) {
  ilog( "add api url: ${c}", ("c",url) );
  //注册api函数,可以参看asio的pos示例
  app().get_io_service().post([=](){
  my->url_handlers.insert(std::make_pair(url,handler));
});
}

由上面的函数我们得知,对url(例如/push_transactions)的请求通过注册的机制放入asio中。
我们来看一下处理http请求的函数的关键代码,片段如下:

auto handler_itr = url_handlers.find( resource );
if( handler_itr != url_handlers.end()) {
con->defer_http_response();
  //这里将数据传递给了api相关的函数
  handler_itr->second( resource, body, [con]( auto code, auto&& body ) {
  con->set_body( std::move( body ));
  con->set_status( websocketpp::http::status_code::value( code ));
  con->send_http_response();
} );
}

小结 :由上面的代码分析,我们基本清楚了一个请求过来是如何关联到具体的api函数的,下面
我们来看一下如何实现合约的部署。 我们先回到如下的代码片段,看具体处理函数是如何运行的

CHAIN_RW_CALL_ASYNC(push_transaction, \
  chain_apis::read_write::push_transaction_results, 202),

将对应的宏进行展开如下:

#define CHAIN_RW_CALL_ASYNC(call_name, call_result, http_response_code) \
  CALL_ASYNC(chain, rw_api, chain_apis::read_write, call_name, call_result, http_response_code)

继续进行展开为如下的lamda表达式函数。

#define CALL_ASYNC(api_name, api_handle, api_namespace, call_name, call_result, \
  http_response_code) \
{std::string("/v1/" #api_name "/" #call_name), \
   [this, api_handle](string, string body, url_response_callback cb) \
   mutable { \
     if (body.empty()) body = "{}"; \
     api_handle.call_name(\
       fc::json::from_string(body).as<api_namespace::call_name ## _ params>(),\
         [cb, body](const fc::static_variant<fc::exception_ptr, call_result>& result){\
            if (result.contains<fc::exception_ptr>()) {\
               try {\
                  result.get<fc::exception_ptr>()->dynamic_rethrow_exception();\
               } catch (...) {\
                  http_plugin::handle_exception(#api_name, #call_name, body, cb);\
               }\
            } else {\
               cb(http_response_code, result.visit(async_result_visitor()));\
            }\
         });\
   }\
}

由上述的关键代码,我们对应得到具体处理的函数cb为函数rw_api,下面我们来看一下chain_pulgin
下的rw_api的具体实现,由如下的代码片段我们得知关键处理的类为read_write

chain_apis::read_write chain_plugin::get_read_write_api() {
   return chain_apis::read_write(chain());
}

我们现在看一下类chain_apis::read_write中push_transaction的具体实现。

void read_write::push_transaction(const read_write::push_transaction_params& params,
   next_function<read_write::push_transaction_results> next) {
   try {
      .............
      .............
      //关键处理在get_method方法所获得的具体的处理函数
      app().get_method<incoming::methods::transaction_async>()(
        pretty_input,//输入数据
        true,
        [this, next](const fc::static_variant<fc::exception_ptr,
          transaction_trace_ptr>& result) -> void{
         if (result.contains<fc::exception_ptr>()) {
           //执行函数next
            next(result.get<fc::exception_ptr>());
         } else {
            auto trx_trace_ptr = result.get<transaction_trace_ptr>();
            try {
               fc::variant pretty_output;
               pretty_output = db.to_variant_with_abi(*trx_trace_ptr);
               chain::transaction_id_type id = trx_trace_ptr->id;
               //执行next函数
               next(read_write::push_transaction_results{id, pretty_output});
            } CATCH_AND_CALL(next);
         }
      });
}}

我们来看一下incoming::methods::transaction_async对应的具体的处理函数:

namespace methods {
 // synchronously push a block/trx to a single provider
 using block_sync            = method_decl<chain_plugin_interface,
 void(const signed_block_ptr&), first_provider_policy>;
 using transaction_async     = method_decl<chain_plugin_interface,
 void(const packed_transaction_ptr&, bool, next_function<transaction_trace_ptr>),
 first_provider_policy>;
}

这里重点关注method_decl(声明在libraries/appbase/include/appbase/method.h)下。 其原型为:

//@tparam Tag - API specific discriminator used to distinguish between otherwise
// identical method signatures
//@tparam FunctionSig - the signature of the method
//@tparam DispatchPolicy - dispatch policy that dictates how providers
//for a method are accessed defaults to @ref first_success_policy
template< typename Tag, typename FunctionSig,
          template <typename> class DispatchPolicy = first_success_policy>
struct method_decl {
  using method_type = method<FunctionSig, DispatchPolicy<FunctionSig>>;
  using tag_type = Tag;
};

最后我们回到开始的get_method来看下其具体做了
什么,参看文件为eos/libraries/appbase/include/appbase/application.hpp

//fetch a reference to the method declared by the passed in type.  This will
//construct the method on first access.  This allows loose and deferred
//binding between plugins
//@tparam MethodDecl - @ref appbase::method_decl
//@return reference to the method described by the declaration
 
template<typename MethodDecl>
auto get_method() ->
std::enable_if_t<is_method_decl<MethodDecl>::value, typename MethodDecl::method_type&>
{
  //我们展开后得到method_type的类型为incoming::methods::method_decl::
  //method<void(const packed_transaction_ptr&, bool, next_function<transaction_trace_ptr>),
  //first_provider_policy>, 这个类型看似比较复杂,但是抓住关键就是函数签名
  //void(const packed_transaction_ptr&, bool , next_function<transaction_trace_ptr>)
  //
  using method_type = typename MethodDecl::method_type;
  auto key = std::type_index(typeid(MethodDecl));
  auto itr = methods.find(key);
  if (itr != methods.end()) {
    //这里我们得到了具体的函数,那么下一步就是看函数如何运行的。
    return * method_type::get_method(itr->second);
  } else {
    methods.emplace(std::make_pair(key, method_type::make_unique()));
    return  * method_type::get_method(methods.at(key));
  }
}

通过上面的代码我们可以得出结论关键的transaction处理函数原型如下:
void(const packed_transaction_ptr&, bool , next_function)
按图索骥,于是我们找到了producer_plugin.cpp的实现,在函数plugin_initialize的结尾处我们
看到如下的代码:

my->_incoming_transaction_async_provider = app().
  get_method<incoming::methods::transaction_async>().register_provider([this](
    //注意此处的函数的签名
    const packed_transaction_ptr& trx,
    bool persist_until_expired,
    next_function<transaction_trace_ptr> next) -> void {
      return my->on_incoming_transaction_async(trx, persist_until_expired, next );
});

这里是将具体处理的函数进行注册到具体的关联的type上,那么下一步我们就着重分析函数:
on_incoming_transaction_async就可以了。其实现在文件producer_plugin.cpp中。其中
关键函数代码为:

auto send_response = [this, &trx, &next](const fc::static_variant<fc::exception_ptr,
  transaction_trace_ptr>& response) {
  next(response);
  if (response.contains<fc::exception_ptr>()) {
    _ transaction_ack_channel.publish(std::pair<fc::exception_ptr, packed_transaction_ptr>
      (response.get<fc::exception_ptr>(), trx));
  } else {
    //将数据发入到channel中,具体的订阅者将会进行处理。
    _ transaction_ack_channel.publish(std::pair<fc::exception_ptr, packed_transaction_ptr>
      (nullptr, trx));
  }
};

上面的transaction_ack_channel由net_plugin进行订阅,这点我们可以理解,主要是发送ack返回消息。 
进一步在函数on_incoming_transaction_async中如下代码对transaction进行了处理:

//调用chain的push_transaction来处理transaction,

auto trace = chain.push_transaction(std::make_shared<transaction_metadata>(*trx), deadline);
if (trace->except) {
  if (failure_is_subjective(*trace->except, deadline_is_subjective)) {
    _ pending_incoming_transactions.emplace_back(trx, persist_until_expired, next);
  } else {
    auto e_ptr = trace->except->dynamic_copy_exception();
    send_response(e_ptr);
  }
}

跟踪代码最后我们知道chain的类型为eosio::chain::controller,具体见文件:
eos\libraries\chain\include\eosio\chain\controller.h,代码如下:

transaction_trace_ptr controller::push_transaction(const transaction_metadata_ptr& trx,
  fc::time_point deadline,
  uint32_t billed_cpu_time_us ) {
  //其中my的类型为controller_impl,
  return my->push_transaction(trx, deadline, false, billed_cpu_time_us);
}

最后我们看一下controller_impl中的具体是如何实现push_transaction的,关键的代码如下,注意
其中的具体的注释:

transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx,
                                        fc::time_point deadline,
                                        bool implicit,
                                        uint32_t billed_cpu_time_us)
{
  try {
    //首先生成transaction的context上下文,    
    transaction_context trx_context(self, trx->trx, trx->id);
    trx_context.deadline = deadline;
    trx_context.billed_cpu_time_us = billed_cpu_time_us;
    trace = trx_context.trace;
    try {
      ......
      //检查actor是否有在黑名单中的
      if (trx_context.can_subjectively_fail &&
          pending->_block_status == controller::block_status::incomplete ) {
        check_actor_list( trx_context.bill_to_accounts );
      }
      //进行权限检查
      trx_context.delay = fc::seconds(trx->trx.delay_sec);
      if(!self.skip_auth_check() && !implicit ) {
        authorization.check_authorization(
        trx->trx.actions,
        trx->recover_keys( chain_id ),
        {},
        trx_context.delay,
        [](){}
        false
        );
      }
      //执行transaction 这里是关键的步骤,这里将涉及到具体的transaction是如何继续
      //往下走的
      trx_context.exec();
      trx_context.finalize();
      emit(self.applied_transaction, trace);
 
      trx_context.squash();
      restore.cancel();
      .....
      //此处省略和本次介绍无关的代码
   } catch (const fc::exception& e) {
      trace->except = e;
      trace->except_ptr = std::current_exception();
   }
   return trace;
  } FC_CAPTURE_AND_RETHROW((trace))
}

我们先重点看下transaction_context的exec函数是如何实现的,关键代码片段如下,从中我们可以
看到action的相关延迟处理逻辑,以及对是否是上下文无关的处理。

if( apply_context_free ) {
  for( const auto& act : trx.context_free_actions ) {
    trace->action_traces.emplace_back();
    dispatch_action( trace->action_traces.back(), act, true );
  }
}
//有延迟的函数被认定为上下文相关
if( delay == fc::microseconds() ) {
  for( const auto& act : trx.actions ) {
    trace->action_traces.emplace_back();
    dispatch_action( trace->action_traces.back(), act );
  }
} else {
  schedule_transaction();
}

我们看下dispacth是如何进行分发action的,这里需要注意,我们上一步关注的实体还是transaction,
这里已经细化到action,后面将看到具体是如何处理的,例如inline action的处理。

void transaction_context::dispatch_action(
  action_trace& trace,//trance跟踪
  const action& a, //传入的action
  account_name receiver, //account 其值为a.account
  bool context_free, //是否上下文无关
  uint32_t recurse_depth ) { //递归的层数,这里用于inline action的递归调用
   //生成apply_context对象
   apply_context  acontext( control, * this, a, recurse_depth );
   acontext.context_free = context_free;
   acontext.receiver     = receiver;//这里设置了账户,来自action
   try {
      acontext.exec();//关键函数,执行apply操作
   } catch( ... ) {
      trace = move(acontext.trace);
      throw;
   }
   trace = move(acontext.trace);
}

我们来看一下apply_context的exec的具体执行流程,如下代码所示。我们可以看到其中递归的调用
但是在递归调用之前调用了exec_one函数,这个最终的关键:

_notified.push_back(receiver);
trace = exec_one();//关键函数
.......//此处省略非重要代码
for ( const auto& inline_action : _ cfa_inline_actions ) {
  trace.inline_traces.emplace_back();
  trx_context.dispatch_action( trace.inline_traces.back(),
    inline_action,
    inline_action.account,
    true, recurse_depth + 1 );
}
//还是上下文无关的分开处理
for ( const auto& inline_action : _ inline_actions ) {
  trace.inline_traces.emplace_back();
  trx_context.dispatch_action( trace.inline_traces.back(),
  inline_action,
  inline_action.account,
  false, recurse_depth + 1 );
}

我们继续展开最后的函数exec_one函数,其涉及到智能合约的关键代码片段如下:

const auto &a = control.get_account(receiver);
  privileged = a.privileged;
  //这里有两种不同的过程要进行处理,分别是native的和传入的,
  //更加准确的是系统的和智能合约的两种不同的形式
  auto native = control.find_apply_handler(receiver, act.account, act.name);
  if( native ) {
     if( trx_context.can_subjectively_fail && control.is_producing_block() ) {
        control.check_contract_list( receiver );
        control.check_action_list( act.account, act.name );
     }
     (* native)(* this);
  }
  //如果说其code大于0,并且账户费系统并且非setcode则执行
  if( a.code.size() > 0 &&
    !(act.account == config::system_account_name &&
      act.name == N(setcode) && receiver == config::system_account_name) )
  {
     if( trx_context.can_subjectively_fail && control.is_producing_block() ) {
        control.check_contract_list( receiver );
        control.check_action_list( act.account, act.name );
     }
     try {
        //最后执行的apply函数
        //调用具体的apply函数进行执行
        control.get_wasm_interface().apply(a.code_version, a.code, *this);
     } catch ( const wasm_exit& ){}
  }

这里我们先分析find_apply_handler的过程,首先我们先找到具体的handler注册的机制
具体见文件eosio\eos\libraries\chain\controller.cpp

void set_apply_handler( account_name receiver, account_name contract,
  action_name action, apply_handler v ) {
  //具体实现为一个map数据结构
  apply_handlers[receiver][make_pair(contract,action)] = v;
}

在controller_impl的初始化函数中,我们看到如下代码片段,到此我们看到了我们的set contract

实际上是调用了系统的一个预设的合约或者说是函数。


#define SET_APP_HANDLER( receiver, contract, action) \
   set_apply_handler( #receiver, #contract, #action, \
     &BOOST_PP_CAT(apply_, BOOST_PP_CAT(contract, BOOST_PP_CAT(_ ,action) ) ) )
 
   SET_APP_HANDLER( eosio, eosio, newaccount );
   SET_APP_HANDLER( eosio, eosio, setcode );
   SET_APP_HANDLER( eosio, eosio, setabi );
   SET_APP_HANDLER( eosio, eosio, updateauth );
   SET_APP_HANDLER( eosio, eosio, deleteauth );
   SET_APP_HANDLER( eosio, eosio, linkauth );
   SET_APP_HANDLER( eosio, eosio, unlinkauth );

这里我们看apply_contract_action的具体实现,代码在eosio\eos\libraries\chain\eosio_contract.cpp
中,这个文件定义了系统的contract的具体实现,关键代码片段如下:

 
auto& db = context.db;
//这里set_code的具体格式如下
//struct setcode {
//   account_name     account;
//   uint8_t          vmtype = 0;
//   uint8_t          vmversion = 0;
//   bytes            code;
//}
auto  act = context.act.data_as<setcode>();
context.require_authorization(act.account);
fc::sha256 code_id; /// default ID == 0
if( act.code.size() > 0 ) {
   //计算具体的code_id
   code_id = fc::sha256::hash( act.code.data(), (uint32_t)act.code.size() );
   wasm_interface::validate(context.control, act.code);
}
const auto& account = db.get<account_object,by_name>(act.account);
int64_t code_size = (int64_t)act.code.size();
int64_t old_size  = (int64_t)account.code.size() * config::setcode_ram_bytes_multiplier;
int64_t new_size  = code_size * config::setcode_ram_bytes_multiplier;
//检查前后的code的版本
FC_ASSERT( account.code_version != code_id,
  "contract is already running this version of code" );
//将code更新到db中
db.modify( account, [&]( auto& a ) {
  a.last_code_update = context.control.pending_block_time();
  a.code_version = code_id;
  a.code.resize( code_size );
  if( code_size > 0 )
     memcpy(a.code.data(), act.code.data(), code_size );
 
  });
}

小结 最后我们看到了我们的合约代码被更新到对应的account中去,也就是智能合约账户中去。

EOS虚拟机服务端合约的调用执行

在EOS虚拟机核心接口一章中我们了解到调用虚拟机执行智能合约的接口函数为apply,通过上面
的分析我们得知在执行action的时候我们发现在exec_one中有如下代码片段:

try {
   //最后执行的apply函数
   //调用具体的apply函数进行执行
   control.get_wasm_interface().apply(a.code_version, a.code, *this);
} catch ( const wasm_exit& ){}

这样我们就能整体的把握了具体的流程,代替顺序如下:

  • transaction分发到nodeos
  • nodeos验证transaction然后进行执行

由于transaction是由action组成的,所以最终落到具体的action上
在执行(exec_one)中调用apply的接口将具体的合约传递到虚拟机去执行 我们现在看一下wasm_interface的apply的实现:

void wasm_interface::apply( const digest_type& code_id,
const shared_string& code,
apply_context& context ) {
my->get_instantiated_module(code_id, code, context.trx_context)->apply(context);
}

其中get_instantiated_module的实现在wasm_interface_private.hpp中,具体如下:

auto it = instantiation_cache.find(code_id);
//如果内部没有该智能合约的缓存则进行创建
if(it == instantiation_cache.end()) {
auto timer_pause = fc::make_scoped_exit([&](){
  trx_context.resume_billing_timer();
});
trx_context.pause_billing_timer();
IR::Module module;
try {
    //加载wasm二进制序列化对象
    Serialization::MemoryInputStream stream((const U8*)code.data(), code.size());
    WASM::serialize(stream, module);
    module.userSections.clear();
 } catch(const Serialization::FatalSerializationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 } catch(const IR::ValidationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 }
 //执行相关的注入代码
 wasm_injections::wasm_binary_injection injector(module);
 injector.inject();
 
 std::vector<U8> bytes;
 try {
    Serialization::ArrayOutputStream outstream;
    WASM::serialize(outstream, module);
    bytes = outstream.getBytes();
 } catch(const Serialization::FatalSerializationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 } catch(const IR::ValidationException& e) {
    EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
 }
 //生成新的wasm_instantiated_module_interface对象插入到map中去。
 it = instantiation_cache.emplace(code_id,
    runtime_interface->instantiate_module((const char*)bytes.data(),
     bytes.size(), parse_initial_memory(module))).first;
}
return it->second;
}

小结 通过以上代码我们得知最后我们获得的是一个wasm_instantiated_module_interface的对象,
然后调用apply函数来实现最后的作用,至此虚拟机应用层分析告一段落。上面代码具体涉及到的
WASM-JIT范畴的内容,下一章将继续详细介绍。

EOS虚拟机Module IR生成

由上文得知函数get_instantiated_module会从传入的二进制字节码生成一个Module实例,下面我们就
具体分析一下其是如何进行解析生成的。现分析get_instantiated_module(wasm_interface_private.hpp)
中。首先看如下代码片段:

IR::Module module;
try {
    Serialization::MemoryInputStream stream((const U8*)code.data(), code.size());
    WASM::serialize(stream, module); //该步骤完成从stream到module的转化
    module.userSections.clear();
 }

函数WASM::serialize的函数原型如下,这里先生成中间语言IR下的Module,然后再进行校验。

void serialize(Serialization::InputStream& stream,Module& module)
{
  //函数serializeModule有两个重载,在于一个参数是InputStream
  //而另外一个是OutputStream
  serializeModule(stream,module);
  IR::validateDefinitions(module);
}

现在看关键函数serializeModule的实现, 以下的为关键代码片段。具体的参见文件:
eos\libraries\wasm-jit\Source\WASM\WASMSerializatin.cpp

//首先读取WASM文件头部的MagicNumber和版本号
serializeConstant(moduleStream,"magic number",U32(magicNumber));
serializeConstant(moduleStream,"version",U32(currentVersion));
SectionType lastKnownSectionType = SectionType::unknown;
while(moduleStream.capacity())
{
  const SectionType sectionType = *(SectionType*)moduleStream.peek(sizeof(SectionType));
  if(sectionType != SectionType::user)
  {
    //这里要求解析的的section的顺序需要和已知的顺序一致,具体的顺序可以参考
    //类型SectionType的定义
    if(sectionType > lastKnownSectionType) { lastKnownSectionType = sectionType; }
    else { throw FatalSerializationException("incorrect order for known section"); }
  }
  switch(sectionType)
  {
    //如果解析的字节对应的类型是type,那么调用反序列化接口
    case SectionType::type: serializeTypeSection(moduleStream,module); break;
    case SectionType::import: serializeImportSection(moduleStream,module); break;
    ........
    case SectionType::user:
    {2
          UserSection& userSection = * module.userSections.insert(
        module.userSections.end(),UserSection());
            serialize(moduleStream,userSection);
            break;
         }
         default: throw FatalSerializationException("unknown section ID");
    }
  ;
}

由上面函数的的代码片段我们得知处理的整体思路是按照已知的SectionType的类型依次向下进行进行
这里我们举例分析serializeTypeSection,如下面的WASM的二进制格式我们得知,首先我们获得一个字节
的type标识,然后一个字节是这个块的大小,目前为00,最后是这个块里面有多少个这样的type的描述。
即其中num types对应的行。

0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                        ; section code
0000009: 00                                        ; section size (guess)
000000a: 01                                        ; num types
; type 0
000000b: 60                                        ; func
000000c: 00                                        ; num params
000000d: 01                                        ; num results
000000e: 7f                                        ; i32
0000009: 05                                        ; FIXUP section size
; section "Function" (3)
000000f: 03                                        ; section code

我们现在看这里我们举例分析serializeTypeSection是如何处理的


serializeSection(moduleStream,SectionType::type,[&module](Stream& sectionStream)
{
  //函数serializeArray用来处理数组形式的type
  serializeArray(sectionStream,module.types,[](Stream& stream,
    const FunctionType*& functionType)
    {
      serializeConstant(stream,"function type tag",U8(0x60));
      if(Stream::isInput)
      {
          std::vector<ValueType> parameterTypes;
          ResultType returnType;
          serialize(stream,parameterTypes);
          serialize(stream,returnType);
      //根据参数列表和返回值列表生成函数的类型是
          functionType = FunctionType::get(returnType,parameterTypes);
      }
      else
      {
          serialize(stream,const_cast<std::vector<ValueType>&>(functionType->parameters));
          serialize(stream,const_cast<ResultType&>(functionType->ret));
      }
    });
});

这里用来lamda表达式我们先看serializeArray函数的函数原型,如下:

template<typename Stream,typename Element,typename Allocator,typename SerializeElement>
void serializeArray(Stream& stream,std::vector<Element,Allocator>& vector,
  SerializeElement serializeElement)
{
//此处省略非重要的代码
for(Uptr index = 0;index < size;++index)
{
    vector.push_back(Element());
  //以下函数代码将调用匿名的lamda函数,
    serializeElement(stream,vector.back());
}
vector.shrink_to_fit()
}

通过上述的代码我们得知serializeElement调用了匿名lamda函数,回到函数serializeSection中我们
得知最后module.types将存储types的函数描述vector容器。
小结 通过上面的代码描述我们知道从一段字节码最后转换为Module对象,Module对象对后续的执行
有很大的帮助。
我们来看一下Module对象中的具体数据结构。

struct Module
{
    std::vector<const FunctionType*> types;
 
    IndexSpace<FunctionDef,IndexedFunctionType> functions;
    IndexSpace<TableDef,TableType> tables;
    IndexSpace<MemoryDef,MemoryType> memories;
    IndexSpace<GlobalDef,GlobalType> globals;
 
    std::vector<Export> exports;
    std::vector<DataSegment> dataSegments;
    std::vector<TableSegment> tableSegments;
    std::vector<UserSection> userSections;
 
    Uptr startFunctionIndex;
 
    Module() : startFunctionIndex(UINTPTR_MAX) {}
};

从上面的的代码我们得知一个Module的具体内部结构,但是我们还没有能进入IR层面。
现在我们回到函数void serialize(Serialization::InputStream& stream,Module& module)
在执行完成serializeModule后将执行IR::validateDefinitions(module);,我们来看下
具体的关键实现,具体见文件\eos\libraries\wasm-jit\Source\IR\Validate.cpp

//检查FunctionType的参数        
for(Uptr typeIndex = 0;typeIndex < module.types.size();++typeIndex)
{
  const FunctionType* functionType = module.types[typeIndex];
  for(auto parameterType : functionType->parameters) { validate(parameterType); }
  validate(functionType->ret);
}
 
.......
//本处代码为依次检查function_import memory_import table_import global_import
//function_def global_def table_def memory_def 以及export的内容
//一下的代码用来获取起始函数的函数类型
//这里函数分支只有单独运行虚拟机通过loadTextModule才会使startFunctionIndex为有效值
//我们这里不需要,因为入口函数就是apply
if(module.startFunctionIndex != UINTPTR_MAX)
{
    VALIDATE_INDEX(module.startFunctionIndex,module.functions.size());
    const FunctionType* startFunctionType = module.types[module.functions.
    getType(module.startFunctionIndex).index];
    VALIDATE_UNLESS("start function must not have any parameters or
    results: ",startFunctionType != FunctionType::get());
}
//剩下为各种segment的检查

接下来执行如下部分代码

wasm_injections::wasm_binary_injection injector(module);
injector.inject();

主要向其中注入check_time函数代码,通过add_export函数来具体实现,这里就不描述。

VirtualMachine实例化

由上一个小节的介绍我们得知调用wasm_inteface的get_instantiated_module获得一个Module
函数get_instantiated的最后代码会调用自身数据成员runtime_interface的initantiate_module
函数来生成wasm_instantiated_module_interface的相关对象。在这里有两个类继承了
wasm_instantiated_module_interface接口分别是:

  • binaryen_instantiated_module
  • wavm_instantiated_module

如下为这部分的相关类的类图

由上图以及过程中的调用关系我们得到,首先我们确认runtime的类型是wavm还是binaryen,然后
我们就能确认接口函数instatiate_module返回的wasm_instantiated_moudle_interface的
具体类型是wavm_instantiated_module还是binayen_instantiated_module。

在类controller中具体见文件\eos\libraries\chain\controller.cpp中的成员定义:

wasm_interface::vm_type  wasm_runtime = chain::config::default_wasm_runtime;

而在文件controller.hpp中:

const static eosio::chain::wasm_interface::vm_type default_wasm_runtime =
eosio::chain::wasm_interface::vm_type::binaryen;

在controller_imp的构造函数中我们可以看到wasmif成员的初始化,如下所示

controller_impl( const controller::config& cfg, controller& s  )
   :self(s),
    db( cfg.state_dir,
        cfg.read_only ? database::read_only : database::read_write,
        cfg.state_size ),
    reversible_blocks( cfg.blocks_dir/config::reversible_blocks_dir_name,
        cfg.read_only ? database::read_only : database::read_write,
        cfg.reversible_cache_size ),
    blog( cfg.blocks_dir ),
    fork_db( cfg.state_dir ),
    wasmif( cfg.wasm_runtime ),//初始化wasm虚拟机的runtime
    resource_limits( db ),

因此我们得出结论,如果不是命令行指定虚拟机的种类,这里默认为binaryen类型。所以关键部分
展开后就为binaryen_runtime的instantiate_module函数得到binaryen_instantiated_module
对象,最后调用其apply的方法。这里需要关注文件eos\libraries\chain\webassembly下的文件

binaryen.cpp
  • binaryen_instantiated_module的定义
  • wasm_instantiated_module_interface的apply接口实现
  • binaryen_runtime的instantiate_module方法实现
wavm.cpp
  • wasm_instantiated_module_interface的apply接口实现
  • wasm_runtime接的instantiate_module方法实现

两种不同的解释器底层,在instantiated_module上不同毕竟一个用的是LLVM的JIT一个是用的
是Binaryen的解释器。

Binaryen底层解释器

ModuleInstance的创建
首先我们看一下binaryen_runtime的instantiate_module方法是如何生成binaryed_instatiated_module
的,具体相关代码如下:

//首先创建WasmBinaryBuilder对象,这里需要注意下,类WasnBinaryBuilder的实现
//在外部编译依赖Binaryen中,文件位于external/binaryen/src/wasm-binaryen.h中
vector<char> code(code_bytes, code_bytes + code_size);
//Module类型为Binaryen中的Module类型并非项目中IR的Module类型
unique_ptr<Module> module(new Module());
WasmBinaryBuilder builder(*module, code, false);
builder.read();
//获取全局变量数值,并保存在global中
TrivialGlobalManager globals;
for (auto& global : module->globals) {
  globals[global->name] = ConstantExpressionRunner<TrivialGlobalManager>(globals).
  visit(global->init).value;
}
//间接调用表
call_indirect_table_type table;
table.resize(module->table.initial);
拷贝segment中的内容到间接调用表中去
for (auto& segment : module->table.segments) {
  Address offset = ConstantExpressionRunner<TrivialGlobalManager>(globals).
  visit(segment.offset).value.geti32();//获得该段的大小
  FC_ASSERT( uint64_t(offset) + segment.data.size() <= module->table.initial);
  for (size_t i = 0; i != segment.data.size(); ++i) {
    table[offset + i] = segment.data[i];
  }
}
//获得import的相关函数,用map数据结构去维护
import_lut_type import_lut;
import_lut.reserve(module->imports.size());
for (auto& import : module->imports) {
  std::string full_name = string(import->module.c_str()) + "." + string(import->base.c_str());
  if (import->kind == ExternalKind::Function) {
    auto& intrinsic_map = intrinsic_registrator::get_map();
    auto intrinsic_itr = intrinsic_map.find(full_name);
    if (intrinsic_itr != intrinsic_map.end()) {
       import_lut.emplace(make_pair((uintptr_t)import.get(), intrinsic_itr->second));
       continue;
    }
  }
}
//最后返回具体的instiated_module
return std::make_unique<binaryen_instantiated_module>(_ memory, initial_memory,
move(table), move(import_lut), move(module))
}

Appply接口的实现和调用
由上面的代码我们得知binaryen的类型的instantiated_module需要的参数为内存,访问表
(线性的)以及导入的对象列表。当我们拿到一个instantiated_module后,我们看一下是如何
执行apply函数的。首先我们看一下函数apply的实现,会调用call函数,而从参数里面我们知道
对于binaryen的相关内存的访问都是线性的。

void apply(apply_context& context) override {
  LiteralList args = {Literal(uint64_t(context.receiver)),
  Literal(uint64_t(context.act.account)),
  Literal(uint64_t(context.act.name))};
  call("apply", args, context);
}

下面我们详细的分析一下call函数具体执行了哪些操作

void call(const string& entry_point, LiteralList& args, apply_context& context){
  const unsigned initial_memory_size = _ module->memory.initial * Memory::kPageSize;
  //声明一个解释器接口,传入的参数关键的为导入的对象map即_import_lut
  interpreter_interface local_interface(_ shared_linear_memory, _ table, _ import_lut,
    initial_memory_size, context);
  //初始化内存和数据
  //zero out the initial pages
  memset(_ shared_linear_memory.data, 0, initial_memory_size);
  //copy back in the initial data
  memcpy(_ shared_linear_memory.data, _ initial_memory.data(), _ initial_memory.size());
 
  //生成module instance,这里的初始化会调用start function
  ModuleInstance instance(* _ module.get(), &local_interface);
  //调用具体执行的函数
  instance.callExport(Name(entry_point), args);
}

如下两个类型需要详细的说明一下:

  • interpreter_interface
  • ModuleInstance

首先是interpreter_insterface,其类型如下

struct interpreter_interface : ModuleInstance::ExternalInterface

位于文件\eos\libraries\chain\include\eosio\chain\webassembly\binaryen.hpp
其中关键的函数为callImport和callTable,现在简要的说明一下:

Literal callImport(Import * import, LiteralList& args) override
{
  //由于import_lut中存储的就是导入的函数或者对象的基本信息,
  //则这里直接进行map的查找操作
  auto fn_iter = import_lut.find((uintptr_t)import);
  EOS_ASSERT(fn_iter != import_lut.end(), wasm_execution_error,\
   "unknown import ${m}:${n}", ("m", import->module.c_str())("n",\
    import->module.c_str()));
  return fn_iter->second(this, args);
}

这里import_lut的类型为unordered_map<uintptr_t, intrinsic_registrator::intrinsic_fn>
我们可以看到具体的导入的函数描述类型为intrinsic_registrator::intrinsic_fn>这里我们
看一下instrinsic_fn的具体类型:

struct intrinsic_registrator {
  using intrinsic_fn = Literal(*)(interpreter_interface*, LiteralList&);
  ......
}
}

参数就是一个实现了ModuleInstance::ExternalInterface的类和参数列表 下面我们看一下callTable的操作

Literal callTable(Index index, LiteralList& arguments, WasmType result,
  ModuleInstance& instance) override
{
  EOS_ASSERT(index < table.size(), wasm_execution_error, "callIndirect: bad pointer");
  //根据函数表类似于ELF中的GOT来获取函数的指针
  auto* func = instance.wasm.getFunctionOrNull(table[index]);
  EOS_ASSERT(func, wasm_execution_error, "callIndirect: uninitialized element");
  EOS_ASSERT(func->params.size() == arguments.size(), \
  wasm_execution_error, "callIndirect: bad # of arguments");
  //进行参数检查
  for (size_t i = 0; i < func->params.size(); i++) {
     EOS_ASSERT(func->params[i] == arguments[i].type,\
        wasm_execution_error, "callIndirect: bad argument type");
  }
  EOS_ASSERT(func->result == result, wasm_execution_error, "callIndirect: bad result type");
  //调用函数,这里的invoke机制就是最后程序执行的最根本依赖,下面将详细的分析一下
  return instance.callFunctionInternal(func->name, arguments);
}

从上面我们可以看到interpreter_interface封装了函数的调用,无论是外部的还是内部自己实现的
下面我们来看一下ModuleInstance的具体实现,文件位于如下的位置:
eos\externals\binaryen\src\wasm-interpreter.h
其他实现相关具体的需要看ModuleInstanceBase,在其中有很多load函数的实现,主要是加载对应
的数据类型到内存中,现在我们看一下它的构造函数的实现:

ModuleInstanceBase(Module& wasm, ExternalInterface* externalInterface) :
  wasm(wasm),
  externalInterface(externalInterface) {
  // 导入外部全局的数据
  externalInterface->importGlobals(globals, wasm);
  // 准备内存
  memorySize = wasm.memory.initial;
  // 处理内部的全局数据
  for (auto& global : wasm.globals) {
    globals[global->name] = ConstantExpressionRunner<GlobalManager>(globals).
    visit(global->init).value;
  }
  //处理外部函数接口相关内容,这里就是上面讲到的interpter_inferface  
  externalInterface->init(wasm, *self());
  //运行函数starFunction
  if (wasm.start.is()) {
    LiteralList arguments;
    callFunction(wasm.start, arguments);
  }
}
CallFunction的实现

现在我们重点看一下callFunction是如何实现的,这样对于我们理解最外层的callExport函数有一定
的帮助作用。下面我们来看下具体实现原理,如下为其代码片段,我们看到了久违的栈。

Literal callFunction(Name name, LiteralList& arguments) {
  callDepth = 0;
  functionStack.clear();//用到了栈
  return callFunctionInternal(name, arguments);
}

现在看关键函数callFunctionInternal的实现,在函数的实现中实现了两个内部类,我们先看其主要
的流程:

Literal callFunctionInternal(Name name, LiteralList& arguments) {
  if (callDepth > maxCallDepth)
    externalInterface->trap("stack limit");
  auto previousCallDepth = callDepth;
  callDepth++;
  //保留之前函数的栈信息
  auto previousFunctionStackSize = functionStack.size();
  functionStack.push_back(name); //将函数名字入栈
  //获得函数指针,这里的函数指针所指向的内容后文将有所介绍
  Function* function = wasm.getFunction(name);
  ASSERT_THROW(function);
  //FunctinScope没有具体的实际操作。基本上都是参数检查和
  //返回值检查
  FunctionScope scope(function, arguments);
  //这个类比较重要,这里涉及到了具体的执行流程控制.
  RuntimeExpressionRunner rer(* this, scope);
  Flow flow = rer.visit(function->body);
  ASSERT_THROW(!flow.breaking() || flow.breakTo == RETURN_FLOW);
  Literal ret = flow.value; //最后获得执行的结果
  if (function->result != ret.type) {
    if (rer.last_call.value.type == function->result && ret.type == 0) {
       ret = rer.last_call.value;
    }
    else {
      std::cerr << "calling " << function->name << " resulted in " << ret
      << " but the function type is " << function->result << '\n';
      WASM_UNREACHABLE();
    }
  }
  return ret;
}

下面我们重点分析一下如下代码段,这段代码段控制了这个数据流程。

RuntimeExpressionRunner rer(* this, scope);
Flow flow = rer.visit(function->body);

下面我们进入visit函数的实现,具体如下:

return Visitor<SubType, Flow>::visit(curr);

最后我们进入Vistior的具体定义实现:

struct Visitor {
  // Expression visitors
  ReturnType visitBlock(Block* curr) {}
  ReturnType visitIf(If* curr) {}
  .........
  // Module-level visitors
  ReturnType visitFunctionType(FunctionType* curr) {}
  ReturnType visitImport(Import* curr) {}
  ReturnType visitExport(Export* curr) {}
  ReturnType visitGlobal(Global* curr) {}
  ReturnType visitFunction(Function* curr) {}
  ReturnType visitTable(Table* curr) {}
  ReturnType visitMemory(Memory* curr) {}
  ReturnType visitModule(Module* curr) {}
  ///从这段代码我们可以知道主要是SubType最后会影响
  ///我们创建的Visitior的类型,并影响调用的方法
  ReturnType visit(Expression* curr) {
    ASSERT_THROW(curr);
    #define DELEGATE(CLASS_TO_VISIT) \
      return static_cast<SubType*>(this)-> \
          visit##CLASS_TO_VISIT(static_cast<CLASS_TO_VISIT*>(curr))
 
    switch (curr->_id) {
      case Expression::Id::BlockId: DELEGATE(Block);
      case Expression::Id::IfId: DELEGATE(If);
      ......
      case Expression::Id::GetGlobalId: DELEGATE(GetGlobal);
      case Expression::Id::NopId: DELEGATE(Nop);
      case Expression::Id::UnreachableId: DELEGATE(Unreachable);
      case Expression::Id::InvalidId:
      default: WASM_UNREACHABLE();
    }
 
    #undef DELEGATE
  }
};

由调用关系我们知道SubType其实最后是受Expression的类型的影响,来自function->body
我们下面看一下Expresssion的类型以及具体的实现,在这里Expression作为Function类内重要
的成员,其定义如下:

class Expression {
public:
  enum Id {
    InvalidId = 0,
    BlockId,
    IfId,
    LoopId,
    .....
    HostId,
    NopId,
    UnreachableId,
    NumExpressionIds
  };
  Id _id;
 
  WasmType type; // the type of the expression: its *output*, not necessarily its input(s)
 
  Expression(Id id) : _ id(id), type(none) {}
 
  void finalize() {}
 
  template<class T>
  bool is() {
    return int(_ id) == int(T::SpecificId);
  }

我们自然想到其中枚举的每一个类型都会有一个对应的子类,例如IfId,其对应的子类如下所示:

class If : public SpecificExpression<Expression::IfId> {
public:
  If() : ifFalse(nullptr) {}
  If(MixedArena& allocator) : If() {}
 
  Expression* condition;
  Expression* ifTrue;
  Expression* ifFalse;
 
  // set the type given you know its type, which is the case when parsing
  // s-expression or binary, as explicit types are given. the only additional work
  // this does is to set the type to unreachable in the cases that is needed.
  void finalize(WasmType type_);
 
  // set the type purely based on its contents.
  void finalize();
};

下面我们来看具体的实现

void If::finalize() {
  if (condition->type == unreachable) {
    type = unreachable;
  } else if (ifFalse) {
    if (ifTrue->type == ifFalse->type) {
      type = ifTrue->type;
    } else if (isConcreteWasmType(ifTrue->type) && ifFalse->type == unreachable) {
      type = ifTrue->type;
    } else if (isConcreteWasmType(ifFalse->type) && ifTrue->type == unreachable) {
      type = ifFalse->type;
    } else {
      type = none;
    }
  } else {
    type = none; // if without else
  }
}

上面为对Expression的类型的介绍,对于语言无论什么样的程序块,最后都会有一个类型。现在
我们以If为例看其如何生成Visitor的,Visitor中的宏展开如下:

#define DELEGATE(CLASS_TO_VISIT) \
     return static_cast<SubType*>(this)-> \
         visit##CLASS_TO_VISIT(static_cast<CLASS_TO_VISIT*>(curr))
--------------------------------------------------------------------
return static_cast<RuntimeExpressionRunner*>(this)-> \
   visitIf(static_cast<If*>(curr)) 

这里需要注意RuntimeExpressionRunner::public ExpressionRunner
虽然在RuntimeExpressionRunner中没有visitIf的实现,但是在public ExpressionRunner中已经
已经有相关具体的代码实现,这里要区分类的继承关系,具体代码片段如下:

Flow visitIf(If *curr) {
  NOTE_ENTER("If");
  Flow flow = visit(curr->condition);//先访问具体的条件
  if (flow.breaking()) return flow;
  NOTE_EVAL1(flow.value);
  if (flow.value.geti32()) {
    Flow flow = visit(curr->ifTrue);
    //处理是否跳转到else中去继续执行
    if (!flow.breaking() && !curr->ifFalse) flow.value = Literal();
    return flow;
  }
  if (curr->ifFalse) return visit(curr->ifFalse);
  return Flow();
}

目前我们会比较疑惑,那么例如a < b这种是如何处理,这个是在visitBinary上来实现的

具体代码如下:

Flow visitBinary(Binary *curr) {
  Flow flow = visit(curr->left);
  if (flow.breaking()) return flow;
  Literal left = flow.value;
  flow = visit(curr->right);
  if (flow.breaking()) return flow;
  Literal right = flow.value;
  ....
  case NeInt64:   return left.ne(right);
  case LtSInt64:  return left.ltS(right);
  case LtUInt64:  return left.ltU(right);
  case LeSInt64:  return left.leS(right);
  case LeUInt64:  return left.leU(right);
  case GtSInt64:  return left.gtS(right);
  case GtUInt64:  return left.gtU(right);
  case GeSInt64:  return left.geS(right);
  case GeUInt64:  return left.geU(right);
}

当然例如加法,减法,位操作等均有具体的case进行处理。
小结 根据上面的描述我们知道了如何从最上层面的函数,到最后的表达计算是如何实现。由于wast是
表达式形式,这里是一种自顶向向下的计算方式。至此,我们从根本上了解了函数的执行流程。

WAVM底层解释器

ModuleInstance的生成

WAVM的底层的实现不同于Binaryen,具体的Module的实现在如下的文件中:
eos\libraries\chain\webassembly\wavm.cpp.我们先看一下是如何生成具体的ModuleInstance的
具体代码如下:

std::unique_ptr<Module> module = std::make_unique<Module>();
try {
  Serialization::MemoryInputStream stream((const U8*)code_bytes, code_size);
  WASM::serialize(stream, *module);
} catch(const Serialization::FatalSerializationException& e) {
  EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
} catch(const IR::ValidationException& e) {
  EOS_ASSERT(false, wasm_serialization_error, e.message.c_str());
}
//上面的代码和Binaryen的没有具体的区别,
eosio::chain::webassembly::common::root_resolver resolver;
//用来解决导入的外部函数符号的问题
LinkResult link_result = linkModule(*module, resolver);
ModuleInstance *instance = instantiateModule(*module,
  std::move(link_result.resolvedImports));
FC_ASSERT(instance != nullptr);
return std::make_unique<wavm_instantiated_module>(instance,
  std::move(module), initial_memory);

这里我们要格外的注意如下代码片段,这段代码内部会调用LLVMJIT的compile方法,把模
块编译
成本地可以执行的代码。

ModuleInstance *instance = instantiateModule(*module,
  std::move(link_result.resolvedImports));

函数内部将执行如下代码:

LLVMJIT::instantiateModule(module,moduleInstance);
///这段代码展开如下:
--------------------------------------------------
auto llvmModule = emitModule(module,moduleInstance);
// Construct the JIT compilation pipeline for this module.
auto jitModule = new JITModule(moduleInstance);
moduleInstance->jitModule = jitModule;
// Compile the module.
jitModule->compile(llvmModule);
Apply接口实现和调用

我们具体看下apply函数的代码实现,在实现上他不同用户Binaryen的方式,首先获取函数运行
的指针,然后初始化相关需要使用的内存,最后调用Invoke来运行函数。

FunctionInstance* call = asFunctionNullable(getInstanceExport(_ instance,entry_point));
if( !call )
  return;
MemoryInstance* default_mem = getDefaultMemory(_ instance);
if(default_mem) {
   resetMemory(default_mem, _ module->memories.defs[0].type);
 
   char* memstart = &memoryRef<char>(getDefaultMemory(_ instance), 0);
   memcpy(memstart, _ initial_memory.data(), _ initial_memory.size());
}
 
the_running_instance_context.memory = default_mem;
the_running_instance_context.apply_ctx = &context;
 
resetGlobalInstances(_ instance);
runInstanceStartFunc(_ instance);
Runtime::invokeFunction(call,args);
}

有上main的代码我们得知,其核心的为invokeFunction实现,其实现如何将中间代码进行运行。

InvokeFunction的实现
我们现在分析一下InvokeFunction函数的实现,并从中我们看一下WAVM是如何实现代码运行的

Result invokeFunction(FunctionInstance* function,const std::vector<Value>& parameters)
{
const FunctionType* functionType = function->type;
//进行简单的参数检查
if(parameters.size() != functionType->parameters.size())
{
   throw Exception {Exception::Cause::invokeSignatureMismatch};
}
//为函数的返回值和参数申请对应的内存
U64* thunkMemory = (U64*)alloca((functionType->parameters.size() +
getArity(functionType->ret)) * sizeof(U64));
//检查函数的参数类型
for(Uptr parameterIndex = 0;parameterIndex < functionType->parameters.size();
  ++parameterIndex)
{
    if(functionType->parameters[parameterIndex] != parameters[parameterIndex].type)
    {
        throw Exception {Exception::Cause::invokeSignatureMismatch};
    }
    thunkMemory[parameterIndex] = parameters[parameterIndex].i64;
}
//获得函数可以执行的指针,这里将用到LLVM相关的IR技术,后面进行详细介绍
LLVMJIT::InvokeFunctionPointer invokeFunctionPointer =
LLVMJIT::getInvokeThunk(functionType);
Result result;
Platform::HardwareTrapType trapType;
Platform::CallStack trapCallStack;
Uptr trapOperand;
trapType = Platform::catchHardwareTraps(trapCallStack,trapOperand,
    [&]
    {
        //调用函数,注意这里的invokeFunctionPointer已经为LLVM可以运行
    //的函数指针
        (* invokeFunctionPointer)(function->nativeFunction,thunkMemory);
    //获得具体的返回值
    if(functionType->ret != ResultType::none)
        {
            result.type = functionType->ret;
            result.i64 = thunkMemory[functionType->parameters.size()];
        }
    });
}

由上面的代码可以知道其关键的流程是如何从Module描述的代码中得到对应的可以执行的函数
代码段,我们先在仔细的分析一下其具体实现,可以先参考LLVM官网的实例,这样会更好的理解
如下函数的具体实现:

//cache 重用已经解析过的函数
auto mapIt = invokeThunkTypeToSymbolMap.find(functionType);
if(mapIt != invokeThunkTypeToSymbolMap.end()) {
  return reinterpret_cast<InvokeFunctionPointer>(mapIt->second->baseAddress);
}
//--------------------------------------------------------------------------
//按照LLVM-JIT的要求现,先声称LLVM的Module对象
auto llvmModule = new llvm::Module("",context);
auto llvmFunctionType = llvm::FunctionType::get(
    llvmVoidType,
    {asLLVMType(functionType)->getPointerTo(),llvmI64Type->getPointerTo()},
    false);
//--------------------------------------------------------------------------
//创建function对象,这里会根据FunctionType中的parameter和return的类型来创建对应
//的函数原型
auto llvmFunction = llvm::Function::Create(
  llvmFunctionType,
  llvm::Function::ExternalLinkage,
  "invokeThunk",
  llvmModule);
auto argIt = llvmFunction->args().begin();
llvm::Value* functionPointer = &*argIt++;
llvm::Value* argBaseAddress = &*argIt;
//--------------------------------------------------------------------------
//接着我们创建function的下一个层次,即为block的结构
auto entryBlock = llvm::BasicBlock::Create(context,"entry",llvmFunction);
llvm::IRBuilder<> irBuilder(entryBlock);
//加载函数的参数,
td::vector<llvm::Value*> structArgLoads;
for(Uptr parameterIndex = 0;parameterIndex < functionType->parameters.size();
++parameterIndex)
{
    structArgLoads.push_back(irBuilder.CreateLoad(
        irBuilder.CreatePointerCast(
            irBuilder.CreateInBoundsGEP(argBaseAddress,{emitLiteral((Uptr)parameterIndex)}),
            asLLVMType(functionType->parameters[parameterIndex])->getPointerTo()
            )
        ));
}
//调用irBuilder创建本地可以执行的函数指针
auto returnValue = irBuilder.CreateCall(functionPointer,structArgLoads);
 
// 如果有返回值,则创建存储,并存储返回值.
if(functionType->ret != ResultType::none)
{
    auto llvmResultType = asLLVMType(functionType->ret);
    irBuilder.CreateStore(
        returnValue,
        irBuilder.CreatePointerCast(
            irBuilder.CreateInBoundsGEP(argBaseAddress,
        {emitLiteral((Uptr)functionType->parameters.size())}),
            llvmResultType->getPointerTo()
            )
        );
}
 
irBuilder.CreateRetVoid();
 
//连接调用的函数
auto jitUnit = new JITInvokeThunkUnit(functionType);
jitUnit->compile(llvmModule);
//最后返回结果,只要将native可以执行的代码传递给这个指针函数就可以运行
return reinterpret_cast<InvokeFunctionPointer>(jitUnit->symbol->baseAddress);

由上面的代码我们得知,LLVMJIT的方式主要还是依赖于LLVM将函数代码编译成本地代码,而上层
需要我们创建Module->Function->Block的这种关系,和对应的参数。

总结

本文深入的分析了EOS虚拟的实现所涉及到的相关技术,从上层到底层,从客户端到服务端分析了
EOS中智能合约是如何运行和落地的,希望对你有所帮助。

转载自:https://blog.csdn.net/SunnyWed/article/details/81078078