レッスン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 セクション:
- 読み — 入力範囲のブロックの tx エンベロープを MDBX から取得
- 計算 — 各 tx の sender を ECDSA で復元(Rayon が登場)
- 書き — 復元した sender を MDBX に書き戻し、checkpoint 更新
バッチループの検索キーワード: commit_threshold、chunk、batch。
バッチ化の 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 秒ごとブロック生成。ターミナルにステージ遷移ログ(headers、bodies、sender_recovery、execution、hashing、merkle、tx_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 SenderRecoveryStage → execute メソッド。
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 パターンが実コードで見える。