Metrics & math

Every published number lives in one of three families: structural, operational, or resilience. Each family is governed by a small set of equations — written down so a reader can disagree precisely.

Structural metrics

These describe the trie at rest.

Account count and storage slot count

For nn accounts and total storage slots ss:

n={accounts},s=ccontractsslots(c).n = |\{\text{accounts}\}|,\qquad s = \sum_{c \in \text{contracts}} |\text{slots}(c)|.

Both are exact counts, sampled directly from the state-composition sensor.

Total size scales linearly with the multiplier

Because every milestone is rebloated to the same byte composition, total trie size is just the mainnet total T1=221.4 GBT_1 = 221.4\text{ GB} scaled by the multiplier:

T(m)=T1m.T(m) = T_1 \cdot m.

The measured points sit right on this line at 1×, 3.5×, and 5× (see Results).

Expected trie depth

The MPT branching factor is 16 (hex). For a balanced trie of nn leaves the expected depth is

E[d]=log16(n)=lnnln16.\mathbb{E}[d] = \log_{16}(n) = \frac{\ln n}{\ln 16}.

The trie is not balanced — but Ethereum addresses are keccak-hashed before insertion, which gives a near-uniform distribution and makes the log-16 estimate accurate to within a fraction of a nibble.

The mainnet baseline measures avg account path depth = 7.61 nibbles and avg storage path depth = 4.84 nibbles (block 24,927,663, 2026-05-05). For comparison, log16(381413929)7.13\log_{16}(381\,413\,929) \approx 7.13 — the trie carries about half a nibble of structural overhead from extension nodes and unbalanced subtrees.

The measured account-trie leaf distribution confirms the log₁₆ prediction — going from mainnet to 5× shifts the whole distribution one level deeper (avg 7.61 → 8.20), not many. At 5× the account trie (avg 8.20) also runs deeper than the storage tries (avg 7.24). Both distributions are plotted on the Results page.

Branch occupancy

For each branch node bb with children kids(b){1,,16}\text{kids}(b) \in \{1,\dots,16\}, occupancy is

ρ(b)=kids(b)16,ρˉ=Eb[ρ(b)].\rho(b) = \frac{|\text{kids}(b)|}{16},\qquad \bar\rho = \mathbb{E}_b[\rho(b)].

A near-saturated trie (high ρˉ\bar\rho) is cheaper per-leaf but more expensive to mutate; the structural metric tells us where on this curve we are. Mainnet sits at ρˉ=3.70/160.23\bar\rho = 3.70 / 16 \approx 0.23 — most branches carry just 3–4 of their 16 possible children, which is the consequence of keccak-distributed insertion against a trie that is far from full.

Operational metrics

These describe the cost of touching state.

Account-proof size

proofacc(n)    c0+c1log16(n),\text{proof}_{\text{acc}}(n) \;\approx\; c_0 + c_1 \cdot \log_{16}(n),

with c0250c_0 \approx 250 bytes (RLP framing for the leaf) and c1530c_1 \approx 530 bytes per internal level (15 sibling hashes + RLP overhead). We publish c0,c1c_0, c_1 as empirical fits per milestone, not as theoretical constants.

The measured mainnet anchor (head 24,935,360, 2,755 unique addresses sampled):

QuantityAccount proofStorage proof
bytes_p503,821 B1,211 B
bytes_p953,929 B2,371 B
nodes_p5093
nodes_p95106

These are the baseline points everything else is measured against. Storage proofs are ~3× smaller and shallower than account proofs because the average storage trie has only 4.84 nibbles of depth and most random slot queries terminate at an absent-leaf (proof of non-existence is shorter than a full leaf traversal).

Proof size tracks log₁₆ of account count, not the raw multiplier — which is why the 3.5× state (392M accounts, barely above mainnet) measures the same as mainnet, while the rebloated 5× (1.99B accounts) sits one log-step higher. The theory-vs-measured plot is on the Results page, under eth_getProof witness size.

Storage-proof size

For a storage slot read on contract cc the proof is account-path + storage-path:

proofslot(c)  =  proofacc(n)+c1log16(slots(c)).\text{proof}_{\text{slot}}(c) \;=\; \text{proof}_{\text{acc}}(n) + c_1 \cdot \log_{16}(|\text{slots}(c)|).

That second term is what makes long-tail contracts disproportionately expensive — see problems.

Block execution latency

For each measured block BB we record wall-clock execution time

Texec(B)=tend(B)tstart(B),T_{\text{exec}}(B) = t_{\text{end}}(B) - t_{\text{start}}(B),

reported as median, p95, and p99 over a window of 1024 consecutive blocks.

Witness density

The bytes of proof emitted per unit of gas consumed:

ω(B)=iaccesses(B)proof(i)gasUsed(B).\omega(B) = \frac{\sum_{i \in \text{accesses}(B)} |\text{proof}(i)|}{\text{gasUsed}(B)}.

This is the metric statelessness has to drive down. A pre-Verkle baseline is the whole point.

Resilience metrics

These describe how a fresh node gets to a measured state.

Snap-sync time-to-tip

For a sync starting at wall time t0t_0 that reaches the chain head at t1t_1:

Tttt=t1t0.T_{\text{ttt}} = t_1 - t_0.

We split into bulk-phase time and heal-phase time, because in practice they scale differently.

Steady-state mutation rate

The fraction of leaves the source node changes during a sync of duration TtttT_{\text{ttt}}:

μ=leaves mutated during Ttttleaves in snapshot.\mu = \frac{|\text{leaves mutated during } T_{\text{ttt}}|}{|\text{leaves in snapshot}|}.

If μ\mu approaches the rate at which the syncing node ingests new ranges, the snap-sync algorithm enters a refetch loop. The 10× ladder is partly designed to find that crossover.

The benchmark round

Every milestone (1× / ~3.5× / 5× / 7× / 10×) runs the same battery across every EL client that supports it — Nethermind, Geth, Besu, Reth, Erigon (issue #21):

Sync — snap-sync wall time from a peer at the milestone state; peak/sustained download bandwidth; disk IOPS and throughput; peak and steady-state RSS; final on-disk DB size.

Execution under live head — block processing time p50/p95/p99 over a fixed 10k-block window; gas processed per second; CPU %; RSS growth rate.

JSON-RPC latency under read load — concurrent clients hitting random recent blocks: eth_getBalance, eth_getStorageAt, eth_call, eth_getProof (account + storage, with proof byte sizes), eth_getCode.

Output is per-milestone {client, state_size, metric, p50, p95, p99, mean} JSON/CSV committed to benchmarks/<milestone>/, plus cross-client Grafana deltas. A metric that degrades >25 % vs the previous milestone is flagged as a regression. Acceptance requires reproducibility within ±5 %.

What we deliberately don’t measure (in the headline)

  • Gas/sec throughput against benchmark workloads — too easy to game with a hand-picked workload.
  • Disk-byte counts — too dependent on the underlying filesystem and compression.
  • CPU utilisation — too dependent on host topology.

These appear in per-run appendices for reproducibility, never in the comparison.