A Rust implementation of a stateless blockchain validator specifically designed for MegaETH. MegaETH is a high-performance, Ethereum-compatible blockchain that achieves exceptional throughput through optimized execution and state management.
This validator enables efficient block verification using cryptographic witness data from SALT (Small Authentication Large Trie) instead of maintaining full blockchain state. The stateless approach eliminates the need for validators to run on high-end hardware comparable to sequencer nodes, making it practical to run validator nodes at scale.
-
Partial Statelessness: Contract bytecode is fetched on-demand from RPC endpoints and cached locally, rather than included in witness data. This hybrid approach dramatically reduces witness size since contract code changes infrequently compared to state data.
-
Embarrassingly Parallel: Validation workers operate independently on different blocks with no coordination overhead. Throughput scales linearly with the number of CPU cores available.
-
Lower Hardware Spec: Thanks to our novel SALT (Small Authentication Large Trie) data structure, the witness is significantly smaller than traditional Merkle Patricia Tree (MPT) or Verkle tree approaches. This reduces the network bandwidth requirements of stateless validators.
-
Simplicity over Performance: Designed with simplicity as the core principle. The validator uses a minimal, single-threaded executor based on vanilla Revm interpreter and in-memory storage to prioritize simplicity over raw performance. This creates a small Trusted Computing Base that can be thoroughly audited, ensuring high confidence in validation correctness.
-
Pluggable Execution Engine: Supports multiple execution implementations to prevent single points of failure. Beyond the default Revm-based executor, the validator also supports an executor based on the formal K semantics of the EVM (developed with Pi²). Combined with the hyper-optimized, parallel, JIT-compiled executor on sequencer nodes, this creates three distinct MegaETH client implementations.
The workspace contains two binaries and five library crates:
| Crate | Path | Purpose |
|---|---|---|
stateless-core |
crates/stateless-core |
Core validation logic, abstract storage traits, generic pipeline, EVM execution |
stateless-db |
crates/stateless-db |
redb-backed persistence: table definitions, read/write helpers, bounded ContractCache |
stateless-common |
crates/stateless-common |
Shared utilities: RPC client, logging, metrics |
stateless-test-utils |
crates/stateless-test-utils |
Test fixtures (blocks, witnesses, contracts) and env-var lock for integration tests |
stateless-r2 |
crates/stateless-r2 |
Shared R2 (S3) witness primitives: SigV4 signer, object-key layout, endpoint parsing, signed PUT; consumed by mega-reth's witness uploaders (write) and this repo's validator (read) |
stateless-validator |
bin/stateless-validator |
Main binary: chain sync, parallel validation workers |
debug-trace-server |
bin/debug-trace-server |
Standalone RPC server for debug/trace methods |
Additional directories: test_data/ (integration test fixtures including genesis config), audits/ (security audit reports).
cargo build --releasecargo run --release --bin stateless-validator -- \
--data-dir /path/to/validator/data \
--rpc-endpoint <public-rpc-endpoint> \
--witness-endpoint <witness-rpc-endpoint> \
--genesis-file /path/to/genesis.json \
--start-block <trusted-block-hash>Required Arguments:
--data-dir: Directory for validator database and data files--rpc-endpoint: JSON-RPC API endpoint URL(s) to retrieve block data. Multiple endpoints can be provided via repeated flags or as a comma-separated list (tried in order on failure, with retry-with-backoff per provider). The env varSTATELESS_VALIDATOR_RPC_ENDPOINTaccepts the same comma-separated form (e.g.http://a:8545,http://b:8545).--witness-endpoint: MegaETH JSON-RPC API endpoint URL(s) to retrieve witness data. Multiple endpoints can be provided via repeated flags or as a comma-separated list (tried in order on failure). The env varSTATELESS_VALIDATOR_WITNESS_ENDPOINTaccepts the same comma-separated form (e.g.http://a:8545,http://b:8545). Required with--witness-source rpc(the default); ignored with--witness-source r2.
Optional Arguments:
--genesis-file: Path to genesis JSON file containing hardfork activation configuration (required on first run, stored in database for subsequent runs)--start-block: Trusted block hash to initialize validation from (required for first-time setup)--end-block: Inclusive end block; validate up to this height, then stop cleanly (useful to slice a fixed range across multiple servers)--witness-source: Where to fetch witnesses from:rpc(default) orr2(straight from the R2 bucket over the S3 API)--r2-endpoint,--r2-bucket,--r2-access-key-id,--r2-secret-access-key: R2 connection settings, all required with--witness-source r2(prefer the env var for the secret)--report-validation-endpoint: RPC endpoint URL for reporting validated blocks viamega_setValidatedBlocks(disabled if not provided)--metrics-enabled: Enable Prometheus metrics endpoint (disabled by default)--metrics-port: Port for Prometheus metrics HTTP endpoint (default: 9090)
cargo run --release --bin debug-trace-server -- \
--rpc-endpoint <public-rpc-endpoint> \
--witness-endpoint <witness-rpc-endpoint> \
--addr 0.0.0.0:8545The debug-trace-server exposes Geth-style and Parity-style tracing RPC methods:
debug_traceBlockByNumber,debug_traceBlockByHash,debug_traceTransactiontrace_block,trace_transactiondebug_getCacheStatus
Two operating modes:
- Stateless mode (no
--data-dir): All data fetched from remote RPC on demand. - Local cache mode (with
--data-dir): Enables chain sync to pre-fetch blocks for faster serving.
Response cache:
The HTTP response cache is keyed by (resource, block hash, tracer variant) — an entry is an immutable fact about that exact block, so reorgs need no serve-time validation.
By-number requests resolve their canonical hash before the lookup (local index, then an in-memory memo, then upstream eth_getHeaderByNumber), so a reorged height resolves to the new hash and misses cleanly.
Cacheable shapes are the five built-in tracers (callTracer, prestateTracer, 4byteTracer, noopTracer, flatCallTracer, keyed by their parsed, typed tracerConfig — equivalent configs collapse onto one entry) and the bare default struct-logger request (no tracer, no tracerConfig, default flags).
JS tracers, muxTracer, and struct-logger requests with non-default flags bypass the cache and are recomputed on every request.
A type-malformed tracerConfig on a config-reading builtin (callTracer/prestateTracer/flatCallTracer) is rejected with -32602 invalid params instead of being silently traced with default settings.
Disable the cache with --response-cache-disabled; --response-cache-estimated-items must be at least 1 (the old =0 disable convention is rejected at startup).
Canonical-hash memo:
CANONICAL_CHAIN stays a bounded, contiguous sync window; heights outside it resolve number → hash upstream once, and the hash-verified answer is memoized in a bounded in-memory LRU.
Only depth-final heights are memoized (more than a safety depth below the observed tip, so a memoized binding can no longer reorg); shallow and above-tip heights resolve upstream on every request.
The observed tip comes from the local window and latest-tag lookups; when neither exists (stateless mode with numeric-only traffic), a throttled upstream eth_blockNumber seed learns it, so the memo fills there too.
Resolution order is window → memo → upstream, so historical heights resolve locally after first touch for the lifetime of the process; a restart clears the memo, and reorg bisection reads only the window.
In local cache mode chain sync also clears the memo on a stale reset and on any reorg at least the safety depth deep; in stateless mode the depth margin alone carries the safety argument (assumed deeper reorgs never happen on the target chain).
The memo holds --canonical-hash-memo-capacity entries (default 8M, roughly 80 bytes each; it fills lazily, so the cap costs nothing until a deep historical scan uses it).
Size-based pruning:
Size-based pruning never removes bodies above tip - --size-prune-min-retain (default 256), so a DB file stuck over --db-max-size cannot consume the whole body retention.
Because redb files never shrink on their own, a file that crosses --db-max-size ratchets body retention down to that floor and keeps it there until --db-max-size is raised or the database is rebuilt.
Block-data cache:
A bounded in-memory BlockData cache keyed by block hash sits between the response cache and the local DB / RPC tiers, so requests that miss the response cache — other tracer variants of a block, and especially debug_traceTransaction calls for different transactions of the same block, which are never response-cached — reuse one witness fetch and contract resolution.
Block-number lookups still resolve number → hash against the DB or upstream first, so canonicality is never cached and reorgs need no invalidation here; when a trace fails for a data-attributable reason (the witness cannot replay the block), the entry is dropped so the next request refetches instead of replaying the poisoned data, and each such eviction is counted (block_data_evictions_total).
Size it with --block-data-cache-max-size (default 1GB; 0 disables); the cache uses a fixed 4 shards, so the largest cacheable entry is max_bytes / 4 on every host (~256 MB at the default), and an insert the cache does not retain is counted (cache_admission_rejects_total) and logged rather than masquerading as a miss.
The byte budget is an estimate of retained payload, not exact RSS: map/allocator overhead is under-counted, and contract bytecode is charged here and in the contract cache (the same refcounted allocations), so evicting an entry can free less than its accounted weight.
Combined default memory across the four caches is roughly 3.1 GB — response cache 1 GB + block-data cache 1 GB + contract cache 512 MB + canonical-hash memo ~0.6 GB at its 8M-entry cap (it fills lazily; the measured working set is 50–130k heights, ~10 MB) — on top of ordinary heap; size hosts (or lower the knobs) accordingly.
Witness endpoints:
Declare the internal witness generator via --witness-generator-endpoint; --witness-endpoint lists the durable fallbacks (e.g. an R2-backed witness service), tried in order.
In local cache mode with a generator plus at least one fallback, requests for blocks at least --witness-local-window blocks below the local tip skip the generator and fetch from the fallbacks, because the generator only retains a recent window (its BACKUP, deployed at 4096) and probing it for pruned blocks is a guaranteed miss.
The background chain-sync prefetch routes by freshness against the last observed remote head: for frontier-fresh blocks the generator gets a short exclusive grace before the full endpoint chain — its "witness not found" at the frontier means "not generated yet", and the fallbacks receive witnesses from the same generation pipeline, so rotating to them on a fresh miss wastes a round trip per block and exposes tip sync to fallback stalls.
The head observation is trusted as a freshness anchor only while itself recent (no older than the grace): during a long catch-up stretch the tip is not re-polled and the real head may advance past the generator's retention, so blocks near the stale ceiling fall back to full-chain failover from the first attempt instead of burning the grace on pruned witnesses.
Deep catch-up blocks (far below the observed head, where the generator may have pruned the witness) keep full failover from the first attempt.
Without --witness-generator-endpoint, historical routing is disabled and the endpoints are plain failover.
Witness routing and sync knobs (each also settable via its DEBUG_TRACE_SERVER_* env var):
--witness-local-window: Block-age threshold for the historical witness route (default: 4096; should match the generator'sBACKUP).--witness-old-block-timeout: Witness-stage budget in seconds for blocks at or below the local tip (defaults to the full--witness-timeoutbudget, tracking it when raised; lower it to fail fast on pruned blocks).--tip-buffer: Stay this many blocks behind the upstream head during chain sync so fetches don't race the witness generator (default: 2; must be smaller than--blocks-to-keep).
Each command-line flag has an equivalent environment variable:
STATELESS_VALIDATOR_DATA_DIR→--data-dirSTATELESS_VALIDATOR_RPC_ENDPOINT→--rpc-endpointSTATELESS_VALIDATOR_WITNESS_ENDPOINT→--witness-endpointSTATELESS_VALIDATOR_GENESIS_FILE→--genesis-fileSTATELESS_VALIDATOR_START_BLOCK→--start-blockSTATELESS_VALIDATOR_END_BLOCK→--end-blockSTATELESS_VALIDATOR_WITNESS_SOURCE→--witness-sourceSTATELESS_VALIDATOR_R2_ENDPOINT/_R2_BUCKET/_R2_ACCESS_KEY_ID/_R2_SECRET_ACCESS_KEY→--r2-*STATELESS_VALIDATOR_REPORT_VALIDATION_ENDPOINT→--report-validation-endpointSTATELESS_VALIDATOR_METRICS_ENABLED→--metrics-enabled(set totrueto enable)STATELESS_VALIDATOR_METRICS_PORT→--metrics-port
Logging Configuration:
STATELESS_LOG_FILE_DIRECTORY: Directory for log files; enables file logging when set. Files rotate daily asstateless-validator.log.YYYY-MM-DD.STATELESS_LOG_FILE: Log level for file output (debug|info|warn|error, default: debug)STATELESS_LOG_STDOUT: Log level for console output (debug|info|warn|error, default: info)
Command-line arguments take precedence over environment variables.
The stateless validator requires a trusted starting point and hardfork configuration for security. On first run, you must specify both a genesis file and a trusted block hash:
cargo run --bin stateless-validator -- \
--data-dir ./validator-data \
--rpc-endpoint https://your-rpc-endpoint.com \
--witness-endpoint https://your-witness-endpoint.com \
--genesis-file /path/to/genesis.json \
--start-block 0x1234567890abcdef...The validator will:
- Load the genesis file and extract hardfork activation rules, then store this configuration in the database
- Fetch the specified block from the RPC endpoint
- Initialize the canonical chain database with this trusted block
- Begin validation from this anchor point, applying the appropriate EVM rules based on hardfork activation
For subsequent runs, you can omit both --genesis-file and --start-block to resume from the existing database:
cargo run --bin stateless-validator -- \
--data-dir ./validator-data \
--rpc-endpoint https://your-rpc-endpoint.com \
--witness-endpoint https://your-witness-endpoint.comThe stateless validator acts as an execution client: it fetches block data from an RPC endpoint and re-executes each block to verify that the state transition function (STF) is correct. It ensures that the computed post-state matches the commitments included in the block, providing execution correctness without maintaining a local state database.
However, the stateless validator does not verify that the blocks it receives form the canonical chain.
It will validate whatever sequence of blocks is supplied, including forks, stale heads, or maliciously injected data.
Determining canonicality requires a consensus client.
For OP-Stack chains, this role is performed by op-node, which derives the canonical L2 chain from L1 and the chain's data-availability (DA) layer and handles reorgs.
To avoid trusting a third-party RPC provider to serve you the correct blocks, the recommended setup is:
- Run
op-nodeto derive the canonical L2 chain from L1 + DA - Run a MegaETH replica node to efficiently sync with the sequencer and serve the derived L2 blocks
- Run the stateless validator to independently verify every block's STF
In this configuration, op-node ensures you see the correct canonical chain, the replica node provides local block data, and the stateless validator verifies correctness.
This forms a trust-minimized pipeline: you rely only on L1 + DA (the rollup's security assumptions) and your own local software, not an external RPC endpoint.
Both binaries share a generic three-stage pipeline defined in stateless-core:
Stage 1: FETCH block_fetcher
Streams blocks + witnesses from RPC via a bounded in-flight window
↓ channel
Stage 2: PROCESS N workers × BlockProcessor
Validator: validate_block (EVM execution)
Trace server: pass-through (no validation)
↓ channel
Stage 3: ADVANCE chain_advancer
Reorders out-of-order results (BTreeMap)
Verifies parent-hash continuity
Detects reorgs → rollback + restart
Persists via ChainStore::advance_chain()
Outer loop (run_pipeline):
Reorg → ReorgResolver decides floor → rollback → restart pipeline
Stale detection (optional) → reset anchor → restart
The pipeline is configured via PipelineConfig and customized through trait implementations:
BlockProcessor: Processing logic per block (validation or pass-through)PipelineHooks: Callbacks for advance/reorg/stale eventsProcessedBlock: Output type of the processing stageReorgResolver: Decides the rollback floor on a detected reorg (BisectResolverwalks local history; embedders can supply an externally-resolved floor)
| File | Purpose |
|---|---|
crates/stateless-core/src/pipeline/{mod,config,traits,fetcher,divergence,advancer,worker}.rs |
Generic three-stage pipeline split by responsibility |
crates/stateless-core/src/executor.rs |
Block validation and EVM replay |
crates/stateless-core/src/db.rs |
Shared storage traits: ChainStore, ContractStore, StoreError (scenario stores live in their binaries) |
crates/stateless-core/src/evm_database.rs |
WitnessDatabase implementing revm::DatabaseRef |
crates/stateless-core/src/withdrawals.rs |
Withdrawal validation and MPT witness handling |
crates/stateless-db/src/{lib,tables,helpers,serialize,cache}.rs |
Shared redb tables, helpers, serialization, and bounded ContractCache |
crates/stateless-common/src/rpc_client.rs |
RpcClient: multi-endpoint HTTP client for blocks, witnesses, and bytecode |
crates/stateless-common/src/metrics.rs |
RpcMethod, RpcMetrics, RpcClientConfig |
crates/stateless-common/src/witness_size.rs |
WitnessSizeBreakdown + estimate_witness_size for RPC and trace-server metrics |
crates/stateless-test-utils/src/fixtures.rs |
TestFixtures loader (blocks, SALT/MPT witnesses, contracts, genesis) |
bin/stateless-validator/src/{main,app,workers,chain_sync,validator_db,metrics}.rs |
Thin entry, CLI/startup wiring, pipeline+reporter, fetcher/processor, DB |
bin/debug-trace-server/src/chain_sync.rs |
TraceFetcher, TraceProcessor, TraceHooks |
bin/debug-trace-server/src/rpc_service.rs |
RPC method definitions and handlers |
bin/debug-trace-server/src/data_provider.rs |
Block data fetching with single-flight coalescing |
bin/debug-trace-server/src/block_data_cache.rs |
Bounded in-memory BlockData cache keyed by block hash |
bin/debug-trace-server/src/server_db.rs |
Defines + implements the bin-local BlockStore trait, backed by stateless-db |
The validator and trace server each have their own redb-backed database, sharing common table definitions and helpers from the stateless-db crate:
| Table | Key | Value | Used By |
|---|---|---|---|
ANCHOR_BLOCK |
"anchor" |
(BlockNumber, BlockHash, StateRoot, WithdrawalsRoot) |
Both |
CANONICAL_CHAIN |
BlockNumber |
(BlockHash, StateRoot, WithdrawalsRoot) |
Both |
CONTRACTS |
CodeHash |
Bincode+LZ4 Bytecode |
Both |
GENESIS_CONFIG |
"genesis" |
JSON Genesis |
Validator |
BLOCK_DATA |
BlockHash |
JSON Block<Transaction> |
Trace server |
WITNESSES |
BlockHash |
Bincode+LZ4 LightWitness |
Trace server |
BLOCK_RECORDS |
(BlockNumber, BlockHash) |
() |
Trace server (pruning index) |
In both binaries, CANONICAL_CHAIN is a bounded contiguous window — exactly the range reorg bisection trusts.
SALT (Small Authentication Large Trie) is the authenticated key-value store that replaces Merkle Patricia Tries for MegaETH state. It uses a static 4-level, 256-ary complete tree with ~16.7 million leaf nodes (buckets), each containing a dynamic strongly history-independent (SHI) hash table. Cryptographic commitments use the Banderwagon elliptic curve with IPA (Inner Product Argument) vector commitments, enabling compact aggregatable proofs.
During validation, the witness is verified against the block's pre-state root, WitnessDatabase serves state reads from the verified witness during EVM execution, and post-execution state deltas are propagated up the trie (max 4 levels) to compute and verify the post-state root.
The validator exposes Prometheus-compatible metrics when --metrics-enabled is set.
Metrics are available at http://0.0.0.0:<port>/metrics.
| Metric | Type | Description |
|---|---|---|
stateless_validator_block_validation_time_seconds |
Histogram | Block validation time |
stateless_validator_witness_verification_time_seconds |
Histogram | Witness verification time |
stateless_validator_block_replay_time_seconds |
Histogram | EVM execution time |
stateless_validator_salt_update_time_seconds |
Histogram | SALT update time |
stateless_validator_transactions_total |
Counter | Total transactions validated |
stateless_validator_gas_used_total |
Counter | Total gas used in validated blocks |
stateless_validator_block_state_reads |
Histogram | State reads per block |
stateless_validator_block_state_writes |
Histogram | State writes per block |
stateless_validator_worker_tasks_completed_total |
Counter | Tasks completed per worker (with worker_id label) |
stateless_validator_worker_tasks_failed_total |
Counter | Tasks failed per worker (with worker_id label) |
stateless_validator_local_chain_height |
Gauge | Current height of local chain |
stateless_validator_remote_chain_height |
Gauge | Current height of remote chain |
stateless_validator_validation_lag |
Gauge | Blocks pending validation (remote - local) |
stateless_validator_reorgs_detected_total |
Counter | Chain reorgs detected |
stateless_validator_reorg_depth |
Histogram | Depth of chain reorganizations |
stateless_validator_rpc_requests_total |
Counter | Total RPC requests (with method label) |
stateless_validator_rpc_errors_total |
Counter | RPC errors (with method label) |
stateless_validator_rpc_retry_attempts_total |
Counter | RPC transient retries (with method label) |
stateless_validator_contract_cache_hits_total |
Counter | Contract cache hits |
stateless_validator_contract_cache_misses_total |
Counter | Contract cache misses |
cargo test # all tests
cargo test -p stateless-core # core crate only
cargo test -p stateless-core -- test_name # single testcargo fmt --all --check
cargo clippy --workspace --all-targets --all-features
cargo sort --check --workspace --grouped --order package,workspace,lints,profile,bin,benches,dependencies,dev-dependencies,features[Add your license information here]