[#138] Finding 2: Indexer — transactional per-block ingestion (crash/reorg ghosts)
**Parent:** Split from [#138 — Pre-deploy review pass 2](https://gitlab.com/PlasticDigits/yieldomega/-/issues/138) (Finding 2).
**Severity:** Medium
## Problem
Per-block ingestion in [`indexer/src/ingestion.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/ingestion.rs) runs **sequential SQL operations without a single database transaction**:
- For each log: `persist_decoded_log` (per event).
- Then: `upsert_indexed_block`.
- Then: `save_chain_pointer`.
If the process crashes **after** some events from block N are committed but **before** `save_chain_pointer`, the chain pointer remains at **N−1** while partial data for **N** exists. On restart, block N is re-fetched; `ON CONFLICT DO NOTHING` on `(tx_hash, log_index)` may skip re-inserts, while **orphan rows** from a reorged-out version of N can remain forever — reorg rollback keyed on `block_number` never deletes them if the pointer never advanced. This violates [`indexer/REORG_STRATEGY.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/REORG_STRATEGY.md): *“reorged events must never stick”* (extended to crash windows at MegaETH-scale block times).
## References
| Kind | Path / link |
|------|-------------|
| Ingestion loop | [`indexer/src/ingestion.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/ingestion.rs) (block loop ~L103–165) |
| Persist API | [`indexer/src/persist.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/persist.rs) — may need `*_tx(&mut Transaction<'_, Postgres>, ...)` helpers |
| Reorg | [`indexer/src/reorg.rs`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/src/reorg.rs) |
| Strategy doc | [`indexer/REORG_STRATEGY.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/indexer/REORG_STRATEGY.md) (“Atomic rollback”, “reorged events must never stick”) |
| Invariant | [`docs/testing/invariants-and-business-logic.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md) — indexer correctness / rollback |
| Skill | [`.cursor/skills/yieldomega-guardrails/SKILL.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/.cursor/skills/yieldomega-guardrails/SKILL.md) |
## Recommended fix
Wrap the per-block body in `sqlx::Transaction`:
```rust
let mut tx = pool.begin().await?;
for lg in &logs { persist_decoded_log_tx(&mut tx, &decoded).await?; }
upsert_indexed_block_tx(&mut tx, next, block_hash).await?;
save_chain_pointer_tx(&mut tx, &pointer).await?;
tx.commit().await?;
```
Refactor `persist_decoded_log` to accept either `&PgPool` or `&mut Transaction` (duplicate thin wrappers acceptable for clarity). Ensure **error paths** roll back the transaction. Consider whether `bootstrap_pointer` and reorg paths already transactional — keep behavior consistent.
## Acceptance criteria
- [ ] Events + `indexed_blocks` + `chain_pointer` updates for a given block are **all-or-nothing** committed.
- [ ] Crash mid-block leaves DB as if the block had not been processed (pointer unchanged, no partial event rows for that block — or define explicit idempotent cleanup; preferred: transactional commit).
- [ ] Existing reorg detection logic unchanged; only durability model improves.
- [ ] New or extended test coverage (see Finding 8 sibling issue for integration test idea).
## Verification checklist
- [ ] `cargo test` in `indexer/`.
- [ ] Stress: kill indexer mid-block (manual or test harness) and confirm no partial block state vs pointer mismatch.
- [ ] Review `ON CONFLICT` behavior after transactional fix — document any remaining edge cases.
- [ ] Load test on local Postgres: no deadlocks under batch inserts.
issue