Lesson 11 — Booting a live Reth EthereumNode in your workspace
Question
Boot an actual Reth EthereumNode in the same process as OpenHlNode. Two tokio tasks; same workspace. First time real Reth runs in openhl.
Principle (minimum model)
EthereumNode::newconfig. Chainspec (custom openhl chainspec), data dir, network config. ~30 lines.- Spawn as a tokio task. Reth runs in its own task; logs to stdout.
- Bind to a local port. Reth's JSON-RPC server on port 8545; Engine API on 8551.
- Wait for ready. Poll Engine API's
engine_getStatusuntil "ready". ~3 seconds at startup. - Tests. Boot Reth; assert JSON-RPC responds; assert Engine API responds. No real consensus driving yet.
- Why bootstrap as a separate lesson. Reth boot involves disk IO, network binding, db migrations. Easier to debug in isolation.
- Production parallel. Hyperliquid HyperEVM is started the same way; the boot pattern is shared.
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 live in one workspace using the same tokio runtime. The dev-deps you add here resolve to a single SHA-coherent dependency closure.
- Production-deps slim, dev-deps thick —
crates/evm/Cargo.tomlkeeps 6 production deps (unchanged from Lesson 5) but gains 11 dev-deps. Downstream crates usingopenhl-evmdon't pull libp2p/MDBX/rpc; only the test binary does. NodeConfig::test().dev()semantics —test()= ephemeral tempdir + ephemeral ports + no peer discovery.dev()= single-block-producer mode, no mempool gossip. Combined: a fully isolated dev/test environment, 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. - 8 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— wiresmod reth_node;(test-cfg only).
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 35 tests workspace-wide (21 consensus + 14 evm). 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 8 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
mod reth_node;intocrates/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—RuntimeandTaskExecutorfor spawning Reth's background tasks (block validation, mempool gossip, payload builder).reth-provider— theBlockchainProviderthat serves historical/canonical chain queries. Lesson 12'sLiveRethEvmBridge::with_live_node()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 constructs a node in a tempdir with MDBX, debug capabilities, ephemeral ports. Withouttest-utils, these methods don'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. Module 6 has 5 lessons (Lessons 11–15); each replaces one stubbed body in the bridge. 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.
What Lesson 11's test is actually booting becomes obvious if you draw the task layout on the shared Tokio runtime. Up through Lessons 9–10 only the left half (Malachite) was running; Lesson 11 onward the right half (Reth) coexists on the same runtime:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ◆ 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 verifies that these two worlds can coexist on one process │
│ / single runtime without colliding on resources (threads / ports / Cargo │
│ features) — a "handshake," not yet a communication link. │
└─────────────────────────────────────────────────────────────────────────────┘
The thing to internalize is "A and B aren't talking to each other yet." All Lesson 11 proves is that both sides come up on the same Tokio runtime without collision; the actual run_engine_app ↔ LiveRethEvmBridge message flow gets wired in Lessons 12–15. 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' canonical "test runtime" — wraps the current tokio runtime so Reth'sTaskExecutorcan spawn into it.dev_chain_spec()— the genesis-derived chain spec we just built.NodeConfig::test().dev().with_chain(chain_spec)— builder chain:test()— sane test defaults (ephemeral ports, etc.).dev()— single-validator dev mode (no peer-discovery, no MEV).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)— declare we're a test (tempdir storage, debug RPC, etc.).node(EthereumNode::default())— say "we want Ethereum mainnet behaviour" (vs. Optimism, custom, etc.)
.launch_with_debug_capabilities()— spawn all the node's services (MDBX, payload builder, RPC, mempool gossip in test mode, etc.). ReturnsNodeHandle { node, node_exit_future }.node.chain_spec().chain.id()assertion — the simplest possible "did the node start correctly?" check. If we can fetch the chain ID off the liveBlockchainProvider, the node booted.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:
//! ... existing docs ...
pub mod bridges; // existing
#[cfg(test)]
mod reth_node;
// ... existing re-exports ...
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.
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 36 tests passing workspace-wide (21 consensus + 15 evm now that we have one new test).
Common errors and fixes:
error[E0432]: unresolved import 'reth_node_builder'—crates/evm/Cargo.tomlis missing thetest-utilsfeature. 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.error: feature 'test-utils' on 'reth-node-builder' requires feature 'X'— version skew. The Reth SHA you're pinning must match whatreth-node-builderexpects from its peer crates. 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 11 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 it's the lesson the whole rest of Module 6 depends on. If the bootstrap fails, nothing in Lessons 12–15 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–15. Each remaining lesson replaces a stubbed body in the bridge —
build_payload,payload_ready,validate_payload,commit. 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()?
test() = ephemeral tempdir for MDBX, bind to :0 (kernel-allocated) ports, no peer discovery, sane test logging. dev() = single-validator mode (no actual consensus among multiple validators), assume the local node is the only block producer, no mempool gossip. Combined: a fully isolated dev/test environment.
Q: Does launch_with_debug_capabilities mean it's slower than normal?
No — it enables additional RPC endpoints (debug_* namespace) that are normally gated. The performance overhead is negligible; the cost is just exposing extra surface that would be security risks in prod. Fine for tests.
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 on the path we use. The expectation is that you drop the handle and let the runtime tear things down. For longer-running tests that need explicit cleanup, you'd call node.task_executor.shutdown(...) — 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::with_live_node() — a constructor that takes the node we just bootstrapped and exposes its BlockchainProvider so build_payload (Lessons 4–5's stubbed bridge methods) 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)
- Boot a live Reth
EthereumNodein the same process. Tokio task; chainspec + data dir + network config. - Wait for Engine API ready before continuing. Tests assert JSON-RPC + Engine API respond.
- Reth bootstrap is its own lesson for isolated debugging. Production parallel: HyperEVM. Next: LiveRethEvmBridge.