レッスン6 — 最小 ExEx を 1 行ずつ読む
問い
動く ExEx は Rust 約 40 行で書ける。Reth を fork せず、別プロセスを立てず、ノードビルダーに渡す関数 1 つ — その各行が前レッスンの組み立てステップにどう対応するか?
原理(最小モデル)
- 40 行で完成する 5 要素. init/run 分割 + 永続 future + 3 アーム match +
committed_chain()+install_exex配線。 committed_chain()は便利アクセサ.ChainCommittedとChainReorged(new)にはSome(Chain)、ChainRevertedにはNone。new_payload_for_finalizedパターン. commit を伴う通知ごとにFinishedHeight送信、忘れるとディスク膨張。- 本物の ExEx の 4 系統. backfill(起動時過去ブロック再生)+ in_memory_state(カスタムインデックス保持)+ tracking-state(別 DB 永続化)+ rollup(ExEx フックだけで最小ロールアップ)。
具体例
最小 ExEx 全コード(paradigmxyz/reth-exex-examples/minimal の main.rs):
use futures::{Future, TryStreamExt};
use reth_exex::{ExExContext, ExExEvent, ExExNotification};
use reth_node_api::FullNodeComponents;
use reth_node_ethereum::EthereumNode;
use reth_tracing::tracing::info;
async fn exex_init<Node: FullNodeComponents>(
ctx: ExExContext<Node>,
) -> eyre::Result<impl Future<Output = eyre::Result<()>>> {
Ok(exex(ctx))
}
async fn exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.try_next().await? {
match ¬ification {
ExExNotification::ChainCommitted { new } => {
info!(committed_chain = ?new.range(), "Received commit");
}
ExExNotification::ChainReorged { old, new } => {
info!(from_chain = ?old.range(), to_chain = ?new.range(), "Received reorg");
}
ExExNotification::ChainReverted { old } => {
info!(reverted_chain = ?old.range(), "Received revert");
}
};
if let Some(committed_chain) = notification.committed_chain() {
ctx.events.send(ExExEvent::FinishedHeight(committed_chain.tip().num_hash()))?;
}
}
Ok(())
}
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("Minimal", exex_init)
.launch_with_debug_capabilities()
.await?;
handle.wait_for_node_exit().await
})
}
各部の対応:
exex_init(init/run 分割): ノード起動時に 1 度、Reth が ExExContext を渡す(notifications + events + ノードコンポーネントへのハンドル)、長時間動く future を返す。最小版は同期セットアップなし。
exex(長時間動く future): メインループ、ctx.notifications は非同期チャンネル、try_next() で next イベント await(スレッドブロックなし、未完なら他 ExEx + Reth 自体を走らせる)。チャンネル閉じる → Ok(None) → while let 抜ける → 関数 Ok(()) 返す。
3 アーム match: ChainCommitted / ChainReorged / ChainReverted。最小版はログのみ、本物は派生状態更新。3 アーム正しく扱えるかが「動くインデクサ vs phantom-data バグ」を分ける。
committed_chain() + FinishedHeight:
notification.committed_chain()— ChainCommitted と ChainReorged(new)にはSome(Chain)、ChainReverted にはNonectx.events.send(ExExEvent::FinishedHeight(...))— Reth pruner に「このブロックまで処理した」- commit を伴う通知ごとに送信、忘却 → ディスク膨張
main 配線: 通常 Reth ノード + 拡張 1 つ。install_exex("Minimal", exex_init) が ExEx 固有の唯一の行、複数 install で拡張コンポーズ可能。
本物の ExEx 4 系統:
| 例 | 内容 |
|---|---|
| backfill | 起動時に過去ブロックを自分のハンドラに再生 |
| in_memory_state | 各ブロックから派生したカスタムインデックス状態を保持 |
| tracking-state | ExEx 内部状態を別 DB に永続化(再起動が安い) |
| rollup | ExEx フックだけで最小ロールアップを実装 |
失敗例(誤解)
「ChainReorged の new だけで十分」— 間違い。new apply 前に old の状態変更を undo 必要、new だけだと古いチェーンの効果を派生状態から巻き戻せず → 静かに二重カウント or 取りこぼし。
「exex_init で同期セットアップを書かなくても良い」— 間違い。File::open 等を exex 内に入れると Reth が通知バッファ後 init 失敗 → ExEx 健全と誤認しつつ通知積上り。exex_init でセットアップ + future 返す。
「install_exex を 1 回で十分」— 不足。複数 ExEx を install すれば独立通知 + 独立 FinishedHeight + 互いに干渉なし。1 つ落ちても他は動く。
ステップで組み立てる
Step 1: exex_init の役割
ノード起動時 1 度、同期セットアップ + future 返す。
Step 2: exex の永続ループ
while let Some(notification) = ctx.notifications.try_next().await? の構造、チャンネル閉じれば抜ける。
Step 3: 3 アーム match の load-bearing 性
本物の ExEx は派生状態更新、3 アーム正しく扱えるかが phantom-data バグ防止。
Step 4: committed_chain() の 3 バリアント挙動
ChainCommitted → Some(new) / ChainReorged → Some(new) / ChainReverted → None。
Step 5: FinishedHeight の送信タイミング
commit を伴う通知ごと、ディスク膨張を防ぐ。
Step 6: install_exex を main で配線
NodeBuilder にチェイン、複数 install で拡張コンポーズ。
答え合わせ
ChainReorgedで old/new 両方渡される理由: インデクサはnewを apply する前にoldの状態変更を undo 必要。newだけだと古いチェーンの効果を派生状態から巻き戻せず、静かに二重カウント or 取りこぼし。アトミック undo+apply のために両方必要。committed_chain()の 3 バリアント挙動: ChainCommitted と ChainReorged(new)にはSome(Chain)、ChainReverted にはNone。「この通知後の canonical 状態は何か」を取り出すアクセサ、if let Some(...) = ...パターンでFinishedHeight送信判定に使う。- 「ExEx としてのロールアップ」が依存するもの: finality と data availability を Reth 本体 に依存。ロールアップは Reth fork ではなく ExEx フックだけで実装可能 = アーキテクチャ上のアンロック。
合格基準
- 40 行の最小 ExEx の 5 要素(init/run + 永続 future + 3 アーム + committed_chain + install_exex)を即答できる。
committed_chain()の 3 バリアント挙動を言える。FinishedHeight送信タイミング(commit を伴う通知ごと)を即答できる。- 本物の ExEx 4 系統(backfill / in_memory_state / tracking-state / rollup)を言える。
- 「ExEx としてのロールアップ」の依存(Reth の finality + DA)を 1 文で説明できる。
まとめ(3行)
- 40 行の最小 ExEx = init/run 分割 + 永続 future(
try_next().await)+ 3 アーム match +committed_chain()でFinishedHeight送信 +install_exex配線。 committed_chain()が ChainCommitted/Reorged(new)→Some、ChainReverted →None、これでFinishedHeight判定。- 本物の ExEx 4 系統(backfill / in_memory_state / tracking-state / rollup)、ロールアップは ExEx フックだけで Reth fork なしに実装可能 = アーキテクチャ上のアンロック。