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.
A 10× state is not a 10× cost
auto-plays · or pick a milestoneThe headline in one picture: as the state climbs to 10×, the costs that actually matter — trie depth and witness size — stay almost flat.
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.
Total state size
Account-trie depth across milestones
Where account leaves sit in the trie, per milestone.
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.
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.
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.
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.
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.
| State | Pair | Wall time | Peak RSS | Final DB | Bit-identical |
|---|---|---|---|---|---|
| 1× | NM → NM | 52 min | 4.0 GB | 385 GB | ✓ |
| 1× | 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 | ✓ |
| 5× | NM → NM | 17 h 13 m | 26 GB | 1.6 TB | ✓ |
| 5× | NM → Geth | ~57 h | n/s | n/s | ✓ |
| 10× | NM → NM | 27 h 24 m | 35 GB | 3.16 TB | ✓ |
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 7× 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).
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.
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 |
|---|---|---|---|---|---|---|
| 1× | 221 GB | 381M | 1.56B | 13 GB | 7.61 | measured |
| 3.5× | 768 GB | 392M | 7.21B | 39 GB | 7.62 | measured |
| 5× | 1.11 TB | 1.99B | 7.79B | 70 GB | 8.2 | measured |
| 7× | 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.