EOS源码解析 使用多线程从签名生成对应公钥

昨天早上,EOS 1.5.0 release 版本发布了。这次比较大改动点是在多线程签名上面。它将同步区块时的 block 签名验证和 trx 签名验证都使用多线程签名验证,来节省同步所需要的时间, 但是生产区块所需要的成本是不变的,但为什么生产区块成本不变呢。接下来介绍一下具体的改动。

 区块多线程签名改动:同步区块时进行多线程签名, replay 过程中依然是单线程签名。因为区块同步时需要回滚 pending block 的 trx 操作, 这块时间刚好可以用来并行处理签名, 但 replay 的时候没有这一步,即使用多线程签名也无法节省时间,反而会让主线程阻塞等待异步结果返回。

 trx 多线程签名改动:同步区块以及 replay 过程都会进行多线程签名, 因为有多个 trx 要执行,所以执行 trx 的时间可以供其他 trx 的签名并行进行。 但生产区块的时候无法使用,因为执行 BP 接受到一个 广播的 trx 就立马去执行了,执行完之后才回去接受下一个广播 trx, 所以无法使用多线程签名。

代码解析:

块签名:

因为 replay 不适用多线程签名, 所以 replay 依旧沿用之前的签名代码, 而同步则使用了新的部分。

// producer_plugin.cpp 接受到广播块
void on_incoming_block(const signed_block_ptr& block) {
   // ...

   // start processing of block
   // 调用一个线程去对块进行签名验证
   auto bsf = chain.create_block_state_future( block );

   // abort the pending block
   // 回滚掉 pending block 的执行 trx, 这段时间刚好可以用来并发执行区块签名验证
   chain.abort_block();

   // ...
}

// controller.cpp
std::future<block_state_ptr> create_block_state_future( const signed_block_ptr& b ) {

   //验证区块是否存在。
   EOS_ASSERT( b, block_validate_exception, "null block" );

   auto id = b->id();

   // no reason for a block_state if fork_db already knows about block
   auto existing = fork_db.get_block( id );
   EOS_ASSERT( !existing, fork_database_exception, "we already know about this block: ${id}", ("id", id) );

   auto prev = fork_db.get_block( b->previous );
   EOS_ASSERT( prev, unlinkable_block_exception, "unlinkable block ${id}", ("id", id)("previous", b->previous) );

   // 进行多线程签名
   return async_thread_pool( [b, prev]() {
      const bool skip_validate_signee = false;
      return std::make_shared<block_state>( *prev, move( b ), skip_validate_signee );
   } );
}

void push_block( std::future<block_state_ptr>& block_state_future ) {
   controller::block_status s = controller::block_status::complete;
   EOS_ASSERT(!pending, block_validate_exception, "it is not valid to push a block when there is a pending block");

   auto reset_prod_light_validation = fc::make_scoped_exit([old_value=trusted_producer_light_validation, this]() {
      trusted_producer_light_validation = old_value;
   });
   try {
      // 获取验证结果, 当区块验证失败时会抛出异常,中止 push block
      block_state_ptr new_header_state = block_state_future.get();
      auto& b = new_header_state->block;
      emit( self.pre_accepted_block, b );

      fork_db.add( new_header_state, false );

      if (conf.trusted_producers.count(b->producer)) {
         trusted_producer_light_validation = true;
      };
      emit( self.accepted_block_header, new_header_state );

      if ( read_mode != db_read_mode::IRREVERSIBLE ) {
         maybe_switch_forks( s );
      }

   } FC_LOG_AND_RETHROW( )
}

交易签名

从改动得知,apply_block 的时候才会启动交易的多线程验证签名,而 bcast_transaction 则不会,因为并没有多余的动作可以与验证签名并行。

void apply_block( const signed_block_ptr& b, controller::block_status s ) { try {
   try {
      EOS_ASSERT( b->block_extensions.size() == 0, block_validate_exception, "no supported extensions" );
      auto producer_block_id = b->id();
      start_block( b->timestamp, b->confirmed, s , producer_block_id);

      // 按顺序启动每个 trx 的多线程验证签名,生产对应公钥
      std::vector<transaction_metadata_ptr> packed_transactions;
      packed_transactions.reserve( b->transactions.size() );
      for( const auto& receipt : b->transactions ) {
         if( receipt.trx.contains<packed_transaction>()) {
            auto& pt = receipt.trx.get<packed_transaction>();
            auto mtrx = std::make_shared<transaction_metadata>( pt );
            if( !self.skip_auth_check() ) {
               std::weak_ptr<transaction_metadata> mtrx_wp = mtrx;
               mtrx->signing_keys_future = async_thread_pool( [chain_id = this->chain_id, mtrx_wp]() {
                  auto mtrx = mtrx_wp.lock();
                  return mtrx ?
                         std::make_pair( chain_id, mtrx->trx.get_signature_keys( chain_id ) ) :
                         std::make_pair( chain_id, decltype( mtrx->trx.get_signature_keys( chain_id ) ){} );
               } );
            }
            packed_transactions.emplace_back( std::move( mtrx ) );
         }
      }

      // 执行 trx
      // ...

      commit_block(false);
      return;
   } catch ( const fc::exception& e ) {
      edump((e.to_detail_string()));
      abort_block();
      throw;
   }
} FC_CAPTURE_AND_RETHROW() } /// apply_block

// trx 执行时获取签名返回的公钥
const flat_set<public_key_type>& recover_keys( const chain_id_type& chain_id ) {
   // Unlikely for more than one chain_id to be used in one nodeos instance
   if( !signing_keys || signing_keys->first != chain_id ) {
      if( signing_keys_future.valid() ) {
         // 获取公钥,如果未签名完则阻塞等待签名完毕
         signing_keys = signing_keys_future.get();
         if( signing_keys->first == chain_id ) {
            return signing_keys->second;
         }
      }
      // 当没开启多线程签名时, 直接验证生成对应公钥
      signing_keys = std::make_pair( chain_id, trx.get_signature_keys( chain_id ));
   }
   return signing_keys->second;
}

总结

从这次的改动可以看出主要优化的地方是节点同步区块的速度, 因为开启了多线程签名,所以在 block 验证以及 apply_block 时节省了一定 CPU 时间, 可供其他地方使用。 例如 EOS 现在是当线程的,所以当你进行 RPC 访问的时候,如果涉及到数据提取,主线程的同步时会暂停的,等待你的操作结束, 这样就会影响节点的同步,所以 get_table_rows API 才会限制 10 ms。 现在同步所需时间减少,降低了节点既要同步数据也要提供 RPC API 的压力。

当大家比较关注的 CPU 使用并没有得到改善, 因为多线程签名无法应该在生产区块上。所以在生产区块时, trx 执行所需要的 CPU 时间并不会减少,也就是 CPU 资源的使用并没有得到改善。

EOS 开发的小伙伴有技术问题可以进群讨论哟

EOS源码解析 使用多线程从签名生成对应公钥

相关推荐