fix(privacy): bind open-note deposits to note ids created in the same batch - #935
Open
avi-starkware wants to merge 1 commit into
Open
fix(privacy): bind open-note deposits to note ids created in the same batch#935avi-starkware wants to merge 1 commit into
avi-starkware wants to merge 1 commit into
Conversation
avi-starkware
force-pushed
the
avi/privacy-open-note-deposit-identity
branch
4 times, most recently
from
August 10, 2026 12:14
91c6b84 to
c2e7fb5
Compare
avi-starkware
force-pushed
the
avi/privacy-open-note-deposit-identity
branch
from
August 11, 2026 15:52
c2e7fb5 to
305afdd
Compare
… batch `_apply_actions` reconciled creation against funding with a counter, which proves only that the two counts match. A returned deposit could name an unfunded open note from an earlier batch while the note this batch emitted stayed unfunded, leaving the target marked funded so its intended deposit can never land. Track the emitted note ids instead and require each `OpenNoteDeposit` to name one, removing the match on use so a single creation cannot absorb two deposits. Deposits may still arrive in any order relative to their creations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
avi-starkware
force-pushed
the
avi/privacy-open-note-deposit-identity
branch
from
August 17, 2026 07:40
305afdd to
9cd6528
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.
What
Replaces the scalar
undeposited_open_notescounter in_apply_actionswith a batch-local list of emitted note ids, and requires every returnedOpenNoteDepositto name one of them.Why
The counter proved only that the number of
EmitOpenNoteCreatedactions equalled the number of deposits — not that each deposit funded a note the batch actually created._deposit_to_open_notevalidates that the target note exists, carriesOPEN_NOTE_SALT, is unfunded and has a matching token, but nothing tied it to the current batch.So a deposit naming an unfunded open note from outside the batch satisfied the invariant while the note the batch had just emitted stayed unfunded. The out-of-batch target is then permanently marked funded (
current_amount != 0), so the deposit it was actually waiting for can never land — and note-index sequencing means that id cannot be recreated.Worth being precise about reachability: the old code already enforced
#deposits == #creationsper transaction, and a deposit can only target an unfunded note, so by inductionapply_actionsnever leaves an unfunded open note in storage. The state this needs is therefore not reachable throughapply_actionson the current class — it requires a legacy note left by a contract replacement. This is hardening of an invariant that was previously only guaranteed by a non-local induction argument, not a fix for a live exploit path.How
EmitOpenNoteCreatedappends the note id already present in the event payload — no recomputation._apply_invoke_and_depositstakesref undeposited_open_note_ids: Array<felt252>.utils::consume_undeposited_open_noteremoves exactly one matching id, or reverts with the newOPEN_NOTE_NOT_CREATED_IN_TX. Cairo arrays are append-only, so removal rebuilds the array minus the first match rather than swap-removing; the one-per-call property is what matters.undeposited_open_note_ids.is_empty(), keepingUNDEPOSITED_OPEN_NOTES.internal_errors::TOO_MANY_OPEN_NOTES_DEPOSITEDis removed — the membership check now fires first in every case that previously underflowed the counter. No references anywhere in the repo.interface.cairodocuments the new precondition and both reverts.Deposits may still arrive in any order relative to their creations; only identity is enforced.
Behavior change for callers
A deposit naming a note outside the current batch now reverts with
OPEN_NOTE_NOT_CREATED_IN_TXinstead ofNOTE_NOT_FOUND/NOTE_NOT_OPEN/NOTE_ALREADY_DEPOSITED, because the membership check runs before_deposit_to_open_note. Five existing tests asserted those errors while deliberately omitting the same-batch creation; each now passes a bareEmitOpenNoteCreatedfor the deposit's id so it still reaches its original assertion, keeping coverage of all four errors.Tests
test_open_note_deposit_rejects_note_created_in_another_tx— the core case, driven through the real client compile path.test_open_note_deposits_matched_out_of_creation_order— two creations, deposits returned reversed, both funded. Proves identity matching, not positional.test_open_note_deposit_rejects_double_consumption—[X, X]for one creation reverts, proving removal happened.test_undeposited_open_notesextended with "2 creations, 1 deposit".Verified the new test has teeth: with the identity check reduced to the old count-only behavior the attack test passes (the out-of-batch note is written funded from the invoked contract's tokens); with the check in place it reverts.
Verification
The 3 ignored are pre-existing
#[ignore]s intests/generate_reference_data.cairo.Gas cost of the added scan: +0.03% l2_gas on open-note tests (e.g.
test_open_note_multiple_depositors96,348,011 to 96,376,651); l1_gas and l1_data_gas unchanged.Note
packages/privacy/README.mddocuments a standalonedeposit_to_open_note()entrypoint with a "caller must be the specified depositor" rule that does not exist in the contract. Pre-existing doc drift, left alone as separable.This change is