Pruning, Archive Nodes, and Snap Sync TODO
Concept
A node can in principle store both history — blocks, receipts — and the state trie at every point in time, but keeping all of it forever is more disk than anyone can afford. Pruning deletes past state-trie nodes that aren't needed to serve the latest state; a pruned node can still answer current balance/storage queries, but not state queries at some old block. An archive node keeps every past state, enabling lookups and re-execution at any block, at a much higher storage cost. Sync methods also diverge: unlike full sync, which re-executes every block, snap sync downloads state as flat key-value ranges rather than individual trie nodes, verifies each range against the state root with a range proof, and then patches whatever changed during the sync in a healing phase. That makes snap sync much faster, but it only yields the latest state — not history.
When a request to 'look up a balance or position at some past block' arrives late, and there's no archive node or separate indexer, there's simply no way to answer it. Node operating cost and queryable range both need to be decided together, up front.
Code & Formula
# 프루닝·아카이브·스냅 싱크 — 오래된 상태 트라이를 지운 프루닝 노드와 전부 보관한 아카이브 노드의 조회 가능 범위 차이.
# 프루닝 노드는 지운 과거 블록의 상태를 조회할 때 "missing trie node"에 해당하는 오류를 낸다.
class ChainState:
def __init__(self):
self.history = {} # block_number -> {account: balance}
def commit_block(self, block_number, state):
self.history[block_number] = dict(state)
class PrunedNode:
def __init__(self, chain, keep_last=3):
self.chain = chain
self.keep_last = keep_last
def get_balance(self, block_number, account):
latest = max(self.chain.history)
if block_number < latest - self.keep_last + 1:
raise LookupError(f"missing trie node: block {block_number} state pruned")
return self.chain.history[block_number].get(account)
class ArchiveNode:
def __init__(self, chain):
self.chain = chain
def get_balance(self, block_number, account):
return self.chain.history[block_number].get(account) # 모든 과거 상태 보존
chain = ChainState()
balance = 1000
for block in range(10):
balance += 10
chain.commit_block(block, {"alice": balance})
pruned = PrunedNode(chain, keep_last=3)
archive = ArchiveNode(chain)
print("최신 블록(9) 잔고 - pruned:", pruned.get_balance(9, "alice"), "/ archive:", archive.get_balance(9, "alice"))
try:
pruned.get_balance(2, "alice")
except LookupError as e:
print("오래된 블록(2) 조회 - pruned 노드:", e)
print("오래된 블록(2) 조회 - archive 노드:", archive.get_balance(2, "alice"))
def snap_sync(chain, latest_block):
# 실제로는 range proof 검증이 들어가지만, 여기선 "최신 상태 스냅샷만 받는다"는 특성만 재현
return dict(chain.history[latest_block])
synced_state = snap_sync(chain, latest_block=9)
print("snap sync로 받은 상태(최신만):", synced_state, "— 과거 블록 상태는 없음")
docs/code/algorithms/algorithms-74.py
Exercise
Call eth_getBalance for the same account at the latest block and at a very old block number, and directly compare the responses — including whether a pruned node returns a 'missing trie node' error — against an archive endpoint's.
Practical Connection
Since Verex's market history, position snapshots, and settlement verification may all require access to past state, reducing archive-node dependency by indexing events into your own reconstructible DB is the better path.
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/.