Ξ
Storage Model
Ethereum
Merkle Patricia Trie (MPT)

Ethereum stores all account state in a Merkle Patricia Trie. The key = keccak256(address). The trie is 16-way (hexadecimal nibbles). This enables cryptographic proofs of any account state.

Interactive: How an Address Becomes a Trie Key

Type any Ethereum address and see exactly how it gets hashed and traversed through the MPT.

MPT Key Formation
1Start with Ethereum Address
0x742d35Cc6634C0532925a3b844Bc454e4438f44e0x742d35Cc6634C0532925a3b844Bc454e4438f44e

An Ethereum address is 20 bytes (40 hex chars). This is the input to the state trie key derivation.

2keccak256(address)
0x742d35Cc6634C0532925a3b844Bc454e4438f44e0x48323d7fabcdef9876543210
3Split into Nibbles
0x48323d7fabcdef98765432104 → 8 → 3 → 2 → 3 → d → 7 → f → a → b → c → d → e → f → 9 → 8
4Traverse 16-way Trie
nibbles[0] = '4'root → child['4'] → child['8'] → ...
5Read Account Data
Leaf node reachedRLP(nonce, balance, storageRoot, codeHash)
Final DB Key:
RLP(nonce, balance, storageRoot, codeHash)
keccak256 → nibbles (first 16 shown)
4
8
3
2
3
d
7
f
a
b
c
d
e
f
9
8
Trie traversal (click nibble to highlight):
root
└── child[4] ← current
└── child[8]
...
Full depth = 64 nibbles → leaf with account state

MPT Node Types — 3 Types, 1 Trie

🌿Branch Node

16-element array [child0...child15, value]. At each branch, we follow the next nibble. The array is always length 17.

[child_a, child_b, ..., child_f, optional_value]
When used: When the key prefix is shared by multiple accounts
🔗Extension Node

Compressed shared prefix: [encoded_path, next_node]. Instead of one branch per nibble, skip identical nibbles.

[encoded("ab3"), → next_node_hash]
When used: When a long prefix is shared — saves space
🍃Leaf Node

End of path: [remaining_path, value]. Contains the compressed remaining nibbles and the actual account data.

[encoded("...remaining"), RLP(account)]
When used: When we've uniquely identified an account
mpt-nodes.jsjs
1// Ethereum MPT — 3 node types in LevelDB
2// DB key = keccak256(RLP(node))
3// DB val = RLP(node)
4
5// 1. BRANCH NODE — 16 children + optional value
6branch_node = [
7 child[0], // pointer to child for nibble '0'
8 child[1], // pointer to child for nibble '1'
9 // ...
10 child[15], // pointer to child for nibble 'f'
11 value // optional value at this path
12]
13
14// 2. EXTENSION NODE — compressed shared prefix
15extension_node = [
16 "shared_prefix", // hex-encoded nibbles (compressed)
17 next_node_hash // pointer to next node
18]
19
20// 3. LEAF NODE — compressed remaining path + value
21leaf_node = [
22 "remaining_path", // nibbles not yet traversed
23 value // the account data (RLP encoded)
24]

Full Traversal — Step by Step

trie-traversal.jsjs
1// How keccak256(address) becomes a trie path
2
3address = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
4
5Step 1: Hash the address
6key = keccak256(address)
7 = "0xabc123def456..." (32 bytes = 64 hex chars)
8
9Step 2: Split into nibbles (0-f)
10nibbles = ['a','b','c','1','2','3','d','e','f','4','5','6'...]
11
12Step 3: Traverse the 16-way trie
13root → [branch at 'a'] → [branch at 'b'] → [branch at 'c'] → ...
14 ↓ ↓ ↓
15child[a] child[b] child[c]
16 ↓ ... → LEAF (account data)
17
18// At each branch: follow the next nibble
19// At extension: skip the shared prefix
20// At leaf: read the value

What's Stored at the Leaf?

account-data.jsjs
1// What's stored at a leaf node (Ethereum account)
2account_value = RLP([
3 nonce: 42, // how many txs sent
4 balance: 1.5 ETH, // in wei (1.5e18)
5 storageRoot: 0xdef456..., // root of THIS account's storage trie
6 codeHash: 0x789abc... // keccak256(bytecode), 0x empty for EOA
7])
8
9// For smart contracts, storageRoot points to ANOTHER MPT:
10// contract_storage_trie[keccak256(slot)] = value
11// e.g., ERC20 balances[address] stored as:
12// key = keccak256(abi.encode(address, slot_index))
👤

EOA vs Contract Account

Externally Owned Account (EOA)
codeHash = keccak256("") // empty
storageRoot = empty_trie_root
Smart Contract
codeHash = keccak256(bytecode)
storageRoot = root of its own storage trie
📦

Storage Trie (per contract)

Each contract has its own second-level MPT. ERC20 balances are stored here:

key = keccak256(address ++ slot)
val = RLP(balance)

This is why Ethereum has 4 tries: state, storage, transactions, receipts.

RLP Encoding — Ethereum's Wire Format

Every node in the trie is encoded with RLP before being hashed and stored in LevelDB.

rlp-encoding.jsjs
1// RLP Encoding — Ethereum's serialization
2// (Recursive Length Prefix)
3
4RLP(string "dog") = [ 0x83, 'd', 'o', 'g' ]
5 ↑ 0x83 means "3-byte string"
6
7RLP(list [1, 2]) = [ 0xC2, 0x01, 0x02 ]
8 ↑ 0xC2 means "2-byte list"
9
10// Account encoded:
11RLP([nonce, balance, storageRoot, codeHash])
12→ 0xf8 44 01 88 0de0b6b3a7640000 a0 def456... a0 789abc...
13 ↑list ↑nonce ↑balance(8 bytes) ↑storageRoot(32) ↑codeHash(32)

4 Tries Per Block

block-tries.jsjs
1// Ethereum block — 4 separate tries
2block = {
3 // Each block has these Merkle roots:
4 stateRoot: 0xabc..., // root of global state trie
5 transactionsRoot: 0xdef..., // root of all block txs
6 receiptsRoot: 0x789..., // root of all tx receipts
7
8 // State trie:
9 // keccak256(address) → RLP(nonce, balance, storageRoot, codeHash)
10
11 // Storage trie (per contract):
12 // keccak256(32-byte-key) → RLP(value)
13}
14
15// Proof that Alice has 1 ETH (Merkle proof):
16// stateRoot → [node hash 1, node hash 2, ..., leaf]
State Trie
Global account state. keccak256(address) → account
Storage Trie
Per-contract. keccak256(slot) → value
Tx Trie
Block's transactions. index → tx
Receipt Trie
Block's receipts. index → receipt

How MPT Storage → Capabilities & Limitations

Merkle tree → cryptographic state root

Any account state can be proven with a Merkle proof. Light clients can verify state without full node.

⚠️
16-way trie with keccak256 keys

Every write to account state touches O(log₁₆ n) nodes — each must be re-hashed and re-stored in LevelDB. High write amplification.

⚠️
4 separate tries per block

Rich data model — query state, txs, receipts all independently. But 4× the Merkle overhead.

⚠️
Sequential execution required

Transactions can read/write any account — must run in order to detect conflicts. No native parallelism.

Storage trie per contract

Smart contracts have isolated, provable storage. Powers DeFi composability.