Workspace IndexDev Notes › Ponder — the indexer is the reorg handler

#26PoC

Ponder — the indexer is the reorg handler

An indexer is not merely a faster RPC cache. It owns rollback, replay, idempotency, and the database state a product shows while the chain can still reorganize.

Point a Ponder app at one small contract on a local Anvil chain and run the five PoC steps. The reorg comes from Anvil itself: evm_snapshot → mine a block with a transfer → evm_revert → mine a different block, so the indexer sees a competing tip hash. Export tables with an explicit ORDER BY before diffing — dump row order is not deterministic, and an unordered byte-for-byte comparison fails for the wrong reason.

Why

The existing replayability card states the invariant; this PoC tests whether a production library actually buys it. A fast query layer that leaves stale rows after a reorg is faster at returning the wrong answer.

An indexer owns the rollback and replay rules behind the database state an application treats as truth. And how much rollback it must support is exactly the finality lag of the chain: only the unfinalized window can be orphaned, so that window is where the undo machinery lives — an RPC cache never has to think about this; an indexer is defined by it.

How it works

Index one small contract with Ponder, force a local reorg, and assert that derived rows match a clean replay byte for byte. Measure initial sync, restart recovery, RPC calls, and schema-change rebuild time.

PoC

  1. Index transfers from one small contract with Ponder.
  2. Index the same range twice and verify idempotency.
  3. Replace the final local block with a competing hash.
  4. Verify stale derived rows disappear and the result matches a clean replay byte for byte.
  5. Record initial sync, restart recovery, RPC use, and rebuild time after one schema change.
Measurement Question
Replay equality Can the database be discarded and rebuilt exactly?
Reorg rollback Do orphaned events leave any derived state?
Schema rebuild Can the product evolve without hand-patching history?

Reorg bugs live almost exclusively in derived state. Deleting orphaned raw events is easy (DELETE WHERE block > N); an aggregate updated as balance += amount can only be rolled back by inverting it or re-deriving it, which is why the test asserts on derived rows and not on the event log.

Reference: Ponder official documentation.

Review clarification

The indexer is not a choice

An RPC node answers "what is X's balance now"; it cannot answer product questions — "all transfers for this user, sorted," "top holders," "volume per day." Every real application ends up copying chain data into a database, so the question is never whether to run an indexer, only who wrote it: you, or a library. And whoever maintains that database inherits the rollback and replay rules — that inheritance is the card's thesis.

Why each PoC step exists

Step What it catches
Index the same range twice Hidden handler state — double-counting is the cheapest determinism smoke test
Replace the tip with a competing hash The reorg itself — evm_snapshot → mine → evm_revert → mine makes one on demand, on a laptop
Check derived rows, not raw rows Where reorg bugs actually live — DELETE WHERE block > N is easy; balance += amount can only be inverted or re-derived
Byte-for-byte vs. a clean replay The gold standard: the DB is a pure function of the chain — no wall-clock timestamps, no order-dependence, no reorg residue
Schema rebuild Whether the DB accidentally became a source of truth — a derived DB evolves by wipe-and-reindex, never by hand-patching history

The finality boundary

How much rollback machinery an indexer must keep is exactly the finality lag of its chain: only the unfinalized window can be orphaned, so the undo log lives there and can be dropped once a block is final. An RPC cache never has to think about this; an indexer is defined by it.

What the card does not say

It does not say "Ponder is good." The reasoning chain — we cannot avoid an indexer, and the indexer must own reorgs — is the card's setup, but the last step, therefore Ponder, is earned by the test, not assumed. Ponder claims to own the problem (its pitch against The Graph's subgraphs and Subsquid is local-first TypeScript with reorg handling built in); the PoC exists to verify that claim before a database is bet on it. Pass, and the reorg machinery came for free. One stale row, and what remains is a fast query layer that returns wrong answers.

← All Dev Notes · Workspace Index · Top ↑

Ponder — 인덱서가 리오그 핸들러다

인덱서는 단순히 빠른 RPC 캐시가 아닙니다. rollback·replay·멱등성, 그리고 체인이 아직 재조직될 수 있는 동안 제품이 보여 주는 DB 상태를 소유합니다.

로컬 Anvil 체인의 작은 컨트랙트 하나에 Ponder 앱을 붙이고 PoC 다섯 단계를 실행합니다. 리오그는 Anvil 로 만듭니다: evm_snapshot → 전송이 담긴 블록 채굴 → evm_revert → 다른 블록 채굴 — 인덱서가 경쟁하는 팁 해시를 보게 됩니다. 표는 diff 전에 반드시 ORDER BY 를 붙여 export 합니다 — 덤프의 행 순서는 결정적이지 않아서, 정렬 없는 바이트 비교는 엉뚱한 이유로 실패합니다.

기존 replayability 카드는 불변식을 말로 세워 두었고, 이 PoC 는 프로덕션 라이브러리가 그 불변식을 실제로 사 주는지 시험합니다. 리오그 뒤에 낡은 행을 남기는 빠른 쿼리 계층은, 틀린 답을 더 빨리 돌려줄 뿐입니다.

인덱서는 애플리케이션이 진실로 취급하는 DB 상태 뒤의 rollback·replay 규칙을 소유합니다. 그리고 지원해야 할 rollback 의 깊이는 정확히 체인의 파이널리티 지연입니다: 미확정 구간만 고아가 될 수 있으므로 undo 장치는 그 구간에만 삽니다 — RPC 캐시는 이걸 고민할 필요가 없지만, 인덱서는 이것으로 정의됩니다.

동작 방식

Ponder 로 작은 컨트랙트 하나를 인덱싱하고, 로컬 리오그를 강제로 만든 뒤, 파생 행이 처음부터 다시 인덱싱한 결과와 바이트 단위로 같은지 확인합니다. 초기 동기화, 재시작 복구, RPC 호출 수, 스키마 변경 후 재구축 시간을 측정합니다.

PoC

  1. Ponder 로 작은 컨트랙트 하나의 전송을 인덱싱한다.
  2. 같은 범위를 두 번 인덱싱해 멱등성을 확인한다.
  3. 마지막 로컬 블록을 경쟁 해시로 교체한다.
  4. 낡은 파생 행이 사라지고 결과가 클린 리플레이와 바이트 단위로 같은지 검증한다.
  5. 초기 동기화, 재시작 복구, RPC 사용량, 스키마 변경 1회 후 재구축 시간을 기록한다.
측정 질문
리플레이 동등성 DB 를 버리고 정확히 재구축할 수 있는가?
리오그 롤백 고아가 된 이벤트가 파생 상태를 남기는가?
스키마 재구축 히스토리를 수작업 패치 없이 제품이 진화할 수 있는가?

리오그 버그는 거의 전부 파생 상태에 삽니다. 고아가 된 원본 이벤트 삭제는 쉽지만(DELETE WHERE block > N), balance += amount 로 증분 갱신된 집계값은 역산하거나 재유도해야만 되돌릴 수 있습니다 — 그래서 테스트는 이벤트 로그가 아니라 파생 행을 검증합니다.

참고: Ponder 공식 문서.

검토 후 보완

인덱서는 선택지가 아니다

RPC 노드는 "X 의 지금 잔액"에는 답하지만, 제품이 던지는 질문 — "이 사용자의 모든 전송 내역 정렬해서", "상위 보유자", "일별 거래량" — 에는 답하지 못합니다. 실제 애플리케이션은 결국 체인 데이터를 DB 로 복사하게 되므로, 질문은 인덱서를 쓸지 말지가 아니라 누가 만들었느냐 — 나냐, 라이브러리냐 — 뿐입니다. 그리고 그 DB 를 유지하는 쪽이 rollback·replay 규칙을 상속받습니다 — 그 상속이 이 카드의 논지입니다.

PoC 각 단계가 존재하는 이유

단계 무엇을 잡아내나
같은 범위 두 번 인덱싱 핸들러의 숨은 상태 — 이중 집계는 결정성을 확인하는 가장 싼 테스트
팁을 경쟁 해시로 교체 리오그 그 자체 — evm_snapshot → 채굴 → evm_revert → 채굴이면 노트북 위에서 즉석으로 만든다
원본 행이 아니라 파생 행 검사 리오그 버그가 실제로 사는 곳 — DELETE WHERE block > N 은 쉽지만 balance += amount 는 역산 또는 재유도로만 되돌릴 수 있다
클린 리플레이와 바이트 단위 비교 최고 기준: DB 는 체인의 순수 함수 — 벽시계 타임스탬프도, 순서 의존성도, 리오그 잔여물도 없다
스키마 재구축 DB 가 어느새 source of truth 가 되어버렸는지 — 파생 DB 는 히스토리 수작업 패치가 아니라 전체 재인덱싱(wipe-and-reindex)으로 진화한다

파이널리티 경계

인덱서가 유지해야 할 rollback 장치의 깊이는 정확히 그 체인의 파이널리티 지연입니다: 미확정 구간만 고아가 될 수 있으므로 undo 로그는 그 구간에만 살고, 블록이 확정되면 버려도 됩니다. RPC 캐시는 이걸 고민할 필요가 없지만, 인덱서는 이것으로 정의됩니다.

카드가 말하지 않는 것

카드는 "Ponder 가 좋다"고 말하지 않습니다. 추론 사슬 — 인덱서는 피할 수 없고, 인덱서가 리오그를 소유해야 한다 — 까지는 카드의 설정이지만, 마지막 걸음인 그러므로 Ponder 는 가정이 아니라 테스트가 벌어다 주는 결론입니다. Ponder 는 이 문제를 소유한다고 주장합니다(The Graph 서브그래프·Subsquid 에 맞서는 셀링 포인트가 리오그 처리 내장 로컬 우선 TypeScript). PoC 는 DB 를 걸기 전에 그 주장을 검증하려고 존재합니다. 통과하면 리오그 장치를 공짜로 얻은 것이고, 낡은 행이 하나라도 남으면 남는 것은 틀린 답을 빨리 돌려주는 쿼리 계층입니다.

← 전체 개발 노트 · 워크스페이스 인덱스 · 맨 위 ↑