feat(indexer): orphaned flag + canonical PDA-join in reconciler (#234)#321
Merged
Conversation
Closes the half-implemented reconciler hardening: schema columns the reconciler already referenced now exist, the canonical-PDA join is wired, and all 3 event tables are covered (not just contribute). Schema ------ - Add `orphaned: Boolean @default(false)` + `resolvedAt: DateTime?` to `ContributeEvent`, `ClaimEvent`, `DefaultEvent`. - Add `contributorWallet: String` to `ContributeEvent` and `recipientWallet: String` to `ClaimEvent` — the subject wallet from the on-chain log was previously discarded by the webhook; the reconciler needs it to resolve canonical Member FKs. - Add `@@index([orphaned, slot])` on all 3 event tables — keeps both the reconciler's `WHERE NOT orphaned` scan and the runbook's orphan- triage SQL fast. Reconciler ---------- - New `resolveCanonicalIds()` helper: fetches the tx with `commitment: "finalized"`, walks static + lookup-table account keys, intersects with `pools.pda` to find the canonical Pool row, then joins to Member via `(poolId, wallet)`. Returns `null` (caller leaves row unresolved + retries next pass) for the three failure modes: RPC miss / Pool not in DB yet / Member not in DB yet. - Three table-specific reconcile loops (was 1, scaffold-only): contribute + claim resolve `{poolId, memberId}`, default resolves `{poolId, slotIndex}` from the canonical Member row (defaults don't carry `memberId` per the existing schema — wallet is the authoritative subject because escape_valve_buy can close the row). - Both success and orphan paths now actually `UPDATE` the row (was: log-only). `orphaned = true` after the 256-slot grace; `resolvedAt` stamped on success. - Skip already-`orphaned` rows in subsequent passes — terminal flag, no recovery path here (backfill is the recovery). Operator surface ---------------- - `docs/operations/indexer-reorg-recovery.md` — quick-triage SQL now uses the `orphaned` column instead of grep-by-slot heuristics; new step in P1 reorg playbook chains backfill + reconcile:once + diff validation; ledger-diff query filters `NOT orphaned`. - `docs/security/indexer-threat-model.md` — R1 (reorg orphans event) flips from 🟡 partial → ✅ shipped; § 4.1 reflects the full set of reconciler hardening that's now in repo. Validation ---------- - `pnpm typecheck` (workspace + indexer) — green - `pnpm lint` (prettier) — green - No indexer tests existed pre-PR; not in scope of this change Follow-ups (not in this PR) --------------------------- - `MAINNET_READINESS.md` § 5.2 still says 🟡 with "Outstanding: Prisma migration + canonical PDA-join (tracked in #234)". That should flip to ✅ after this lands. Leaving it for a separate doc sync to avoid conflicting with PR #320 (also touching MAINNET_READINESS). - Webhook auth (HMAC) + rate-limit + program-id allow-list remain unshipped (threat-model § 4.2). - `roundfiReputation`-program event ingestion + reconciliation: not in this PR; mainnet not on critical path until B2B oracle endpoint. https://claude.ai/code/session_01YapZy1Z5gzbV5EammBkSQm
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #234.
Summary
The reconciler at
services/indexer/src/reconciler.tswas a half-shipped feature: the orphan-marking logic referenced anorphanedcolumn that didn't exist on the schema, the canonical PDA-join was aTODO, and onlyContributeEventwas looped (claim + default skipped). This PR finishes the wiring.What ships
Schema (
prisma/schema.prisma)orphaned: Boolean @default(false)+resolvedAt: DateTime?on all 3 event tables.contributorWallet: String(ContributeEvent) +recipientWallet: String(ClaimEvent) — the subject wallet from the on-chain log was previously discarded; the reconciler needs it to resolve canonical Member FKs.@@index([orphaned, slot])on all 3 event tables — keeps both the reconciler scan and the runbook's orphan-triage SQL fast.Reconciler (
reconciler.ts)New
resolveCanonicalIds()helper:commitment: "finalized".staticAccountKeys+meta.loadedAddresses.{writable,readonly}(covers Address Lookup Table-using txs).pools.pdain the DB → canonical Pool row.(poolId, wallet).null(caller leaves row unresolved + retries next pass) for the three failure modes: RPC miss / Pool not in DB yet / Member not in DB yet.Three table-specific loops (was: 1, scaffold-only):
reconcileContributeEvents— resolves{poolId, memberId}fromevt.contributorWallet.reconcileClaimEvents— resolves{poolId, memberId}fromevt.recipientWallet.reconcileDefaultEvents— resolves{poolId, slotIndex}fromevt.defaultedWallet;slotIndexrecovered from the canonical Member row when present (falls back to placeholder when the Member was closed byescape_valve_buy).Both the success and orphan paths actually
UPDATEthe row (was: log-only).orphaned = trueafter the 256-slot grace;resolvedAtstamped on success.Skip already-
orphanedrows in subsequent passes — terminal flag, no recovery here (backfill is the recovery path).Operator surface
docs/operations/indexer-reorg-recovery.md— quick-triage SQL now uses theorphanedcolumn instead of grep-by-slot heuristics; new step in P1 reorg playbook chainsbackfill+reconcile:once+ diff validation; ledger-diff query filtersNOT orphaned.docs/security/indexer-threat-model.md— R1 (reorg orphans event) flips from 🟡 partial → ✅ shipped; § 4.1 reflects the full set of reconciler hardening that's now in repo.Validation
pnpm typecheck(workspace + indexer) → green ✅pnpm lint(prettier) → green ✅pnpm exec prisma generate→ fresh client generated, type-checks against new fieldsTrust-path posture (unchanged)
The indexer is off the fund-movement trust path per
AUDIT_SCOPE.mdanddocs/security/self-audit.md§ 2. A buggy reconciler can misreport scores to a future B2B oracle but cannot move funds. This PR tightens the off-chain correctness story for B2B Phase 3.Follow-ups (deliberately out of scope)
MAINNET_READINESS.md§ 5.2 status flip 🟡 → ✅roundfi-reputationprogram event ingestion + reconciliationTest plan
pnpm typecheck+pnpm lintgreen locallyreconcile:once, verify rows transition_unresolved→ canonical IDs withresolvedAtstampedGenerated by Claude Code