From e4ffa84524b59167c472648de81befc8d7a73613 Mon Sep 17 00:00:00 2001 From: Julien Lucca Date: Sun, 27 Sep 2026 21:27:11 +0200 Subject: [PATCH] fix(claims): resolve a replayed claim from its own block's watermark A claim skipped by a ResolveError could never come back through a reindex: the resolver searched above the table-wide max claim id, which by replay time sits above every claim being revisited, so the lookup threw again. Bound the watermark by created_block <= the action's block. Indexing live this is the same number as before; on replay it is where the watermark stood when the claim was first seen. Co-Authored-By: Claude Opus 5.5 --- src/chain.js | 4 ++-- src/updaters/community.js | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/chain.js b/src/chain.js index 676cad5..4edf72e 100644 --- a/src/chain.js +++ b/src/chain.js @@ -135,8 +135,8 @@ async function claimPage (communityContract, lowerBound, limit) { // ascending, never reused and never deleted (verifyclaim only mutates status). // event-source processes actions in chain order, so the claim created by the // action being processed is the FIRST claim on chain for this (action, claimer) -// with an id above every claim id already recorded — `afterId`, the DB's current -// max claim id. +// with an id above every claim id already recorded up to this block — `afterId`, +// the DB's max claim id with created_block <= the action's block (see claimAction). // // Reading forward from a watermark, rather than counting a pair's claims and // taking the nth, is what makes this safe under truncation: a short page just diff --git a/src/updaters/community.js b/src/updaters/community.js index 9c16549..0e8f599 100644 --- a/src/updaters/community.js +++ b/src/updaters/community.js @@ -700,7 +700,19 @@ async function claimAction (db, payload, blockInfo, context) { // catches it, un-claims this action's global_seq in _processed_actions so a // later reindex can pick it up, and pages via Sentry. It is therefore // impossible for a resolve failure to reach the INSERT below with a serial id. - const { watermark } = await db.instance.one('SELECT coalesce(max(id), 0) AS watermark FROM claims') + // + // The watermark is the highest claim id recorded AT OR BEFORE this block, not the + // table-wide max. Indexing live, the two are the same number (nothing later exists + // yet). On a reindex they differ: the table-wide max sits above every claim the + // replay revisits, so a claim skipped by a ResolveError could never be resolved — + // the lookup started above its id and threw again. Bounding by block puts the + // watermark back where it stood when the claim was first seen. A same-block claim + // recorded after this one can still push it too high; that only throws (skip and + // retry), never resolves to a wrong id. + const { watermark } = await db.instance.one( + 'SELECT coalesce(max(id), 0) AS watermark FROM claims WHERE created_block <= $1', + [blockInfo.blockNumber] + ) let claimId try { claimId = await resolveClaimId(