アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

gethのsyncingについてのソース解析メモ その3

今回はgethのsyncingの全体的な処理の流れについてまとめる。

前回のまでの記事はこちら。

syncの開始

remote peerからblock headerやtransactionなどのデータを取得する処理の開始場所はsync.goの以下の位置。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/sync.go#L210-L212

通信するremote peerはpeerSetから取得される。基本的にblockchainデータを同期する処理は、各remote peerに対して、それぞれ非同期に行われる。peerSetはデータ同期を行うremote peerを管理するためのデータリストである。

remote peerのhandshakeとpeerSetへの登録

remote peerをpeerSetに登録するのは、eth protocolによるhandshakeが終わったあと。具体的には以下の箇所で行なっている。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/handler.go#L285-L289

なお、これらのhandshake処理を登録しているのは以下の箇所である。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/protocols/eth/handler.go#L103-L132
この処理が実際に呼ばれるのは、多分 node discoveryあたりの処理の中だと思われる。が、見つけられなかった。
また、今回はsyncingの処理を注目したいので、Disocvery関連については深くは調べないことにした。

fetching処理

remote peerから各種データを取得する処理は以下の箇所。これはその1でも紹介した部分。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L559-L574

fast syncの場合について整理する。以下の5つの処理が並列に起動される。

  1. d.fetchHeaders(p, origin+1)
    • block headerを同期する処理。引数で指定された特定のpeerに対して行われる。
  2. d.fetchBodies(origin + 1)
    • task queueに登録されているheadersのtransactionを同期する処理。peerSetの中からidle状態のpeerに対して行われる。
  3. d.fetchReceipts(origin + 1)
    • task queueに登録されているheadersのtransaction receiptを同期する処理。こちらもidle状態のpeerに対して行われる。
  4. d.processHeaders(origin+1, td)
    • 取得したheaderをvalidationしてcurrent headerなどを更新する処理。fetchBodiesやfetchReceiptsのtask queueも更新する。task queueについては後述する。
  5. d.processFastSyncContent()
    • pivotブロック(最新のブロック番号-64)のstate dataを取得する処理。state dataの取得はpeerSetの中のidle状態のpeerに対して行われる。

fetchHeadersについて

fetchHeadersは他のfetch処理とは異なり、特定のpeerに対して実行される。この特定のpeerとは、現時点で登録されているpeerSetの中で一番大きなtotal difficultyを持ったpeerのことである。

各fetch処理が行われる、downloader#syncWIthPeerの呼び出し元を辿ると最終的に以下に示す、chainSyncer#loopにたどり着く。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/sync.go#L210-L212

ここで、引数として渡されている、chainSyncOpにfetchHeadersで渡しているpeerが含まれている。
このpeerの取得処理を辿ると、

nextSyncOpでpeerSet#peerWithHighestTDを呼び出している。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/sync.go#L254
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/peerset.go#L232-L248

このメソッドはその名の通り、peerSetの中から、一番高いTotal Difficultyを持つpeerを探し出している。
ただ、この処理がどのタイミングで呼び出されるのかはよくわからなかった。fast syncのログを見ている限りだと、新しいremote peerとhandshakeするときにチェックされている様に思われる。


なお、新しくbestTDをもつpeerが見つかったとしても、それまでにfetchHeadersを行なっていたpeerとの接続を切るわけではなく、新しく見つかったpeerも含めてそれぞれ並列にfetchHeadersが実行される。
なお、fast syncには時間がかかるため(ネットワークやpeerの状況次第で、おおよそ1日〜3日? 程度)、その間にmain chainが伸びていくためbestTDをもつpeerは常に更新されていく。

また、一度に同期するheaderの数は192である。ここで定義されている。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L43


idle peerとtask queueとprocessHeaders

前述した様に、transactionを同期するfetchBodiesとrecieptを同期するfetchReceiptsが、どのブロックに対するtransactionとreceiptを同期するかについては、task queueにて管理される。また同期のリクエストはidle peerに対して送信される。

これらの同期処理については、その2でも紹介した、downloader#fetchPartsの中で行われているので、まずはその部分を見ていく。
まず各データを同期するためのrequestは以下の箇所のfetchでremote peerに対して送っている。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1502

fetchメソッドの引数に渡しているpeerとrequestについてそれぞれ追っていく。

peerの取得

peerは以下の箇所から、idlesの配列から取得していることがわかる。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1470

idlesについては、fetchBodiesとfetchReceiptsから渡されたidle()関数から取得されており、以下の箇所である。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1292
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1318

ここから、それぞれ
d.peers.BodyIdlePeersとd.peers.ReceiptIdlePeersがidle()関数と渡されていることがわかる。
上記2つの関数が定義されている箇所は以下である。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/peer.go#L464-L490

このコードから、それぞれpeerSetの中の、idle状態のpeer一覧を取得していることがわかる。
ちなみに、idle状態のpeerとはその他のheaderに対してfetch requestを送っていないpeerのことである。
つまり、idleBodiesであれば、他のbodiesをfetchしていない状態もの。また、idleReceiptsの場合は、他のreceiptsをfetchしていない状態のものである。言い換えれば、1つのpeerに対して、bodiesとreceiptsをfetchの両方をfetchすることもある。

idle状態の判定と切り替えを行なっているのは以下の2箇所からもわかる。(ここではbodiesについてのみ抜粋)
判定処理は以下。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/peer.go#L467-L469

non idle状態への切り替えは以下。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/peer.go#L161-L163

requestの取得

続いて、fetch関数に渡しているrequestについてみていく。
requestを取得している部分を探したところ、reserve関数から取得していることがわかる。これも、fetchPartsの引数として渡されている関数である。
呼び出し元を含めて、該当の箇所はそれぞれ以下になる。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1482
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1291
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1317

reserve関数の本体はつまり、d.queue.ReserveBodiesとd.queue.ReserveReceiptsになる。それぞれの関数が定義されているのは以下の箇所。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/queue.go#L451-L469

どちらの関数も、q.reserveHeadersを呼び出している。この関数で、最終的に取得されるheadersを作成している箇所は以下の箇所。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/queue.go#L498-L502

上記ソースから、taskQueueに格納されているheaderを元にfetch対象が生成されていることがわかる。このtaskQueueはそれぞれq.blockTaskQueueq.receiptTaskQueueである。

では、これらのtaskQueueにheaderがいつ追加されるか?追加されている箇所は以下になる。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/queue.go#L311-L328

で、このSchedule関数が呼ばれているのは以下。
https://github.com/ethereum/go-ethereum/blob/v1.10.2/eth/downloader/downloader.go#L1671

つまり、processHeaderが実行されるたびに、taskQueueに同期対象となるheaderが追加されていくことになる。

まとめ

fast syncの場合の同期の流れは、

  1. fetchHeaderでTDの一番大きい remote peerからblock headerを取得する。なお、handshakeしてより大きいTDを持つremote peerが見つかった場合はそこからもheaderを取得しようとする。
  2. processHeaderで同期したheaderをchainに繋げていき、currentHeaderを更新する。また、taskQueueにheader hashを追加して、transactionとreceiptを取得する処理のtaskを更新する。
  3. fetchBodies、fetchReceiptsでtransactionとreceiptを取得する。これらはidle状態のpeerから取得する。また、取得するtransactionとreceiptについては、それぞれのtaskQueueに套路されているblock headerのものを取得する。

stateの取得についてはまた少し異なる流れで行われている。そちらについてはその4としてまとめたいと思う。