FABRKNT
Step 1. Consensus — cargo init から single-validator devnet 構築
Live Reth
レッスン 12 / 16·CONTENT40 分80 XP
コース
Step 1. Consensus — cargo init から single-validator devnet 構築
レッスンの役割
CONTENT
順序
12 / 16

レッスン11 — workspace で live Reth EthereumNode を boot する

問い

Reth(~600 crate のツリー)と Malachite を同じ workspace に同居させたとき、依存が衝突せず両方が同じ tokio runtime で boot するか? そして、integration コードを書く にそれをどう検証するか?

原理(最小モデル)

  • bootstrap-only test も一級の成果物。 node を spin up して chain ID を読むだけ。ビジネスロジックゼロの段階で依存解決と runtime bootstrap の regression を捕まえる。これが落ちたら L12–15 は何も動かない。
  • production-dep は薄く、dev-dep は厚く。 crates/evm/Cargo.toml の production dep は 6 個のまま、dev-dep を 11 個に増やす。openhl-evm を使う下流 crate は libp2p/MDBX/rpc を引かず、テストバイナリだけが引く。
  • NodeConfig::test().dev() test() = ephemeral tempdir + :0 port + peer discovery なし。dev() = single block producer、mempool gossip なし。組合せで CI 上で再現可能な完全 isolated 環境。
  • chain ID 2600。 Reth 上流 custom-dev-node example と一致(diff 用)、public chain と衝突しない。OpenHL 的な意味はない。

具体例

共有 tokio runtime(worker_threads=4)上の task 配置:

[側A: Malachite] Engine actors / libp2p / WAL / run_engine_app loop
[側B: Live Reth]  TaskExecutor / MDBX / Payload Builder / Mempool / RPC stub
   ↑ L11 はこの 2 世界が同一 runtime で衝突せず立ち上がる「ハンドシェイク」だけを検証
     (A↔B の直接通信線は L12–15 で接続)

失敗例(誤解)

「integration コードを先に書けばいい」は誤り。大インフラ crate 2 つの衝突は integration を書いて初めて判明し、その時点で「動くはずなのにコンパイルできない」コードに大量投資済みになる。両方を同時に exercise する最小 test を先に書く — 失敗の blast radius が小さい。


ここまでで「共存検証パターン・薄い prod / 厚い dev」は着地した。ここから reth_node.rs(test module のみ)を組み立てる。コードは完全形。

🛑 予測。 なぜ bootstrap test を --release で走らせるのか? ヒント: compile time の支配要因と、test 自体が何をするか。Reth の MDBX/libp2p/alloy スタックは巨大(~600 crate、debug 初回 ~2:34〜)。test は bootstrap + chain-ID チェックだけ → 初回コンパイル後は fast compile より fast runtime が欲しい。

ステップで組み立てる

Step 1: root Cargo.toml に Reth 依存を追加(全て v2.2.0 SHA 88505c7f... に pin)

reth-node-builder         = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-node-ethereum        = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-node-core            = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-tasks                = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-chainspec            = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-evm                  = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-evm-ethereum         = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-ethereum-primitives  = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-engine-primitives    = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-payload-primitives   = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-payload-builder      = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
reth-provider             = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" }
alloy-genesis             = { version = "2.0", default-features = false }

新規 4 個の用途: reth-node-coreNodeConfig 型)/ reth-tasksRuntime/TaskExecutor)/ reth-provider(L12 が保持する BlockchainProvider)/ alloy-genesis(Genesis JSON → ChainSpec)。main HEAD でなく release-tag SHA に pin が不変条件(crates.io は GitHub tag より数週〜数ヶ月遅れる)。

Step 2: crates/evm/Cargo.toml[dev-dependencies](production scope 不変)

[dev-dependencies]
tokio                = { workspace = true }
reth-node-builder    = { workspace = true, features = ["test-utils"] }
reth-node-ethereum   = { workspace = true, features = ["test-utils"] }
reth-node-core       = { workspace = true }
reth-tasks           = { workspace = true }
reth-chainspec       = { workspace = true }
reth-provider        = { workspace = true }
alloy-genesis        = { workspace = true }
serde_json           = { workspace = true }
eyre                 = { workspace = true }
tempfile             = "3"

test-utils feature が NodeBuilder::testing_node(runtime)(tempdir MDBX + debug + ephemeral port)を提供する。全て dev-dep なので、#[cfg(test)] 外で使うと compile が落ちる(test-only dep がガードレール)。

Step 3: crates/evm/src/reth_node.rs — doc + imports

//! Live Reth node bootstrap — Stage 7a.
//!
//! Demonstrates that a full `EthereumNode` can be spun up in our workspace
//! via `NodeBuilder::testing_node`. Stage 7b will wire `RethEvmBridge` to
//! consume this node's provider + payload builder; for now this module is a
//! validated bootstrap recipe (the smoke test confirms it works) and a
//! placeholder for the future `live_node()` constructor.
//!
//! ```text
//! +----------------------+  Stage 7a (this commit)
//! | NodeBuilder          |--+
//! |   .testing_node      |  |  EthereumNode spins up with MDBX in tempdir,
//! |   .node(Ethereum)    |  |  payload builder, mempool, RPC stub, etc.
//! |   .launch_with_dbg() |--+
//! +----------------------+
//!
//! +----------------------+  Stage 7b (next)
//! | RethEvmBridge        |  Bridge methods (build_payload, payload_ready,
//! |   ::with_live_node() |  validate_payload, commit) route through the
//! +----------------------+  live node's services instead of in-process maps.
//! ```

#[cfg(test)]
mod tests {
    use alloy_genesis::Genesis;
    use eyre::Result;
    use reth_chainspec::ChainSpec;
    use reth_node_builder::{NodeBuilder, NodeHandle};
    use reth_node_core::node_config::NodeConfig;
    use reth_node_ethereum::EthereumNode;
    use reth_tasks::Runtime;
    use std::sync::Arc;

    fn dev_chain_spec() -> Arc<ChainSpec> {
        // Minimal post-merge dev genesis. ChainID 2600 mirrors the upstream
        // custom-dev-node example so we can compare behaviour 1:1 if needed.
        let custom_genesis = r#"{
            "nonce": "0x42",
            "timestamp": "0x0",
            "extraData": "0x5343",
            "gasLimit": "0x5208",
            "difficulty": "0x400000000",
            "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "coinbase": "0x0000000000000000000000000000000000000000",
            "alloc": {},
            "number": "0x0",
            "gasUsed": "0x0",
            "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "config": {
                "ethash": {},
                "chainId": 2600,
                "homesteadBlock": 0,
                "eip150Block": 0,
                "eip155Block": 0,
                "eip158Block": 0,
                "byzantiumBlock": 0,
                "constantinopleBlock": 0,
                "petersburgBlock": 0,
                "istanbulBlock": 0,
                "berlinBlock": 0,
                "londonBlock": 0,
                "terminalTotalDifficulty": 0,
                "terminalTotalDifficultyPassed": true,
                "shanghaiTime": 0
            }
        }"#;
        let genesis: Genesis =
            serde_json::from_str(custom_genesis).expect("dev genesis json parses");
        Arc::new(genesis.into())
    }

    /// Bootstrap a real Reth `EthereumNode` and verify the provider responds.
    /// Returns nothing if successful; panics on launch or assertion failure.
    async fn launch_and_check() -> Result<()> {
        let runtime = Runtime::test();
        let chain_spec = dev_chain_spec();
        let expected_chain_id = chain_spec.chain.id();

        let node_config = NodeConfig::test().dev().with_chain(chain_spec);

        let NodeHandle {
            node,
            node_exit_future: _,
        } = NodeBuilder::new(node_config)
            .testing_node(runtime)
            .node(EthereumNode::default())
            .launch_with_debug_capabilities()
            .await?;

        // The provider should serve canonical chain queries off the genesis state.
        let observed_chain_id = node.chain_spec().chain.id();
        assert_eq!(observed_chain_id, expected_chain_id);

        // NOTE: not awaiting node_exit_future — drop the NodeAdapter and let
        // its background tasks tear themselves down when the runtime drops.
        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
    async fn reth_dev_node_bootstraps() {
        if let Err(e) = launch_and_check().await {
            panic!("Reth dev node bootstrap failed: {e:?}");
        }
    }
}

要点:

  • chain spec は raw JSON → Genesisgenesis.into()ChainSpec builder は 50+ フィールドで複雑なので、Reth 自身の deserializer に default/validity を強制させる(JSON は chain の外部 IF でもある)。EIP block は全て 0、terminalTotalDifficultyPassed: true(post-merge から開始)。
  • NodeConfig::test().dev().with_chain(spec)NodeBuilder::new(config).testing_node(runtime).node(EthereumNode::default()).launch_with_debug_capabilities()
  • node_exit_future: _ は await しない(await すると shutdown を待ってブロック)。NodeHandle を drop して runtime に tear down させる。
  • worker_threads = 4 は Reth 内部 task(MDBX/payload builder/RPC/network)に余裕を持たせる。

Step 4: crates/evm/src/lib.rs に test-cfg で組み込む

#[cfg(test)]
mod reth_node;

#[cfg(test)] がキー — bootstrap モジュールは test-only で consumer から見えず non-test ビルドでコンパイルされない。

答え合わせ

cd ~/code/openhl-reference && git checkout e6b4ebb
diff -u ~/code/my-openhl/Cargo.toml ./Cargo.toml
diff -u ~/code/my-openhl/crates/evm/src/reth_node.rs ./crates/evm/src/reth_node.rs
git checkout main

e6b4ebb に workspace dep update + 11 dev-dep + 105 行の reth_node.rs。genesis JSON / builder chain / test 属性は厳密一致するべき。

合格基準

cargo test -p openhl-evm reth_dev_node_bootstraps --release

reth_dev_node_bootstraps 1 個 pass(フル Reth EthereumNode v2.2.0 を ~2.7 秒で spin up、provider に chain ID query)。初回コールド ~2:34。よくあるミス: test-utils feature 忘れ / reth-* 12 個の SHA 不一致(version skew)/ current_thread で hang / 直前 run のゾンビプロセスが socket/MDBX lock 占有(数秒待つ→pgrep→kill)。

まとめ(3行)

  • bootstrap-only test(spin up + chain ID 確認)が、ビジネスロジック前にインフラ regression を捕まえる一級成果物。
  • production dep は 6 個のまま、test-utils 付き dev-dep 11 個でフル Reth スタックを検証 — openhl-evm は slim を保つ。
  • Reth v2.2.0 と Malachite v0.5.0 が同一 workspace・同一 runtime で衝突なく共存することを build と test の両方で証明。