Workspace IndexAlgorithms › Day 74

Pruning, Archive Nodes, and Snap Sync TODO

Algorithms · Day 74 / 100 · E. Data & Storage Engines (Day 69-81)

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, "— 과거 블록 상태는 없음")

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/.


한국어

프루닝·아카이브·스냅 싱크 TODO

Algorithms · Day 74 / 100 · E. 데이터·스토리지 엔진 (Day 69–81)

개념

노드는 블록·영수증 같은 히스토리와 각 시점의 상태 트라이를 모두 저장할 수 있지만, 전체를 영구 보관하면 디스크가 감당되지 않는다. 프루닝은 최신 상태를 유지하는 데 필요 없는 과거 상태 트라이 노드를 지우는 것으로, 이 노드는 현재 잔고·스토리지 조회는 되지만 오래된 블록 시점의 상태 조회는 못 한다. 아카이브 노드는 모든 과거 상태를 보존해 임의 블록 시점의 조회와 재실행이 가능한 대신 저장 비용이 훨씬 크다. 동기화 방식도 갈리는데, 모든 블록을 재실행하는 full sync와 달리 snap sync는 상태를 트라이 노드 단위가 아니라 평면 키-값 구간 단위로 내려받고 각 구간을 상태 루트에 대한 range proof로 검증한 뒤, 동기화 중 변한 부분을 healing 단계에서 메운다. 그래서 snap sync는 훨씬 빠르지만 최신 상태만 얻고 과거 상태는 얻지 못한다.

"과거 블록 시점의 잔고나 포지션을 조회하라"는 요구가 뒤늦게 들어오면 아카이브 노드나 별도 인덱서가 없어 대응이 불가능해진다. 노드 운영 비용과 조회 가능 범위는 처음부터 같이 결정해야 한다.

코드 · 수식

# 프루닝·아카이브·스냅 싱크 — 오래된 상태 트라이를 지운 프루닝 노드와 전부 보관한 아카이브 노드의 조회 가능 범위 차이.
# 프루닝 노드는 지운 과거 블록의 상태를 조회할 때 "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, "— 과거 블록 상태는 없음")

연습

같은 계정에 대해 최신 블록과 아주 오래된 블록 번호로 eth_getBalance를 호출해, 프루닝된 노드와 아카이브 엔드포인트의 응답 차이(missing trie node 오류 여부)를 직접 확인하라.

실무 · Verex 연결

Verex의 시장 이력·포지션 스냅샷·정산 검증은 과거 시점 상태 접근을 요구할 수 있으므로, 아카이브 의존을 줄이려면 이벤트를 자체 DB로 인덱싱해 재구성 가능한 형태로 쌓아 두는 편이 낫다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 73. 상태 트리 저장 문제75. 인덱싱 파이프라인 설계 →