Skip to content

feat: add cancel_authwit to Token, NFT and MultiToken - #36

Draft
alejoamiras wants to merge 1 commit into
stack/closeout-warningsfrom
stack/closeout-cancel-authwit
Draft

feat: add cancel_authwit to Token, NFT and MultiToken#36
alejoamiras wants to merge 1 commit into
stack/closeout-warningsfrom
stack/closeout-cancel-authwit

Conversation

@alejoamiras

@alejoamiras alejoamiras commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Part of a 4-PR all-or-nothing stack (#35#36#37#38). Drafts, proposed as one unit.

Adds cancel_authwit to Token, NFT and MultiToken. Every upstream reference token contract has it; none of ours did, so a private authwit that has been granted but not yet consumed could not be revoked.

Ported verbatim from upstream (aztec-packages v5.1.0):

#[external("private")]
fn cancel_authwit(inner_hash: Field) {
    let on_behalf_of = self.msg_sender();
    let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash);
    self.context.push_nullifier_unsafe(nullifier);
}

Cancellation pre-emits the same nullifier the consume path emits, so a later authwit-gated call fails on the duplicate.

Additive — a new selector; no existing ABI touched.

Tests (3 per contract, 9 total)

  • negative — grant → cancel → attempt to consume ⇒ fails with duplicate nullifiers
  • positive control — identical flow without the cancel succeeds, attributing the failure to the cancellation rather than to setup
  • caller isolation — a foreign account cancelling with the owner's exact inner_hash does not revoke the owner's authwit, proving the msg_sender binding

Validation

Full Noir suites: token 84 · nft 64 · multitoken 80. aztec compile OK.

Codex adversarial review: "safe and upstream-equivalent." Both of its suggestions applied. Note its proposed assertion string (already present) was empirically wrong — that's the world-state DB log line; the matchable revert reason is duplicate nullifiers, verified by running.

Upstream-parity function present in every upstream reference token
contract but absent from all three of ours. Without it, a private
authwit that has been granted but not yet consumed cannot be revoked.

Ported verbatim from the upstream Token/NFT contracts (aztec-packages
v5.1.0):

    #[external("private")]
    fn cancel_authwit(inner_hash: Field) {
        let on_behalf_of = self.msg_sender();
        let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash);
        self.context.push_nullifier_unsafe(nullifier);
    }

Cancellation pre-emits the same authwit nullifier the consume path
emits, so a later authwit-gated call fails with a duplicate nullifier.

Three tests per contract: the negative case (cancel then attempt to
consume -> duplicate nullifiers), a positive control (same flow without
the cancel succeeds, attributing the failure to the cancel), and caller
isolation (a foreign account cancelling with the owner's exact inner
hash does NOT revoke the owner's authwit, since the nullifier is bound
to msg_sender). READMEs list the new function.

Additive: a new selector, no existing ABI touched.

Validated: token 84 / nft 64 / multitoken 80 Noir tests, aztec compile
OK. Codex adversarial review: safe and upstream-equivalent; its two
suggestions (tighten the negative assertion, add the caller-isolation
test) are applied — note its proposed error string 'already present'
was empirically wrong, the matchable revert is 'duplicate nullifiers'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alejoamiras
alejoamiras force-pushed the stack/closeout-cancel-authwit branch from c6ba431 to 9c47655 Compare August 19, 2026 16:16
@github-actions

Copy link
Copy Markdown

Benchmark Comparison

CPU Cores RAM Arch
AMD EPYC 7763 64-Core Processor 4 16 GiB x64

Contract: escrow

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
🆕 (partial) withdraw 0 485,780 +Inf% 0 3,744 +Inf% 0 587,800 +Inf% N/A 14,023 +Inf%
🆕 withdraw 0 330,054 +Inf% 0 832 +Inf% 0 499,700 +Inf% N/A 11,274 +Inf%
🆕 withdraw_nft 0 378,231 +Inf% 0 1,440 +Inf% 0 527,400 +Inf% N/A 12,389 +Inf%

Contract: logic

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
🆕 get_escrow 0 390,336 +Inf% 0 192 +Inf% 0 456,000 +Inf% N/A 12,581 +Inf%
🆕 secret_key_to_public_keys 0 387,695 +Inf% 0 192 +Inf% 0 456,000 +Inf% N/A 12,616 +Inf%
🆕 share_escrow 0 304,211 +Inf% 0 1,952 +Inf% 0 520,700 +Inf% N/A 11,064 +Inf%

Contract: multitoken

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
🆕 burn_private 0 260,232 +Inf% 0 832 +Inf% 0 499,700 +Inf% N/A 10,046 +Inf%
🆕 burn_public 0 223,800 +Inf% 0 416 +Inf% 0 638,292 +Inf% N/A 9,662 +Inf%
🆕 initialize_transfer_commitment 0 225,054 +Inf% 0 768 +Inf% 0 474,500 +Inf% N/A 9,644 +Inf%
🆕 mint_to_private 0 297,795 +Inf% 0 1,408 +Inf% 0 511,400 +Inf% N/A 10,974 +Inf%
🆕 mint_to_public 0 223,800 +Inf% 0 416 +Inf% 0 637,800 +Inf% N/A 9,721 +Inf%
🆕 transfer_private_to_commitment 0 263,571 +Inf% 0 1,024 +Inf% 0 511,400 +Inf% N/A 10,105 +Inf%
🆕 transfer_private_to_private 0 336,124 +Inf% 0 2,048 +Inf% 0 555,100 +Inf% N/A 11,445 +Inf%
🆕 transfer_private_to_public 0 306,785 +Inf% 0 1,056 +Inf% 0 714,677 +Inf% N/A 10,990 +Inf%
🆕 transfer_public_to_commitment 0 223,800 +Inf% 0 640 +Inf% 0 663,516 +Inf% N/A 9,709 +Inf%
🆕 transfer_public_to_private 0 298,556 +Inf% 0 1,024 +Inf% 0 683,958 +Inf% N/A 10,856 +Inf%
🆕 transfer_public_to_public 0 223,800 +Inf% 0 480 +Inf% 0 673,505 +Inf% N/A 9,663 +Inf%

Contract: nft

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
🆕 burn_private 0 275,232 +Inf% 0 416 +Inf% 0 661,052 +Inf% N/A 10,560 +Inf%
🆕 burn_public 0 223,800 +Inf% 0 448 +Inf% 0 670,034 +Inf% N/A 9,686 +Inf%
🆕 mint_to_private 0 341,272 +Inf% 0 1,600 +Inf% 0 735,342 +Inf% N/A 11,406 +Inf%
🆕 mint_to_public 0 223,800 +Inf% 0 448 +Inf% 0 670,718 +Inf% N/A 9,638 +Inf%
🆕 transfer_private_to_private 0 249,433 +Inf% 0 832 +Inf% 0 499,700 +Inf% N/A 9,959 +Inf%
🆕 transfer_private_to_public 0 275,261 +Inf% 0 416 +Inf% 0 659,258 +Inf% N/A 10,582 +Inf%
🆕 transfer_public_to_private 0 292,557 +Inf% 0 992 +Inf% 0 683,268 +Inf% N/A 10,728 +Inf%
🆕 transfer_public_to_public 0 223,800 +Inf% 0 384 +Inf% 0 633,285 +Inf% N/A 9,678 +Inf%

Contract: token

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
🆕 burn_private 0 303,054 +Inf% 0 1,024 +Inf% 0 713,957 +Inf% N/A 10,931 +Inf%
🆕 burn_public 0 223,800 +Inf% 0 448 +Inf% 0 672,632 +Inf% N/A 9,648 +Inf%
🆕 initialize_transfer_commitment 0 225,054 +Inf% 0 768 +Inf% 0 474,500 +Inf% N/A 9,662 +Inf%
🆕 mint_to_private 0 355,507 +Inf% 0 2,144 +Inf% 0 738,070 +Inf% N/A 11,803 +Inf%
🆕 mint_to_public 0 223,800 +Inf% 0 448 +Inf% 0 672,227 +Inf% N/A 9,772 +Inf%
🆕 transfer_private_to_commitment 0 259,896 +Inf% 0 992 +Inf% 0 511,400 +Inf% N/A 10,107 +Inf%
🆕 transfer_private_to_private 0 343,699 +Inf% 0 2,592 +Inf% 0 557,600 +Inf% N/A 11,531 +Inf%
🆕 transfer_private_to_public 0 303,110 +Inf% 0 1,024 +Inf% 0 714,023 +Inf% N/A 11,004 +Inf%
🆕 transfer_private_to_public_with_commitment 0 307,280 +Inf% 0 1,600 +Inf% 0 747,323 +Inf% N/A 10,973 +Inf%
🆕 transfer_public_to_commitment 0 223,800 +Inf% 0 576 +Inf% 0 662,529 +Inf% N/A 9,625 +Inf%
🆕 transfer_public_to_private 0 296,520 +Inf% 0 992 +Inf% 0 683,304 +Inf% N/A 10,785 +Inf%
🆕 transfer_public_to_public 0 223,800 +Inf% 0 448 +Inf% 0 672,548 +Inf% N/A 9,706 +Inf%

Contract: vault

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
🆕 deposit_private_to_private 0 507,983 +Inf% 0 1,312 +Inf% 0 878,248 +Inf% N/A 14,257 +Inf%
🆕 deposit_private_to_private_exact 0 611,708 +Inf% 0 1,888 +Inf% 0 915,316 +Inf% N/A 16,134 +Inf%
🆕 deposit_private_to_public 0 435,487 +Inf% 0 768 +Inf% 0 862,273 +Inf% N/A 13,184 +Inf%
🆕 deposit_public_to_private 0 424,931 +Inf% 0 1,984 +Inf% 0 965,045 +Inf% N/A 13,042 +Inf%
🆕 deposit_public_to_private_exact 0 424,942 +Inf% 0 1,952 +Inf% 0 949,469 +Inf% N/A 13,115 +Inf%
🆕 deposit_public_to_public 0 223,800 +Inf% 0 832 +Inf% 0 897,563 +Inf% N/A 9,641 +Inf%
🆕 issue_private_to_private_exact 0 611,708 +Inf% 0 1,888 +Inf% 0 915,976 +Inf% N/A 16,095 +Inf%
🆕 issue_private_to_public_exact 0 484,222 +Inf% 0 1,344 +Inf% 0 899,998 +Inf% N/A 13,791 +Inf%
🆕 issue_public_to_private 0 346,383 +Inf% 0 1,376 +Inf% 0 921,661 +Inf% N/A 11,563 +Inf%
🆕 issue_public_to_public 0 223,800 +Inf% 0 832 +Inf% 0 898,253 +Inf% N/A 9,697 +Inf%
🆕 redeem_private_to_private_exact 0 614,712 +Inf% 0 1,888 +Inf% 0 915,823 +Inf% N/A 16,059 +Inf%
🆕 redeem_private_to_public 0 435,430 +Inf% 0 768 +Inf% 0 862,717 +Inf% N/A 13,015 +Inf%
🆕 redeem_public_to_private_exact 0 428,003 +Inf% 0 1,952 +Inf% 0 949,790 +Inf% N/A 13,135 +Inf%
🆕 redeem_public_to_public 0 223,800 +Inf% 0 832 +Inf% 0 898,085 +Inf% N/A 9,652 +Inf%
🆕 withdraw_private_to_private 0 510,987 +Inf% 0 1,312 +Inf% 0 878,458 +Inf% N/A 14,131 +Inf%
🆕 withdraw_private_to_private_exact 0 614,712 +Inf% 0 1,888 +Inf% 0 915,769 +Inf% N/A 16,129 +Inf%
🆕 withdraw_private_to_public_exact 0 484,165 +Inf% 0 1,344 +Inf% 0 900,232 +Inf% N/A 13,865 +Inf%
🆕 withdraw_public_to_private 0 441,260 +Inf% 0 2,528 +Inf% 0 967,356 +Inf% N/A 13,197 +Inf%
🆕 withdraw_public_to_public 0 223,800 +Inf% 0 832 +Inf% 0 898,388 +Inf% N/A 9,727 +Inf%

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant