FABRKNT
Inside Reth — シンク・拡張・SDK
Reth スタック — シンク・拡張・SDK
レッスン 5 / 17·CONTENT12 分25 XP
コース
Inside Reth — シンク・拡張・SDK
レッスンの役割
CONTENT
順序
5 / 17

レッスン3 — ドリル: SenderRecoveryStage を端から端まで読む

問い

読むのはリハーサル、実装するのが記憶SenderRecoveryStage を 1 行ずつ読み、3 つのアーキ問いに答える。読み / 計算 / 書きの構造、バッチループ、Rayon 並列化、done フラグの戻り — どこにあるか?

原理(最小モデル)

  • ターゲットファイル. crates/stages/stages/src/stages/sender_recovery.rs
  • execute の 3 セクション. 読み(MDBX から tx エンベロープ取得)+ 計算(Rayon で ECDSA 復元)+ 書き(MDBX に sender 書き戻し + checkpoint 更新)。
  • バッチループ = commit_threshold. 全ブロックを一度に処理せず分割 → メモリ有界 + backpressure(done: false でオーケストレータに「次のバッチ呼んで」)。
  • done: true の条件. ExecInput.target までの全ブロック処理完了。
  • Rayon は内側 ECDSA ループに. chunk.par_iter() で各 tx の sender 復元を並列、MDBX 書き込みは順次(シングル writer)。
  • サブ線形スケール. tx 20 倍でも実時間は 15-18 倍程度(バッチごとのオーバーヘッドが増えた仕事に償却)。
  • tracing で動作観測. RUST_LOG=reth_stages=debug で本物のステージ遷移ログを観察。

具体例

セットアップ:

git clone https://github.com/paradigmxyz/reth
cd reth

ビルド不要(読みのドリル、コンパイルのドリルではない)。

ターゲットファイル: crates/stages/stages/src/stages/sender_recovery.rs

メソッド本体の 3 セクション:

  1. 読み — 入力範囲のブロックの tx エンベロープを MDBX から取得
  2. 計算 — 各 tx の sender を ECDSA で復元(Rayon が登場)
  3. 書き — 復元した sender を MDBX に書き戻し、checkpoint 更新

バッチループの検索キーワード: commit_thresholdchunkbatch

バッチ化の 2 理由:

  • メモリ: 1000 万署名分のエンベロープバッファ = 高コスト RAM。バッチで working set 有界に
  • Backpressure: 各バッチ後に done: false 返し → オーケストレータが「commit して次に進むか、また呼ぶか」判断

commit_threshold フィールドがバッチサイズ制御、本番でチューニング可能。

done: true への切替条件: ExecInput.target までの全ブロック処理完了 = 「この範囲でこれ以上仕事なし」。それまで done: false でオーケストレータに「次のバッチでまた呼んで」。

Rayon の並列化(内側 ECDSA 復元ループ):

chunk.par_iter()
    .map(|tx| recover_signer(tx))
    .collect::<Vec<_>>()

各 tx の sender 復元は独立 → コア間展開安全 → Rayon が捌く。

スケール分析: tx 20 倍でコア数同じ → 各 Rayon バッチも大きくなる + 実時間は総署名数に概ね線形だがバッチごとオーバーヘッドが償却 → 20 倍の署名でも ~15-18 倍程度の遅さ にとどまる。

tracing で観測(任意):

# reth リポジトリのルートで:
RUST_LOG=reth_stages=debug,reth_stages_api=debug \
  cargo run --bin reth --release -- node --dev --dev.block-time 5s

--dev = 単一ノード devnet、--dev.block-time 5s = 5 秒ごとブロック生成。ターミナルにステージ遷移ログ(headersbodiessender_recoveryexecutionhashingmerkletx_lookup)が読んだ順序で。

観察ポイント: 特定ブロック番号で sender_recovery の開始 → commit → 完了の流れ。execute()何回呼ばれているか を数える(バッチサイズ小なら複数回 = done: false 戻りパターンの実物)。

失敗例(誤解)

「Rayon は全ループに適用すれば速くなる」— 間違い。Rayon が効くのは 独立計算 + 共有状態なし + 純粋関数 が揃ったとき。MDBX 書き込みは順次(シングル writer)なので Rayon 化しても勝てない。ECDSA 復元のみ並列化対象。

「バッチサイズは大きいほど速い」— 間違い。大きすぎると ① RAM 圧迫、② コミット間隔長すぎでクラッシュ時の損失大、③ backpressure 効かず他ステージが詰まる。commit_threshold のデフォルトは経験的最適点、本番で調整。

done: false を返すと次のバッチを自動で実行」— 半分間違い。オーケストレータが判断して「次のバッチ呼ぶ vs 次のステージに進む」を決める。done: false は backpressure シグナル、自動実行ではない。

ステップで組み立てる

Step 1: Stage 実装の場所を見つける

impl<Provider> Stage<Provider> for SenderRecoveryStageexecute メソッド。

Step 2: 3 セクション特定

読み(MDBX → tx エンベロープ)+ 計算(Rayon → ECDSA)+ 書き(MDBX → sender + checkpoint)。

Step 3: バッチループ確認

commit_threshold / chunk / batch で検索 → メモリ + backpressure の 2 理由。

Step 4: Rayon 並列化を辿る

par_iter / rayon:: 検索 → 内側 ECDSA ループに限定。

Step 5: tracing で実動作を観察

RUST_LOG=reth_stages=debug で dev チェーンを起動 → ステージ遷移ログ + execute() 呼び出し回数を数える。

答え合わせ

  • 読み/計算/書きの構造の意味: パイプライン全体のパターン、このステージ固有ではない。全ステージが読み(MDBX 範囲取得)+ 計算(ステージ固有処理)+ 書き(MDBX commit)の 3 セクション構成
  • commit_threshold の存在理由: メモリ有界化(10M 署名バッファ回避)+ Backpressure シグナル(done: false でオーケストレータに「また呼んで」)。デフォルトは経験的最適、本番でチューニング可能。
  • Rayon が ECDSA に適用され MDBX 書き込みに適用されない理由: ① ECDSA = embarrassingly parallel(独立 + 純粋 + 共有状態なし)→ Rayon の勝ち、② MDBX 書き込み = シングル writer 制約(並列化不可)+ 順次書き込みで十分速い → Rayon 化しても勝てない。

合格基準

  • SenderRecoveryStage::execute の読み / 計算 / 書き 3 セクションを言える。
  • commit_threshold の 2 理由(メモリ + backpressure)を即答できる。
  • Rayon が ECDSA に適用される 3 条件を言える。
  • done: false が backpressure シグナルである意味を 1 文で説明できる。
  • tracing で実動作を観測する手順を辿れる。

まとめ(3行)

  • SenderRecoveryStage::execute = 読み(MDBX → tx)+ 計算(Rayon → ECDSA)+ 書き(MDBX → sender + checkpoint)の 3 セクション。
  • commit_threshold でバッチ化(メモリ有界 + backpressure シグナル)、done: false がオーケストレータに「次のバッチ呼んで」を伝える。
  • Rayon は内側 ECDSA に限定、MDBX 書き込みはシングル writer。tracing で実動作を観測 + execute() 呼び出し回数を数えると backpressure パターンが実コードで見える。