Deep Storage Performance Comparison
A detailed, beginner-friendly guide explaining how different blockchains store data internally, why some are faster than others, what a "Merkle proof" actually is, and every term explained — no jargon left behind.
🔐 1. What is a Merkle Proof?
What problem does it solve?
Blockchains can have millions of accounts and transactions. If a mobile wallet had to download all of that just to check your balance, it would take hours. Merkle proofs let a light client (e.g. a phone app) ask a full node: "Can you prove my account has 5 ETH?" and get a tiny proof back that it can verify in milliseconds — without trusting the full node blindly.
How does it work — step by step?
Visual — Merkle Tree
Blue = proof path. To prove B is in the tree, you only need hash(A) and H2 — not C or D.
What a proof contains in real systems
- • Multiple trie nodes (branch, extension, leaf)
- • Each node has up to 16 child references
- • RLP-encoded (adds byte overhead)
- • Typical size: 5KB – 15KB
- • A simple list of sibling hashes
- • One hash per "level" in the tree
- • Binary tree = fewer levels
- • Typical size: ~1KB (log₂ n × 32 bytes)
🌳 2. What is a Trie?
Why do blockchains use Tries?
Trie vs Regular Tree — Key Difference
1Regular Binary Tree (BST):2 Node stores: value + left pointer + right pointer3 Search: compare at each node (balanced = O(log n))45Trie:6 Node stores: children indexed by KEY CHARACTERS7 Search: follow the key characters one by one89Example - Ethereum 16-way trie:10 Key = "a7f3..." (hex characters 0-f)11 Root → [a] → [7] → [f] → [3] → ... → leaf (account data)12 Each step: pick the next nibble, follow that branch
🔑 3. How Keys are Formed
Ethereum: Single Hash Key
1// User wants: account data for address 0x742d35...2// Step 1: Hash the address3key = keccak256(0x742d35Cc6634C0532925a3b844Bc454e4438f44e)4 = 0xabc1234567890abc1234567890abc1234567890abc1234567890abc123456789056// Step 2: Convert to nibbles (each hex char = one nibble)7nibbles = [a, b, c, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, a, b, c, ...]8 ↑ ↑ ↑ ↑ total: 64 nibbles910// Step 3: Traverse the 16-way trie, one nibble at a time11root → [a] → [b] → [c] → [1] → ... → leaf (account state)1213// Result: 64 steps deep, completely random order14// No two related addresses will be "near" each other in the trie
Substrate: Composed Structured Key
1// User wants: Balances pallet, Account storage, for Alice2// Step 1: Hash each part separately3pallet_hash = Twox128("Balances") // = 0x26aa394eea5630e07c48ae0c9558cef74storage_hash = Twox128("Account") // = 0xb99d880ec681799c0cf30e8886371da95key_hash = Blake2_128Concat(Alice) // = 0xde1e86a9a8c739864cf3cc5ec2bea59f...67// Step 2: Concatenate all parts8final_key = pallet_hash ++ storage_hash ++ key_hash9 = 0x26aa394e...b99d880e...de1e86a9...1011// Key insight: ALL accounts in Balances pallet share the same PREFIX12// → they are physically grouped together in the database13// → cache hits are much more likely when reading multiple accounts
- • Single keccak256 hash of the address
- • 64 nibbles long (32 bytes)
- • Completely random output — no logical ordering
- • Alice and Bob's accounts are at random spots in the trie
- ❌ Poor cache locality — every read likely causes a cache miss
- ✅ Prevents prefix attacks (no predictable ordering)
- • Pallet hash + storage hash + key hash (concatenated)
- • Structured and human-readable prefix
- • All keys in the same pallet share the same prefix
- • Alice and Bob's accounts are near each other in storage
- ✅ Great cache locality — reading multiple accounts is fast
- ✅ Can enumerate all keys of a storage map efficiently
Hash Functions Used — Explained
A cryptographic hash — extremely secure, output is random-looking. Takes 35–50 CPU cycles per byte. Used for trie node hashing too, so every node access involves a keccak hash.
Used for: Ethereum state/storage key derivation + node hashing
A very fast non-cryptographic hash (xxHash). Not resistant to brute-force key attacks, so it's only used for PALLET and STORAGE names (which are public). ~5× faster than keccak256.
Used for: Pallet name and storage item name prefix hashing
A cryptographic hash that ALSO preserves the original key by appending it. This is used for map keys (like AccountId) where the key must be readable from the full storage key.
Used for: Map keys where user-controlled input needs protection
🗂 4. Node Types & How They're Stored in DB
Ethereum: 3 Fixed Node Types
1// Branch Node structure — always 17 elements2[3 child_0, // reference to child if key[i] == '0'4 child_1, // reference to child if key[i] == '1'5 child_2, // ...6 child_3,7 child_4,8 child_5,9 child_6,10 child_7,11 child_8,12 child_9,13 child_a,14 child_b,15 child_c,16 child_d,17 child_e,18 child_f, // reference to child if key[i] == 'f'19 value // optional value stored at this node20]21// Even if only slots [3] and [7] are used,22// the other 15 slots still take up space (as empty = 0x80 in RLP)
Why this matters: even a sparse trie wastes space in branch nodes. A typical Ethereum state trie has ~100 million accounts, but most branch nodes only use 2–3 of their 16 slots.
How every node is stored in LevelDB
1// For EVERY node (branch, extension, leaf):2DB_key = keccak256(RLP(node)) // 32 bytes — hash of the encoded node3DB_value = RLP(node) // the full encoded node data45// To look up any node, you:6// 1. Receive a 32-byte hash reference7// 2. Do a DB lookup with that hash as key8// 3. Get back the RLP-encoded node9// 4. Decode the RLP to get the actual children / value1011// Cost per node: 1 DB read + 1 RLP decode + 1 keccak hash verification12// For a 64-nibble path: up to 64 nodes × (DB read + decode + hash)
Substrate: One Flexible Node Type
1// Substrate uses the Patricia Merkle Trie (different from Ethereum's MPT)2// Node structure:3Node {4 partial_key: Vec<u8>, // compressed shared path5 children: BTreeMap<u8, NodeRef>, // DYNAMIC — only used children stored6 value: Option<Vec<u8>>, // value if this is a leaf-level node7}89// Key difference: only USED children are stored10// If only 2 of 16 positions are occupied, only 2 children exist11// No wasted space for empty slots1213// DB storage:14DB_key = hash(encoded_node) // 32 bytes15DB_value = scale_encoded(node) // SCALE codec — more compact than RLP
- • 3 separate node types (branch, extension, leaf)
- • Branch always stores 17 slots (16 children + value)
- • Empty slots still take space in RLP encoding
- • Node hash = keccak256(RLP(node))
- • DB key = 32-byte keccak hash
- • Single unified node type
- • Children are dynamic — only used children stored
- • No wasted space for empty positions
- • Node hash = Blake2 or Keccak (configurable)
- • Encoded with SCALE — more compact than RLP
🔢 5. RLP vs SCALE Encoding
What is RLP? (Ethereum's encoding)
1// RLP Encoding Example2// Encoding the string "cat":3// 1. String length = 34// 2. Length prefix = 0x83 (0x80 + 3 for short strings)5// RLP("cat") = 0x83 0x63 0x61 0x7467// Encoding a list ["cat", "dog"]:8// 1. Encode each item: 0x83636174, 0x83646f679// 2. Total payload length = 8 bytes10// 3. List prefix = 0xc8 (0xc0 + 8 for short lists)11// RLP(["cat", "dog"]) = 0xc8 0x83 0x63 0x61 0x74 0x83 0x64 0x6f 0x671213// For a branch node with 16 children:14// EACH child reference is a 32-byte hash15// Plus length prefix for each + list prefix16// Total branch node size: ~600+ bytes even when mostly empty1718// Why this is a problem:19// Every node read = 1 RLP decode operation20// Deep trie = many decode operations per lookup
What is SCALE? (Substrate's encoding)
1// SCALE Encoding Example2// Encoding a u32 integer (e.g. 1000000):3// SCALE: just the 4 bytes in little-endian4// SCALE(1000000) = 0x40 0x42 0x0f 0x0056// For a vector of bytes:7// SCALE: compact-encoded length + raw bytes8// No nested prefix like RLP — simpler structure910// Substrate node encoding:11// - partial_key length + partial_key bytes12// - bitmask of which children exist (2 bytes) ← KEY OPTIMIZATION13// - only the hashes of EXISTING children14// - optional value1516// Example sparse node (2 children out of 16):17// RLP would encode 16 slots (mostly empty = 0x80 each) → ~530 bytes18// SCALE: bitmask (2 bytes) + 2 hashes (64 bytes) → ~70 bytes19// SCALE is ~7.5x smaller for sparse nodes!
- • Recursive, general-purpose encoding
- • Prepends length before every value
- • Empty branch slots = 0x80 each (1 byte × 16 = 16 bytes overhead)
- • Branch node avg size: ~500-600 bytes
- ❌ Decode cost on every node access
- ❌ More bytes = more I/O = slower
- • Flat, optimised binary encoding
- • Uses bitmask (2 bytes) to mark which children exist
- • Only existing children stored — zero overhead for empty
- • Node avg size: significantly smaller
- ✅ Faster encode/decode
- ✅ Less data per node = fewer I/O bytes
⚡ 6. Read Performance — Step by Step
Ethereum — Read Flow
1// Cost analysis for ONE account read in Ethereum:2keccak256(address) → 1 hash operation3convert to 64 nibbles → trivial4traverse ~15-20 nodes:5 × 15 DB reads → disk seeks (expensive!)6 × 15 RLP decodes → CPU work7 × 15 hash verifications → keccak per node8final RLP decode (value) → 1 decode910// Total: ~15 disk reads + ~30 keccak operations + ~15 RLP decodes11// In the WORST case (cold cache, deep path): ~20+ operations
Substrate — Read Flow
Read Cost Comparison
Note: bars show relative overhead (higher % = worse). Absolute numbers vary by hardware and state size.
✍️ 7. Write Performance — Why Ethereum Writes are Expensive
The Write Cascade Problem
1// Writing to Ethereum state (e.g. Alice sends Bob 1 ETH):23// Step 1: Update Alice's leaf node4alice_leaf_new = RLP([nonce+1, balance-1eth, storageRoot, codeHash])5DB[keccak256(alice_leaf_new)] = alice_leaf_new67// Step 2: Alice's parent branch node now has a new child hash8parent_branch_new = [...15_children..., NEW_alice_hash, value]9DB[keccak256(parent_branch_new)] = parent_branch_new1011// Step 3: The grandparent now has a new child hash12// ... repeats all the way to root ...1314// Step N: New state root15new_state_root = keccak256(new_root_node)1617// COST per write:18// depth × (1 RLP encode + 1 keccak256 + 1 DB write)19// For depth=15: 15 encodes + 15 hashes + 15 DB writes2021// Plus: OLD nodes stay in DB (for history) until pruning22// So every write = 2× DB writes (old + new node versions)
Ethereum Write Cost Factors
Every ancestor node must be re-hashed after a leaf changes. Depth of ~15-20 means 15-20 keccak256 operations per write.
Each updated node is re-encoded in RLP before being hashed and stored. CPU cost on every level of the tree.
Ethereum creates new nodes instead of modifying old ones (for history preservation). Each write = new nodes inserted into DB.
A single state update results in ~15 new DB entries (one per updated ancestor node), each requiring a disk write.
Substrate Write Improvements
Substrate can use Blake2b for node hashing (faster than keccak256) or even xxHash for non-security-critical paths.
Compact SCALE-encoded nodes mean each DB write is smaller. Less I/O bandwidth per write operation.
Flexible node structure (only used children stored) means tree rebalancing is cheaper and fewer nodes need updating.
Substrate accumulates writes in an in-memory overlay during block execution, batching all DB writes at block commit time.
Write Cost Comparison
📦 8. Proof Size — Why Ethereum Proofs are Larger
What goes into a proof?
To prove a value in a Merkle trie, you need every node along the path from root to the leaf, PLUS any sibling nodes needed to recompute parent hashes. The more nodes, and the bigger each node, the larger the proof.
Why Ethereum proofs are large
Every branch node in Ethereum has 16 child slots. Even if only 2 are used, all 16 must be included in the proof (verifiers need all sibling hashes to recompute the parent hash). At 32 bytes per hash × 16 slots = 512 bytes per branch node, and a path has ~15 branch nodes.
Each node includes RLP length prefixes and type tags. On a typical branch node this adds 50-100 bytes of overhead that carries no data — just encoding metadata.
With 64-nibble paths and random key distribution, the tree is maximally deep. More depth = more nodes = bigger proof.
Proof Size by Blockchain
Binary tree, O(log₂ n) × 32 bytes. ~20 hashes for 1M txs.
Compact nodes + SCALE encoding. Fewer nodes, smaller nodes.
AVL tree is balanced — path length is O(log n), similar to Substrate.
16-way trie + RLP + random keys = many large nodes in proof.
No global state Merkle trie — cannot generate state proofs for light clients.
Polynomial commitment — one proof for MANY leaves simultaneously!
Why proof size matters in practice
🌐 9. All Chains — Performance Overview
hash(tx) → binary path- •No global account trie
- •UTXO spending = simple key delete + insert
- •Proof = O(log₂ n) hashes
keccak256(address) → nibbles- •64-nibble random path
- •RLP encode/decode per node
- •15+ DB reads per lookup
Twox(pallet)++Twox(storage)++hash(key)- •Structured keys = cache locality
- •SCALE encoding is more compact
- •Flexible node = less waste
module_prefix + key → AVL path- •AVL self-balancing = O(log n) guaranteed
- •Versioned — historical queries built-in
- •Rebalancing on writes adds overhead
pubkey → direct DB key- •No trie at all — pure flat KV
- •Can't generate account state proofs
- •Uses PoH + merkle for tx history only
address ++ suffix → 256-way path- •256-way → tree is much shallower
- •One proof can cover many leaves
- •Enables stateless Ethereum clients
🎯 10. Final Summary & Mental Model
All blockchains use essentially the same underlying database — LevelDB or RocksDB. Both are high-performance key-value stores. The performance difference between Ethereum and Substrate is not the database. It's:
- ▸How the key is derived: Random (keccak256) vs structured (pallet+storage+key)
- ▸How nodes are designed: Fixed 16-slot arrays vs compact flexible nodes
- ▸How data is encoded: RLP (verbose) vs SCALE (compact)
- ▸How hashing is done: keccak256 (expensive) vs Twox128+Blake2 (cheaper)
Final Comparison Table
| Feature | Bitcoin | Ethereum | Substrate | Cosmos | Solana |
|---|---|---|---|---|---|
| Key style | hash(tx) | keccak(addr) | pallet+storage+key | module+key | pubkey direct |
| Tree type | Binary Merkle | 16-way MPT | Custom Trie | AVL (IAVL) | None (flat) |
| Encoding | Custom | RLP | SCALE | Protobuf-like | Custom |
| Node hashing | SHA256 | keccak256 | Blake2/keccak | SHA256 | SHA256 |
| Read cost | Very Low | High | Medium | Medium | Very Low |
| Write cost | Low | High | Medium | Medium | Very Low |
| Proof size | ~1KB | 5–15KB | 2–8KB | 3–8KB | N/A |
| Historical queries | No | No* | No* | ✅ Built-in | No |
| Parallelism | UTXO natural | Limited | Per-pallet | Per-module | Account-based |
| Cache locality | Good | Poor | Good | Good | Excellent |
One-Line Summaries (no jargon)
No account model — just track unspent coins. Simple, fast, can't do smart contracts.
Secure and expressive but heavy — random keys mean poor caching, RLP adds overhead, 16-slot nodes waste space. Security was prioritised over speed.
Structured keys act like a namespace. Nearby data stays nearby in storage. Compact encoding reduces I/O. The right trade-off for a developer-first platform.
Self-balancing AVL tree keeps depth predictable. Versioned by design — historical queries are free. Great for multi-chain use cases via IBC.
Throws out the trie entirely. Direct key-to-account lookup. Maximum speed, minimum overhead — but cannot prove account state to light clients.
Uses polynomial math to make proofs 100× smaller. One proof covers many accounts at once. Will make light clients and stateless nodes practical for Ethereum.