Lesson 11 — Booting a live Reth EthereumNode in your workspace
Question
Add Reth v2.2.0's node crates to the workspace and boot a real EthereumNode dev node inside a test, checking its chain ID, before writing any integration code.
Principle (minimum model)
- Dependencies first. 4 new workspace deps (
reth-node-core,reth-tasks,reth-provider,alloy-genesis) and 10 new dev-deps incrates/evm. Production deps stay at 6.Cargo.lockgrows from 616 to 1053 packages, all resolving next to Malachite v0.5.0 without patches. - Chain spec from JSON. A minimal post-merge genesis (chain ID 2600, every fork at 0 through Shanghai) parsed into
Genesis, thengenesis.into()→Arc<ChainSpec>. - The builder chain.
NodeConfig::test().dev().with_chain(spec)→NodeBuilder::new(config).testing_node(Runtime::test()).node(EthereumNode::default()).launch_with_debug_capabilities().test()sets all ports to 0 (kernel-assigned),dev()turns on dev mode and disables discovery,testing_nodeputs the database in a temp dir. - One assertion. The launched node's
chain_spec().chain.id()equals the one we passed in. No RPC calls, no blocks, no consensus. - Test-only module.
reth_node.rscontains only a#[cfg(test)]module, so nothing about this lesson reaches production code. - Why a separate lesson. If the dependency tree doesn't resolve or the node can't boot, every later lesson fails; this test isolates that failure.
- Design note. Running the EL in the same process as the CL is OpenHL's choice. Hyperliquid's node is closed source and not Reth-based, so we can't say how HyperEVM is started.
Worked example + steps
Lesson 11 — Booting a live Reth EthereumNode in your workspace
Goal
Concepts you'll grasp in this lesson:
- Bootstrap-only tests are first-class artifacts — this lesson's test does nothing except spin up Reth and read its chain ID. It catches dependency-resolution and runtime-bootstrap regressions before any business logic exists. If this test fails, nothing in Lessons 12–15 can possibly work.
- Reth and Malachite coexistence proof — two of the largest crate trees in the Rust L1 ecosystem resolve in one workspace and one
Cargo.lock. The dev-deps you add here resolve to a single SHA-coherent dependency closure. (The test boots only Reth; Malachite isn't started in it.) - Production-deps slim, dev-deps thick —
crates/evm/Cargo.tomlkeeps 6 production deps (unchanged from Lesson 5) and grows from 1 to 11 dev-deps. Downstream crates usingopenhl-evmdon't pull libp2p/MDBX/rpc; only the test binary does. NodeConfig::test().dev()semantics —test()= default config with every port set to 0 (kernel-assigned).dev()= sets the--devflag and disables peer discovery. The temp-dir database comes fromtesting_node. Combined: an isolated dev/test node, repeatable in CI.- Why chain ID 2600 — matches Reth's upstream
custom-dev-nodeexample and doesn't collide with any public chain. The number itself has no OpenHL semantic meaning; it's a coordination convention with the Reth example you can diff against.
Verification:
cargo test -p openhl-evm reth_dev_node_bootstraps --release
…passes one new test:
test reth_node::tests::reth_dev_node_bootstraps ... ok
…that spins up a full Reth EthereumNode v2.2.0 (MDBX storage, payload builder, mempool, RPC stub, the whole stack) in ~2.7 seconds, queries its provider for the chain ID, and asserts the result. This is your proof that Reth and Malachite — the two largest pieces of infra in an L1 reference impl — coexist in one workspace without conflict.
Specific changes:
- 4 new workspace deps added to root
Cargo.toml:reth-node-core,reth-tasks,reth-provider,alloy-genesis. - 10 new dev-dependencies added to
crates/evm/Cargo.toml(test-utils variants of Reth's node-builder/ethereum + their support crates) — test-only, production scope unchanged. crates/evm/src/reth_node.rs— new file (~100 lines), test module only. Builds a dev chain spec, launchesEthereumNodeviaNodeBuilder::testing_node, verifies the provider responds.crates/evm/src/lib.rs— wires inreth_node(its contents are all#[cfg(test)]).
No production code. No bridge changes. Just validation that the dependency tree resolves before we start writing the live-bridge code in Lesson 12.
Recap
After Lesson 10 your workspace has:
crates/types/ — BlockHash, PayloadId, PayloadAttrs, ExecutedBlock, PayloadStatus
crates/evm/ — InMemoryEvmBridge, RethEvmBridge (alloy types)
crates/consensus/ — Full BFT engine: Context, signing, codec, node, engine_app
bin/openhl/ — Empty binary stub
cargo test passes 34 tests workspace-wide (4 types + 21 consensus + 9 evm; the reference commit reports 35 because its consensus crate also has runner.rs's 5 tests). The engine produces real blocks through InMemoryEvmBridge. But the EL is still a placeholder. RethEvmBridge exists (Lesson 5) but it doesn't actually call Reth — it just uses alloy types to compute hashes.
Plan
Four things:
- Add 4 workspace-level deps to
Cargo.toml:reth-node-core,reth-tasks,reth-provider,alloy-genesis— all pinned to the same Reth SHA we've been using since Lesson 1. - Add 10 dev-dependencies to
crates/evm/Cargo.toml(test-utils variants of Reth's node-builder/ethereum + their support crates). - Create
crates/evm/src/reth_node.rswith a test module that builds a dev chain spec, launchesEthereumNodeviaNodeBuilder::testing_node, and verifies the provider responds. - Wire
reth_nodeintocrates/evm/src/lib.rs(test-cfg only — keep production scope clean).
This lesson teaches the dependency-coexistence validation pattern. When you depend on two large infrastructure crates (Reth and Malachite, in our case), you don't find out they conflict until you write the integration code — by which point you've invested heavily in code that should work but doesn't compile. The validation pattern is to write the smallest possible test that exercises both at once, before writing the integration. If the test passes, both deps resolve and link. If it fails, the failure is visible immediately, with a small blast radius.
Walk-through
Step 1: Add workspace-level Reth deps
Open the root Cargo.toml. Find the # --- Reth (pinned to v2.2.0 release tag) --- block. After Lesson 10 it ends like:
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" }
Add 4 more lines so the block becomes:
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 }
What each adds:
reth-node-core—NodeConfigand related types (defines the node's config structure: chain spec, datadir, JSON-RPC endpoints, etc.).reth-tasks—Runtime(TaskExecutoris an alias for it) for spawning Reth's background tasks.reth-provider— theBlockchainProviderthat serves historical/canonical chain queries. Lesson 12'sLiveRethEvmBridge::new(provider)will hold one of these.alloy-genesis— Genesis JSON deserialization. Reth'sChainSpecis constructed from aGenesisviagenesis.into().
The Reth SHA 88505c7f... is the v2.2.0 release tag — same SHA we've used in Lesson 1 for reth-evm, reth-evm-ethereum, etc. Pinning to a release-tag SHA, not main HEAD, is the invariant. Bumping Reth happens in a dedicated PR.
Step 2: Update crates/evm/Cargo.toml
Open crates/evm/Cargo.toml. The current [dev-dependencies] is just tokio:
[dev-dependencies]
tokio = { workspace = true }
Replace it with:
[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"
Three categories:
reth-node-builder+reth-node-ethereumwithtest-utilsfeature — gives usNodeBuilder::testing_node(runtime), which points the node's datadir at a temp directory and opens a test MDBX database there. Withouttest-utils, that method doesn't exist.reth-node-core+reth-tasks+reth-chainspec+reth-provider— runtime-supporting crates the test uses directly (NodeConfig,Runtime,ChainSpec, provider access).alloy-genesis+serde_json+eyre+tempfile— test support: JSON parsing for the dev genesis, error handling, temp directory creation.
All in [dev-dependencies] — production scope unchanged. If we accidentally use any of these in lib.rs's non-#[cfg(test)] code, the compile fails. Test-only deps are a guardrail.
Step 3: Create crates/evm/src/reth_node.rs
Top of the file — module doc with an ASCII roadmap showing where we are in Stage 7:
//! 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.
//! ```
The ASCII roadmap is intentional. Lessons 12–14 each replace one stubbed piece of the bridge (parent lookup, header validation, commit). Note the roadmap's with_live_node() name: the real Lesson 12 constructor ended up as LiveRethEvmBridge::new(provider) in a new file. The roadmap gives you the mental scaffold so you know where the current lesson sits in the larger arc.
The file has no non-test code. Everything below is #[cfg(test)] mod tests:
#[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;
// ... helpers + test ...
}
The imports are dense but each has a single role:
Genesis— deserialize the dev genesis JSONChainSpec— Reth's chain configuration (we get one viaGenesis::into())NodeBuilder,NodeHandle— the builder pattern that constructs and launches a nodeNodeConfig— node-level configuration (datadir, RPC endpoints, etc.)EthereumNode— the concrete node type we're spinning up (mainnet Ethereum behaviour)Runtime—reth-tasks' wrapper around tokio runtimeArc—ChainSpecis passed around asArc<ChainSpec>
Step 4: The dev_chain_spec helper
Inside the test module:
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())
}
A handful of things to notice:
chainId: 2600— matches Reth's upstreamcustom-dev-nodeexample so you can compare behaviour line-by-line if debugging. 2600 is not a magic OpenHL number; it's whatever Reth's docs use.- All EIP block numbers = 0 — every Ethereum hardfork is active from height 0. This is "post-merge dev" — we don't simulate the historical sequence of forks.
terminalTotalDifficulty: 0+terminalTotalDifficultyPassed: true— the chain starts post-merge. No pre-merge proof-of-work blocks ever existed.shanghaiTime: 0— Shanghai (withdrawals) is active at genesis.alloc: {}— no pre-funded accounts. For tests that need balances, you'd add entries here.
The JSON is parsed via serde_json::from_str(...) into Genesis, then converted to ChainSpec via genesis.into() (alloy-genesis provides the impl). Arc::new(...) because the node holds it as Arc<ChainSpec> — multiple subsystems share it.
The picture below puts the two stacks side by side. Lesson 10's test ran the left half (Malachite); Lesson 11's test runs only the right half (Reth). What they share so far is the workspace and Cargo.lock, not a running process:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ◆ Shared single multi-threaded Tokio Runtime (worker_threads = 4) │
│ │
│ ┌────────────────────────────────────┐ ┌─────────────────────────────────┐│
│ │ [Side A: Malachite consensus world]│ │ [Side B: Live Reth EL world] ││
│ │ (Already up since Lessons 9–10) │ │ (Boots in Lesson 11; wired Lesson 12 on) ││
│ │ │ │ ││
│ │ ├─ Engine Driver actor tasks │ │ ├─ TaskExecutor ││
│ │ │ (BFT state machine, proposer) │ │ │ (Reth background task mgr) ││
│ │ ├─ libp2p networking task │ │ ├─ MDBX storage engine task ││
│ │ │ (P2P gossip; isolated in CI) │ │ │ (state DB in tempdir) ││
│ │ ├─ WAL / storage tasks │ │ ├─ Payload builder task ││
│ │ └─ run_engine_app loop task │ │ ├─ Mempool task ││
│ │ (the Lesson 10 message router) │ │ └─ Engine API / RPC stub tasks ││
│ └────────────────────────────────────┘ └─────────────────────────────────┘│
│ │
│ Lesson 11's test boots side B only. Together with Lesson 10's test it shows │
│ both stacks build from one Cargo.lock and boot in a tokio test runtime — │
│ a "handshake," not yet a communication link. │
└─────────────────────────────────────────────────────────────────────────────┘
The thing to internalize is "A and B aren't talking to each other yet." Lesson 11 proves that Reth boots from the same dependency graph Malachite already uses; Lessons 12–14 then build LiveRethEvmBridge on top of the live node. Even so, this is the moment Reth v2.2.0 and Malachite v0.5.0 — two of the largest crate trees in the L1 reference implementation universe — are first shown to slip past Cargo's feature unification and version constraints and live in one workspace at both build time and test time.
Step 5: The launch_and_check helper
Below dev_chain_spec:
/// 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(())
}
Walk through:
Runtime::test()—reth-tasks' test runtime. Inside a tokio test it reuses the current tokio handle, so Reth's tasks spawn into the test's runtime.dev_chain_spec()— the genesis-derived chain spec we just built.NodeConfig::test().dev().with_chain(chain_spec)— builder chain:test()— default config with all ports set to 0.dev()— sets the--devflag and disables peer discovery.with_chain(...)— bind to our dev chain spec
NodeBuilder::new(config).testing_node(runtime).node(EthereumNode::default())— the four-stage builder:new(config)— pull in the config.testing_node(runtime)— temp-dir datadir + test MDBX database.node(EthereumNode::default())— say "we want Ethereum mainnet behaviour" (vs. Optimism, custom, etc.)
.launch_with_debug_capabilities()— launch the node through Reth'sDebugNodeLauncher. Besides the normal services (MDBX, payload builder, pool, RPC), in--devmode this also spawns Reth'sLocalMiner, the built-in dev block producer. ReturnsNodeHandle { node, node_exit_future }.node.chain_spec().chain.id()assertion — the simplest possible "did the node start correctly?" check. If launch returned and the node reports our chain ID, it booted with our chain spec.node_exit_future: _— we don't await this future. Awaiting would block waiting for the node to shut down (which it never will until killed). Instead, we dropNodeHandleat end-of-function and let the runtime tear down the background tasks.
Step 6: The test itself
Finally:
#[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:?}");
}
}
The body is 2 lines. The validation is in launch_and_check; the test just calls it and surfaces failures as panics with the inner error preserved.
flavor = "multi_thread", worker_threads = 4 — same setup as Lesson 10's integration test. Reth's internal tasks (MDBX commits, payload builder, RPC handler, network service) all want their own thread; 4 gives them room without contention.
Step 7: Wire reth_node.rs into crates/evm/src/lib.rs
Open crates/evm/src/lib.rs. Currently it has the in-memory and Reth bridges from Lessons 4–5 plus their re-exports. Add one line, gated to test cfg:
pub mod engine;
pub mod in_memory;
#[cfg(test)]
mod reth_node;
pub use engine::RethEvmBridge;
pub use in_memory::InMemoryEvmBridge;
The #[cfg(test)] is the key. The Reth bootstrap module is test-only — not visible to consumers of openhl-evm, not compiled in non-test builds. This is consistent with all the deps being [dev-dependencies]: nothing about Lesson 11 affects production scope. (The reference writes pub mod reth_node; instead; since everything in the file is inside #[cfg(test)] mod tests, the module is empty outside tests either way.)
Test
cargo test -p openhl-evm reth_dev_node_bootstraps --release
First run: ~2:34 cold compile (Reth's MDBX, libp2p, payload builder, RPC stack all build for the first time), then ~3 seconds run.
Subsequent runs: ~30 seconds (Cargo's incremental compilation), then ~3 seconds run.
Output:
running 1 test
test reth_node::tests::reth_dev_node_bootstraps ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Full suite check:
cargo test
…should produce 35 tests passing workspace-wide (4 types + 21 consensus + 10 evm now that we have one new test; the reference commit reports 36 = 26 + 10, with no openhl-types tests).
Common errors and fixes:
no method named 'testing_node' found—crates/evm/Cargo.tomlis missing thetest-utilsfeature (testing_nodeis behind it). Re-check Step 2: it must befeatures = ["test-utils"].error: failed to resolve: use of undeclared crate or module 'reth_provider'— workspace-levelreth-provider = ...is missing. Re-check Step 1.- Type mismatches between two versions of the same reth crate (e.g. two
ChainSpectypes) — version skew. All 12 reth-* deps must use the same SHA — re-check Step 1. Reth dev node bootstrap failed: Failed to bind...—NodeConfig::test()asks the kernel for:0(auto-assigned ephemeral ports) internally, so physical port collisions aren't really possible by design. When this error does surface, the cause is almost always that the previous test run died via panic or Ctrl+C, the Tokio runtime didn't drain cleanly, and the socket / MDBX lock Reth was holding is still owned by a zombie process. The right fix isn'tcargo clean(it doesn't release OS-level port holds): (1) wait a handful of seconds and re-run — this clears most cases; (2) if it persists, find the leftover withpgrep -f openhl-evm/pgrep -f rethandkillit; (3) as a last resort, open a fresh shell so you're in a new process space.- Test compiles but hangs > 30s —
Runtime::test()not working right. Check that you're using#[tokio::test(flavor = "multi_thread", worker_threads = 4)], not the single-threaded default.
Design reflection
Three load-bearing decisions encoded here:
-
Production deps stay minimal; test-only deps validate the entire stack.
crates/evm/Cargo.tomlhas 6 production deps (unchanged from Lesson 5) plus 11 dev-deps. The dev-deps validate that Reth's full node-builder + provider stack works now — but a downstream crate that usesopenhl-evmdoesn't pull them in. This is how you keepopenhl-evmslim while still proving the integration works. -
A bootstrap-only test is a meaningful artifact. This lesson's test does nothing except spin up the node and check the chain ID. It doesn't build a block, doesn't execute a transaction, doesn't query historical state. And yet the rest of the course depends on it. If the bootstrap fails, nothing in Lessons 12–14 can possibly work. Bootstrap-only tests catch infrastructure regressions before any business logic is involved.
-
The ASCII roadmap in the module doc is the trail marker for Lessons 12–14. Each replaces a stubbed piece of the bridge — the parent lookup in
build_payload(12),validate_payload(13),commit(14). The roadmap shows where in the larger arc each lesson sits. Module docs are for orientation, not implementation details.
Answer key
cd ~/code/openhl-reference
git checkout e6b4ebb
diff -u ~/code/my-openhl/Cargo.toml ./Cargo.toml
diff -u ~/code/my-openhl/crates/evm/Cargo.toml ./crates/evm/Cargo.toml
diff -u ~/code/my-openhl/crates/evm/src/reth_node.rs ./crates/evm/src/reth_node.rs
diff -u ~/code/my-openhl/crates/evm/src/lib.rs ./crates/evm/src/lib.rs
The reference at e6b4ebb includes the workspace dep updates, the 11 dev-deps, and the 105-line reth_node.rs. The JSON genesis string, the builder chain, and the test attribute should match closely. Doc comments and exact wording can vary.
Return:
git checkout main
Common questions
Q: Why is the chain ID 2600 instead of 1 (mainnet) or a random number?
Two reasons: (1) it doesn't collide with any public network, so peer discovery never accidentally connects you to a real chain; (2) it matches Reth's upstream custom-dev-node example, letting you diff behaviour against the canonical reference. You can change it freely later — there's no semantic significance to 2600 in OpenHL specifically.
Q: What's NodeConfig::test().dev() doing differently from NodeConfig::default()?
In Reth v2.2.0, test() is NodeConfig::default().with_unused_ports(): every port is 0, so the kernel assigns free ones. dev() sets the --dev flag and disables discovery. The temp-dir database isn't from either; it comes from testing_node. Combined: an isolated node that won't collide with other test runs.
Q: What does launch_with_debug_capabilities add?
It launches through DebugNodeLauncher. That launcher can attach a debug consensus client if you configure one (--debug.rpc-consensus-url or --debug.etherscan), and in --dev mode it spawns Reth's LocalMiner, which produces blocks on its own (by default when transactions arrive). This test sends no transactions, so nothing gets mined. Keep the LocalMiner in mind for Lesson 14, where our own bridge drives Reth's engine.
Q: Why don't we kill() the node like we did OpenHlNodeHandle in Lesson 9?
Because the NodeHandle Reth returns doesn't have a kill() method. The expectation is that you drop the handle and let the runtime tear things down. For longer-running tests that need explicit cleanup, node.task_executor (a reth_tasks::Runtime) has graceful_shutdown_with_timeout(...) — but for a 3-second smoke test, drop suffices.
Next lesson (Lesson 12)
Reth and Malachite now coexist. But the bridge still doesn't talk to Reth. Lesson 12 builds LiveRethEvmBridge::new(provider), a new bridge that takes the bootstrapped node's BlockchainProvider so build_payload can do real parent-block lookups against the live MDBX state. This is the moment when "Reth is in our workspace" becomes "Reth is producing data the consensus engine reads."
Summary (3 lines)
- Add 4 workspace deps + 10 evm dev-deps; Reth v2.2.0 resolves next to Malachite v0.5.0 in one
Cargo.lock(616 → 1053 packages). reth_dev_node_bootstrapslaunches anEthereumNodeviaNodeBuilder::testing_nodewith a JSON dev chain spec (chain ID 2600) and asserts the chain ID; ~2.7 s.- The module is test-only, so production deps don't change. Next:
LiveRethEvmBridgereads parents from this node.