Workspace IndexAlgorithms › Day 75

Designing an Indexing Pipeline — Replayability TODO

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

Concept

An indexing pipeline reads a chain's blocks, logs, and traces and transforms them into a queryable form; replayability is the property that running the whole thing again from the raw source data, at any time, reaches the exact same result. Achieving this requires the transformation logic to be a deterministic pure function — inputs that change on re-run, like the current time, randomness, or external API responses, must never enter the transformation. Progress should be represented as a cursor, such as (block number, log index), and writes must be idempotent, so that restarting after an interruption picks back up without duplication or gaps. Chains undergo reorgs, so results for unfinalized ranges must be stored together with their block hash so they can be rolled back and reprocessed; only ranges past finality are treated as immutable. In the end, replayability is a design that buys an operational freedom: the ability to throw away the data and rebuild it whenever the schema or the logic changes.

Indexers get their bugs fixed and their schemas changed often; without replayability, you end up hand-patching historical data — a special kind of hell. Skip reorg handling and wrong data sits there silently.

Code & Formula

# 인덱싱 파이프라인 설계 — 재생 가능성(replayability) — 동일 입력을 두 번 인덱싱해도 같은 결과가 나오고, reorg 시 롤백·재처리된다.
# 커서(블록번호, 로그인덱스)로 진행 상태를 추적하고 쓰기를 멱등하게 만들어, 중단 후 재시작·재구성이 안전하도록 한다.

def index_blocks(blocks, table=None):
    table = table if table is not None else {}
    for b in blocks:
        for log in b["logs"]:
            key = (b["number"], log["index"])          # 커서 = (블록번호, 로그인덱스) → 자연스러운 멱등 키
            table[key] = {"block_hash": b["hash"], "value": log["value"]}
    return table

def reorg_rollback(table, from_block):
    # 확정되지 않은 구간을 되돌린다: from_block 이상인 항목을 모두 제거
    stale_keys = [k for k in table if k[0] >= from_block]
    for k in stale_keys:
        del table[k]
    return len(stale_keys)

chain_v1 = [
    {"number": 0, "hash": "0xa0", "logs": [{"index": 0, "value": 100}]},
    {"number": 1, "hash": "0xa1", "logs": [{"index": 0, "value": 200}]},
    {"number": 2, "hash": "0xa2", "logs": [{"index": 0, "value": 300}]},
]

table_a = index_blocks(chain_v1)
table_b = index_blocks(chain_v1)   # 같은 구간을 처음부터 다시 인덱싱

print("두 번 인덱싱한 결과가 동일한가:", table_a == table_b)

# 블록 2가 reorg로 다른 해시·값으로 교체되었다고 가정
removed = reorg_rollback(table_a, from_block=2)
chain_v2_block2 = {"number": 2, "hash": "0xb2-reorged", "logs": [{"index": 0, "value": 999}]}
index_blocks([chain_v2_block2], table=table_a)

print(f"reorg 롤백: 블록 2 이상 {removed}개 항목 제거 후 재처리")
print("reorg 이후 블록 2 상태:", table_a[(2, 0)])
print("블록 0,1은 그대로 유지:", table_a[(0, 0)], table_a[(1, 0)])

Exercise

Build a test that indexes an arbitrary block range twice and checks the final tables are byte-for-byte identical, then swap in different hashes for the last few blocks and verify that reorg rollback actually works.

Practical Connection

If Verex indexes order, fill, and settlement events to show users their positions and P&L, without replayability, every time the calculation logic gets fixed, past balances are left wrong.

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 75 / 100 · E. 데이터·스토리지 엔진 (Day 69–81)

재생 가능성(replayability)

개념

인덱싱 파이프라인은 체인의 블록·로그·트레이스를 읽어 쿼리 가능한 형태로 변환해 저장하는 시스템이고, 재생 가능성은 원본 데이터로부터 언제든 처음부터 다시 돌려도 동일한 결과에 도달하는 성질을 뜻한다. 이를 얻으려면 변환 로직이 결정론적 순수 함수여야 하며, 현재 시각·난수·외부 API 응답처럼 재실행 시 달라지는 입력이 변환 안에 들어가면 안 된다. 진행 상태는 (블록 번호, 로그 인덱스) 같은 커서로 표현하고 쓰기는 멱등해야, 중단 후 재시작이 중복이나 누락 없이 이어진다. 체인은 재구성(reorg)이 일어나므로 확정되지 않은 구간의 결과는 블록 해시를 함께 저장해 되돌리거나 재처리할 수 있어야 하고, 확정 이후 구간만 불변으로 취급한다. 결국 재생 가능성은 '스키마나 로직이 바뀌었을 때 데이터를 버리고 다시 만들 수 있는가'라는 운영상의 자유를 사는 설계다.

인덱서는 버그 수정이나 스키마 변경이 잦은데, 재생이 불가능하면 과거 데이터를 손으로 패치하는 지옥에 들어간다. reorg 처리를 빠뜨리면 조용히 틀린 데이터가 남는다.

코드 · 수식

# 인덱싱 파이프라인 설계 — 재생 가능성(replayability) — 동일 입력을 두 번 인덱싱해도 같은 결과가 나오고, reorg 시 롤백·재처리된다.
# 커서(블록번호, 로그인덱스)로 진행 상태를 추적하고 쓰기를 멱등하게 만들어, 중단 후 재시작·재구성이 안전하도록 한다.

def index_blocks(blocks, table=None):
    table = table if table is not None else {}
    for b in blocks:
        for log in b["logs"]:
            key = (b["number"], log["index"])          # 커서 = (블록번호, 로그인덱스) → 자연스러운 멱등 키
            table[key] = {"block_hash": b["hash"], "value": log["value"]}
    return table

def reorg_rollback(table, from_block):
    # 확정되지 않은 구간을 되돌린다: from_block 이상인 항목을 모두 제거
    stale_keys = [k for k in table if k[0] >= from_block]
    for k in stale_keys:
        del table[k]
    return len(stale_keys)

chain_v1 = [
    {"number": 0, "hash": "0xa0", "logs": [{"index": 0, "value": 100}]},
    {"number": 1, "hash": "0xa1", "logs": [{"index": 0, "value": 200}]},
    {"number": 2, "hash": "0xa2", "logs": [{"index": 0, "value": 300}]},
]

table_a = index_blocks(chain_v1)
table_b = index_blocks(chain_v1)   # 같은 구간을 처음부터 다시 인덱싱

print("두 번 인덱싱한 결과가 동일한가:", table_a == table_b)

# 블록 2가 reorg로 다른 해시·값으로 교체되었다고 가정
removed = reorg_rollback(table_a, from_block=2)
chain_v2_block2 = {"number": 2, "hash": "0xb2-reorged", "logs": [{"index": 0, "value": 999}]}
index_blocks([chain_v2_block2], table=table_a)

print(f"reorg 롤백: 블록 2 이상 {removed}개 항목 제거 후 재처리")
print("reorg 이후 블록 2 상태:", table_a[(2, 0)])
print("블록 0,1은 그대로 유지:", table_a[(0, 0)], table_a[(1, 0)])

연습

임의 블록 구간을 두 번 인덱싱해도 최종 테이블이 바이트 단위로 같아지는지 확인하는 테스트를 만들고, 마지막 몇 블록을 다른 해시로 바꿔 넣어 reorg 롤백이 실제로 동작하는지 검증하라.

실무 · Verex 연결

Verex가 주문·체결·정산 이벤트를 인덱싱해 사용자 포지션과 손익을 보여 준다면, 재생 가능성이 없으면 계산 로직을 고칠 때마다 과거 잔액이 틀린 채로 남는다.

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

← 74. 프루닝·아카이브·스냅 싱크76. 컬럼 스토어와 벡터화 실행(OLAP) →