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.
Where it lands in Jayverse
- Devnet: decide node operating cost vs queryable history range up front. For the hosted Anvil and any future L2, set the pruning/archive tradeoff before an incident needs a past-state query no node was configured to answer.
- Verex: build the event-indexed DB as the reconstructible source of truth. Market history, position snapshots and settlement verification should read from your own indexer, so a pruned node is enough and archive-node dependency drops out of the critical path.
- gitboard: surface indexer completeness and lag as a dashboard metric. The archive-node-avoidance plan only holds if the indexed DB stays caught up with chain history — make that visible, not assumed.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| in principle | 원칙적으로는 · "A node can in principle store both" |
| diverge | (방식이) 갈라지다, 달라지다 · "Sync methods also diverge" |
| patch whatever changed | 바뀐 부분만 메꿔 넣다 · "patches whatever changed during the sync" |
| healing phase | 동기화 뒤 어긋난 부분을 보정하는 단계 · "in a healing phase" |
| arrive late | 뒤늦게 들어오다 · "arrives late" |
| queryable range | 조회 가능한 범위 · "queryable range both need to be decided" |
| yield (the latest state) | (결과로) ~을 내놓다 · "it only yields the latest state" |
| range proof | 레인지 증명(range proof) · 다운로드한 키-값 구간이 상태 루트와 일치함을 검증하는 암호 증명. "against the state root with a range proof" |
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/.