Skip to content

feat: guard against duplicate (recipient, nonce) pairs across open mints - #421

Closed
rouzwelt wants to merge 1 commit into
2026-09-29-rai-2729from
2026-09-24-rai-2620
Closed

rouzwelt wants to merge 1 commit into
2026-09-29-rai-2729from
2026-09-24-rai-2620

Conversation

@rouzwelt

@rouzwelt rouzwelt commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

The mint-authorization endpoint now refuses a (recipient, nonce) pair another mint already holds, returning 409 Conflict. The orchestrator keys nonceUsed on the recipient across every token, so one pair produces at most one landing. Without this guard two same-sized mints for one recipient would both full-match that landing and both complete — one AP's tokens for shares journaled twice. The check runs before the on-chain validation RPCs (refusing a duplicate cheaply) and again, under a static admission lock, immediately before the AuthorizeMint send (catching a pair claimed while the validation was in flight). A mint holds its pair for its whole lifecycle except JournalRejected (which never submitted) and an acknowledged close (where the operator verified the nonce is free on an independent chain view).

Closes RAI-2620

Contributes to RAI-2601
Contributes to RAI-1215

Live effect: none, refusal path on the internal endpoint · Risk: medium (money path — new admission lock serializes concurrent deliveries for different mints at record stage, lost view write makes claim invisible until rebuild) · Ships: on merge

Decisions

Risks

  • Admission lock serializes deliveries for different mints at the record stage — bounded by one command round-trip, so throughput impact is low unless delivery volume spikes
  • Lost view write makes the claim invisible until startup rebuild — confirmed loud with ERROR so the delivery is not treated as success (test coverage at confirm_claim_visible)

Proof

  • Concurrent deliveries of one pair admit exactly one, however many race (test)
  • Identical redelivery to the same mint excludes itself from the duplicate check (test)
  • Pair claimed during validation is refused at record stage (test with parking gate)
  • held_authorization_nonce exhaustive match pins every state's hold/release decision (test coverage)
  • Query plan confirms expression index usage (test)
  • Not verified: behavior under sustained high delivery volume against the admission lock

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@linear-code

linear-code Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

RAI-2620

RAI-1215

RAI-2601

rouzwelt commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more


How to use the Graphite Merge Queue

Add the label add-to-gt-merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@agryaznov agryaznov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some comments

Comment thread src/mint/mod.rs Outdated
Comment thread src/mint/api/authorize.rs Outdated
Comment thread src/mint/api/authorize.rs
@rouzwelt
rouzwelt force-pushed the 2026-09-24-rai-2610 branch 2 times, most recently from 500d36c to 6bc1044 Compare September 24, 2026 22:52
@rouzwelt
rouzwelt force-pushed the 2026-09-24-rai-2620 branch 2 times, most recently from 89dd8c3 to 90eb4d0 Compare September 25, 2026 00:09
@rouzwelt
rouzwelt requested a review from agryaznov September 25, 2026 00:10

@ueco-jb ueco-jb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state classification is correct: held_authorization_nonce holds the pair for every state that could have submitted a nonce-consuming transaction, releases only JournalRejected and a close that supplied a matching, state-gated nonce acknowledgement, and the close capture, redelivery self-exclusion, and legacy-row rebuild all resolve to the safe holding direction. The gap is the concurrency the guard targets: the record-stage re-check narrows but does not close the two-delivery race, and several comments plus the SPEC state the one-holder property as absolute. Details inline, plus doc and test-coverage follow-ups.

Comment thread src/mint/api/authorize.rs
Comment thread src/mint/api/authorize.rs
Comment thread src/mint/api/authorize.rs Outdated
Comment thread src/mint/api/authorize.rs Outdated
Comment thread src/mint/mod.rs Outdated
Comment thread src/mint/mod.rs Outdated

@agryaznov agryaznov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2nd round

Comment thread src/mint/mod.rs Outdated
Comment thread src/mint/api/authorize.rs
Comment thread SPEC.md Outdated

Copy link
Copy Markdown
Collaborator Author

@rain-marvin review

@rain-marvin

rain-marvin Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

🔎 Reviewing 58d5a50, started by @rouzwelt. The review will appear here when it's done.

@rain-marvin rain-marvin Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Opus 5.5 (Claude 1)

This PR stops two mints from holding the same (recipient, nonce) authorization pair. The orchestrator lets one pair land only once, but two mints with that pair would both match the landing and both complete, so one AP would get tokens once for shares journaled twice. The mint-authorization endpoint now returns 409 when another mint still holds the pair. It checks before the on-chain validation reads, and again under a static admission lock right before the AuthorizeMint send. After the send it confirms that mint_view shows the claim. A mint keeps its pair for its whole life, except after JournalRejected or a close that supplied the nonce acknowledgement. An expression index on mint_view serves the holder lookup, and Mint::SCHEMA_VERSION is now 5.

Overall the design is sound, and every earlier thread is addressed at this head. The endpoint is the only production path that records an authorization, so the admission lock closes the concurrent race between deliveries in this process. Two minor points remain, and neither blocks the merge. The first: the check added for a lost mint_view write reports the loss, but it does not keep the pair blocked, because the bot's retry gets a clean 200. The second: a journal rejection that lands at the same moment can cause a false lost-write error.

Two more points were raised but not posted. Holder lookup ignores network and orchestrator address, but nonces are random bytes32, so a collision across chains needs a deliberate reuse. An ordinary close of a mint that never signed a transaction keeps its pair, which is conservative, and the bot can pick a new nonce.

Comment thread src/mint/api/authorize.rs
Comment thread src/mint/api/authorize.rs

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 8ce2433e-698e-43c5-a53b-a11f2603170d

📥 Commits

Reviewing files that changed from the base of the PR and between 98841be and d7b1aec.

📒 Files selected for processing (7)
  • SPEC.md
  • migrations/20260924005309_create_mint_view_authorization_nonce_index.sql
  • src/mint/api/authorize.rs
  • src/mint/job.rs
  • src/mint/mod.rs
  • src/mint/view.rs
  • src/vault/mock.rs

Included review availability: This review used your included allowance. 5 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 8 reviews per hour.


Walkthrough

The mint model now tracks recipient/nonce pairs held across its lifecycle, and the mint view can find holders using an indexed candidate query. The authorization endpoint returns a conflict when another mint holds the pair, checks ownership before validation and again during serialized recording, and verifies that the recorded claim appears in the view. Tests cover concurrent deliveries, duplicate claims, state refusals, and nonce retention and release.

Priority: ➖ Normal

Unblocks: 2 PRs

Merge Risk: 🔵 Low · up to d7b1a

This change blocks duplicate recipient/nonce mint authorizations and serializes recording so that concurrent deliveries cannot both claim a pair. A rare case remains. If a view write is lost and a nonce is reused before the service restarts, two mints could hold the same pair. The change is mergeable if the owner accepts that edge case.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: preventing duplicate (recipient, nonce) pairs across mints. It is concise and directly related to the changeset.
Description check ✅ Passed The description accurately explains the duplicate-pair guard, conflict behavior, lifecycle rules, admission locking, risks, and test coverage. It is directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 5 files. (2 skipped: 2 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@rouzwelt
rouzwelt force-pushed the 2026-09-29-rai-2729 branch 2 times, most recently from d73c8ff to fa58c57 Compare October 1, 2026 17:00
@rouzwelt
rouzwelt force-pushed the 2026-09-24-rai-2620 branch from ff25a48 to 729d161 Compare October 1, 2026 17:00

@ueco-jb ueco-jb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record_under_admission now holds the process-wide AUTHORIZATION_ADMISSION lock from the record-stage holder read through the AuthorizeMint send and confirm_claim_visible. It is the only production sender of AuthorizeMint, Store::send writes mint_view before it returns, and SPEC limits the issuer to one process per store, so two mints can no longer both record one (recipient, nonce); concurrent_deliveries_of_one_pair_admit_exactly_one covers it. The identical-redelivery path, the releasing close through MintingFailed { NonceReplayUnresolved }, the "held by another mint" wording and the legacy-close and test docs are fixed.

Comment thread src/mint/api/authorize.rs

@ueco-jb ueco-jb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit is unchanged since 729d161 (git range-diff reports it identical); only the base moved to the new #427 head.

@agryaznov agryaznov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me — the hold/release boundary is now an exhaustive match with the schema bump behind it, the admission lock covers exactly the read-then-commit it has to, and the recipient key comes from the aggregate rather than the request body. Nothing further from me.

@graphite-app

graphite-app Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Merge activity

  • Oct 6, 4:11 PM UTC: rouzwelt added this pull request to the Graphite merge queue.
  • Oct 6, 4:12 PM UTC: The Graphite merge queue removed this pull request due to downstack failures on PR #419.
  • Oct 6, 4:12 PM UTC: The Graphite merge queue removed this pull request due to downstack failures on PR #419.
  • Oct 6, 11:43 PM UTC: rouzwelt added this pull request to the Graphite merge queue.
  • Oct 6, 11:44 PM UTC: CI is running for this pull request on a draft pull request (#455) due to your merge queue CI optimization settings.
  • Oct 6, 11:44 PM UTC: Merged by the Graphite merge queue via draft PR: #455.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

externally-merged Graphite MQ merged this PR; Linear should treat the close as a merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants