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

レッスン6 — 最小 ExEx を 1 行ずつ読む

問い

動く ExEx は Rust 約 40 行で書ける。Reth を fork せず、別プロセスを立てず、ノードビルダーに渡す関数 1 つ — その各行が前レッスンの組み立てステップにどう対応するか?

原理(最小モデル)

  • 40 行で完成する 5 要素. init/run 分割 + 永続 future + 3 アーム match + committed_chain() + install_exex 配線。
  • committed_chain() は便利アクセサ. ChainCommittedChainReorged(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/minimalmain.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 &notification {
            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 には None
  • ctx.events.send(ExExEvent::FinishedHeight(...)) — Reth pruner に「このブロックまで処理した」
  • commit を伴う通知ごとに送信、忘却 → ディスク膨張

main 配線: 通常 Reth ノード + 拡張 1 つ。install_exex("Minimal", exex_init) が ExEx 固有の唯一の行、複数 install で拡張コンポーズ可能。

本物の ExEx 4 系統:

内容
backfill起動時に過去ブロックを自分のハンドラに再生
in_memory_state各ブロックから派生したカスタムインデックス状態を保持
tracking-stateExEx 内部状態を別 DB に永続化(再起動が安い)
rollupExEx フックだけで最小ロールアップを実装

失敗例(誤解)

ChainReorgednew だけで十分」— 間違い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 なしに実装可能 = アーキテクチャ上のアンロック。