Indexer: ingestion hardening — cursor on error, tx pagination, reorg recovery (C1–C3) #236
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#236
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reference
Gap analysis:
gaps/GAP_1780200149.md— findings C1, C2, C3.Current codebase
The Rust indexer polls Terra Classic LCD, parses txs per block, and persists swaps, candles, limit-order lifecycle, and trader aggregates to Postgres. Progress is tracked as
last_indexed_heightinindexer_state.C1 — Cursor advances on processing failure: In
indexer/src/indexer/poller.rs, afterparser::process_block_txsreturnsErr, the poller only logstracing::error!and still callsstate::set_last_indexed_height(&pool, height). Failed blocks are never retried and are permanently skipped.C2 — ≤100 txs/block, no pagination:
indexer/src/lcd/mod.rsget_block_txscallssearch_txswithpage=1, limit=100and no follow-up pages. Busy blocks silently truncate swaps and limit-order events.C3 — No automatic reorg handling: The poller is forward-only. Recovery is documented manually in
docs/runbooks/indexer-reorg-replay-dedup.md. Swap inserts dedupe on(tx_hash, pair_id)viaON CONFLICT DO NOTHING, but candles, positions, and aggregates use merge/upsert — replay without cleanup can leave inconsistent derived state.Why this is needed
The indexer is the off-chain source of truth for charts, CG/CMC listings, portfolio, route solving, and trader analytics. Silent block skips, truncated busy blocks, and undetected reorgs produce permanent data loss or corruption that users and integrators cannot distinguish from correct state. This is the top off-chain blocker for trustworthy analytics and listings.
Constraints / guardrails
last_indexed_heightuntil block processing succeeds (or define an explicit, documented partial-commit policy — default: no advance on error).LcdClient.docs/runbooks/indexer-reorg-replay-dedup.md.Relevant files
indexer/src/indexer/poller.rsindexer/src/lcd/mod.rs,indexer/src/lcd/types.rsindexer/src/indexer/parser.rs(and submodules)indexer/src/db/queries/state.rsindexer/src/db/queries/swap_events.rsdocs/runbooks/indexer-reorg-replay-dedup.mddocs/indexer-invariants.mdindexer/src/config.rs(START_BLOCK)Recommended direction
set_last_indexed_heightafter successfulprocess_block_txs. On error, retry with backoff; after N failures, halt catch-up and surface alert (do not skip). Consider afailed_blockstable or metric for operator visibility.search_txswith pagination untilpagination.totalis exhausted or next page is empty. Add integration test with wiremock returning >100 txs for one height.(height, block_hash)checkpoint per indexed block. On each poll, verify parent hash chain; on mismatch, enter rewind mode: stop indexer, roll back cursor to fork point (and document SQL cleanup for derived tables), replay from known-good height. Ship an operator script (scripts/indexer-reorg-recover.shor similar) automating the runbook where safe.Acceptance criteria
last_indexed_height.Test plan — all paths
process_block_txserror; cursor unchanged; retry succeedsSTART_BLOCKon fresh DBRun:
cd indexer && cargo test --tests -j 1 -- --test-threads=1(Postgres required).Test plan — attack / abuse / failure vectors
Verification criteria
docs/indexer-invariants.md.mentioned in commit
b3592e103bmentioned in commit
1b9957a0b1Implementation complete (pushed to
main)Indexer ingestion hardening for gap findings C1–C3 (#236) is merged to
main(b3592e1).What changed
C1 — Cursor on error
last_indexed_height/last_indexed_block_hashcommit only after successful block ingest (block_indexer.rs).process_block_txsnow propagates per-tx errors (no warn-and-skip).BLOCK_PROCESS_MAX_RETRIES,BLOCK_PROCESS_RETRY_BACKOFF_MS); persistent failures recorded inindexer_failed_blocks.C2 — Tx pagination
LcdClient::get_block_txspaginates untilpagination.totalis satisfied; rejects incomplete totals; bounded byBLOCK_TX_MAX_PAGES×BLOCK_TX_PAGE_LIMIT.C3 — Reorg detection
last_indexed_block_hash; mismatch halts the indexer../scripts/indexer-reorg-recover.sh --height H [--apply].Docs / agent playbooks
docs/indexer-invariants.md— C1–C3 rowsdocs/runbooks/indexer-reorg-replay-dedup.md— updated recovery flowskills/AGENTS_INDEXER_INGESTION_HARDENING.md— third-party agent playbookVerification checklist (QA / agents)
cd indexer && cargo test --lib— passes (includes LCD pagination wiremock tests)cd indexer && cargo test --test indexer_ingestion_hardening -j 1 -- --test-threads=1— 4/4 pass (Postgres required)cargo test --tests -j 1 -- --test-threads=1— no regressions in existing indexer integration tests20260531120000_indexer_ingestion_hardening.sqlapplies cleanly (indexer_failed_blockstable)last_indexed_heightunchanged andindexer_failed_blocksrow existslast_indexed_block_hashin DB → indexer halts with reorg log (no forward progress)./scripts/indexer-reorg-recover.sh --height Ndry-run →./scripts/indexer-reorg-recover.sh --height N --apply→ indexer replays fromNON CONFLICT DO NOTHING)Request
@qa-team / QA agents: please run the checklist above on a fresh deploy (LocalTerra or staging) and confirm ingestion behavior under failure and busy-block scenarios. Leave findings on this issue.
Issue remains open until QA sign-off.
QA verification complete — all criteria pass
Verified on branch
main@841c321using git worktreeverify/issue-236against LocalTerra + Postgres (dex_indexer/dex_indexer_test).Environment
127.0.0.1:5432—cl8y_legal/dex_indexerhttp://127.0.0.1:3001/health→{"status":"ok"}last_indexed_height=18040,last_indexed_block_hashsetAutomated tests (all pass)
cargo test --liblcd::tests::get_block_txs_multi_page,get_block_txs_rejects_incomplete_total)cargo test --test indexer_ingestion_hardening -j 1 -- --test-threads=1cargo test --tests -j 1 -- --test-threads=1C1 —
cursor_does_not_advance_on_parser_failure: height unchanged after max retries;indexer_failed_blocksrow inserted.C2 —
multi_page_block_txs_ingested_count_matches_lcd_total: 155 txs across 2 pages (page_count=2).C3 —
reorg_detection_halts_on_hash_mismatch:BlockIndexError::ReorgDetectedon hash mismatch.Additional block-time invariants:
missing_tx_timestamp_uses_block_header_time,invalid_tx_and_header_timestamp_fails_block(cursor unchanged).Migration & schema
20260531120000_indexer_ingestion_hardening.sqlapplied in test DBindexer_failed_blockstable present with expected columnsOperator recovery
./scripts/indexer-reorg-recover.sh --height 18040dry-run: correct SQL preview (cursor → 18039, hash cleared,indexer_failed_blockstruncated)docs/indexer-invariants.md(C1–C3 rows) andskills/AGENTS_INDEXER_INGESTION_HARDENING.mdAcceptance criteria mapping
last_indexed_height(C1 test)(tx_hash, pair_id) ON CONFLICT DO NOTHINGpreservedManual verification checklist (for operators)
indexer_failed_blocksrow (integration test)./scripts/indexer-reorg-recover.sh --height Ndry-run produces expected SQLNo bugs found. No code changes required.
Closing issue — ingestion hardening C1–C3 verified.
mentioned in issue #292
mentioned in merge request !738
mentioned in issue #335
mentioned in merge request !872
mentioned in issue #362