[翻訳]EOS ソースコード解析　マルチスレッドを使ってサインからパブリックキーを生成する

**本記事は、本人の許可を得て翻訳する内容です。**
**翻訳元：[EOS LIVE - 发现 EOS 最热门产品及最新优质资讯](https://eos.live/detail/18643?from=groupmessage&isappinstalled=0)**

昨日の朝（訳者：2018/12/05） `EOS 1.5.0` リリースされました。このバージョンの大きい変更点は、マルチスレッドサインのところです。ブロック同期する際の `block` サイン検証と `trx` サイン検証処理をマルチスレッドにすることで、ブロック同期の時間を短縮しました。ただ、ブロック生成のコストは変わっていません。何故でしょうか。具体的な変更点を紹介します。

### マルチスレッドサイン検証の変更
ブロック同期する際、マルチスレッドでサインしますが、`replay` の処理ではまだシングルスレッドのままです。それは、ブロック同期する際は `pending block` の `trx` 操作をロールバックする必要があるからです。この時間は、並列でサインを処理することができますが、 `replay` の時のこのステップがないので、マルチスレッドでサインしても時間を短縮できませんし、メインスレッドが非同期処理結果を待たないと行けません。

### `trx` マルチサイン変更
ブロックの同期および `replay` 処理ともマルチスレッドでサインするようになります。複数の `trx` を実行するので、並列実行出来ます。ただ、ブロック生成する時は使えません。`BP` がブロードキャストされた `trx` を受信するとすぐ実行し、実行完了してから次の `trx` を受信するようにしているので、マルチスレッドを使えません。

## コード解析
### ブロックサイン
`replay` にマルチスレッドを適用できないため、`replay` は今までのサインコードを流用しています。同期は新しい処理を使ってます。

```c++
// 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` はマルチスレッドにしていません。サインの検証処理の他に並列で実行できる処理がないからです。

```c++
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` 時間は減りません。

EOS LIVE DApp 開発者コミュニティ
![](https://img.bitasset.info/FlY4DmmxJz6KCCJJ6srny_M22pkc)
QRコードは ７ 日以内(12 月 13 日までに）有効