fix(indexer): make handleTokensWithdrawn idempotent under replay (#802)#968
Open
jadonamite wants to merge 1 commit into
Open
fix(indexer): make handleTokensWithdrawn idempotent under replay (#802)#968jadonamite wants to merge 1 commit into
jadonamite wants to merge 1 commit into
Conversation
…sCrypt#802) handleTokensWithdrawn incremented withdrawnAmount before the dedup guard, which only protected the StreamEvent insert. The admin POST /v1/admin/indexer/replay resets the cursor and re-polls processed ledgers, so every replay re-added `amount` even though the event row was skipped — inflating withdrawnAmount and shrinking the recipient's claimable balance. - check the (txHash, WITHDRAWN) dedup guard first and return early on replay, so the financial field is only mutated for a newly-seen event - insert the StreamEvent before the stream.update inside the same transaction; the unique (transactionHash, eventType) constraint makes a concurrent replay roll back the whole transaction, so withdrawnAmount can't be double-applied - add a worker test that processes the same tokens_withdrawn event twice and asserts withdrawnAmount changes only once handleStreamCancelled/handleStreamCompleted audited: both set withdrawnAmount to an absolute value, so they remain idempotent under replay.
61b41ae to
5d1e5dd
Compare
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.
Fixes #802
Problem
handleTokensWithdrawncomputednewWithdrawnAmount = BigInt(stream.withdrawnAmount) + BigInt(amount)and appliedtx.stream.updateunconditionally; the dedup check only guarded theStreamEventrow insert, not the financial field.The admin
POST /v1/admin/indexer/replayresets the cursor and re-polls the same ledgers, so every replay re-addedamounttowithdrawnAmounteven though theStreamEventrow was skipped. Result:withdrawnAmountinflates → claimable shrinks → recipient under-paid. The replay endpoint even advertises idempotency.Fix
(transactionHash, WITHDRAWN)dedup guard first andreturnearly on a duplicate, so the additivewithdrawnAmountupdate only runs for a newly-seen event.StreamEventbeforestream.updateinside the same transaction. The unique(transactionHash, eventType)constraint makes a concurrent replay fail and roll back the whole transaction, sowithdrawnAmountcan never be double-applied.Audit
handleStreamCancelledandhandleStreamCompletedboth setwithdrawnAmountto an absolute value (amount_withdrawn/total_withdrawn), not additively, so they remain idempotent under replay. No change needed.Tests
Added a worker test that processes the same
tokens_withdrawnevent twice and assertswithdrawnAmountchanges only once (1000 → 1500, unchanged on replay) andstream.updateis called exactly once. Allsoroban-event-worker.test.tstests pass.Acceptance criteria
withdrawnAmountupdate is idempotent (mutated only when the StreamEvent row is newly created, in the same transaction)tokens_withdrawnevent twice and assertswithdrawnAmountchanges only oncehandleStreamCancelled/handleStreamCompleted— confirmed idempotent