From 8c07137f3feccdbbcf1c9e9dea75adcb050bfd6c Mon Sep 17 00:00:00 2001 From: Jamie Newton <33573418+newtsjamie@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:03:51 +0900 Subject: [PATCH] fix: remove auto-revoke from authorize_session_key, increase MAX_SESSION_KEYS to 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Aztec's UTXO model, atomically revoking existing session key notes and inserting a new one causes nullifier conflicts when the PXE has stale state or a previous TX is still in the mempool. Two TXs cannot consume the same note — this is fundamental, not a sync issue. Changes: - authorize_session_key is now insert-only (no get_notes + remove loop) - MAX_SESSION_KEYS increased from 5 to 10 for orphaned note headroom - Stale notes cleaned up via revoke_session_key (called from client on a best-effort basis before each new authorization) Companion PR: Apertrue/web#24 Co-Authored-By: Claude Opus 4.6 --- webauthn_account/src/main.nr | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/webauthn_account/src/main.nr b/webauthn_account/src/main.nr index 2b04c3c..906f458 100644 --- a/webauthn_account/src/main.nr +++ b/webauthn_account/src/main.nr @@ -53,9 +53,13 @@ pub contract WebAuthnAccount { // Session key: 1 (discriminator) + 2 (pubkey) + 64 (sig) = 67 // Use 524 (max); session key path ignores trailing zeros. global AUTH_WITNESS_LEN: u32 = 524; - // Max number of concurrent session keys per account. - // Used as compile-time loop bound for note iteration. - global MAX_SESSION_KEYS: u32 = 5; + // Max number of session key notes the PXE will read per query. + // Used as compile-time loop bound for note iteration in is_valid_impl + // and revoke_session_key. Higher = more headroom for orphaned notes + // (browser refresh loses the in-memory key but the on-chain note + // persists until explicitly revoked). 10 gives ample margin for + // typical usage while keeping circuit size reasonable. + global MAX_SESSION_KEYS: u32 = 10; // P-256 public key stored as an encrypted note. // Written once in constructor, read every time the account signs a TX. @@ -179,9 +183,13 @@ pub contract WebAuthnAccount { // This TX goes through the account entrypoint (requires biometric). // After this, subsequent TXs can use the session key path (no biometric). // - // Auto-revokes ALL existing session key notes before inserting the new - // one. This prevents orphaned notes accumulating when the browser page - // is refreshed (in-memory key lost but on-chain note persists). + // INSERT-ONLY: does NOT revoke existing session key notes. In Aztec's + // UTXO model, atomically revoking + inserting causes nullifier conflicts + // when the PXE has stale state or a previous TX is still in the mempool. + // Stale notes are cleaned up separately via revoke_session_key() (called + // from the client on a best-effort basis before each new authorization). + // The MAX_SESSION_KEYS limit (10) provides headroom for orphaned notes + // between cleanup cycles. #[external("private")] fn authorize_session_key( pubkey_x: Field, @@ -194,16 +202,6 @@ pub contract WebAuthnAccount { let owner = self.context.this_address(); - // Revoke all existing session key notes (prevents orphan accumulation) - let mut options = ::aztec::note::note_getter_options::NoteGetterOptions::new(); - options = options.set_limit(MAX_SESSION_KEYS); - let existing = self.storage.session_keys.at(owner).get_notes(options); - for i in 0..MAX_SESSION_KEYS { - if i < existing.len() { - self.storage.session_keys.at(owner).remove(existing.get(i)); - } - } - let note = SessionKeyNote { pubkey_x, pubkey_y, expiry, scope }; self.storage.session_keys.at(owner).insert(note).deliver( MessageDelivery.ONCHAIN_CONSTRAINED,