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

レッスン11 — Stage と ExEx のテスト — fixture chain・インプロセスノード・golden state

問い

Stage トレイト・ExEx API・NodeBuilder SDK を歩いてきた。Reth と Reth を拡張するアプリは、これらの動作をどう検証しているか? dev で「動いて見える」コンポーネントは本番で何千人ものユーザの状態を静かに壊する。Reth 自身の CI がそれを防ぐパターンは?

原理(最小モデル)

  • 2 テスト層が必要. Stage/ExEx ユニットテスト(trait 実装単体、純 Rust + fixture)+ NodeBuilder 統合テスト(ノード全体、インプロセス起動)。
  • Stage ユニットテスト = 一時 DB + 事前状態 + execute + assert. TestStageDB + create_test_provider_factory がephemeral MDBX 提供。
  • ExEx ユニットテスト = ハーネス + 合成通知. test_exex_context() がフルノード起動なしで ExEx 駆動可能。
  • NodeBuilder 統合テスト = インプロセス起動. カスタムコンポーネント間相互作用を検証、~1 秒/テスト。
  • Fixture-chain テスト = canned data 再生. 既知正常ノードからキャプチャ + golden state-root と完全一致 assert。
  • Reth 自身の CI が両層使う. stage コードの全変更が state-root 不一致として検出 → mainnet 前にコンセンサスバグ捕獲。
  • Building tier との接続. Read a Real Production Indexer — tidx の test gate がこの §2 ExEx ユニットテストパターンをアプリケーション層で適用。

具体例

2 テスト層:

何をテストするか何を起動するか
Stage / ExEx ユニットテストtrait 実装単体に、用意したチェーンイベントを与える何も起動しない — 純 Rust + fixture
NodeBuilder 統合テスト自前コンポーネントを差したノード全体Reth ノードをインプロセスで起動

Stage ユニットテスト:

use reth_provider::test_utils::create_test_provider_factory;
use reth_stages::test_utils::{TestRunnerError, TestStageDB};

#[tokio::test]
async fn execute_advances_checkpoint() {
    let db = TestStageDB::default();
    seed_blocks(&db, 0..=10).await;             // fixture ヘルパ

    let mut stage = MyStage::default();
    let input = ExecInput { target: Some(10), checkpoint: None };
    let output = stage.execute(&db.factory.provider_rw().unwrap(), input).await.unwrap();

    assert_eq!(output.checkpoint.block_number, 10);
    assert!(output.done);
    assert_my_derived_state(&db, 10).await;
}

#[tokio::test]
async fn unwind_rolls_back_to_checkpoint() {
    let db = TestStageDB::default();
    seed_blocks(&db, 0..=10).await;
    let mut stage = MyStage::default();

    // 10 まで前進
    stage.execute(&db.factory.provider_rw().unwrap(),
        ExecInput { target: Some(10), checkpoint: None }).await.unwrap();

    // 5 まで unwind
    let output = stage.unwind(&db.factory.provider_rw().unwrap(),
        UnwindInput { unwind_to: 5, checkpoint: ..., bad_block: None }).await.unwrap();

    assert_eq!(output.checkpoint.block_number, 5);
    assert_my_derived_state(&db, 5).await;
}

要のツール: create_test_provider_factoryTestStageDBreth_provider::test_utils および reth_stages::test_utils)。テストごとに ephemeral MDBX DB = 共有状態なし + cleanup 定型コードなし + stale fixture なし。

ExEx ユニットテスト(ハーネス):

use reth_exex_test_utils::{test_exex_context, PollOnce};
use reth_exex::{ExExEvent, ExExNotification};

#[tokio::test]
async fn handles_committed_then_reverted() {
    let (ctx, mut handle) = test_exex_context().await.unwrap();
    let exex = my_exex(ctx);
    tokio::spawn(exex);

    // ブロック N..N+5 をカバーする committed-chain 通知を送る
    handle.send_notification_chain_committed(committed_chain(N..=N+5)).await.unwrap();
    handle.assert_event_finished_height(N+5).await;

    // N+3..N+5 を reorg
    handle.send_notification_chain_reverted(reverted_chain(N+3..=N+5)).await.unwrap();
    handle.assert_event_finished_height(N+2).await;

    // 自前の導出状態を検証
    assert_my_state_at_height(N+2).await;
}

ハーネスなしの場合: フルノード起動 + 本物チェーンデータ再生 + イベント待ち = テストサイクル数分。ハーネスあり: 各通知が単一関数呼び出し = テスト時間が数秒に。

NodeBuilder 統合テスト:

use reth_node_builder::NodeBuilder;
use reth_node_ethereum::EthereumNode;
use reth_tasks::TokioTaskExecutor;

#[tokio::test]
async fn custom_pool_builder_filters_blob_txs() {
    let node = NodeBuilder::new(test_node_config())
        .testing_node(TokioTaskExecutor::default())
        .with_types::<EthereumNode>()
        .with_components(EthereumNode::components().pool(MyPoolBuilder))
        .with_add_ons(EthereumNode::add_ons())
        .launch()
        .await
        .unwrap();

    // pool の API 経由で tx を提出
    let pool = node.pool();
    let blob_tx = test_blob_tx();
    let result = pool.add_external_transaction(blob_tx).await;
    assert!(matches!(result, Err(PoolError::BlobsExcluded)));
}

パターン: NodeBuilder::new(...).testing_node(...) がカスタムコンポーネント付きでノードをインプロセス起動 → ハンドル(node.pool()node.provider()node.network())露出 → 直接駆動。ユニットテストより遅い(~1 秒/テスト)が、コンポーネント間挙動には不可欠。

Fixture-chain テスト:

#[tokio::test]
async fn full_sync_to_pinned_block_matches_golden_state() {
    let chain_fixture = load_fixture("tests/fixtures/sepolia_blocks_0_to_1000.rlp").await;
    let db = TestStageDB::default();

    // fixture を通して全 stage を駆動
    for stage in default_stages_for_test() {
        run_stage_to_completion(&mut stage, &db, chain_fixture.range()).await;
    }

    // ブロック 1000 時点の derived state-root を既知の正答と比較
    let derived = db.factory.provider().header(1000).unwrap().state_root;
    assert_eq!(derived, GOLDEN_SEPOLIA_STATE_ROOT_AT_1000);
}

これが Reth が史実データに対して sync 正しさを検証する方法。fixture は既知正常ノードからの 1 回キャプチャ、CI が push のたびに再生。任意 stage の回帰が state-root 不一致として現れる = コンセンサスバグで mainnet 前に捕獲

失敗例(誤解)

「ユニットテストだけで十分」— 間違い。コンポーネント間相互作用(カスタム pool ビルダーがカスタム payload validator から read)はユニットテストでは見えない。統合テスト必須

「ExEx は手で ExExNotification 値を構築すれば良い」— 間違い。ハーネスがイベント / 完了シグナルの 裏チャンネルを所有 → テストが poll なしで同期可能。手作りだと race condition + flaky test。

「Fixture-chain は重すぎて CI で回せない」— 間違い。Reth 自身が push のたびに再生。コンセンサスクリティカルバグを mainnet 前に捕獲する唯一の方法

ステップで組み立てる

Step 1: 2 テスト層を即答

ユニット(trait 実装単体)+ 統合(ノード全体)。

Step 2: Stage ユニットテストパターン

TestStageDB + seed_blocks + execute + assert + unwind + assert。

Step 3: ExEx ユニットテストハーネス

test_exex_context() + send_notification_* + assert_event_finished_height

Step 4: NodeBuilder 統合テスト

testing_node() でインプロセス起動 + ハンドル露出 + 直接駆動。

Step 5: Fixture-chain テスト

既知正常ノードからキャプチャ + 全 stage 駆動 + golden state-root 比較。

Step 6: Building tier との接続

tidx の test gate = §2 のアプリケーション層適用。

答え合わせ

  • reth-exex-test-utils を別 crate で同梱する理由: ハーネスがイベント / 完了シグナルの 裏チャンネルを所有 → テストが poll なしで同期可能。手作り通知だと race condition で flaky test、ハーネスで決定的テストに。同期がテストの決定性を保証
  • Fixture-chain テストが Reth の CI に必須な理由: 任意 stage の回帰が state-root 不一致 として現れる = コンセンサスクリティカルバグを mainnet 前に捕獲。fixture は既知正常ノードから 1 回キャプチャ、CI が push のたびに再生 → state 一致なら全 stage 正しい。
  • 2 層テストの補完関係: ユニット = trait 実装単体の正しさ(速い、純 Rust)+ 統合 = コンポーネント間相互作用(~1 秒、ノード組み立て)。どちらかを省くと特定クラスのバグが通り抜ける

Expert への接続

Reth コンポーネント層のテスト規律。Systems 層では Expert tier の 2 レッスンがこれを発展させる:

合格基準

  • 2 テスト層と各々が何をテストするかを言える。
  • TestStageDB + create_test_provider_factory のパターンを書ける。
  • test_exex_context() のハーネスパターンを言える。
  • testing_node() での統合テスト構造を即答できる。
  • Fixture-chain テストの golden state-root 検証を 1 文で説明できる。

まとめ(3行)

  • 2 テスト層(ユニット = trait 単体 / 統合 = ノード全体)+ Fixture-chain テスト = Reth 自身の CI 規律、両方が補完関係。
  • TestStageDB + test_exex_context() + testing_node() で ephemeral / 決定的 / 高速テスト、手作り通知だと flaky test 化。
  • Fixture-chain テスト + golden state-root = コンセンサスクリティカルバグを mainnet 前に捕獲、tidx の test gate がこの §2 ExEx ユニットテストパターンをアプリケーション層で適用。