Results

What we've measured.

All five milestones carry exact full-scan data — mainnet (1×, our reference), the inherited 3.5× Bloatnet starting state, and the 5× / 7× / 10× states we grew it to. The headline 10× target is reached. No projected numbers.

State at 10×
measured
2.22 TB
10.03× mainnet
Accounts at 10×
measured
3.94B
381M at mainnet
Storage slots at 10×
measured
14.18B
1.56B at mainnet
storage / acct / code
measured
67%/27%/6%
composition ≈ mainnet

A 10× state is not a 10× cost

auto-plays · or pick a milestone

The headline in one picture: as the state climbs to 10×, the costs that actually matter — trie depth and witness size — stay almost flat.

State size221 GB×1.00
Trie depth7.61 lvl×1.00
Witness / proof3,820 B×1.00

Same axis. At 1× the state is ×1.00 mainnet, but trie depth and the per-proof witness barely move — both grow like log₁₆(N), so a 10× state adds only ~0.8 of a trie level and ~11% per proof. That is why a 10× state is not a 10× cost.

Composition: correcting the inherited state

Steered back to mainnet's real byte split as we grew it — so every milestone is a fair comparison.

0%
3.5× storage share — skewed off-mainnet
0%
5× → 10× storage — back to mainnet

Total state size

Account-trie depth across milestones

Where account leaves sit in the trie, per milestone.

+0.00 lvl
avg depth added · over a 10× state
9 → 10
the leaf peak walks just one level, 1× → 10×
log₁₆(N)
the depth law — confirmed at all 5 milestones
why depth barely moves — the log₁₆ law

A 16-way trie has expected depth ≈ log₁₆(N). Mainnet → 5× deepens by +0.59 levels (7.61 → 8.20), and log₁₆(5.2×) = 0.59 — a textbook match; 5× → 10× adds only +0.25 (= log₁₆(2×)). The account trie is provably full through depth 6 — exactly 16⁰…16⁶ branch nodes were counted — so leaves land at depth 8–9 and the whole 1× → 10× climb moves the median under one level. This is why witnesses stay small and why node counts scale linearly with leaf count, which let us predict the 7× / 10× counts before scanning them.

Account-trie depth vs account size

The leaf-weighted average account-trie depth against the exact state size — toggle the x-axis between count and bytes. The dashed line is the fitted theoretical model, extended past the measured range.

depth = log₁₆(N) + 0.48slope 1.00 in log₁₆ · offset 0.48 · R² 0.99998 (a linear fit reaches only R² 0.94)
is it linear? — theory & extrapolation

Not linear — logarithmic. A uniformly-filled 16-way (hexary) trie of N leaves has expected depth ≈ log₁₆(N). Fitting the five measured points gives depth = 0.99·log₁₆(N) + 0.52 — slope 1.00 in log₁₆-space, a constant +0.48 offset, and R² = 0.99998. A straight line fits far worse (R² = 0.94) and would over-predict at the top end.

Because depth grows with the logarithm of account count, the extrapolation is almost flat: a 20× state (~7.9B accounts) reaches only ~8.7 levels, 50× ~9.0, and a 100× state (~39B accounts) still only ~9.3 levels — under one extra level over the entire 10×→100× climb. This is the structural reason witness sizes stay bounded.

Storage-trie depth across milestones

The mirror image of account depth: mainnet tries are shallow; bloat piles into depths 9–10.

0.00 lvl
storage avg depth at 10× (5× was 7.24)
9–10
the depths where storage-spam slots pile up
the detail

The opposite of account depth: mainnet storage tries are shallow (most leaves at depths 5–7, since most contracts hold 1–3 slots), but 3.5× → 10× pile up at depths 9–10 — the storage-spam mega-tries deepening each milestone (avg 7.24 → 7.81 from 5× to 10×). The per-contract histogram's open "16k+" bucket hides this; the depth distribution exposes it. (The x10 scan listed depths 6+ explicitly; the shallowest x10 levels are carried from x5/x7, where the upper storage trie is identical.)

Storage-trie depth vs storage size

Same, for the storage trie — toggle count ⇄ bytes. The leaf-weighted average jumps at the inherited 3.5× state (its storage-spam mega-tries are already deep), then keeps climbing. Dashed line = fitted model + extrapolation.

depth ≈ 3.8 · log₁₆(S) − 24.2logarithmic, R² 0.99 (linear only 0.83); slope ~3.8 levels per 16× slots — ~4× the account trie's slope-1
is it linear? — theory & extrapolation

Still logarithmic, not linear (log fit R² 0.99 vs linear 0.83) — but with a much steeper slope: ~3.8 levels per 16× slots, roughly 4× the account trie's slope. Two effects stack on top of the log law: storage growth concentrates into the same ~4,097 mega-tries rather than spreading (so each added order of magnitude deepens those specific tries), and the structural-depth metric counts extension edges — the long shared-prefix chains that sequential storage-spam keys create — not just branch edges.

Mainnet (1×) is a deliberate outlier on this curve: its storage is mostly tiny, random-key tries, so the slot-weighted depth (4.84) sits well below the bloatnet trend. Extrapolating the fitted line, a 20× state lands near ~8.9 levels and 50× near ~10.2 — still sub-linear, but the depth budget here is the one to watch as state grows.

Storage slots per contract — the long tail

Contracts by how many slots they own (log scale). ~87% own 1–3; a tiny tail owns 16k+ and dominates.

the detail

~87% of contracts own 1–3 slots; a tail of ~4k own 16k+. All milestones nearly overlap — the per-contract slot-count shape is preserved as the state grows. The open "16k+" bucket hides where the bloat actually goes: from 5× to 10× contractsWithStorage was unchanged (24.30M) while slots grew ×1.82, so the extra ~6.4B slots landed entirely in the same ~4,097 mega-contracts — storage-spam concentrates rather than spreads. The storage-trie depth chart exposes this. (7× is omitted here: the #35 report binned distinct storage tries — deduplicating ~3.3M shared storage roots — rather than per contract, so it isn't on the same basis as the other milestones. 7× is included in every other chart.)

eth_getProof witness size vs account count

The same fixed 742-address set in every state, plotted against the exact account count — toggle the x-axis between account count and account-trie size. Dashed line = fitted log₁₆ model for the account p50, extended past the measured range.

proof ≈ 362 B + 484 · log₁₆(N)+484 B per trie level · R² 0.995 (a linear fit reaches only 0.97)
is it linear? — theory & extrapolation

Not linear — logarithmic, the direct consequence of the depth law, which is also why the witness tracks account count rather than the raw multiplier (3.5× has only 392M accounts, barely above mainnet, so it measures the same; the rebloated 5× at 1.99B accounts sits one log-step higher). An account proof carries one node per trie level and depth ≈ log₁₆(N), so proof bytes track size ≈ 362 B + 484·log₁₆(N)R² 0.995, versus R² 0.97 for a straight line. The slope ~484 B per trie level is one branch node's worth of sibling hashes (≈15 × 32 B + overhead) — so the same +0.48-level depth offset the account-depth chart measures shows up here as the +11% proof growth over the whole 1× → 10× range.

Because it grows with log of account count, the witness stays bounded far beyond the measured range: a 20× state reaches only ~4.35 KB, 50× ~4.5 KB, and a 100× state (~39B accounts) just ~4.6 KB — about +21% over mainnet's 3.82 KB across a hundred-fold state increase. Across the full 1× → 10× range a single proof grows only ~11% (EOA account p50 3820 → 4241 B) and witness depth barely moves — account-proof maxDepth p50 holds at 8 and ticks to 9 only at 10× (log₁₆(10.2×) ≈ 0.83 levels). A 16-slot combined witness goes 22.9 KB (mainnet) → 24.7 KB (10×): a slow, near-linear creep, not a blow-up. The per-block witness cost is dominated by how much state is touched, not by individual proof size.

A block's witness ≈ (state accesses) × (per-access witness). The per-access term is log-bounded, so the lever that moves a block's witness budget is the number of accesses (and gas repricing), not individual proof size — the pre-Verkle MPT baseline a binary tree (EIP-7864) is measured against.

Caveat: per-proof nodeLookups drops on the bloatnets (EOA p50 112 → 72) — a flat-DB residency artifact (freshly-written state is densely packed; mainnet is fragmented by years of compaction), not a real cost reduction. Witness bytes are client-agnostic — EIP-1186 proofs are byte-identical across clients for the same state root — and stay the comparable size metric.

Snap sync — Nethermind vs Geth

Snap-sync wall time from a single frozen peer, by state size (lower is better). NM→NM is measured through the headline 10× (27 h 24 m); NM→Geth through 5×. 7× and the 10× Geth pairs are pending.

t ≈ 5.3 h + 10.0 · (state TB) R² 0.99 across the cross-VM 3.5×/5×/10× runs; effective single-peer throughput ~60–80 GB/h. A power law t ∝ TB^0.74 fits marginally better (R² 0.994) — slightly sub-linear as denser ranges pipeline better.
State Pair Wall time Peak RSS Final DB Bit-identical
NM → NM 52 min 4.0 GB 385 GB
NM → Geth 2 h 50 m 4.8 GB 307 GB
3.5× NM → NM 12 h 23 m 18.4 GB 1.2 TB
3.5× NM → Geth 16 h 18 m 6.7 GB 1.2 TB
NM → NM 17 h 13 m 26 GB 1.6 TB
NM → Geth ~57 h n/s n/s
10× NM → NM 27 h 24 m 35 GB 3.16 TB
✓ 1×–10×
every run reconstructs the root bit-for-bit
~0 h
one storage trie = ⅓ of the 10× NM sync
~0%
box idle at 5× — single-peer bound, not HW
is it linear? — theory & extrapolation

For Nethermind, yes — snap sync is fundamentally a bulk state transfer, so wall time scales linearly with downloaded bytes: t ≈ 5.3 h + 10.0·(state TB), R² 0.99 across the cross-VM 3.5×/5×/10× runs. The ~5 h intercept is fixed setup (account ranges + healing); the slope is a single-peer throughput of ~60–80 GB/h. A slightly sub-linear power law (t ∝ TB^0.74) fits marginally better still, because denser freshly-written ranges pipeline more efficiently as the state grows. Extrapolating the model, the un-run milestone should land near ~21 h for NM → NM.

Geth is the opposite — super-linear. Its effective throughput collapses as state grows (~78 → 47 → 19 GB/h from 1× → 3.5× → 5×) because a single peer's in-flight request cap throttles the serial mega-trie tail. That's why 5× NM → Geth took ~57 h (3.3× the NM target) on an ~80%-idle box: the ceiling is request concurrency, not bytes or hardware — so the lever is more serving peers, not a bigger machine.

why a 10× state isn't a 10× sync

At 1× Geth is ~3.3× slower from a single NM peer. At 3.5× the trade-off flips two ways: NM finishes ~4 h sooner (it drains the storage-spam mega-tries as a clean final phase, while Geth front-loads them and stalls stop-go on each one), but Geth is far leaner on memory (~6.7 GB peak vs NM's ~18.4 GB). 5× NM → NM completes in ~17 h over the cross-VM link, ~1.6 TB on disk and ~26 GB RSS — same shape, the storage-spam mega-tries dominating the tail. At 10× the headline run, NM → NM, finishes in 27 h 24 m (3.16 TB on disk, ~35 GB peak RSS, single frozen peer at ~115 GB/h) — and the tail is now extreme: the final single storage trie alone took ~9 h, roughly a third of the whole sync, the x10 amplification of the ~3.31 M storage-spam clones that share one giant trie. Every run reconstructs the source state root bit-for-bit.

5× NM → Geth (loopback, single peer) completed bit-identical in ~57 h at a steady 2.38M slots/min across the 7.79B slots — bound by single-peer request concurrency, not hardware (the box sat ~80% idle; both clients <1 core). Stock Nethermind couldn't even serve the x5 chain over devp2p until nethermind#12059: 8-billion-gas blocks make 26–47 MB bodies that overflow the 16 MB devp2p limit and desync the RLPx framer; the fix added an accurate size estimator + a 15 MB outgoing-bodies cap (0 framing errors across the whole run). Geth is ~3.3× slower than the NM target from a single peer — its per-peer in-flight request cap is the ceiling.

Snap sync parallelizes across contracts, but a single contract's storage trie downloads as one sequential chain of range requests. The ~4,097 mega-contracts that absorb the bulk of the slots are the non-parallelizable serial fraction — Amdahl's law — so they stall the account frontier for hours (Geth) and form the long final phase (NM). And in the 5× run the box sat ~80% idle — the ceiling is single-peer request concurrency, so the highest-leverage fix is more serving peers, not faster hardware.

Wall time and bandwidth are single-dedicated-peer, controlled figures — read as relative scaling across state sizes, not real-world multi-peer sync time. Setups vary (the 5× NM→NM ran cross-VM, the 5× NM→Geth on loopback), so compare within a pair, not across. "n/s" = not sampled this run (final DB size + peak RSS weren't captured for 5× NM→Geth). Source states are frozen; NM uses the additive Sync.StaticSnapPivot flag (nethermind#11943), Geth a minimal fake-CL forkchoice. 7× snap-sync and the 10× NM→Geth / Geth→NM pairs are still pending (issues #17 / #18).

verified

Witness bytes are client-agnostic — EIP-1186 proofs are byte-identical across clients for the same state root. Verified Geth v1.17.3 vs Nethermind at 3.5× (per-proof Δ = 0); holds structurally through 10×. Besu / Reth / Erigon use the same fixed address set; pending.

Cross-client read latency — Geth vs Nethermind

JSON-RPC p99 latency per method under 50 concurrent readers at the 3.5× state, 5-min duration-matched (log scale, lower is better). Geth's read tail is ~10× tighter across every method.

0 ms
Geth aggregate p99 (Nethermind 270 ms)
~0×
Geth read-latency tail advantage
0 / 0
RPC failures — both clients, ~48k requests each

Execution & resource cost over the same run — opposite optima. Each bar shows how many × the winning client beats the other on that metric: Geth leans leaner (memory, disk), Nethermind leans faster (throughput, DB size).

Metric Geth Nethermind Leaner
Gas/s — average 684 Mgas/s 1,186 Mgas/s NM
Gas/s — peak 1,535 Mgas/s 3,981 Mgas/s NM
Block processing time ~2.43 s ~2.09 s NM
Final DB size 2.34 TB 1.81 TB NM
Disk IOPS — peak 38,826 48,565 Geth
Sustained RSS 9.4 GB 29.4 GB Geth
Peak RSS 12.2 GB 42.9 GB Geth
Disk throughput — peak 409 MB/s 1,438 MB/s Geth
what the cross-client benchmark shows

Matched at 5 minutes against the same 3.5× state and read load, the two clients land at opposite optima. Geth wins read-serving decisively: aggregate p99 27 ms vs 270 ms (~10×), mean 1.6 ms vs 10.3 ms, with a tight tail uniform across all five methods — and it does it on ~12 GB peak RSS vs Nethermind's ~43 GB (~3.5×). Nethermind wins execution: ~1,186 vs 684 Mgas/s average (~1.7×) and slightly faster per-block processing, plus a smaller on-disk DB (1.81 vs 2.34 TB) — but it pays for the throughput in disk I/O (1,438 vs 409 MB/s peak) and memory. Net: Geth optimizes for cheap, low-latency state reads with a small footprint; Nethermind trades memory + disk I/O for higher execution throughput.

eth_getProof byte sizes are near-identical across clients — p50 8,072 B (Geth) vs 8,071 B (NM), p95/p99 13,896 vs 13,895 B — exactly as expected for the same state root. EIP-1186 proofs are client-agnostic, so the witness-size metric is comparable regardless of which client serves it.

Block-processing time and gas/s are NOT a like-for-like statistic — Geth reports a go-metrics summary (chain_execution / chain_mgasps) while Nethermind reports gauges (last_block_processing_time / mgas_per_sec) pulled from Prometheus; treat the execution-throughput row as indicative, not a head-to-head. The read window (recent_block_window=64) sits inside both clients' served-state window, so latency is fair — but it does not exercise Nethermind's deeper-history advantage (FlatDb MinReorgDepth=1024 vs Geth pathdb's fixed ~128-block window). Single milestone (3.5×); other milestones and clients (Besu/Reth/Erigon) pending per issue #21.

5-min duration-matched runs at the 3.5× bloatnet (perf-devnet-3, head 24,358,000), under live head + 50 concurrent Locust read clients. Snapshot isolated via overlayfs; head driven by a mock CL replaying blocks 24,358,001+. 0 RPC failures on either client.

Milestone comparison

Milestone Total Accounts Storage slots Code Acct depth Source
221 GB 381M 1.56B 13 GB 7.61 measured
3.5× 768 GB 392M 7.21B 39 GB 7.62 measured
1.11 TB 1.99B 7.79B 70 GB 8.2 measured
1.55 TB 2.77B 10.37B 94 GB 8.32 measured
10× 2.22 TB 3.94B 14.18B 134 GB 8.45 measured
sources & notes

All five milestones are exact full-scan measurements (state-benchmarks issues #5, #29, #35, and the inherited x3.5 statecomp scan @ block 24,358,000). 3.5× is the inherited Bloatnet starting state — storage-skewed and off-ratio; 5×, 7×, and 10× hold the true mainnet composition (10× converged tightest at 27.30 / 66.65 / 6.05). The 10× state is the project's headline target — 2.22 TB, 10.03× mainnet.