Lesson 15 — What you built, what's still stub, where to go next
Question
Retrospective. Single-validator devnet running on Malachite + Reth, end-to-end. What's built, what's stub, the openhl track in context.
Principle (minimum model)
- You built. Cargo workspace + Reth pin + Malachite pin + shared contract types + ConsensusBridge trait + InMemory bridge + RethEvmBridge with Alloy + OpenHlContext + 10 Malachite sub-types + SigningProvider + Codec + OpenHlNode + run_engine_app + Reth bootstrap + LiveRethEvmBridge + validate_payload via EthBeaconConsensus + commit via Engine API forkchoice. ~16 lessons of integration.
- Stubs. Multi-validator BFT (only single-validator working) + persistence beyond restart (not yet GC'd) + production-tier signing (HSM not wired) + adversarial vote-counting + GossipSub for inter-validator messaging.
- Hyperliquid parallel. What you built parallels HyperBFT + HyperEVM at the architectural level. Different code; same shape.
- Career angle. Custom-L1 builders + perp engine engineers are highly demanded; openhl is the only open-source reference. This course is the differentiator.
- Where to go next. Funding (Stage 10) + Liquidation (Stage 11) + CLOB (Stage 12) + Precompiles (Stage 9) are the rest of the openhl stack. Or apply this pattern to your own custom L1.
- Composition guarantee. Every stub above is a known-deferred piece; the deferral list is explicit; no hidden gotchas.
- Devnet running.
cargo run -p openhl-nodeproduces blocks. Hyperliquid-shaped node running locally.
Worked example + steps
Lesson 15 — What you built, what's still stub, where to go next
The system you built
Over 16 lessons you went from cargo init on an empty directory to a single-validator BFT chain that decides real blocks through a real Reth EL in ~0.02 seconds. Your workspace now looks like this:
~/code/my-openhl/
├── Cargo.toml ← 16 reth-* deps, 8 malachite deps, all SHA-pinned
├── bin/openhl/ ← (stub binary — production wiring is a future course)
├── crates/
│ ├── types/ Lesson 2: shared CL↔EL contract types
│ │ └── src/lib.rs BlockHash, PayloadId, PayloadAttrs,
│ │ ExecutedBlock, PayloadStatus
│ ├── evm/ EL side (test double → live Reth)
│ │ ├── src/bridges/
│ │ │ ├── in_memory.rs Lesson 4: InMemoryEvmBridge (HashMap state)
│ │ │ └── reth.rs Lesson 5: RethEvmBridge (alloy types, real hash_slow)
│ │ ├── src/reth_node.rs Lesson 11: bootstrap proof (test-only)
│ │ └── src/live_node.rs Lessons 12–14: LiveRethEvmBridge<P>
│ │ - Lesson 12: parent lookup via BlockNumReader
│ │ - Lesson 13: EthBeaconConsensus validate
│ │ - Lesson 14: ConsensusEngineHandle forkchoice
│ └── consensus/ CL side (full BFT engine)
│ ├── src/bridge.rs Lesson 3: ConsensusBridge trait
│ ├── src/types/ Lesson 6: 10 Malachite Context sub-types
│ ├── src/context.rs Lesson 6: Context<OpenHlContext> impl
│ ├── src/signing.rs Lesson 7: canonical encoding for vote/proposal
│ ├── src/signing_provider.rs Lesson 7: SigningProvider<OpenHlContext>
│ ├── src/codec.rs Lesson 8: OpenHlCodec (1 real + 7 stub Codec impls)
│ ├── src/node.rs Lesson 9: OpenHlNode + start_engine
│ └── src/engine_app.rs Lesson 10: run_engine_app (AppMsg routing)
About 40-50 source files total. Workspace tests: 38 passing.
Drawing the full CL ↔ EL integration you opened across this course in one picture makes the boundary you stitched together immediately legible:
[ CL: openhl-consensus ] [ EL: openhl-evm ]
┌──────────────────────────────────────────┐ ┌──────────────────────────────────────────┐
│ Malachite BFT engine (actor system) │ │ LiveRethEvmBridge<P> │
│ │ │ │
│ ├── OpenHlContext │ │ ├── provider: P (BlockNumReader │
│ │ (10 associated types — Lesson 6) │ │ │ + HeaderProvider) │
│ ├── OpenHlSigningProvider │ │ ├── chain_spec: Arc<ChainSpec> │
│ │ (Ed25519 + canonical encoding — Lesson 7)│ │ │ (shared source of truth — Lesson 13)│
│ ├── OpenHlCodec │ │ ├── validator: │
│ │ (1 real + 7 stub — Lesson 8) │ │ │ EthBeaconConsensus<ChainSpec> (Lesson 13)│
│ ├── OpenHlNode / OpenHlNodeHandle (Lesson 9)│ │ ├── engine_handle: │
│ └── run_engine_app loop │ │ │ Option<ConsensusEngineHandle> (Lesson 14)│
│ (12 AppMsg arms — Lesson 10) │ │ └── state: Mutex<{ pending, chain, │
│ │ │ head, … }> │
└──────────────────┬──────────────────────────┘ └──────────────────┬──────────────────────┘
│ ▲
│ ── all chatter goes through 4 ConsensusBridge ───┘
│ methods (the trait surface defined in Lesson 3)
│
├── ① build_payload(parent, attrs)
│ CL ──► EL : "Assemble the next block."
│ EL ──► CL : PayloadId (returns immediately; Reth assembles async)
│ Under the hood: pull parent_header from provider →
│ ChainSpec::next_block_base_fee + gas_limit copy
│ + timestamp monotonicity → header synth → stash in pending
│
├── ② payload_ready(id)
│ CL ──► EL : "Hand me the block for that PayloadId."
│ EL ──► CL : ExecutedBlock (retrieved from pending)
│ ※ The only seam where data flows EL → CL among the four methods
│
├── ③ validate_payload(&block)
│ CL ──► EL : "A peer's proposal — validate it."
│ EL ──► CL : PayloadStatus { Valid / Invalid / Syncing }
│ Under the hood: EthBeaconConsensus::validate_header_against_parent
│ (4 sub-checks: number / timestamp / gas-limit / EIP-1559)
│
└── ④ commit(hash)
CL ──► EL : "Quorum reached; finalize this."
EL ──► CL : Ok(())
Phase 1 (must succeed): state.chain.insert + update head
Phase 2 (best-effort): ConsensusEngineHandle::fork_choice_updated
→ Reth's in-process Engine API (no body yet → SYNCING reply; discarded)
Three things this picture pins down: (a) The two worlds on either side talk through exactly the four ConsensusBridge methods defined in Lesson 3 — the entire seam between two huge infrastructure stacks fits into that one trait surface. (b) Because run_engine_app (Lesson 10) is generic over B: ConsensusBridge, the same loop runs against four bridge implementations — StubBridge / InMemoryEvmBridge / RethEvmBridge / LiveRethEvmBridge. That's the polymorphism payoff. (c) The chain_spec: Arc<ChainSpec> inside LiveRethEvmBridge is the shared source of truth referenced by both build_payload and validate_payload — split that, and self-forks appear the moment a hard fork shifts the base-fee formula. Every L1-architect design decision in this course lives somewhere on this single diagram.
The four ConsensusBridge methods — all live
Each row is the closing state of a method after the course:
| Method | First impl | Live impl | Real Reth code now reached |
|---|---|---|---|
build_payload | Lesson 4 (in-memory) | Lesson 13 | HeaderProvider::sealed_header_by_hash, ChainSpec::next_block_base_fee (same helper as the validator) |
payload_ready | Lesson 4 (in-memory) | Lesson 13 | (no Reth call — bridge's pending map, by design) |
validate_payload | Lesson 4 (stub Valid) | Lesson 13 | EthBeaconConsensus::validate_header_against_parent (4 sub-checks: number / timestamp / gas-limit / EIP-1559 base fee) |
commit | Lesson 4 (HashMap insert) | Lesson 14 | ConsensusEngineHandle::fork_choice_updated via in-process Engine API |
The bridge talks to Reth's storage layer (HeaderProvider), Reth's chain config (ChainSpec), Reth's consensus validator (EthBeaconConsensus), and Reth's engine actor (ConsensusEngineHandle). That's most of Reth's public surface that a CL client would touch.
What's still placeholder
This course shipped a working single-validator chain. It's honest to call out what's not yet there. Each item below is a deliberate scope cut, not an accident:
1. Engine newPayload integration
Status: missing.
commit sends ForkchoiceUpdated, and Reth's engine responds SYNCING because it doesn't have the matching block body. To progress to VALID, you'd need to:
- Encode
build_payload's output as a realExecutionPayload(with a transaction list, even if empty). - Send it via
handle.new_payload(payload).awaitbefore thefork_choice_updatedcall. - Match the response chain:
newPayload → VALID→forkchoice → VALID→ canonical head advances.
The blocker is that we don't have EVM-executable transactions to put in the payload yet. OpenHL's matching engine (CLOB) produces fills, not ECDSA-signed user transactions of the kind that flow through a regular Ethereum mempool. Trying to route fills as user-signed transactions through a mempool would erase the entire point of an HL-shape chain — the gas cost and mempool latency would destroy the price-time-priority CLOB's performance characteristics. Instead, real Hyperliquid-shape chains inject the consensus-agreed fill data into ExecutionPayload at build_payload / newPayload time as "protocol-initiated system transactions" or as direct state injections into dedicated precompiles, with no user signature, opening a path for Vec<Fill> to land in EVM state from the consensus side. Building this "fills → privileged system tx / precompile injection in the payload" path is the next big chunk of work after this course — likely a full Module 2 of openhl's build arc.
2. Real Codec impls
Status: 1 real (OpenHlProposalPart — empty bytes), 7 stubs (return CodecStub error).
In single-validator mode, the codecs for gossiped messages (SignedConsensusMsg, LivenessMsg, StreamMessage), WAL writes (ProposedValue), and peer sync (Status, Request, Response) never fire. Once you add a second validator, every cross-validator message hits one of these stubs.
To extend: pick a wire format (protobuf, borsh, JSON) and write the encode/decode for each type. Malachite's code/crates/test/src/codec/ is ~400 lines of hand-written protobuf and is the canonical reference.
3. Multi-validator gossip
Status: never exercised.
OpenHlNode already configures libp2p (/ip4/127.0.0.1/tcp/0). What's untested:
- Two
OpenHlNodeinstances discovering each other. - Vote propagation under network partition.
- Vote-extension exchange.
- Sync of a lagging validator.
Once Codec stubs (#2) are real and you have N=2 nodes spinning up against a shared chain spec, a multi-validator integration test is the natural next step.
4. Persistent WAL
Status: ephemeral tempdir.
Every test uses tempfile::tempdir() so MDBX state is gone after each run. Production needs a configurable home_dir that survives restarts. Adding it is mechanical (just route the path through OpenHlNode::new), but verifying crash recovery (kill the node mid-commit, restart, assert the chain head is right) needs real WAL codec impls and a Test Plan that's specifically chaos-engineering shaped.
5. Slashing + double-sign detection
Status: none.
Production BFT chains track validator misbehaviour (signing two different blocks at the same height, voting twice in the same round). Malachite has hooks for this in LivenessMsg; OpenHL hasn't wired them up. Building a multi-validator chain without slashing is fine for testnets, dangerous for value-handling networks.
6. Custom Hyperliquid-shape behaviour
Status: vanilla Ethereum.
The whole point of an "openhl-shape" chain is the precompiles and CLOB-driven payload assembly that distinguish Hyperliquid from generic EVM. Stage 8 (CLOB matching engine, fills-into-payload) and Stage 9 (custom precompiles, clob_place_order write path) live in psyto/openhl but aren't covered here. They're the natural Module 2 of a future course.
Production-readiness checklist
Working from "I have a passing test" to "I'd let this take real value":
- All 7 Codec stubs replaced with real protobuf/borsh/JSON impls.
-
engine_newPayloadintegration so the engine matches the bridge's view of canonical chain. - Multi-validator integration test passing with N=2+ nodes against a shared chainspec.
- WAL crash-recovery test (kill mid-commit, restart, verify chain head).
- Persistent
home_dir(not tempdir) configured for production deployments. - Engine
SYNCING/VALID/INVALIDresponses logged withtracing::warn/ structured fields, not discarded. - Slashing/double-sign hooks wired and unit-tested.
- Key rotation procedure (Ed25519 key swap during a chain restart, not at runtime).
- Operational telemetry: Prometheus metrics for round duration, payload build latency, validate failures.
- Performance baseline: blocks-per-second under continuous load (not just smoke test).
- Independent security review of the canonical encoding format (the Lesson 7 byte layout is part of your wire spec).
- Threat model for proposer manipulation under partial network partition.
If you're forking this course's code into a production chain, treat this list as the long-pole work — most of it is harder than the course itself.
What you can now do that you couldn't 16 lessons ago
- Bootstrap a full Rust BFT engine against a real EL. Not "with a mocked EL", not "with an FFI to Go" — actually with
EthereumNoderunning in the same Rust workspace. - Reason about producer/validator self-consistency. When you have a builder and a validator for the same artifact, they must share a source of truth. You've seen this pattern in
chain_spec.next_block_base_feedriving bothbuild_payloadandvalidate_payload. - Apply the incremental-stub pattern. Trait bounds force surface area; if you can't fill it all at once, stub with a clear failure mode. Lesson 8's
CodecStub("SignedConsensusMsg<OpenHlContext>")is the model. - Wire two pieces of generic infrastructure together. Reth and Malachite were written by different teams with different sensibilities. The handshake interface (
Nodetrait,ConsensusBridgetrait) is what made them composable. Future courses will use the same pattern with other infra. - Distinguish protocol errors from operational errors.
BridgeError::RejectedvsBridgeError::Internal.PayloadStatus::Invalidvs propagating up. The conversational level matters. - Write tests that prove the live read happened. Lesson 12's
assert_eq!(block.number, 1)was the load-bearing check — anything else would have let an in-memory fallback slip past.
Where to go next
Within rethlab:
- Reth Expert (track
reth-l1-architect, course 7+) — deep dives onBlockExecutor, state-root verification, MDBX internals. Natural next once you wantvalidate_payloadto actually execute transactions. - Reth Expert L11 — Running a Reth fork in production — once your devnet runs, this is the immediate next step. Same shape (build flags + monitoring + diff testing + upgrade discipline), applied to a fork that ships.
- Reth Consensus Engineering — covers slashing, vote extensions, fault tolerance at depth. Where you'd go after multi-validator gossip is working.
Outside rethlab:
psyto/openhlStages 8-9 — the CLOB and custom precompiles. Source code in the public repo; no walkthrough course yet.- Malachite spec docs (
informalsystems/malachite) — read thecore-typescrate's docs straight through. Half of it is now familiar; the other half is what multi-validator requires. - A real Reth full node — clone
paradigmxyz/reth, runcargo run --bin reth -- node --chain dev. YourEthereumNode::default()in Lesson 11 is the same thing, minus the consensus layer. Compare the surface. category-labs/monad-bft— a second mature Rust BFT consensus implementation, actively developed (672★ as of mid-2026, GPLv3-licensed). Where Malachite treats consensus as a generic state-machine library with a context type the embedding chain plugs into, Monad-BFT is purpose-built for a single execution layer and pipelines block proposal with execution to amortize finality latency. The two represent opposite honest trade-offs: Malachite optimizes for embeddability (easy to wire into anything, which is exactly what Lessons 0–7 of this course did); Monad-BFT optimizes for single-chain throughput (faster, but harder to reuse). Worth reading after this course to internalize that "BFT in Rust" isn't a single shape. License note: GPLv3 means citing or studying it is fine; never copy code into your openhl tree — openhl is permissive-licensed and would inherit the copyleft.
Closing note
You wrote roughly 1,400 lines of Rust across the consensus and EVM crates, plus ~250 lines of integration tests. That code is a working single-validator Hyperliquid-shape L1. It's not production-ready; it doesn't need to be. What you have is a foundation that's honest about its scope, has every load-bearing decision visible, and is one extensible interface away from each next capability.
The hardest part of an L1 isn't writing the engine — Malachite did most of it, and we just wired it. The hardest part is being honest about what your code can and can't do, and writing tests that prove the can side. Every lesson in this course had a happy-path assertion and a negative-path assertion. That's the discipline that takes you from "the test passes" to "the system works."
Now go build something that uses this.
Summary (3 lines)
- You built: cargo workspace + Reth + Malachite + bridge trait + InMemory & Reth impls + Context + 10 sub-types + Signing + Codec + Node + engine app + live Reth + forkchoice. 16 lessons.
- Stubs: multi-validator BFT / persistence / HSM / adversarial votes / GossipSub. Hyperliquid parallel = HyperBFT + HyperEVM architecturally.
- Devnet running:
cargo run -p openhl-nodeproduces blocks. Next: Funding / Liquidation / CLOB / Precompiles for the rest of the openhl stack.