The State Trie Storage Problem — Flat DBs and Path-Based Storage TODO
Concept
Ethereum state is logically a Merkle Patricia Trie, but storing that structure directly in a key-value database means reading a single account requires several random lookups from root to leaf, and since nodes are keyed by hash there's no storage locality at all. So execution clients maintain a separate flat/snapshot layout that stores accounts and storage slots directly under flat keys, turning a read into a single lookup, while keeping the trie itself around only for computing the root and generating proofs. Trie node storage splits into hash-based schemes, which key a node by the hash of its contents, and path-based schemes, which key a node by its position in the trie; path-based storage overwrites the previous version at the same path to curb disk growth, but then needs a separate rollback journal to support querying historical state. In the end the design is a tradeoff among read speed, disk growth, retention of past state, and proof-generation capability — and the state-growth problem is fought on top of exactly this storage-layout and pruning-policy choice.
Node sync speed, disk usage, and archive-node operating cost are all determined by this layout choice — infrastructure-cost discussions are, underneath, actually about this.
Code & Formula
# 상태 트리 저장 문제 — flat DB·경로 기반 스토리지 — 해시 기반 트리 순회 vs 평평한 키-값 조회의 비용을 비교한다.
# 계정 하나를 읽을 때 해시 기반 트리는 루트부터 여러 번의 랜덤 조회가 필요하지만, flat 레이아웃은 조회 1회로 끝난다.
import hashlib
def h(x):
return hashlib.sha256(x).hexdigest()
class HashTrieDB:
"""각 노드가 (부모+값)의 해시로 키잉되어, 리프까지 가려면 노드 수만큼 랜덤 조회가 필요하다."""
def __init__(self):
self.node_content = {} # node_hash -> 원래 값 (증명 등에 쓰이는 실제 데이터)
self.parent_of = {} # node_hash -> parent node_hash (None = 체인의 시작)
self.disk_seeks = 0
def build_path(self, values):
parent = None
for v in values:
node_hash = h((str(parent) + v).encode())
self.node_content[node_hash] = v
self.parent_of[node_hash] = parent
parent = node_hash
return parent # 마지막 노드 해시 (조회 대상)
def get_leaf(self, leaf_hash):
cur = leaf_hash
while cur is not None:
self.disk_seeks += 1 # 노드 하나 읽을 때마다 랜덤 조회 1회
cur = self.parent_of[cur]
return self.node_content[leaf_hash]
class FlatDB:
"""계정 주소를 바로 키로 써서 조회가 O(1)에 끝난다."""
def __init__(self):
self.store = {}
self.disk_seeks = 0
def put(self, key, value):
self.store[key] = value
def get(self, key):
self.disk_seeks += 1
return self.store[key]
DEPTH = 8
trie = HashTrieDB()
leaf_hash = trie.build_path([f"node{i}" for i in range(DEPTH)])
trie.get_leaf(leaf_hash)
flat = FlatDB()
flat.put("account-0xabc", "balance=1000")
flat.get("account-0xabc")
print(f"해시 기반 트리: 계정 1개 조회에 {trie.disk_seeks}회 랜덤 조회 (트리 깊이={DEPTH})")
print(f"flat DB: 계정 1개 조회에 {flat.disk_seeks}회 조회")
print(f"→ flat 레이아웃이 조회를 {trie.disk_seeks}배 줄인다 (트리는 루트 계산·증명 용도로 별도 유지)")
docs/code/algorithms/algorithms-73.py
Exercise
Read an execution client's documentation on its hash-based vs. path-based storage scheme, then table out the cost of three operations — account balance lookup, historical state lookup at a specific past block, and Merkle proof generation — under each scheme.
Practical Connection
When building an indexer for Verex that looks up past market state or balances at settlement time, whether to depend on an archive node or reconstruct state into your own DB from events is decided exactly by this cost structure.
Where it lands in Jayverse
- Number: the research site's historical-reading queries face the same archive-node-versus-reconstructed-DB tradeoff. Decide up front whether Number reconstructs state from events into its own store rather than paying per-query archive-node cost.
- Devnet: document whether the hosted Anvil keeps full historical state or prunes it. This snapshot/pruning policy is the same storage-layout choice, and decides what queries any service can run against devnet.
- gitboard: price any dashboard query of past settlement state against this lookup-cost table. Do this before choosing to hit an archive node live instead of a maintained index.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| flat/snapshot layout | 플랫(평면) 레이아웃 · 트라이를 거치지 않고 계정을 바로 조회하도록 저장하는 방식. "a separate flat/snapshot layout that stores accounts... directly" |
| storage locality | 저장소 지역성 · 데이터가 물리적으로 가까이 있어 접근이 빠른 성질. "there's no storage locality at all" |
| curb | 억제하다, 줄이다 · 디스크 용량 증가를 억누른다는 뜻. "overwrites the previous version... to curb disk growth" |
| underneath | (겉으로 안 보이지만) 근본적으로, 저변에는 · 논의의 진짜 원인이 무엇인지 가리킬 때. "infrastructure-cost discussions are, underneath, actually about this" |
| fought on top of | ~을 기반으로 다투어지다, 그 전제 위에서 벌어지다 · 문제 해결이 특정 선택을 전제로 이루어짐을 표현. "the state-growth problem is fought on top of exactly this... choice" |
| rollback journal | 롤백 저널(되돌리기 기록) · 과거 상태 조회를 위해 변경 이력을 따로 남기는 장치. "needs a separate rollback journal to support querying historical state" |
| Merkle Patricia Trie | 머클 패트리샤 트라이(MPT) · 이더리움 상태를 저장하는 해시 기반 트리 자료구조. "Ethereum state is logically a Merkle Patricia Trie" |
| archive node | 아카이브 노드 · 과거 모든 블록의 상태를 보존해 과거 시점 조회를 가능하게 하는 풀노드. "whether to depend on an archive node or reconstruct state" |
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.