Develop with pleasure!

福岡でCloudとかBlockchainとか。

witプロトコルを利用したBeam Sync

こないだGethに導入された新しい同期方法Snap Syncについて書いたけど↓

techmedia-think.hatenablog.com

今回は、Beam Syncという同期方法↓

https://github.com/ethereum/stateless-ethereum-specs/blob/master/beam-sync-phase0.md

を調べてみる。

Beam Syncの同期方法

Beam SyncもSnap Syncと同様、スタンドアロンの同期プロトコルではなく、既存のethプロトコルと一緒に動作する同期方法になる。

現在Gethのデフォルトの同期方法であるFast Syncは、現在のブロックのステートをピアからダウンロードすることで、同期速度を向上させるアプローチを採っている。ただ、それでも同期には結構時間がかかり、ノードとして機能できるまで待つ必要がある。

これに対し、Beam Syncは、最新のブロックを選択し、そのブロックを実行しながらそのブロックで必要とされるステートをピアからオンデマンドに取得することで、クライアントが初期起動後すぐに利用可能にできるようにしようというアプローチを採っている。

https://github.com/ethereum/stateless-ethereum-specs/raw/master/assets/beam-sync-flow-phase0.png

基本的な処理のフローは:

  1. 先頭ブロックを選択すると、そのブロックの全トランザクションを実行する。
  2. EVM実行中に、必要なステート(アカウントの残高やストレージのデータ、コントラクトのバイトコードなど)を保持していない場合、EVMを一時停止する。
    1. 不足しているステートのトライノードをリモートピアに要求する。
    2. ステートがダウンロードできたら、EVMの実行を再開する。
  3. EVMの実行がすべて終わると、次のブロックを処理する。

もちろん、ブロックで必要とされるステートだけを常にオンデマンドで取得していたのでは、完全なステートツリーを構築することはできないので、バックグラウンドで不足しているステートをピアから随時ダウンロードする。この2つの処理を並行して実行することで、クライアントを素早く利用可能にしようというアプローチだ。完全にステートツリーが同期されると、その後は他の同期モードと同様Full syncに移行する。

Ethereumの研究用クライアントの1つであるTrinityがこのBeam Syncを実装しており、こないだ、新規クライアントを起動後80秒で先頭のブロックの検証を終えたという内容が投稿されていた↓

snakecharmers.ethereum.org

ステート入手の課題

一見すると、シンプルなソリューションに見えるけど、オンデマンドでデータを入手する際のパフォーマンスが重要なポイントになる。

初回起動時には、ステートを1つも持っていないので、その状態でブロックを処理すると、EVM実行中にすぐにアカウントの残高など必要なステートを要求することになる。ただ、そのステートをリモートピアに要求するにしても、アカウントのキーを指定したらそのステートを返ってくるような単純な処理ではない。ethプロトコルで定義されているGetNodeDataメッセージは、Merkle Patricia Trieのツリー上のノードのハッシュを指定してそのトライノードのデータを要求するメッセージで、アカウントであればツリーのリーフノードにその残高等のデータが保持されている。

つまり、アカウントのステートを取得しようとすると、そのツリーのルートノードからリーフノードまでのすべてのトライノードのハッシュの情報が必要になり、これを取得するためにルートノードから順番に子ノードのハッシュを知るためにGetNodeDataを実行していく必要がある。現在のmainnetではアカウントのステートが保持されているのはルートから大体7階層めくらいとされているので、その分のGetNodeData/NodeDataのやりとりが必要になる。

ピボット

オンデマンドでステートを要求しながらブロックの全トランザクションを実行していくと、リモートピアとのRTTなども影響し、ブロックタイムが15秒なので、処理が完了するまでに次のブロックが到着するということが容易に想像できる。また、クライアントによって異なるがGethだと直近128ブロックのステートをインメモリで保持しているが、それを超えたブロックの処理を続けるようなケースになるとリモートピアからステートが取得できなくなり処理がスタックしてしまう。

そのため、チェーンの先頭から離されると、そのブロックの実行を中止し、再度先頭のブロックを選択し、そのブロックの実行するようピボットする必要がある。このピボットを繰り返しながら、ローカルのステートを満たしていくことで、ピボットの機会は減っていく。

Beam Syncを使った実験で、起動後22時間経過しても、1つの新しいブロックを処理するのに中央値で約300の新しいトライノードを必要とする模様。

witプロトコルによる高速化

↑のステート入手を高速化するため(最終的にはステートレスクライアントを支援するため)にdevp2p上に設計されたのがwitプロトコル

https://github.com/ethereum/devp2p/blob/master/caps/wit.md

現状このプロトコルのバージョンは0で、以下のP2Pメッセージが定義されている。

  • GetBlockWitnessHashes(0x01):
    指定されたブロックで使用されるトライノードハッシュのリストを要求するメッセージ。
  • BlockWitnessHashes(0x02):
    指定されたブロックの実行および検証中に読み取られたトライノードのリストを返すメッセージ。

witというのはwitnessから来ており、ステートレスクライアントで必要となるステートをwitnessとして提供するのが目的だと思われるが、↑のメッセージからもわかるように現時点ではwitnessは提供されず、そのメタデータ(ハッシュ)のみを提供するプロトコル仕様になっている。

Beam Syncではこのwitプロトコルを使うことで、ブロック内のトランザクションを実行する際に必要となるトライノードのハッシュを事前にリストで取得できるようになる。この結果、Beam Syncのオンデマンドのステート取得の際に余分なGetNodeData/NodeDataのやりとりをしなくて済むことになり、同期速度を向上させることができる。↑のTrinityの80秒で利用可能にしたというのも、このwitプロトコルを使った結果。

もちろん、この高速化のためには、リモートピアがwitプロトコルに対応している必要がある。ただ、witに限らず、Beam Syncのノードは直近のブロックの実行に必要なステートを保持しているノードでもあるので、Beam Syncノードは互いにデータを提供でき、ネットワークに多くのBeam Syncノードが参加しても、ステートの要求はそれらの間で分散して共有される可能性がある。

というのがBeam Syncの仕組み。フェーズ0〜フェーズ2までの段階があり、witを使った高速化がフェーズ1。フェーズ2はブロックヘッダーにwitness proofを追加するコンセンサスの変更が必要なので、まだ先っぽい。