Validator Architecture
Architecture and implementation guide for building a MegaETH-compatible stateless validator.
This page describes the reference architecture of MegaETH's stateless validator and the per-block validation pipeline it runs. It is written for engineers building a compatible validator from scratch — in another language or against a different EVM stack.
The reference implementation lives at megaeth-labs/stateless-validator and is used throughout this page as the source of truth. For day-to-day operation of that client, see Stateless Validation. For the wire format of the witness, see Get Block Witness.
What a stateless validator does
A stateless validator independently re-executes every MegaETH block against a compact cryptographic witness, then checks that every commitment in the block header matches the resulting post-state. It holds no chain state of its own — a fresh witness arrives with each block and supplies just the slice of state that block touches.
Input
A (block, witness) pair fetched per height. The witness is the response of mega_getBlockWitness.
Output
A locally-persisted record that the block validates.
The validator's only startup trust input is a genesis JSON (chain ID + hardfork schedule) and an anchor block hash that the next validated block must extend. Both are detailed in Genesis configuration below.
Non-goal: picking the canonical fork. The validator validates whatever block sequence it is fed; pair it with a consensus client (e.g. op-node) to derive canonicality.
Genesis configuration
The genesis JSON is the validator's primary configuration anchor. Misconfigure it and every subsequent fork-conditional check silently runs against the wrong rules — the validator will produce mismatched state roots with no "wrong chain" error to point you at the cause. Treat it like a chain-identity contract: load it once, persist it, and never edit it by hand. Pull a fresh copy of the canonical mainnet genesis whenever a new hardfork is scheduled. For the file layout (not a runtime artifact), see the schema-shaped sample at test_data/mainnet/genesis.json — alloc is stripped to keep the repo small.
Reference impl. Loads genesis via --genesis-file on first run, stores it in the local database with store_genesis, and re-reads the stored copy on every subsequent boot.
Despite the file carrying the full Genesis schema (allocations, gas limit, timestamp, base fee, ...), the validator consumes only two pieces of state from it:
Derived value
Source in config
Use during validation
Chain ID
chainId
Drives the EVM CHAINID opcode and EIP-155 transaction-signature checks.
Hardfork schedule
<fork>Block and <fork>Time fields
Activates Ethereum (Cancun, Shanghai, ...), OP-Stack (Ecotone, Granite, Holocene, Isthmus, ...), and MegaETH (MiniRex, MiniRex1, MiniRex2, Rex, Rex1, Rex2, Rex3, Rex4) at their pre-declared block numbers or timestamps.
The genesis alloc, gasLimit, baseFeePerGas, and other initial-state fields are not consumed — once the chain has produced a single block, initial state is served by the witness, not by the genesis file.
Reference impl. ChainSpec::from_genesis reads genesis.config.chain_id directly, hands the full Genesis to OpChainSpec::from_genesis to extract Ethereum and OP-Stack fork conditions, and pulls MegaETH-specific forks via MegaethGenesisHardforks::extract_from. The three sets are merged into a single ordered hardfork schedule that drives every fork-conditional code path: opcode availability, gas-cost tables, system-contract pre/post-block hooks, and resource limits.
All replicas of the chain MUST use byte-identical genesis JSON. A divergence in any single hardfork timestamp produces a fork that the rest of the network will reject — and because the divergence only manifests as a state_root mismatch on the first affected block, it is hard to attribute after the fact.
Reference architecture
The current implementation of the stateless validator is a three-stage async pipeline. Each (block, witness) pair flows through the same stages; only the validator workers run in parallel.
Block fetcher
Streams (block, witness) pairs from RPC. Independent semaphores cap data and witness concurrency.
Validator worker
Verifies the witness, replays the block, computes post-state, compares against the header.
crates/stateless-core/src/executor.rs:411 (validate_block)
Chain advancer
Reorders out-of-order results, detects reorgs by parent-hash mismatch, persists in height order.
crates/stateless-core/src/pipeline/mod.rs:44 (run_pipeline)
Contract cache
Resolves contract bytecode by code hash with three tiers: in-memory → disk (redb) → RPC.
Workers do not coordinate. A custom implementation can collapse the pipeline into a single sequential loop without changing correctness — parallelism is purely a throughput choice.
Validation pipeline
The per-block sequence below is what validate_block performs. A different implementation MUST run every numbered step. Reorderings are allowed only when they preserve the data dependencies between steps — most importantly, the SALT proof MUST verify (step 3) before any state is read (steps 4+), bytecode MUST be hash-verified before being installed in the cache, and each header recompute MUST include all the state changes it commits to.
Fetch the block and witness
Call eth_getBlockByHash (or eth_getBlockByNumber) for the block, and mega_getBlockWitness for the witness. Pin the witness call to (blockNumber, blockHash); a blockNumber-only call is non-deterministic across forks.
Reference impl. Fetches both in parallel from independent RPC pools — see get_block and get_witness.
Decode the witness payload
Strip the v0: prefix, base64-decode, Zstd-decompress, and bincode-deserialize (legacy config) into (SaltWitness, MptWitness). The exact pipeline and a Rust reference snippet live on Get Block Witness.
Verify the SALT proof against the previous state root
The SaltProof inside SaltWitness is a multi-point IPA opening on the Banderwagon curve. Run the verifier against the previous block's state_root (taken from the parent header, or from the trusted anchor on the very first block).
If the proof does not verify, reject the block immediately — every subsequent step assumes the witnessed key-value pairs are authenticated.
For the proof's mathematical structure, see the SALT repository.
Build a state-read backend over the witness
Treat SaltWitness.kvs as the only source of state for the duration of replay. Every account or storage read during execution falls through to a lookup in this map and resolves to one of three outcomes:
Some(value) — present
Return the decoded account or slot.
Some(None) — proven empty
Return the EVM "empty" sentinel (zero balance / nonce, no code; 0 for storage).
Key absent from kvs
Error. Halt validation immediately.
The third case is what blocks witness omission attacks. A malicious witness producer that left a key out (rather than proving it empty) would otherwise let the verifier silently treat real state as zero. The verifier MUST treat "absent from kvs" as a fatal error, not as "empty".
A custom implementation needs the equivalent surface for whatever EVM it embeds.
Reference impl. This backend is WitnessDatabase, which implements revm::DatabaseRef.
Resolve contract bytecode by hash
Account entries in the witness carry the codehash, not the bytecode itself. This is intentional — bytecode is large, changes infrequently, and is content-addressed, so the witness only references it.
Maintain a local cache keyed by codehash. On a miss, fetch via eth_getCodeByHash — a MegaETH RPC extension that takes a code hash and returns the bytecode whose keccak256 equals that hash — and verify that keccak256(code) == codehash before using it. If the endpoint does not support eth_getCodeByHash, fall back to eth_getCode against a known holder address, and always pin the call to the exact block at which the witness anchors (the parent block's number). Apply the same keccak256(code) == codehash verification to the result. A miss that cannot be resolved is a fatal error for the block being validated.
Apply pre-execution system updates
Before the first transaction, apply hardfork-conditional system calls. Two layers run in order:
OP-Stack base hooks (always active on MegaETH, since Isthmus is the floor):
EIP-2935 history-storage contract update.
EIP-4788 beacon root contract update.
MegaEVM system-contract deployments / updates, gated by the active MegaETH hardfork:
MiniRex — deploy the oracle contract and the high-precision timestamp oracle contract.
Rex2 — deploy the keyless-deploy contract.
Rex4 — deploy the access-control contract and the
MegaLimitControlcontract.MiniRex1, MiniRex2, Rex, Rex1, Rex3 — no new system-contract deployments. The fork still gates EVM behavior changes; the pre-execution hook list is just empty.
The L1-attributes deposit is not a pre-block hook: it is the block's first transaction and runs in the regular tx loop in step 7.
The exact hook set is fixed by the active hardfork — see System Contracts for the canonical addresses and behaviors.
Reference impl. apply_pre_execution_changes inside replay_block delegates to mega-evm's pre_execution_changes, which runs the OP-Stack base hooks followed by the MegaEVM-specific system-contract deployments listed above.
Replay the block's transactions
Execute every transaction with the chain's hardfork rules and accumulate state changes, receipts, and the cumulative gas counter.
A custom EVM must match MegaEVM's semantics exactly — see Re-execution requirements.
Re-implementers can either link mega-evm directly or build a compatible EVM from the MegaEVM specification — Dual Gas Model and the related spec pages linked under Re-execution requirements.
Reference impl. Wires this through MegaBlockExecutorFactory and MegaEvmFactory from the mega-evm crate, which extends revm rather than forking it.
The BLOCKHASH opcode is served from the witnessed EIP-2935 history-storage contract entries — see evm_database.rs:149. There is no separate "ancestor headers" field in the witness.
Apply post-execution system updates
After the last transaction, apply hardfork-conditional post-block system calls — including primarily EIP-7002 withdrawal-request and EIP-7251 consolidation-request processing on Isthmus+. The withdrawals_root is not computed here: it is recomputed separately in step 9 against the L1 message-passer storage trie. As with pre-execution, the exact hook set is fixed by the active hardfork.
Reference impl. apply_post_execution_changes inside replay_block delegates to op-reth's BlockExecutor for the canonical hook list.
Update the withdrawals MPT and recompute withdrawals_root
withdrawals_rootMptWitness carries the storage trie of the L2-to-L1 message-passer contract (0x4200000000000000000000000000000000000016) as RLP-encoded MPT nodes plus its pre-state root.
Apply the block's withdrawal-message writes against this trie, then recompute the root. This must match block.withdrawals_root.
Reference impl. MptWitness::verify.
Apply state changes to SALT and recompute state_root
state_rootFlatten the EVM's collected state changes into (SaltKey, SaltValue) pairs.
Reference impl. Uses two intermediate types — PlainKey (account address or address ++ slot) and PlainValue (encoded account or 32-byte slot) — defined in crates/stateless-core/src/data_types.rs.
Encoding rules (mirrored on Get Block Witness for the reverse direction):
Update
key_len
value_len
Layout
EOA account
20
40
8-byte big-endian nonce ‖ 32-byte big-endian balance.
Contract account
20
72
8-byte big-endian nonce ‖ 32-byte big-endian balance ‖ 32-byte keccak256 code hash.
Storage slot
52
32
Key is address(20) ‖ slot(32); value is the 32-byte big-endian U256.
Apply these updates to the SALT trie in canonical (sorted) key order and recompute the root. This must match block.state_root.
Compare every header commitment
The block validates only if every check below passes:
state_root
Recomputed SALT root from the previous step.
withdrawals_root
Recomputed MPT root from step 9.
receipts_root
Merkle root of the transactions' receipts collected during replay.
logs_bloom
Aggregated 256-byte bloom filter over emitted logs.
gas_used
Cumulative gas counter from replay.
A single mismatch is a fatal error for the block — do not advance the local chain.
Reference impl. Comparisons are at executor.rs:534-559.
Re-execution requirements
A custom EVM must implement OP-Stack Isthmus semantics — MegaETH's baseline, inherited unless explicitly overridden — plus the MegaEVM-specific extensions below. Each link points to the normative specification.
Dual gas model
Dual Gas Model — compute gas vs. storage gas accounting per opcode.
Resource limits
Resource Limits — per-block and per-transaction caps.
System contracts
System Contracts — addresses and behaviors that MUST be replicated.
Precompiles
OP-Stack Isthmus set, with MegaETH gas-cost overrides at a few standard addresses. See Precompiles for the full list and per-address details.
Volatile data
Volatile Data Access — non-deterministic reads and how they are handled at re-execution.
Hardfork schedule
The genesis JSON the validator is started with. Mirror the same schedule in your own client.
Reference impl. If block_replay_time_seconds exceeds the chain's block period, you are not real-time — diagnose with the per-stage histograms in Stateless Validation.
Trust model and reorgs
The validator has two trust inputs, both supplied at startup:
The genesis JSON — supplies the chain ID and hardfork schedule (see Genesis configuration). Persisted on first run; reused thereafter.
The anchor block hash — pins the chain head. The next validated block's
parent_hashmust equal this value.
Everything downstream is verified:
The witness is verified cryptographically against the parent's state root before replay.
Bytecode is verified by recomputing
keccak256(code)on every cache miss.The post-state is verified by recomputing every header commitment.
The validator does not verify:
Block canonicity. The validator validates whatever block sequence is fed to it; it does not decide which fork is canonical. To derive canonicality from L1 instead of trusting the upstream RPC, pair the validator with
op-node, which derives the canonical block sequence directly from L1, and the replica feeds those blocks to the validator.
Reorgs are detected when a freshly validated block's parent_hash does not match the local tip. The chain advancer truncates back to the divergence height and re-validates the new branch from there; the canonical-chain row cap (canonical-chain-max-length) bounds how far back this can reach.
Reference implementation
The reference client is a Cargo workspace. The crates below are the entry points a re-implementation will most often want to mirror:
stateless-core
Validation pipeline, witness-backed revm database, SALT update path.
stateless-common
Multi-endpoint RPC client with retry/backoff and independent concurrency caps.
Companion repositories:
megaeth-labs/salt— the authenticated key-value store and IPA proof system. DefinesSaltWitness,SaltKey,SaltValue, andSaltProof.megaeth-labs/mega-evm— the MegaEVM execution layer, layered on top ofrevm.
Related pages
Get Block Witness — wire format, decoding pipeline, and field-by-field type definitions for the witness payload.
Stateless Validation — operator guide for running the reference client (CLI, metrics, anchoring, troubleshooting).
Architecture — how transactions move through MegaETH and where validators fit in.
MegaEVM specification — Dual Gas Model — normative MegaEVM behavior; entry point into the spec space.
Last updated