Repository navigation
Record chain payment receipts durably before acknowledging them - #374
Merged
Merged
Conversation
added 8 commits
October 2, 2026 15:17
Compose the account recovery and provider configuration change with the CILogon production deployment that landed on main. The merged deployment keeps main's variable names, its production enablement, its credential preflight that counts both entries without reading them, and its single example file, and adds the browser-approved CLI authentication settings, the issuer and URL checks, the configuration-update refresh of the unit and credentials, and the extended render fixture. The duplicate example file is dropped; the installed credential file name is unchanged. Go, SQL and documentation changes are those already on the branch; the render fixture passes inside the pinned provisioner against a retained package. GitHub issues #226, #223 and #227.
The payment decision of a terminal run was applied by an order-state write without a previous-state check and nothing recorded which run settled which order; when the write after the terminal report failed, the order stayed outstanding for good. Dispatcher schema 21 adds order_settlements, one immutable row per settled order (credit or refund, amount, currency, executor and run), and the pricing_rule column of transactions. The decision now lives in one place: the order moves from outstanding only through a conditional write, the settlement row and the earnings credit or chain refund commit with it, and a repeated or concurrent delivery leaves a settled order as it is. The maintenance loop settles terminal runs with a recorded exit code whose inline settlement failed, a bounded number per pass. A failed TEST run is refunded locally, moving nothing; TEST intents are still never refunded as a whole. Every intent records the name of the rule that priced it, and the API guide states that a USDC intent is a local record created without a chain call. GitHub issues #76, #77 and #78.
The receipt listener applied each receipt through a void callback whose write failures were only logged, advanced its checkpoint cursor whatever happened, converted the chain amount to a signed integer unchecked, only warned about a receipt for a non-chain method and reset an invalid stored cursor silently; its expiry check never fired because the decoder read nothing into the timestamp. Dispatcher schema 22 adds payment_receipts, one row per receipt addressed to this dispatcher keyed by chain digest and nonce with its disposition, and chain_transfers, the record of outbound payouts and refunds with the signed transaction persisted before anything is broadcast. A receipt is validated strictly, applied by a conditional write on an outstanding transaction and recorded in the same local transaction, so a receipt read again after a restart is recognized instead of applied twice; the cursor moves only after every receipt of a range is recorded, a gap between the catch-up and the first streamed checkpoint is read through the indexer first, an invalid stored cursor is a startup error, and a first start persists the chain tip it begins at. GitHub issue #79.
The executor's rejected-allocation test still asserted that the failed run's TEST order stays outstanding, the limitation the settlement change removes: a failed TEST run now refunds its order locally and no credit is invented. The assertion follows the new behaviour. GitHub issue #78.
Brings in the executor test expectation of a locally refunded TEST order so this branch builds on the same settlement behaviour it extends. GitHub issue #79.
ctfbruce
marked this pull request as ready for review
October 5, 2026 08:48
ctfbruce
marked this pull request as draft
October 5, 2026 09:00
added 3 commits
October 5, 2026 11:11
The maintenance pass retried every outstanding order of a terminal run, including USDC refunds, which the terminal path sends inside the local transaction: a transfer whose response was lost or whose commit failed left the order outstanding and the next pass paid the buyer again. The pass also always listed the oldest rows, so a page of orders that keep failing kept every later order from being attempted. The pass now settles only wholly local outcomes, credits of any currency and TEST refunds, and defers a chain refund without touching the backend; chain refunds are attempted once from the terminal path until a durable transfer record takes them over. The listing continues after the last run the previous pass attempted and starts over once it is exhausted, so a failing order never blocks the ones after it. GitHub issue #78.
The catch-up read PaymentReceipt events through the GraphQL indexer up to the full node's checkpoint height and stored that height as the cursor. An indexer behind the node has not served the events of the checkpoints between its own mark and the tip yet, so a payment in that range was passed over for good. Ask the indexer for the last checkpoint whose events it serves (serviceConfig.availableRange) and read and store no further than that. Bound each events query above as well, so a pass reads exactly the range it claims. A streamed checkpoint ahead of a lagging indexer records its own receipts and leaves the cursor at the proven bound; the gap between them is read through the indexer once it has ingested it, and the cursor moves on only when the run of streamed checkpoints is joined to it, never backwards. A payment_receipts row was keyed by transaction digest and nonce, so a transaction carrying several receipt events for one nonce kept only the first: a valid event after a rejected one in the same transaction was treated as already recorded and did not pay. Key the row by transaction digest and event position instead (GraphQL Event.sequenceNumber, the event's index in the streamed transaction), index the nonce, and record a second valid event of an already paid intent as a mismatch naming the paying event. Migration 22 is edited in place; it has not shipped. Regressions: node at 110 with the indexer at 105 keeps the cursor at 105 and applies the payment at 108 exactly once when the indexer reaches it; a streamed checkpoint 115 at indexer 105 records its receipt, holds the cursor and completes the gap when the indexer has it; a rejected and a valid event of one transaction pay in either order, replay without effect, and a second valid event is a mismatch. Both GraphQL fields are taken from the published schema and not yet confirmed against a live service; receipts read during catch-up still record no checkpoint. GitHub issue #79.
Bring the settlement ledger branch at its corrected head (local settlement retries and a fair scan of pending orders) under the receipts branch, so that the receipt and transfer tables build on the order_settlements ledger as shipped. GitHub issue #79.
ctfbruce
marked this pull request as ready for review
October 5, 2026 09:37
This was referenced Oct 5, 2026
5 tasks
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.
Behaviour
Chain payment receipts are processed durably and acknowledged only after their effect is recorded:
payment_receipts: everyPaymentReceiptaddressed to this dispatcher is stored under its chain transaction digest and event position (one transaction may carry several receipts, even for one nonce) with a disposition (applied,duplicate,mismatchnaming the field that differed,unknown_intent,expired). Marking a transaction paid is a conditional write on its outstanding status in the same local transaction, so a duplicate, reordered or re-read receipt cannot pay twice, and a second payment for a paid intent, by another transaction or another event of the same one, is recorded as a mismatch naming the paying digest and event.main: the decoder read nothing into the timestamp field.chain_transfers, the record of outbound payouts and refunds with the intended transfer, the signed transaction and its digest persisted before anything is broadcast, used by the following change.Proof
internal/dispatcher/payments/suiscratch test onmain).-raceon the composed tree.Upgrade
Dispatcher schema 22 is required: stop the dispatcher, back up the database and run the explicit upgrade before starting this build.
Requires #364 (schema 20) and #373 (schema 21); this branch is based on #373's head.
Limits
timestamp_mslast) and the coin-type string form are assumptions until a deployed package is checked.serviceConfig.availableRangeforQuery.events) and the event position (Event.sequenceNumber) are taken from the published Sui GraphQL schema and have not been confirmed against a live service; the streamed event index andEvent.sequenceNumberare assumed to agree, and if they did not, a re-read receipt would be recorded as a second row whose effect is a mismatch, never a second payment. Receipts read during catch-up record no checkpoint number.GitHub issue #79.
Candidate CI
Run 37284811811 on 2554a22: 16 of 16 lanes and
requiredsucceeded. Corrections (indexer-bounded cursor, receipt identity by event) are in f7c0b14, with #373's corrected head merged forward: run 37290240068 on f7c0b14, 16 of 16 lanes andrequiredsucceeded.