BitrootBlog
Back to website ↗
© 2026 Bitroot · Content is for general information only and is not financial, investment, legal, or tax advice.
Editorial StandardsBack to website
← All articles
EVM foundations·2026/09/27·About 14 min

World State and the Merkle Patricia Trie: How stateRoot Commits to Everything

The stateRoot field in a block header is only 32 bytes, yet it binds every account, balance, and contract storage slot across the network. This piece takes apart the node structure of the Merkle Patricia Trie and the design of the storage trie hanging beneath each account, explains how Merkle proofs support off-chain verification, and asks who ends up paying for state bloat.

An Ethereum block header contains a 32-byte field called stateRoot. It holds no account data of its own, yet it claims to pin down every account's balance, nonce, contract code, and contract storage across the whole network at once: if this hash matches, you know which version of the state you are looking at. The field is the root hash of the Merkle Patricia Trie (MPT), and it is the only commitment the execution layer makes about the world state.

To grasp how much weight that commitment carries, three things have to be taken apart: the structure of the tree, why a root hash can bind the entire state, and what it costs to maintain the tree. The third is the starting point for the later discussion of state sharding and replacing the state tree.

Three tries in a single block header

An Ethereum block header carries three trie roots that describe execution results: stateRoot (the world state trie), transactionsRoot (the transaction trie), and receiptsRoot (the receipt trie). The Yellow Paper writes them as H_r, H_t, and H_e. After the Shanghai upgrade the header also gained withdrawalsRoot (H_w) for the withdrawal list, structured like the transaction trie but with less data per withdrawal and fewer withdrawals per block. The Yellow Paper lists “H_r equaling the state root obtained after executing every transaction in the block in order and then executing every withdrawal” as one of the header's validity conditions. stateRoot describes cumulative state; the other roots describe only this block.

trieKeyValueLifetime
World state triekeccak256(account address)RLP-encoded account four-tupleUpdated continuously across blocks
Transaction trierlp(index of the transaction within the block)rlp(transaction); for typed transactions, the type prefix concatenated with the encoded transactionOne per block, never modified afterward
Receipt trierlp(index of the transaction within the block)Typed receipt, or rlp([status, cumulativeGasUsed, logsBloom, logs])One per block, never modified afterward

The transaction trie and the receipt trie have as many leaves as the block has transactions, so proving that a given transaction was included costs the same no matter how long the chain is — it depends only on the size of this block. The world state trie is globally unique, and its size grows with the number of accounts and storage slots on the network. The logsBloom in the block header is aggregated from the log addresses and topics in the receipts and serves as a probabilistic filter for queries; it is an index accelerator, not a commitment.

Addresses enter the trie only after hashing, and an account is a four-tuple

The key in the world state trie is keccak256(account address), and the value is the RLP-encoded account four-tuple [nonce, balance, storageRoot, codeHash]. The address itself does not enter the trie; its 256-bit hash does: 32 bytes correspond to 64 nibbles, which means at most 64 levels from root to leaf.

Each of the four fields has a precise meaning. nonce is the number of transactions the account has sent, which for a contract account also includes the contracts it has created. balance is the balance denominated in wei. codeHash is the keccak256 of the account's code, and the code itself is stored in the state database under that hash, so contracts with identical bytecode share the same data. storageRoot is the root of another trie, one that belongs to this account alone.

The structure is therefore a tree inside a tree: a leaf node of the world state trie holds some account's storageRoot, and that root points at the account's own storage trie. The keys of the storage trie are keccak256(32-byte slot number), and the values are the RLP encoding of the slot's contents (256 bits). A slot value of zero is equivalent to the slot not existing under the specification, so writing a slot back to 0 has the effect, at the state layer, of deleting it.

Hashing the keys first lets the hash distribution determine the shape of the tree, so an attacker cannot mold it by choosing keys. If addresses or slot numbers were used directly as keys, an attacker could pick a batch of keys sharing a long prefix and squash a subtree into an extremely deep chain, inflating access and proof costs; once keccak256 spreads the keys uniformly, that construction no longer works. In arguing for a new tree structure, the EIP-8297 draft also lists hash-determined positions — and therefore balance — as a design rationale. The price is that slot numbers are not invertible: a contract cannot walk its storage trie backward to recover which slots it has written, and outsiders can only query known slot numbers one by one.

Two empty-value constants are worth remembering, because the proofs and verification that follow both use them: the root of an empty trie, and the hash of empty account code.

# pip install eth-utils rlp
import rlp
from eth_utils import keccak

# Root of the empty trie: keccak over the RLP-encoded empty byte string
print(keccak(rlp.encode(b"")).hex())
# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421

# Hash of empty account code
print(keccak(b"").hex())
# c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470

Three node types and a two-bit flag: how 64-level paths get shortened

The MPT is a 16-ary (hexary) tree, not a binary tree. Appendix D of the Yellow Paper defines three node types plus an empty node. A branch node has 17 items: the first 16 correspond to the 16 possible values of the next nibble, and the 17th is reserved for the case where the key ends here. An extension node is [encodedPath, key], skipping a shared prefix of at least two nibbles. A leaf node is [encodedPath, value], carrying the remainder of the key and the final value. The empty node is represented by an empty byte string. The specification also has an invariant that is easy to overlook: a branch node with only one non-zero item is not allowed. That is why a given set of key-value pairs has exactly one encoding, and why the root hash has a determinate value.

Paths are organized by nibble rather than by byte, which calls for a compact encoding that packs two facts — the parity of the path length and the node type — into the first nibble:

First nibbleBinaryNode typePath length
00000extensioneven
10001extensionodd
20010leafeven
30011leafodd

For even lengths, a further 0 nibble is appended after the first nibble so that the total nibble count is even and the result can be packed into a byte string. The reference implementation from ethereum.org follows (the return value is presented here as bytes):

def compact_encode(hexarray):
    # A leaf path ends with 16; when detected, strip it and set the leaf flag
    term = 1 if hexarray[-1] == 16 else 0
    if term:
        hexarray = hexarray[:-1]
    oddlen = len(hexarray) % 2
    flags = 2 * term + oddlen                 # The first nibble encodes both type and parity
    if oddlen:
        hexarray = [flags] + hexarray
    else:
        hexarray = [flags] + [0] + hexarray   # Even length: pad with a 0 nibble
    return bytes(16 * hexarray[i] + hexarray[i + 1] for i in range(0, len(hexarray), 2))

Path compression explains why the theoretical depth of 64 nibbles is nowhere near fully used on mainnet today: the EIP-8297 draft estimates in its motivation section that the account trie currently has a maximum depth of about 12 levels. That is the draft's own estimate, not a measured mainnet dataset; the shallower the tree, the shorter the proofs.

How nodes reference one another is governed by the 32-byte rule. If a child node's RLP encoding is under 32 bytes, its contents are inlined directly into the parent; at or above 32 bytes, the parent stores only keccak(RLP(child node)) as the reference. The rule saves a great many one-off reads of small nodes, at the cost that proof verification has to reassemble nodes in their actual encoded form rather than assuming that every level is a separate database lookup.

stateRoot is a commitment to the entire state

Folding the whole structure into a single hash takes one line: take the keccak256 of the root node's RLP encoding. The reason the Yellow Paper gives when describing the world state is direct — the root node cryptographically depends on all the data inside it, so this hash can serve as the secure identity of the entire system state.

The strength of the commitment comes from how references are made. A parent references a child by the child's hash, so a change to any single bit in any leaf changes its own node and then propagates level by level up to the root. Given keccak256's collision resistance, finding two different states that produce the same root is equivalent to finding a hash collision.

Three properties follow. A root hash uniquely determines one set of key-value pairs. Old state can be retrieved by root, because nodes are content-addressed and immutable in structure: as long as the nodes are still in the database, knowing an old root is enough to reconstruct the state as it was then. A verifier can also rebuild the commitment along a path — the sibling hashes on the path are enough to recompute from a given leaf up to the root — and Appendix D of the Yellow Paper records the space requirement of a proof as O(log N).

The definition in the block header also fixes the point in time: stateRoot is the state root “after executing all transactions and withdrawals in the block and applying final processing.” It commits to the key-value set at that moment, not to how the state became what it is: different histories can converge on the same state, and the same root may repeat across several consecutive blocks. Nor does it commit to the availability of the state. The root hash itself contains no node data, so verifying any account requires obtaining the nodes along the path separately.

From root to account: how a Merkle proof is used

An account proof is a list of nodes, starting at the root node and walking down the nibbles of keccak256(address): at each level it either hits the corresponding item of a branch or the path prefix of an extension or leaf. The verifier's actions are mechanical: recompute keccak(RLP(node)) for the first node and confirm that it equals the known stateRoot; decode it, follow the nibbles to the next reference, and repeat down to a leaf; the value in that leaf is the RLP-encoded account. Proving that an account does not exist works the same way, and the key is to hand over the last matching node on the path: if it is a branch, the corresponding branch is empty; if it is a leaf, it diverges from the target path at some nibble. A storage proof takes a second pass, with the starting point changed from stateRoot to the account's storageRoot.

EIP-1186 standardizes this process as eth_getProof; here is a sample request (the address and slot number can be substituted):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_getProof",
  "params": [
    "0x7f0d15c7faae65896648c8273b6d7e43f58fa842",
    ["0x0000000000000000000000000000000000000000000000000000000000000000"],
    "latest"
  ]
}

In the response, accountProof is an array of nodes starting from stateRoot, while each entry in storageProof starts from that account's storageRoot. Both are just data; whether they can be trusted is decided by the verifier's own recomputation.

Light clients such as Helios work along these lines: a consensus-layer light client first verifies the block header from the beacon chain, obtains an authenticated execution-layer payload and stateRoot, then asks any untrusted RPC for eth_getProof, and finally performs the MPT verification locally. EIP-1186's motivation section also notes that proofs of this kind let IoT devices and mobile apps verify account and storage data from untrusted sources using nothing but a trusted block hash.

The boundaries of the commitment: proofs, old roots, and light clients

A proof has a clear ceiling on what it can do. It covers only the path being proved and cannot say anything about the rest of the state; proof size grows with path depth and branch width. The EIP-8297 draft illustrates the order of magnitude with a set of sample estimates: assuming a maximum account-trie depth of about 12 levels, an account branch needs 12 levels with 15 sibling hashes each, for a total of 15 × 32 × 12 = 5760 bytes; if all 60M gas were spent touching single bytes of a large number of different contract codes, with the code unchunked, the draft estimates a proof size of about 1.8 GB. These are the draft's own estimates, not rechecked against mainnet measurements; the directional conclusion can be accepted, but the specific figures should not be treated as benchmarks. This is also why the MPT is considered unfriendly to validity proofs: RLP encoding, Keccak hashing, the tree-inside-a-tree structure, and the fact that code cannot be proved segment by segment.

Proofs for old roots are not available forever. The commitment stays in the block header, but whether it can be honored depends on how the client stores state. Geth's path-based archive since v1.16 keeps historical diffs of the flat state, so historical state can be read, but on v1.16.x it cannot generate Merkle proofs for old roots; only from v1.17 onward can historical proofs be supported by explicitly retaining trie history. The traditional hash-based archive keeps historical trie nodes and can produce proofs for any old root. Disk costs come in the next section. The semantics of the commitment belong to consensus; the way it is honored is an implementation choice.

Execution-layer light clients have also gone through a retrenchment. Mainnet's LES protocol failed to work reliably for a long time after the Merge, and Geth removed the related code at the end of 2023. Off-chain verification today more often takes the combined form of “the consensus layer authenticates the root, the execution layer supplies the proof,” and a consensus-layer light client verifies beacon chain block headers without re-executing transactions itself.

State bloat: the bill for the commitment lands on disk

stateRoot's expressive power is independent of state size, while the cost of maintaining it is proportional to state size. Every state update rewrites the whole path from leaf to root, and the deeper and wider the state, the more reads and writes each transaction requires; a new node catching up with the chain also has to pull this state down in full.

A November 2025 analysis on the Ethereum Research forum offers a set of orders of magnitude. It reports that in May 2025 a state-only Geth node had an uncompressed database of about 340 GiB; after the gas limit was raised from 30M to 36M, the median daily state growth doubled from about 102 MiB to about 205 MiB. The bloatnet project page marks 650 GB as a critical threshold, saying that near that size state access time grows by about 40% and memory usage and sync time get noticeably worse; the page provides no reproducible benchmark data, so the wording here follows the project's own account, and it uses GB rather than GiB. The forum analysis then extrapolates along three gas-limit paths — conservative, baseline, and aggressive (rising to 200M, 400M, and 700M respectively by mid-2027) — and arrives at a total state size between 686 GiB and 1.08 TiB in mid-2027. This is a scenario extrapolation, not an established fact: the values depend on client implementation, compression and pruning strategy, and where the gas limit actually goes.

In node operations, ethereum.org puts the disk requirement for a Geth full node (snap sync) at over 500 GB; Geth's own figures for archive nodes are about 2 TB for path-based, about 6.5 TB when retaining historical trie data, and over 20 TB for hash-based, while ethereum.org's cross-client archive requirement ranges from 3 TB to over 12 TB. These numbers depend on client implementation, compression method, pruning strategy, and the gas limit, so comparing them directly across implementations is meaningless; state size is also not the same as on-chain history size — historical transactions and receipts are a separate account.

Ethereum mainnet's state only grows and never shrinks: once an account or storage slot is written, it occupies space permanently, with no state expiry or state rent mechanism, so the pressure accumulates in one direction. There are roughly two directions for relief: replace the tree structure, for example the long-discussed Verkle tree, or the still-draft Partitioned Binary Tree (the EIP-8297 draft); or cut the state apart so that a single node maintains only a portion of it. The latter is the subject of a later article; here it is enough to note where the pressure comes from. For how slots are laid out at the contract layer, see 0.19 “Storage Layout”.

Sources

  • Ethereum Yellow Paper: the state transition function in Section 2, the block header fields and overall validity in Chapter 4 (the definition and validation conditions of stateRoot), the MPT node definitions in Appendix D, hex-prefix encoding, the 32-byte inlining rule, and the O(log N) proof space of D.1.
  • ethereum.org: Merkle Patricia Trie: node types, the reference compact encoding, the key and value definitions of the three tries, and the value encodings for transactions and receipts.
  • ethereum.org: Ethereum accounts: the official wording for storageRoot and codeHash.
  • EIP-1186: RPC-Method to get Merkle Proofs: the fields of eth_getProof, how non-existence is proved, and the usage scenarios; the official example's empty storageHash (0x56e81f…) and empty codeHash (0xc5d246…) also serve to check the two empty-value constants in the code block above.
  • EIP-8297: Partitioned Binary Tree (Draft, created 2026-06-11, hash function not yet finalized): the account trie's maximum depth of about 12 levels, the 5760-byte branch proof, the worst-case proof size of about 1.8 GB, and why the MPT is unfriendly to validity proofs. Each of these items is marked in the body as the draft's own estimate.
  • Ethereum Research: State growth scenarios and the impact of repricings (2025-11-19): the 340 GiB state size, the daily growth from 102 MiB to 205 MiB, and the mid-2027 scenario extrapolation along three gas-limit paths. That piece is a scenario analysis, not an established fact.
  • Bloatnet Initiative: the project's own account of the 650 GB critical threshold and the roughly 40% increase in state access time; the page includes no reproducible benchmark data.
  • go-ethereum: Archive mode: the 2 TB and 6.5 TB figures for path-based archives, hash-based archives exceeding 20 TB, and the difference between v1.16.x and v1.17 in historical proof support.
  • ethereum.org: Ethereum archive node and Spin up your own Ethereum node: the cross-client disk requirement ranges for archive and full nodes (archive 3 TB to over 12 TB; Geth snap sync over 500 GB).
  • go-ethereum PR #28586: removal of LES and the related light client code.
  • a16z crypto: Building Helios: the light client path of authenticating stateRoot at the consensus layer and verifying locally at the execution layer with eth_getProof.

Further reading

The Tension Between Decentralization and Performance: Validator Requirements, Hardware, and Geographic DistributionRelatedBitroot Multi-Engine Parallel Execution: Scheduling, Sharding, and the Conflict SurfaceRelatedWhy a Single-Threaded EVM Caps TPS: Congestion History and the Execution ModelNext
← PreviousGas Mechanics in Detail: Metering Units, the Fee Market, and Execution Halts
Contents
Three tries in a single block headerAddresses enter the trie only after hashing, and an account is a four-tupleThree node types and a two-bit flag: how 64-level paths get shortenedstateRoot is a commitment to the entire stateFrom root to account: how a Merkle proof is usedThe boundaries of the commitment: proofs, old roots, and light clientsState bloat: the bill for the commitment lands on diskSourcesFurther reading
Reading settings