From 29334a439c3e2aa469d8f7f4003873a4640c2e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 16 Mar 2026 21:45:42 +0000 Subject: [PATCH 1/9] split compute note hash and nullif into two fns --- .../aztec-nr/aztec/src/macros/aztec.nr | 228 ++++++++++++----- .../aztec/src/messages/discovery/mod.nr | 25 +- .../src/messages/discovery/nonce_discovery.nr | 238 +++++++++++------- .../src/messages/discovery/partial_notes.nr | 10 +- .../src/messages/discovery/private_notes.nr | 18 +- .../src/messages/discovery/process_message.nr | 18 +- .../src/test/helpers/test_environment.nr | 70 +++--- 7 files changed, 392 insertions(+), 215 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 30aaa434c197..eeb95edc729c 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -105,7 +105,7 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { let contract_library_method_compute_note_hash_and_nullifier = if !m.functions().any(|f| { f.name() == quote { _compute_note_hash_and_nullifier } }) { - generate_contract_library_method_compute_note_hash_and_nullifier() + generate_contract_library_methods_compute_note_hash_and_nullifier() } else { quote {} }; @@ -220,15 +220,75 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } -/// Generates a contract library method called `_compute_note_hash_and_nullifier` which is used for note discovery (to -/// create the `aztec::messages::discovery::ComputeNoteHashAndNullifier` function) and to implement the -/// `compute_note_hash_and_nullifier` unconstrained contract function. -comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() -> Quoted { - if NOTES.len() > 0 { +/// Generates two contract library methods called `_compute_note_hash` and `_compute_nullifier`, plus a (deprecated) +/// wrapper called `_compute_note_hash_and_nullifier`, which are used for note discovery (i.e. these are of the +/// `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNullifier` types). +comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() -> Quoted { + let compute_note_hash = generate_contract_library_method_compute_note_hash(); + let compute_nullifier = generate_contract_library_method_compute_nullifier(); + + quote { + $compute_note_hash + $compute_nullifier + + /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash tree with `note_nonce`. + /// + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHashAndNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + #[deprecated("This function has been deprecated in favor of compute_note_hash and compute_nullifier")] + #[allow(dead_code)] + unconstrained fn _compute_note_hash_and_nullifier( + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + storage_slot: Field, + note_type_id: Field, + contract_address: aztec::protocol::address::AztecAddress, + randomness: Field, + note_nonce: Field, + ) -> Option { + _compute_note_hash(packed_note, owner, storage_slot, note_type_id, contract_address, randomness).map(|note_hash| { + + let siloed_note_hash = aztec::protocol::hash::compute_siloed_note_hash(contract_address, note_hash); + let unique_note_hash = aztec::protocol::hash::compute_unique_note_hash(note_nonce, siloed_note_hash); + + let inner_nullifier = _compute_nullifier(unique_note_hash, packed_note, owner, storage_slot, note_type_id, contract_address, randomness); + + aztec::messages::discovery::NoteHashAndNullifier { + note_hash, + inner_nullifier, + } + }) + } + } +} + +comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { + if NOTES.len() == 0 { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. + quote { + /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_hash( + _packed_note: BoundedVec, + _owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + panic(f"This contract does not use private notes") + } + } + } else { // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and - // compute the note hash (non-siloed) and inner nullifier (also non-siloed). + // compute the note hash (non-siloed). let mut if_note_type_id_match_statements_list = @[]; for i in 0..NOTES.len() { @@ -251,12 +311,6 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() - quote { compute_note_hash }, ); - let compute_nullifier_unconstrained = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteHash }, - quote { compute_nullifier_unconstrained }, - ); - let if_or_else_if = if i == 0 { quote { if } } else { @@ -280,40 +334,7 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() - } else { let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); - let note_hash = $compute_note_hash(note, owner, storage_slot, randomness); - - // The message discovery process finds settled notes, that is, notes that were created in - // prior transactions and are therefore already part of the note hash tree. We therefore - // compute the nullification note hash by treating the note as a settled note with the - // provided note nonce. - let note_hash_for_nullification = - aztec::note::utils::compute_note_hash_for_nullification( - aztec::note::HintedNote { - note, - contract_address, - owner, - randomness, - storage_slot, - metadata: - aztec::note::note_metadata::SettledNoteMetadata::new( - note_nonce, - ) - .into(), - }, - ); - - let inner_nullifier = $compute_nullifier_unconstrained( - note, - owner, - note_hash_for_nullification, - ); - - Option::some( - aztec::messages::discovery::NoteHashAndNullifier { - note_hash, - inner_nullifier, - }, - ) + Option::some($compute_note_hash(note, owner, storage_slot, randomness)) } } }, @@ -323,28 +344,31 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() - let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); quote { - /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash tree with `note_nonce`. + /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed). /// - /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHashAndNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHash` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. /// /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - unconstrained fn _compute_note_hash_and_nullifier( + unconstrained fn _compute_note_hash( packed_note: BoundedVec, owner: aztec::protocol::address::AztecAddress, storage_slot: Field, note_type_id: Field, - contract_address: aztec::protocol::address::AztecAddress, + _contract_address: aztec::protocol::address::AztecAddress, randomness: Field, - note_nonce: Field, - ) -> Option { + ) -> Option { $if_note_type_id_match_statements else { Option::none() } } } - } else { + } +} + +comptime fn generate_contract_library_method_compute_nullifier() -> Quoted { + if NOTES.len() == 0 { // Contracts with no notes still implement this function to avoid having special-casing, the implementation // simply throws immediately. quote { @@ -352,18 +376,102 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() - /// /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - unconstrained fn _compute_note_hash_and_nullifier( + unconstrained fn _compute_nullifier( + _unique_note_hash: Field, _packed_note: BoundedVec, _owner: aztec::protocol::address::AztecAddress, _storage_slot: Field, _note_type_id: Field, _contract_address: aztec::protocol::address::AztecAddress, _randomness: Field, - _nonce: Field, - ) -> Option { + ) -> Option { panic(f"This contract does not use private notes") } } + } else { + // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the + // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we + // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // compute the note hash (non-siloed) and inner nullifier (also non-siloed). + + let mut if_note_type_id_match_statements_list = @[]; + for i in 0..NOTES.len() { + let typ = NOTES.get(i); + + let get_note_type_id = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteType }, + quote { get_id }, + ); + let unpack = get_trait_impl_method( + typ, + quote { crate::protocol::traits::Packable }, + quote { unpack }, + ); + + let compute_nullifier_unconstrained = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteHash }, + quote { compute_nullifier_unconstrained }, + ); + + let if_or_else_if = if i == 0 { + quote { if } + } else { + quote { else if } + }; + + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( + quote { + $if_or_else_if note_type_id == $get_note_type_id() { + // As an extra safety check we make sure that the packed_note BoundedVec has the expected + // length, since we're about to interpret its raw storage as a fixed-size array by calling the + // unpack function on it. + let expected_len = <$typ as $crate::protocol::traits::Packable>::N; + let actual_len = packed_note.len(); + if actual_len != expected_len { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", + [expected_len as Field, actual_len as Field, note_type_id], + ); + Option::none() + } else { + let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); + + // The message discovery process finds settled notes, that is, notes that were created in + // prior transactions and are therefore already part of the note hash tree. The note hash + // for nullification is hence the unique note hash. + $compute_nullifier_unconstrained(note, owner, unique_note_hash) + } + } + }, + ); + } + + let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); + + quote { + /// Computes a note's inner nullifier (non-siloed) given its unique note hash, preimage and extra data. + /// + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_nullifier( + unique_note_hash: Field, + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + $if_note_type_id_match_statements + else { + Option::none() + } + } + } } } @@ -382,7 +490,8 @@ comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_ let address = aztec::context::UtilityContext::new().this_address(); aztec::messages::discovery::do_sync_state( address, - _compute_note_hash_and_nullifier, + _compute_note_hash, + _compute_nullifier, $process_custom_message_option, $offchain_inbox_sync_option, ); @@ -412,7 +521,8 @@ comptime fn generate_process_message(process_custom_message_option: Quoted) -> Q aztec::messages::discovery::process_message::process_message_ciphertext( address, - _compute_note_hash_and_nullifier, + _compute_note_hash, + _compute_nullifier, $process_custom_message_option, message_ciphertext, message_context, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index 2c76990dee42..e1fa8b688e32 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -88,6 +88,14 @@ pub type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* randomness */ Field, /* note nonce */ Field) -> Option; +pub type ComputeNoteHash = unconstrained fn(/* packed_note */BoundedVec, /* + owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* +randomness */ Field) -> Option; + +pub type ComputeNullifier = unconstrained fn(/* unique_note_hash */Field, /* packed_note */ BoundedVec, /* + owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* +randomness */ Field) -> Option; + /// A handler for custom messages. /// /// Contracts that emit custom messages (i.e. any with a message type that is not in [`crate::messages::msg_type`]) @@ -108,9 +116,10 @@ pub type CustomMessageHandler = unconstrained fn[Env]( /// /// The private state will be synchronized up to the block that will be used for private transactions (i.e. the anchor /// block. This will typically be close to the tip of the chain. -pub unconstrained fn do_sync_state( +pub unconstrained fn do_sync_state( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, process_custom_message: Option>, offchain_inbox_sync: Option>, ) { @@ -127,7 +136,8 @@ pub unconstrained fn do_sync_state( +pub(crate) unconstrained fn attempt_note_nonce_discovery( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, contract_address: AztecAddress, owner: AztecAddress, storage_slot: Field, @@ -42,67 +43,84 @@ pub(crate) unconstrained fn attempt_note_nonce_discovery( [unique_note_hashes_in_tx.len() as Field, contract_address.to_field(), storage_slot], ); - // We need to find nonces (typically just one) that result in a note hash that, once siloed into a unique note - // hash, is one of the note hashes created by the transaction. - // The nonce is meant to be derived from the index of the note hash in the transaction effects array. However, due - // to an issue in the kernels the nonce might actually use any of the possible note hash indices - not necessarily - // the one that corresponds to the note hash. Hence, we need to try them all. - for i in 0..MAX_NOTE_HASHES_PER_TX { - let nonce_for_i = compute_note_hash_nonce(first_nullifier_in_tx, i); - - // Given note nonce, note content and metadata, we can compute the note hash and silo it to check if - // the resulting unique note matches any in the transaction. - // TODO(#11157): handle failed note_hash_and_nullifier computation - let hashes = compute_note_hash_and_nullifier( - packed_note, - owner, - storage_slot, - note_type_id, - contract_address, - randomness, - nonce_for_i, - ) - .expect(f"Failed to compute a note hash for note type {note_type_id}"); - - let siloed_note_hash_for_i = compute_siloed_note_hash(contract_address, hashes.note_hash); - let unique_note_hash_for_i = compute_unique_note_hash(nonce_for_i, siloed_note_hash_for_i); - - let matching_notes = bvec_filter( - unique_note_hashes_in_tx, - |unique_note_hash_in_tx| unique_note_hash_in_tx == unique_note_hash_for_i, + let maybe_note_hash = compute_note_hash( + packed_note, + owner, + storage_slot, + note_type_id, + contract_address, + randomness, + ); + + if maybe_note_hash.is_none() { + aztecnr_warn_log_format!( + "Unable to compute note hash for note of id {0} with packed length {1}, skipping nonce discovery", + )( + [note_type_id, packed_note.len() as Field], ); - if matching_notes.len() > 1 { - let identical_note_hashes = matching_notes.len(); - // Note that we don't actually check that the note hashes array contains unique values, only that the note - // we found is unique. We don't expect for this to ever happen (it'd indicate a malicious node or PXE, - // which - // are both assumed to be cooperative) so testing for it just in case is unnecessary, but we _do_ need to - // handle it if we find a duplicate. - panic( - f"Received {identical_note_hashes} identical note hashes for a transaction - these should all be unique", - ) - } else if matching_notes.len() == 1 { - // Note that while we did check that the note hash is the preimage of a unique note hash, we perform no - // validations on the nullifier - we fundamentally cannot, since only the application knows how to compute - // nullifiers. We simply trust it to have provided the correct one: if it hasn't, then PXE may fail to - // realize that a given note has been nullified already, and calls to the application could result in - // invalid transactions (with duplicate nullifiers). This is not a concern because an application already - // has more direct means of making a call to it fail the transaction. - discovered_notes.push( - DiscoveredNoteInfo { - note_nonce: nonce_for_i, - note_hash: hashes.note_hash, - // TODO: The None case will be handled in a followup PR. - // https://linear.app/aztec-labs/issue/F-265/store-external-notes - inner_nullifier: hashes.inner_nullifier.expect( - f"Failed to compute nullifier for note type {note_type_id}", - ), - }, + } else { + let note_hash = maybe_note_hash.unwrap(); + let siloed_note_hash = compute_siloed_note_hash(contract_address, note_hash); + + // We need to find nonces (typically just one) that result in the siloed note hash that being uniqued into one + // of the transaction's effects. + // The nonce is meant to be derived from the index of the note hash in the transaction effects array. However, + // due to an issue in the kernels the nonce might actually use any of the possible note hash indices - not + // necessarily the one that corresponds to the note hash. Hence, we need to try them all. + for i in 0..MAX_NOTE_HASHES_PER_TX { + let nonce_for_i = compute_note_hash_nonce(first_nullifier_in_tx, i); + let unique_note_hash_for_i = compute_unique_note_hash(nonce_for_i, siloed_note_hash); + + let matching_notes = bvec_filter( + unique_note_hashes_in_tx, + |unique_note_hash_in_tx| unique_note_hash_in_tx == unique_note_hash_for_i, ); + if matching_notes.len() > 1 { + let identical_note_hashes = matching_notes.len(); + // Note that we don't actually check that the note hashes array contains unique values, only that the + // note we found is unique. We don't expect for this to ever happen (it'd indicate a malicious node or + // PXE, which are both assumed to be cooperative) so testing for it just in case is unnecessary, but we + // _do_ need to handle it if we find a duplicate. + panic( + f"Received {identical_note_hashes} identical note hashes for a transaction - these should all be unique", + ) + } else if matching_notes.len() == 1 { + let maybe_inner_nullifier_for_i = compute_nullifier( + unique_note_hash_for_i, + packed_note, + owner, + storage_slot, + note_type_id, + contract_address, + randomness, + ); - // We don't exit the loop - it is possible (though rare) for the exact same note content to be present - // multiple times in the same transaction with different nonces. This typically doesn't happen due to notes - // containing random values in order to hide their contents. + if maybe_inner_nullifier_for_i.is_none() { + // TODO: down the line we want to be able to store notes for which we don't know their nullifier, + // e.g. notes that belong to someone that is not us (and for which we therefore don't know their + // associated app-siloed nullifer hiding secret key). + // https://linear.app/aztec-labs/issue/F-265/store-external-notes + aztecnr_warn_log!("Unable to compute nullifier, skipping nonce discovery"); + } else { + // Note that while we did check that the note hash is the preimage of a unique note hash, we + // perform no validations on the nullifier - we fundamentally cannot, since only the application + // knows how to compute nullifiers. We simply trust it to have provided the correct one: if it + // hasn't, then PXE may fail to realize that a given note has been nullified already, and calls to + // the application could result in invalid transactions (with duplicate nullifiers). This is not a + // concern because an application already has more direct means of making a call to it fail the + // transaction. + discovered_notes.push( + DiscoveredNoteInfo { + note_nonce: nonce_for_i, + note_hash, + inner_nullifier: maybe_inner_nullifier_for_i.unwrap(), + }, + ); + } + // We don't exit the loop - it is possible (though rare) for the exact same note content to be present + // multiple times in the same transaction with different nonces. This typically doesn't happen due to + // notes containing random values in order to hide their contents. + } } } @@ -129,9 +147,8 @@ unconstrained fn bvec_filter( mod test { use crate::{ - messages::{discovery::NoteHashAndNullifier, logs::note::MAX_NOTE_PACKED_LEN}, + messages::logs::note::MAX_NOTE_PACKED_LEN, note::{ - HintedNote, note_interface::{NoteHash, NoteType}, note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullification, @@ -151,33 +168,35 @@ mod test { // This implementation could be simpler, but this serves as a nice example of the expected flow in a real // implementation, and as a sanity check that the interface is sufficient. - unconstrained fn compute_note_hash_and_nullifier( + + unconstrained fn compute_note_hash( packed_note: BoundedVec, owner: AztecAddress, storage_slot: Field, note_type_id: Field, - contract_address: AztecAddress, + _contract_address: AztecAddress, randomness: Field, - note_nonce: Field, - ) -> Option { - if note_type_id == MockNote::get_id() { + ) -> Option { + if (note_type_id == MockNote::get_id()) & (packed_note.len() == ::N) { let note = MockNote::unpack(array::subarray(packed_note.storage(), 0)); - let note_hash = note.compute_note_hash(owner, storage_slot, randomness); - - let note_hash_for_nullification = compute_note_hash_for_nullification( - HintedNote { - note, - contract_address, - owner, - randomness, - storage_slot, - metadata: SettledNoteMetadata::new(note_nonce).into(), - }, - ); - - let inner_nullifier = note.compute_nullifier_unconstrained(owner, note_hash_for_nullification); + Option::some(note.compute_note_hash(owner, storage_slot, randomness)) + } else { + Option::none() + } + } - Option::some(NoteHashAndNullifier { note_hash, inner_nullifier }) + unconstrained fn compute_nullifier( + unique_note_hash: Field, + packed_note: BoundedVec, + owner: AztecAddress, + _storage_slot: Field, + note_type_id: Field, + _contract_address: AztecAddress, + _randomness: Field, + ) -> Option { + if (note_type_id == MockNote::get_id()) & (packed_note.len() == ::N) { + let note = MockNote::unpack(array::subarray(packed_note.storage(), 0)); + note.compute_nullifier_unconstrained(owner, unique_note_hash) } else { Option::none() } @@ -198,7 +217,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -210,22 +230,41 @@ mod test { assert_eq(discovered_notes.len(), 0); } - #[test(should_fail_with = "Failed to compute a note hash")] - unconstrained fn failed_hash_computation() { + #[test] + unconstrained fn failed_hash_computation_is_ignored() { let unique_note_hashes_in_tx = BoundedVec::from_array([random()]); - let packed_note = BoundedVec::new(); - let note_type_id = 0; // This note type id is unknown to compute_note_hash_and_nullifier let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + |_, _, _, _, _, _| Option::none(), + compute_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, RANDOMNESS, - note_type_id, - packed_note, + MockNote::get_id(), + BoundedVec::new(), + ); + + assert_eq(discovered_notes.len(), 0); + } + + #[test] + unconstrained fn failed_nullifier_computation_is_ignored() { + let unique_note_hashes_in_tx = BoundedVec::from_array([random()]); + + let discovered_notes = attempt_note_nonce_discovery( + unique_note_hashes_in_tx, + FIRST_NULLIFIER_IN_TX, + compute_note_hash, + |_, _, _, _, _, _, _| Option::none(), + CONTRACT_ADDRESS, + OWNER, + STORAGE_SLOT, + RANDOMNESS, + MockNote::get_id(), + BoundedVec::new(), ); assert_eq(discovered_notes.len(), 0); @@ -276,7 +315,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -315,7 +355,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -354,7 +395,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -387,7 +429,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -421,7 +464,8 @@ mod test { let _ = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index bf31e253f1d9..ba862e4ca822 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -1,7 +1,7 @@ use crate::{ capsules::CapsuleArray, messages::{ - discovery::{ComputeNoteHashAndNullifier, nonce_discovery::attempt_note_nonce_discovery}, + discovery::{ComputeNoteHash, ComputeNullifier, nonce_discovery::attempt_note_nonce_discovery}, encoding::MAX_MESSAGE_CONTENT_LEN, logs::partial_note::{decode_partial_note_private_message, MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN}, processing::{ @@ -72,9 +72,10 @@ pub(crate) unconstrained fn process_partial_note_private_msg( /// Searches for logs that would result in the completion of pending partial notes, ultimately resulting in the notes /// being delivered to PXE if completed. -pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( +pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, ) { let pending_partial_notes = CapsuleArray::at( contract_address, @@ -130,7 +131,8 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( let discovered_notes = attempt_note_nonce_discovery( log.unique_note_hashes_in_tx, log.first_nullifier_in_tx, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, contract_address, pending_partial_note.owner, storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr index c7aef71b9688..41fb73d59716 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr @@ -1,19 +1,20 @@ use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}; use crate::messages::{ - discovery::{ComputeNoteHashAndNullifier, nonce_discovery::attempt_note_nonce_discovery}, + discovery::{ComputeNoteHash, ComputeNullifier, nonce_discovery::attempt_note_nonce_discovery}, encoding::MAX_MESSAGE_CONTENT_LEN, logs::note::{decode_private_note_message, MAX_NOTE_PACKED_LEN}, processing::enqueue_note_for_validation, }; use crate::protocol::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX}; -pub(crate) unconstrained fn process_private_note_msg( +pub(crate) unconstrained fn process_private_note_msg( contract_address: AztecAddress, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, msg_metadata: u64, msg_content: BoundedVec, ) { @@ -28,7 +29,8 @@ pub(crate) unconstrained fn process_private_note_msg( unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, owner, storage_slot, randomness, @@ -46,13 +48,14 @@ pub(crate) unconstrained fn process_private_note_msg( /// Attempts discovery of a note given information about its contents and the transaction in which it is suspected the /// note was created. -pub unconstrained fn attempt_note_discovery( +pub unconstrained fn attempt_note_discovery( contract_address: AztecAddress, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, owner: AztecAddress, storage_slot: Field, randomness: Field, @@ -62,7 +65,8 @@ pub unconstrained fn attempt_note_discovery( let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, first_nullifier_in_tx, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, contract_address, owner, storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr index db54d39ecd65..c52ae152de2c 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr @@ -1,6 +1,6 @@ use crate::messages::{ discovery::{ - ComputeNoteHashAndNullifier, CustomMessageHandler, partial_notes::process_partial_note_private_msg, + ComputeNoteHash, ComputeNullifier, CustomMessageHandler, partial_notes::process_partial_note_private_msg, private_events::process_private_event_msg, private_notes::process_private_note_msg, }, encoding::{decode_message, MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN}, @@ -26,9 +26,10 @@ use crate::protocol::address::AztecAddress; /// /// Events are processed by computing an event commitment from the serialized event data and its randomness field, then /// enqueueing the event data and commitment for validation. -pub unconstrained fn process_message_ciphertext( +pub unconstrained fn process_message_ciphertext( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, process_custom_message: Option>, message_ciphertext: BoundedVec, message_context: MessageContext, @@ -38,7 +39,8 @@ pub unconstrained fn process_message_ciphertext( +pub(crate) unconstrained fn process_message_plaintext( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, process_custom_message: Option>, message_plaintext: BoundedVec, message_context: MessageContext, @@ -73,7 +76,8 @@ pub(crate) unconstrained fn process_message_plaintext = |packed_note, owner, storage_slot, note_type_id, contract_address, randomness, note_nonce| { + // We also need to provide an implementation for the `compute_note_hash` and `compute_nullifier` functions, + // which would typically be created by the macros for the different notes in a given contract. Here we build + // variants specialized for `Note`. + let compute_note_hash: ComputeNoteHash = |packed_note, owner, storage_slot, note_type_id, _contract_address, randomness| { assert_eq(note_type_id, Note::get_id()); assert_eq(packed_note.len(), ::N); let note = Note::unpack(subarray(packed_note.storage(), 0)); - let note_hash = note.compute_note_hash(owner, storage_slot, randomness); - let note_hash_for_nullification = compute_note_hash_for_nullification( - HintedNote { - note, - contract_address, - owner, - randomness, - storage_slot, - metadata: SettledNoteMetadata::new(note_nonce).into(), - }, - ); + Option::some(note.compute_note_hash(owner, storage_slot, randomness)) + }; - let inner_nullifier = note.compute_nullifier_unconstrained(owner, note_hash_for_nullification); + let compute_nullifier: ComputeNullifier = |unique_note_hash, packed_note, owner, _storage_slot, note_type_id, _contract_address, _randomness| { + assert_eq(note_type_id, Note::get_id()); + assert_eq(packed_note.len(), ::N); - Option::some(NoteHashAndNullifier { note_hash, inner_nullifier }) + let note = Note::unpack(subarray(packed_note.storage(), 0)); + + note.compute_nullifier_unconstrained(owner, unique_note_hash) }; let process_custom_message: Option> = Option::none(); @@ -814,7 +802,8 @@ impl TestEnvironment { message_plaintext, opts.contract_address, Option::some(note_message.new_note.owner), - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, process_custom_message, ); } @@ -907,12 +896,18 @@ impl TestEnvironment { event_message.new_event.randomness, )); - // We also need to provide an implementation for the `compute_note_hash_and_nullifier` function, which would - // typically be created by the macros for the different notes in a given contract. Here we build an empty one, - // since it will never be invoked as this is an event and not a note message. - let compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier<_> = |_packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness, _note_nonce| { + // We also need to provide an implementation for the `compute_note_hash` and `compute_nullifier` functions, + // which would typically be created by the macros for the different notes in a given contract. Here we build + // empty ones, since they will never be invoked as this is an event and not a note message. + let compute_note_hash: ComputeNoteHash = |_packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { + panic( + f"Unexpected compute_note_hash invocation in TestEnvironment::discover_event", + ) + }; + + let compute_nullifier: ComputeNullifier = |_unique_note_hash, _packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { panic( - f"Unexpected compute_note_hash_and_nullifier invocation in TestEnvironment::discover_event", + f"Unexpected compute_nullifier invocation in TestEnvironment::discover_event", ) }; @@ -921,17 +916,19 @@ impl TestEnvironment { message_plaintext, opts.contract_address, Option::some(recipient), - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, process_custom_message, ); } - unconstrained fn discover_data_in_message_plaintext( + unconstrained fn discover_data_in_message_plaintext( self, message_plaintext: BoundedVec, contract_address: Option, recipient: Option, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_nullifier: ComputeNullifier, process_custom_message: Option>, ) { // This function will emulate the message discovery and processing that would happen in a real contract, based @@ -960,7 +957,8 @@ impl TestEnvironment { self.utility_context_opts(UtilityContextOptions { contract_address }, |context| { process_message_plaintext( context.this_address(), - compute_note_hash_and_nullifier, + compute_note_hash, + compute_nullifier, process_custom_message, message_plaintext, message_context, From 5d909a1b8e6b7841243bcaf4abf160eb6832faa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 16 Mar 2026 21:54:29 +0000 Subject: [PATCH 2/9] rename compute_nullifier --- .../aztec-nr/aztec/src/macros/aztec.nr | 25 ++++++++++--------- .../aztec/src/messages/discovery/mod.nr | 10 ++++---- .../src/messages/discovery/nonce_discovery.nr | 22 ++++++++-------- .../src/messages/discovery/partial_notes.nr | 6 ++--- .../src/messages/discovery/private_notes.nr | 10 ++++---- .../src/messages/discovery/process_message.nr | 10 ++++---- .../src/test/helpers/test_environment.nr | 22 ++++++++-------- 7 files changed, 54 insertions(+), 51 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index eeb95edc729c..4264538b3968 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -220,16 +220,17 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } -/// Generates two contract library methods called `_compute_note_hash` and `_compute_nullifier`, plus a (deprecated) +/// Generates two contract library methods called `_compute_note_hash` and `_compute_note_nullifier`, plus a +/// (deprecated) /// wrapper called `_compute_note_hash_and_nullifier`, which are used for note discovery (i.e. these are of the -/// `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNullifier` types). +/// `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNoteNullifier` types). comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() -> Quoted { let compute_note_hash = generate_contract_library_method_compute_note_hash(); - let compute_nullifier = generate_contract_library_method_compute_nullifier(); + let compute_note_nullifier = generate_contract_library_method_compute_note_nullifier(); quote { $compute_note_hash - $compute_nullifier + $compute_note_nullifier /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash tree with `note_nonce`. /// @@ -237,7 +238,7 @@ comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() /// /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - #[deprecated("This function has been deprecated in favor of compute_note_hash and compute_nullifier")] + #[deprecated("This function has been deprecated in favor of compute_note_hash and compute_note_nullifier")] #[allow(dead_code)] unconstrained fn _compute_note_hash_and_nullifier( packed_note: BoundedVec, @@ -253,7 +254,7 @@ comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() let siloed_note_hash = aztec::protocol::hash::compute_siloed_note_hash(contract_address, note_hash); let unique_note_hash = aztec::protocol::hash::compute_unique_note_hash(note_nonce, siloed_note_hash); - let inner_nullifier = _compute_nullifier(unique_note_hash, packed_note, owner, storage_slot, note_type_id, contract_address, randomness); + let inner_nullifier = _compute_note_nullifier(unique_note_hash, packed_note, owner, storage_slot, note_type_id, contract_address, randomness); aztec::messages::discovery::NoteHashAndNullifier { note_hash, @@ -367,7 +368,7 @@ comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { } } -comptime fn generate_contract_library_method_compute_nullifier() -> Quoted { +comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted { if NOTES.len() == 0 { // Contracts with no notes still implement this function to avoid having special-casing, the implementation // simply throws immediately. @@ -376,7 +377,7 @@ comptime fn generate_contract_library_method_compute_nullifier() -> Quoted { /// /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - unconstrained fn _compute_nullifier( + unconstrained fn _compute_note_nullifier( _unique_note_hash: Field, _packed_note: BoundedVec, _owner: aztec::protocol::address::AztecAddress, @@ -453,11 +454,11 @@ comptime fn generate_contract_library_method_compute_nullifier() -> Quoted { quote { /// Computes a note's inner nullifier (non-siloed) given its unique note hash, preimage and extra data. /// - /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. /// /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - unconstrained fn _compute_nullifier( + unconstrained fn _compute_note_nullifier( unique_note_hash: Field, packed_note: BoundedVec, owner: aztec::protocol::address::AztecAddress, @@ -491,7 +492,7 @@ comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_ aztec::messages::discovery::do_sync_state( address, _compute_note_hash, - _compute_nullifier, + _compute_note_nullifier, $process_custom_message_option, $offchain_inbox_sync_option, ); @@ -522,7 +523,7 @@ comptime fn generate_process_message(process_custom_message_option: Quoted) -> Q aztec::messages::discovery::process_message::process_message_ciphertext( address, _compute_note_hash, - _compute_nullifier, + _compute_note_nullifier, $process_custom_message_option, message_ciphertext, message_context, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index e1fa8b688e32..f2bde27fd775 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -92,7 +92,7 @@ pub type ComputeNoteHash = unconstrained fn(/* packed_note */BoundedVec Option; -pub type ComputeNullifier = unconstrained fn(/* unique_note_hash */Field, /* packed_note */ BoundedVec, /* +pub type ComputeNoteNullifier = unconstrained fn(/* unique_note_hash */Field, /* packed_note */ BoundedVec, /* owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* randomness */ Field) -> Option; @@ -119,7 +119,7 @@ pub type CustomMessageHandler = unconstrained fn[Env]( pub unconstrained fn do_sync_state( contract_address: AztecAddress, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option>, offchain_inbox_sync: Option>, ) { @@ -137,7 +137,7 @@ pub unconstrained fn do_sync_state( process_message_ciphertext( contract_address, compute_note_hash, - compute_nullifier, + compute_note_nullifier, process_custom_message, message_ciphertext, pending_tagged_log.context, @@ -156,7 +156,7 @@ pub unconstrained fn do_sync_state( process_message_ciphertext( contract_address, compute_note_hash, - compute_nullifier, + compute_note_nullifier, process_custom_message, msg.message_ciphertext, msg.message_context, @@ -172,7 +172,7 @@ pub unconstrained fn do_sync_state( partial_notes::fetch_and_process_partial_note_completion_logs( contract_address, compute_note_hash, - compute_nullifier, + compute_note_nullifier, ); // Finally we validate all notes and events that were found as part of the previous processes, resulting in them diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr index 44b162d377c7..27b850e66dd5 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr @@ -1,4 +1,4 @@ -use crate::messages::{discovery::{ComputeNoteHash, ComputeNullifier}, logs::note::MAX_NOTE_PACKED_LEN}; +use crate::messages::{discovery::{ComputeNoteHash, ComputeNoteNullifier}, logs::note::MAX_NOTE_PACKED_LEN}; use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log, aztecnr_warn_log_format}; use crate::protocol::{ @@ -27,7 +27,7 @@ pub(crate) unconstrained fn attempt_note_nonce_discovery( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, contract_address: AztecAddress, owner: AztecAddress, storage_slot: Field, @@ -85,7 +85,7 @@ pub(crate) unconstrained fn attempt_note_nonce_discovery( f"Received {identical_note_hashes} identical note hashes for a transaction - these should all be unique", ) } else if matching_notes.len() == 1 { - let maybe_inner_nullifier_for_i = compute_nullifier( + let maybe_inner_nullifier_for_i = compute_note_nullifier( unique_note_hash_for_i, packed_note, owner, @@ -185,7 +185,7 @@ mod test { } } - unconstrained fn compute_nullifier( + unconstrained fn compute_note_nullifier( unique_note_hash: Field, packed_note: BoundedVec, owner: AztecAddress, @@ -218,7 +218,7 @@ mod test { unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, compute_note_hash, - compute_nullifier, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -238,7 +238,7 @@ mod test { unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, |_, _, _, _, _, _| Option::none(), - compute_nullifier, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -316,7 +316,7 @@ mod test { unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, compute_note_hash, - compute_nullifier, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -356,7 +356,7 @@ mod test { unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, compute_note_hash, - compute_nullifier, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -396,7 +396,7 @@ mod test { unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, compute_note_hash, - compute_nullifier, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -430,7 +430,7 @@ mod test { unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, compute_note_hash, - compute_nullifier, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -465,7 +465,7 @@ mod test { unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, compute_note_hash, - compute_nullifier, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index ba862e4ca822..37d20096f871 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -1,7 +1,7 @@ use crate::{ capsules::CapsuleArray, messages::{ - discovery::{ComputeNoteHash, ComputeNullifier, nonce_discovery::attempt_note_nonce_discovery}, + discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery}, encoding::MAX_MESSAGE_CONTENT_LEN, logs::partial_note::{decode_partial_note_private_message, MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN}, processing::{ @@ -75,7 +75,7 @@ pub(crate) unconstrained fn process_partial_note_private_msg( pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( contract_address: AztecAddress, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, ) { let pending_partial_notes = CapsuleArray::at( contract_address, @@ -132,7 +132,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( log.unique_note_hashes_in_tx, log.first_nullifier_in_tx, compute_note_hash, - compute_nullifier, + compute_note_nullifier, contract_address, pending_partial_note.owner, storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr index 41fb73d59716..1070ba4856bf 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr @@ -1,6 +1,6 @@ use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}; use crate::messages::{ - discovery::{ComputeNoteHash, ComputeNullifier, nonce_discovery::attempt_note_nonce_discovery}, + discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery}, encoding::MAX_MESSAGE_CONTENT_LEN, logs::note::{decode_private_note_message, MAX_NOTE_PACKED_LEN}, processing::enqueue_note_for_validation, @@ -14,7 +14,7 @@ pub(crate) unconstrained fn process_private_note_msg( first_nullifier_in_tx: Field, recipient: AztecAddress, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, msg_metadata: u64, msg_content: BoundedVec, ) { @@ -30,7 +30,7 @@ pub(crate) unconstrained fn process_private_note_msg( first_nullifier_in_tx, recipient, compute_note_hash, - compute_nullifier, + compute_note_nullifier, owner, storage_slot, randomness, @@ -55,7 +55,7 @@ pub unconstrained fn attempt_note_discovery( first_nullifier_in_tx: Field, recipient: AztecAddress, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, owner: AztecAddress, storage_slot: Field, randomness: Field, @@ -66,7 +66,7 @@ pub unconstrained fn attempt_note_discovery( unique_note_hashes_in_tx, first_nullifier_in_tx, compute_note_hash, - compute_nullifier, + compute_note_nullifier, contract_address, owner, storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr index c52ae152de2c..260f198ec33c 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr @@ -1,6 +1,6 @@ use crate::messages::{ discovery::{ - ComputeNoteHash, ComputeNullifier, CustomMessageHandler, partial_notes::process_partial_note_private_msg, + ComputeNoteHash, ComputeNoteNullifier, CustomMessageHandler, partial_notes::process_partial_note_private_msg, private_events::process_private_event_msg, private_notes::process_private_note_msg, }, encoding::{decode_message, MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN}, @@ -29,7 +29,7 @@ use crate::protocol::address::AztecAddress; pub unconstrained fn process_message_ciphertext( contract_address: AztecAddress, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option>, message_ciphertext: BoundedVec, message_context: MessageContext, @@ -40,7 +40,7 @@ pub unconstrained fn process_message_ciphertext( process_message_plaintext( contract_address, compute_note_hash, - compute_nullifier, + compute_note_nullifier, process_custom_message, message_plaintext_option.unwrap(), message_context, @@ -53,7 +53,7 @@ pub unconstrained fn process_message_ciphertext( pub(crate) unconstrained fn process_message_plaintext( contract_address: AztecAddress, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option>, message_plaintext: BoundedVec, message_context: MessageContext, @@ -77,7 +77,7 @@ pub(crate) unconstrained fn process_message_plaintext( message_context.first_nullifier_in_tx, message_context.recipient, compute_note_hash, - compute_nullifier, + compute_note_nullifier, msg_metadata, msg_content, ); diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr index 13591dd8394e..d88d123824a8 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -13,7 +13,7 @@ use crate::{ hash::hash_args, messages::{ discovery::{ - ComputeNoteHash, ComputeNullifier, CustomMessageHandler, process_message::process_message_plaintext, + ComputeNoteHash, ComputeNoteNullifier, CustomMessageHandler, process_message::process_message_plaintext, }, encoding::MESSAGE_PLAINTEXT_LEN, logs::{event::encode_private_event_message, note::encode_private_note_message}, @@ -776,7 +776,8 @@ impl TestEnvironment { note_message.new_note.randomness, )); - // We also need to provide an implementation for the `compute_note_hash` and `compute_nullifier` functions, + // We also need to provide an implementation for the `compute_note_hash` and `compute_note_nullifier` + // functions, // which would typically be created by the macros for the different notes in a given contract. Here we build // variants specialized for `Note`. let compute_note_hash: ComputeNoteHash = |packed_note, owner, storage_slot, note_type_id, _contract_address, randomness| { @@ -788,7 +789,7 @@ impl TestEnvironment { Option::some(note.compute_note_hash(owner, storage_slot, randomness)) }; - let compute_nullifier: ComputeNullifier = |unique_note_hash, packed_note, owner, _storage_slot, note_type_id, _contract_address, _randomness| { + let compute_note_nullifier: ComputeNoteNullifier = |unique_note_hash, packed_note, owner, _storage_slot, note_type_id, _contract_address, _randomness| { assert_eq(note_type_id, Note::get_id()); assert_eq(packed_note.len(), ::N); @@ -803,7 +804,7 @@ impl TestEnvironment { opts.contract_address, Option::some(note_message.new_note.owner), compute_note_hash, - compute_nullifier, + compute_note_nullifier, process_custom_message, ); } @@ -896,7 +897,8 @@ impl TestEnvironment { event_message.new_event.randomness, )); - // We also need to provide an implementation for the `compute_note_hash` and `compute_nullifier` functions, + // We also need to provide an implementation for the `compute_note_hash` and `compute_note_nullifier` + // functions, // which would typically be created by the macros for the different notes in a given contract. Here we build // empty ones, since they will never be invoked as this is an event and not a note message. let compute_note_hash: ComputeNoteHash = |_packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { @@ -905,9 +907,9 @@ impl TestEnvironment { ) }; - let compute_nullifier: ComputeNullifier = |_unique_note_hash, _packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { + let compute_note_nullifier: ComputeNoteNullifier = |_unique_note_hash, _packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { panic( - f"Unexpected compute_nullifier invocation in TestEnvironment::discover_event", + f"Unexpected compute_note_nullifier invocation in TestEnvironment::discover_event", ) }; @@ -917,7 +919,7 @@ impl TestEnvironment { opts.contract_address, Option::some(recipient), compute_note_hash, - compute_nullifier, + compute_note_nullifier, process_custom_message, ); } @@ -928,7 +930,7 @@ impl TestEnvironment { contract_address: Option, recipient: Option, compute_note_hash: ComputeNoteHash, - compute_nullifier: ComputeNullifier, + compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option>, ) { // This function will emulate the message discovery and processing that would happen in a real contract, based @@ -958,7 +960,7 @@ impl TestEnvironment { process_message_plaintext( context.this_address(), compute_note_hash, - compute_nullifier, + compute_note_nullifier, process_custom_message, message_plaintext, message_context, From 2177f6bf3704cbf94a748905c1ebe9195d508637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 16 Mar 2026 22:27:18 +0000 Subject: [PATCH 3/9] adjustments, add migration notes --- .../docs/resources/migration_notes.md | 29 +++++++++ .../aztec-nr/aztec/src/macros/aztec.nr | 14 +++-- .../aztec/src/messages/discovery/mod.nr | 59 +++++++++---------- .../src/messages/discovery/process_message.nr | 4 +- .../app/token_blacklist_contract/src/main.nr | 3 +- .../src/main.nr | 3 +- 6 files changed, 72 insertions(+), 40 deletions(-) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 1ee9c122a7da..3b4165544b9b 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,35 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.nr] `attempt_note_discovery` now takes two separate functions instead of one + +The `attempt_note_discovery` function (and related discovery functions like `do_sync_state`, `process_message_ciphertext`) now takes separate `compute_note_hash` and `compute_note_nullifier` arguments instead of a single combined `compute_note_hash_and_nullifier`. The corresponding type aliases are now `ComputeNoteHash` and `ComputeNoteNullifier` (instead of `ComputeNoteHashAndNullifier`). + +This split improves performance during nonce discovery: the note hash only needs to be computed once, while the old combined function recomputed it for every candidate nonce. + +Most contracts are not affected, as the macro-generated `sync_state` and `process_message` functions handle this automatically. Only contracts that call `attempt_note_discovery` directly need to update. + +**Migration:** + +```diff + attempt_note_discovery( + contract_address, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, +- _compute_note_hash_and_nullifier, ++ _compute_note_hash, ++ _compute_note_nullifier, + owner, + storage_slot, + randomness, + note_type_id, + packed_note, + ); +``` + +**Impact**: Only contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. ### [Aztec.js] `TxReceipt` now includes `epochNumber` diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 4264538b3968..f38b7110e1db 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -100,9 +100,14 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { // We generate ABI exports for all the external functions in the contract. let fn_abi_exports = create_fn_abi_exports(m); - // We generate `_compute_note_hash_and_nullifier`, `sync_state` and `process_message` functions only if they are - // not already implemented. If they are implemented we just insert empty quotes. + // We generate `_compute_note_hash`, `_compute_note_nullifier` (and the deprecated + // `_compute_note_hash_and_nullifier` wrapper), `sync_state` and `process_message` functions only if they are not + // already implemented. If they are implemented we just insert empty quotes. let contract_library_method_compute_note_hash_and_nullifier = if !m.functions().any(|f| { + // Note that we don't test for `_compute_note_hash` or `_compute_note_nullifier` in order to make this simpler + // - + // users must either implement all three or none. + // Down the line we'll remove this check and use `AztecConfig`. f.name() == quote { _compute_note_hash_and_nullifier } }) { generate_contract_library_methods_compute_note_hash_and_nullifier() @@ -221,9 +226,8 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } /// Generates two contract library methods called `_compute_note_hash` and `_compute_note_nullifier`, plus a -/// (deprecated) -/// wrapper called `_compute_note_hash_and_nullifier`, which are used for note discovery (i.e. these are of the -/// `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNoteNullifier` types). +/// (deprecated) wrapper called `_compute_note_hash_and_nullifier`, which are used for note discovery (i.e. these are +/// of the `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNoteNullifier` types). comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() -> Quoted { let compute_note_hash = generate_contract_library_method_compute_note_hash(); let compute_note_nullifier = generate_contract_library_method_compute_note_nullifier(); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index f2bde27fd775..2944378d0f85 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -31,15 +31,14 @@ pub struct NoteHashAndNullifier { pub inner_nullifier: Option, } -/// A contract's way of computing note hashes and nullifiers. +/// A contract's way of computing note hashes. /// -/// Each contract in the network is free to compute their note's hash and nullifiers as they see fit - the hash -/// function itself and the nullifier derivation are not enshrined or standardized. Some aztec-nr functions however do -/// need to know the details of this computation (e.g. when finding new notes), which is what this type represents. +/// Each contract in the network is free to compute their note's hash as they see fit - the hash function itself is not +/// enshrined or standardized. Some aztec-nr functions however do need to know the details of this computation (e.g. +/// when finding new notes), which is what this type represents. /// -/// This function takes a note's packed content, storage slot, note type ID, address of the emitting contract, -/// randomness and note nonce, and attempts to compute its inner note hash (not siloed by address nor uniqued by nonce) -/// and inner nullifier (not siloed by address). +/// This function takes a note's packed content, storage slot, note type ID, address of the emitting contract and +/// randomness, and attempts to compute its inner note hash (not siloed by address nor uniqued by nonce). /// /// ## Transient Notes /// @@ -51,31 +50,16 @@ pub struct NoteHashAndNullifier { /// /// The [`[#aztec]`](crate::macros::aztec::aztec) macro automatically creates a correct implementation of this function /// for each contract by inspecting all note types in use and the storage layout. This injected function is a -/// `#[contract_library_method]` called `_compute_note_hash_and_nullifier`, and it looks something like this: +/// `#[contract_library_method]` called `_compute_note_hash`, and it looks something like this: /// /// ```noir -/// |packed_note, owner, storage_slot, note_type_id, contract_address, randomness, note_nonce| { +/// |packed_note, owner, storage_slot, note_type_id, _contract_address, randomness| { /// if note_type_id == MyNoteType::get_id() { /// if packed_note.len() != MY_NOTE_TYPE_SERIALIZATION_LENGTH { /// Option::none() /// } else { /// let note = MyNoteType::unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); -/// -/// let note_hash = note.compute_note_hash(owner, storage_slot, randomness); -/// let note_hash_for_nullification = aztec::note::utils::compute_note_hash_for_nullification( -/// HintedNote { -/// note, contract_address, owner, randomness, storage_slot, -/// metadata: SettledNoteMetadata::new(note_nonce).into(), -/// }, -/// ); -/// -/// let inner_nullifier = note.compute_nullifier_unconstrained(owner, note_hash_for_nullification); -/// -/// Option::some( -/// aztec::messages::discovery::NoteHashAndNullifier { -/// note_hash, inner_nullifier -/// } -/// ) +/// Option::some(note.compute_note_hash(owner, storage_slot, randomness)) /// } /// } else if note_type_id == MyOtherNoteType::get_id() { /// ... // Similar to above but calling MyOtherNoteType::unpack @@ -84,17 +68,30 @@ pub struct NoteHashAndNullifier { /// }; /// } /// ``` -pub type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */BoundedVec, /* - owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* -randomness */ Field, /* note nonce */ Field) -> Option; - pub type ComputeNoteHash = unconstrained fn(/* packed_note */BoundedVec, /* owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* randomness */ Field) -> Option; +/// A contract's way of computing note nullifiers. +/// +/// Like [`ComputeNoteHash`], each contract is free to derive nullifiers as they see fit. This function takes the +/// unique note hash (used as the note hash for nullification for settled notes), plus the note's packed content and +/// metadata, and attempts to compute the inner nullifier (not siloed by address). +/// +/// ## Automatic Implementation +/// +/// The [`[#aztec]`](crate::macros::aztec::aztec) macro automatically creates a correct implementation of this function +/// for each contract called `_compute_note_nullifier`. It dispatches on `note_type_id` similarly to +/// [`ComputeNoteHash`], then calls the note's +/// [`compute_nullifier_unconstrained`](crate::note::note_interface::NoteHash::compute_nullifier_unconstrained) method. pub type ComputeNoteNullifier = unconstrained fn(/* unique_note_hash */Field, /* packed_note */ BoundedVec, /* - owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* -randomness */ Field) -> Option; + * owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* + contract_address */ AztecAddress, /* randomness */ Field) -> Option; + +/// Deprecated: use [`ComputeNoteHash`] and [`ComputeNoteNullifier`] instead. +pub type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */BoundedVec, /* + * owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* + contract_address */ AztecAddress, /* randomness */ Field, /* note nonce */ Field) -> Option; /// A handler for custom messages. /// diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr index 260f198ec33c..1becbd92cecd 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr @@ -18,8 +18,8 @@ use crate::protocol::address::AztecAddress; /// /// Notes result in nonce discovery being performed prior to delivery, which requires knowledge of the transaction hash /// in which the notes would've been created (typically the same transaction in which the log was emitted), along with -/// the list of unique note hashes in said transaction and the `compute_note_hash_and_nullifier` function. Once -/// discovered, the notes are enqueued for validation. +/// the list of unique note hashes in said transaction and the `compute_note_hash` and `compute_note_nullifier` +/// functions. Once discovered, the notes are enqueued for validation. /// /// Partial notes result in a pending partial note entry being stored in a PXE capsule, which will later be retrieved /// to search for the note's completion public log. diff --git a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr index e2b5cd42dfe9..15d680e0909c 100644 --- a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr @@ -292,7 +292,8 @@ pub contract TokenBlacklist { unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, - _compute_note_hash_and_nullifier, + _compute_note_hash, + _compute_note_nullifier, AztecAddress::zero(), storage_slot, TRANSPARENT_NOTE_RANDOMNESS, diff --git a/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr index f077b11497ae..abb31baf8d26 100644 --- a/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr @@ -3,7 +3,8 @@ mod test; use aztec::macros::aztec; -/// A minimal contract used to test the macro-generated `_compute_note_hash_and_nullifier` function. +/// A minimal contract used to test the macro-generated `_compute_note_hash`, `_compute_note_nullifier` and +/// (deprecated) `_compute_note_hash_and_nullifier` functions. #[aztec] pub contract NoteHashAndNullifier { use aztec::{ From 7e96a9d176397652db061cb34708886f6502457b Mon Sep 17 00:00:00 2001 From: AztecBot Date: Mon, 16 Mar 2026 23:15:57 +0000 Subject: [PATCH 4/9] fix: update do_sync_state test to use split note hash/nullifier functions --- .../aztec/src/messages/discovery/mod.nr | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index 0f5b633b6449..904cbe830e5c 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -185,7 +185,7 @@ mod test { use crate::{ capsules::CapsuleArray, messages::{ - discovery::{CustomMessageHandler, do_sync_state, NoteHashAndNullifier}, + discovery::{CustomMessageHandler, do_sync_state}, logs::note::MAX_NOTE_PACKED_LEN, processing::{ offchain::OffchainInboxSync, pending_tagged_log::PendingTaggedLog, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, @@ -212,7 +212,8 @@ mod test { let no_inbox_sync: Option> = Option::none(); do_sync_state( contract_address, - dummy_compute_nhnn, + dummy_compute_note_hash, + dummy_compute_note_nullifier, no_handler, no_inbox_sync, ); @@ -221,15 +222,26 @@ mod test { }); } - unconstrained fn dummy_compute_nhnn( + unconstrained fn dummy_compute_note_hash( _packed_note: BoundedVec, _owner: AztecAddress, _storage_slot: Field, _note_type_id: Field, _contract_address: AztecAddress, _randomness: Field, - _note_nonce: Field, - ) -> Option { + ) -> Option { + Option::none() + } + + unconstrained fn dummy_compute_note_nullifier( + _unique_note_hash: Field, + _packed_note: BoundedVec, + _owner: AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: AztecAddress, + _randomness: Field, + ) -> Option { Option::none() } } From 2d1ca48eb005e93cf4306a45babadd663b658a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 17 Mar 2026 12:31:23 +0000 Subject: [PATCH 5/9] remove deprecation attr --- noir-projects/aztec-nr/aztec/src/macros/aztec.nr | 1 - 1 file changed, 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index b16c66104c68..3d0a5d1b4880 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -242,7 +242,6 @@ comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() /// /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] - #[deprecated("This function has been deprecated in favor of compute_note_hash and compute_note_nullifier")] #[allow(dead_code)] unconstrained fn _compute_note_hash_and_nullifier( packed_note: BoundedVec, From 25bd79122a8a446b604e965f7770a246583dc966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 17 Mar 2026 12:35:24 +0000 Subject: [PATCH 6/9] improve err msg --- .../aztec/src/messages/discovery/nonce_discovery.nr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr index 27b850e66dd5..825c1a712971 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr @@ -100,7 +100,11 @@ pub(crate) unconstrained fn attempt_note_nonce_discovery( // e.g. notes that belong to someone that is not us (and for which we therefore don't know their // associated app-siloed nullifer hiding secret key). // https://linear.app/aztec-labs/issue/F-265/store-external-notes - aztecnr_warn_log!("Unable to compute nullifier, skipping nonce discovery"); + aztecnr_warn_log_format!( + "Unable to compute nullifier of discovered note with note type id {0} and owner {1}, skipping PXE insertion", + )( + [note_type_id, owner.to_field()], + ); } else { // Note that while we did check that the note hash is the preimage of a unique note hash, we // perform no validations on the nullifier - we fundamentally cannot, since only the application From 8e731e586f037ba78d45ccb322791ed3181a1515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 17 Mar 2026 12:53:23 +0000 Subject: [PATCH 7/9] remove unused import --- .../aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr index 825c1a712971..42d2634b6004 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr @@ -1,6 +1,6 @@ use crate::messages::{discovery::{ComputeNoteHash, ComputeNoteNullifier}, logs::note::MAX_NOTE_PACKED_LEN}; -use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log, aztecnr_warn_log_format}; +use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}; use crate::protocol::{ address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX, From dd6f95c00f8bba9fd8a37094f4405ace63bda8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 17 Mar 2026 21:10:02 +0000 Subject: [PATCH 8/9] Address review comments --- .../docs/resources/migration_notes.md | 2 +- .../aztec-nr/aztec/src/macros/aztec.nr | 271 +----------------- .../aztec/compute_note_hash_and_nullifier.nr | 261 +++++++++++++++++ .../aztec/src/messages/discovery/mod.nr | 12 +- .../src/messages/discovery/nonce_discovery.nr | 14 +- .../src/test/helpers/test_environment.nr | 10 +- 6 files changed, 288 insertions(+), 282 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 3b4165544b9b..52cd3e0ee0cc 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -37,7 +37,7 @@ Most contracts are not affected, as the macro-generated `sync_state` and `proces ); ``` -**Impact**: Only contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. +**Impact**: Contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. Aditionally, if you had a custom `_compute_note_hash_and_nullifier` function then compilation will now fail as you'll need to also produce the corresponding `_compute_note_hash` and `_compute_note_nullifier` functions. ### [Aztec.js] `TxReceipt` now includes `epochNumber` diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 3d0a5d1b4880..7b1d85286faf 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -1,3 +1,5 @@ +mod compute_note_hash_and_nullifier; + use crate::{ macros::{ calls_generation::{ @@ -7,16 +9,14 @@ use crate::{ dispatch::generate_public_dispatch, emit_public_init_nullifier::generate_emit_public_init_nullifier, internals_functions_generation::{create_fn_abi_exports, process_functions}, - notes::NOTES, storage::STORAGE_LAYOUT_NAME, - utils::{ - get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, - module_has_storage, - }, + utils::{is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, module_has_storage}, }, messages::discovery::CustomMessageHandler, }; +use compute_note_hash_and_nullifier::generate_contract_library_methods_compute_note_hash_and_nullifier; + /// Configuration for the [`aztec`] macro. /// /// This type lets users override different parts of the default aztec-nr contract behavior, such @@ -105,8 +105,7 @@ pub comptime fn aztec(m: Module, args: [AztecConfig]) -> Quoted { // already implemented. If they are implemented we just insert empty quotes. let contract_library_method_compute_note_hash_and_nullifier = if !m.functions().any(|f| { // Note that we don't test for `_compute_note_hash` or `_compute_note_nullifier` in order to make this simpler - // - - // users must either implement all three or none. + // - users must either implement all three or none. // Down the line we'll remove this check and use `AztecConfig`. f.name() == quote { _compute_note_hash_and_nullifier } }) { @@ -225,264 +224,6 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } -/// Generates two contract library methods called `_compute_note_hash` and `_compute_note_nullifier`, plus a -/// (deprecated) wrapper called `_compute_note_hash_and_nullifier`, which are used for note discovery (i.e. these are -/// of the `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNoteNullifier` types). -comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() -> Quoted { - let compute_note_hash = generate_contract_library_method_compute_note_hash(); - let compute_note_nullifier = generate_contract_library_method_compute_note_nullifier(); - - quote { - $compute_note_hash - $compute_note_nullifier - - /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash tree with `note_nonce`. - /// - /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHashAndNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. - /// - /// This function is automatically injected by the `#[aztec]` macro. - #[contract_library_method] - #[allow(dead_code)] - unconstrained fn _compute_note_hash_and_nullifier( - packed_note: BoundedVec, - owner: aztec::protocol::address::AztecAddress, - storage_slot: Field, - note_type_id: Field, - contract_address: aztec::protocol::address::AztecAddress, - randomness: Field, - note_nonce: Field, - ) -> Option { - _compute_note_hash(packed_note, owner, storage_slot, note_type_id, contract_address, randomness).map(|note_hash| { - - let siloed_note_hash = aztec::protocol::hash::compute_siloed_note_hash(contract_address, note_hash); - let unique_note_hash = aztec::protocol::hash::compute_unique_note_hash(note_nonce, siloed_note_hash); - - let inner_nullifier = _compute_note_nullifier(unique_note_hash, packed_note, owner, storage_slot, note_type_id, contract_address, randomness); - - aztec::messages::discovery::NoteHashAndNullifier { - note_hash, - inner_nullifier, - } - }) - } - } -} - -comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { - if NOTES.len() == 0 { - // Contracts with no notes still implement this function to avoid having special-casing, the implementation - // simply throws immediately. - quote { - /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. - /// - /// This function is automatically injected by the `#[aztec]` macro. - #[contract_library_method] - unconstrained fn _compute_note_hash( - _packed_note: BoundedVec, - _owner: aztec::protocol::address::AztecAddress, - _storage_slot: Field, - _note_type_id: Field, - _contract_address: aztec::protocol::address::AztecAddress, - _randomness: Field, - ) -> Option { - panic(f"This contract does not use private notes") - } - } - } else { - // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the - // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we - // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and - // compute the note hash (non-siloed). - - let mut if_note_type_id_match_statements_list = @[]; - for i in 0..NOTES.len() { - let typ = NOTES.get(i); - - let get_note_type_id = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteType }, - quote { get_id }, - ); - let unpack = get_trait_impl_method( - typ, - quote { crate::protocol::traits::Packable }, - quote { unpack }, - ); - - let compute_note_hash = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteHash }, - quote { compute_note_hash }, - ); - - let if_or_else_if = if i == 0 { - quote { if } - } else { - quote { else if } - }; - - if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( - quote { - $if_or_else_if note_type_id == $get_note_type_id() { - // As an extra safety check we make sure that the packed_note BoundedVec has the expected - // length, since we're about to interpret its raw storage as a fixed-size array by calling the - // unpack function on it. - let expected_len = <$typ as $crate::protocol::traits::Packable>::N; - let actual_len = packed_note.len(); - if actual_len != expected_len { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", - [expected_len as Field, actual_len as Field, note_type_id], - ); - Option::none() - } else { - let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); - - Option::some($compute_note_hash(note, owner, storage_slot, randomness)) - } - } - }, - ); - } - - let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); - - quote { - /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed). - /// - /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHash` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. - /// - /// This function is automatically injected by the `#[aztec]` macro. - #[contract_library_method] - unconstrained fn _compute_note_hash( - packed_note: BoundedVec, - owner: aztec::protocol::address::AztecAddress, - storage_slot: Field, - note_type_id: Field, - _contract_address: aztec::protocol::address::AztecAddress, - randomness: Field, - ) -> Option { - $if_note_type_id_match_statements - else { - Option::none() - } - } - } - } -} - -comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted { - if NOTES.len() == 0 { - // Contracts with no notes still implement this function to avoid having special-casing, the implementation - // simply throws immediately. - quote { - /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. - /// - /// This function is automatically injected by the `#[aztec]` macro. - #[contract_library_method] - unconstrained fn _compute_note_nullifier( - _unique_note_hash: Field, - _packed_note: BoundedVec, - _owner: aztec::protocol::address::AztecAddress, - _storage_slot: Field, - _note_type_id: Field, - _contract_address: aztec::protocol::address::AztecAddress, - _randomness: Field, - ) -> Option { - panic(f"This contract does not use private notes") - } - } - } else { - // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the - // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we - // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and - // compute the note hash (non-siloed) and inner nullifier (also non-siloed). - - let mut if_note_type_id_match_statements_list = @[]; - for i in 0..NOTES.len() { - let typ = NOTES.get(i); - - let get_note_type_id = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteType }, - quote { get_id }, - ); - let unpack = get_trait_impl_method( - typ, - quote { crate::protocol::traits::Packable }, - quote { unpack }, - ); - - let compute_nullifier_unconstrained = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteHash }, - quote { compute_nullifier_unconstrained }, - ); - - let if_or_else_if = if i == 0 { - quote { if } - } else { - quote { else if } - }; - - if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( - quote { - $if_or_else_if note_type_id == $get_note_type_id() { - // As an extra safety check we make sure that the packed_note BoundedVec has the expected - // length, since we're about to interpret its raw storage as a fixed-size array by calling the - // unpack function on it. - let expected_len = <$typ as $crate::protocol::traits::Packable>::N; - let actual_len = packed_note.len(); - if actual_len != expected_len { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", - [expected_len as Field, actual_len as Field, note_type_id], - ); - Option::none() - } else { - let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); - - // The message discovery process finds settled notes, that is, notes that were created in - // prior transactions and are therefore already part of the note hash tree. The note hash - // for nullification is hence the unique note hash. - $compute_nullifier_unconstrained(note, owner, unique_note_hash) - } - } - }, - ); - } - - let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); - - quote { - /// Computes a note's inner nullifier (non-siloed) given its unique note hash, preimage and extra data. - /// - /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. - /// - /// This function is automatically injected by the `#[aztec]` macro. - #[contract_library_method] - unconstrained fn _compute_note_nullifier( - unique_note_hash: Field, - packed_note: BoundedVec, - owner: aztec::protocol::address::AztecAddress, - _storage_slot: Field, - note_type_id: Field, - _contract_address: aztec::protocol::address::AztecAddress, - _randomness: Field, - ) -> Option { - $if_note_type_id_match_statements - else { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Unknown note type id {0}. Skipping note.", - [note_type_id], - ); - Option::none() - } - } - } - } -} - /// Generates the `sync_state` utility function that performs message discovery. comptime fn generate_sync_state(process_custom_message_option: Quoted, offchain_inbox_sync_option: Quoted) -> Quoted { quote { diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr new file mode 100644 index 000000000000..7f8887b04315 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr @@ -0,0 +1,261 @@ +use crate::macros::{notes::NOTES, utils::get_trait_impl_method}; + +/// Generates two contract library methods called `_compute_note_hash` and `_compute_note_nullifier`, plus a +/// (deprecated) wrapper called `_compute_note_hash_and_nullifier`, which are used for note discovery (i.e. these are +/// of the `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNoteNullifier` types). +pub(crate) comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() -> Quoted { + let compute_note_hash = generate_contract_library_method_compute_note_hash(); + let compute_note_nullifier = generate_contract_library_method_compute_note_nullifier(); + + quote { + $compute_note_hash + $compute_note_nullifier + + /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash tree with `note_nonce`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + #[allow(dead_code)] + unconstrained fn _compute_note_hash_and_nullifier( + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + storage_slot: Field, + note_type_id: Field, + contract_address: aztec::protocol::address::AztecAddress, + randomness: Field, + note_nonce: Field, + ) -> Option { + _compute_note_hash(packed_note, owner, storage_slot, note_type_id, contract_address, randomness).map(|note_hash| { + + let siloed_note_hash = aztec::protocol::hash::compute_siloed_note_hash(contract_address, note_hash); + let unique_note_hash = aztec::protocol::hash::compute_unique_note_hash(note_nonce, siloed_note_hash); + + let inner_nullifier = _compute_note_nullifier(unique_note_hash, packed_note, owner, storage_slot, note_type_id, contract_address, randomness); + + aztec::messages::discovery::NoteHashAndNullifier { + note_hash, + inner_nullifier, + } + }) + } + } +} + +comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { + if NOTES.len() == 0 { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. + quote { + /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_hash( + _packed_note: BoundedVec, + _owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + panic(f"This contract does not use private notes") + } + } + } else { + // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the + // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we + // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // compute the note hash (non-siloed). + + let mut if_note_type_id_match_statements_list = @[]; + for i in 0..NOTES.len() { + let typ = NOTES.get(i); + + let get_note_type_id = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteType }, + quote { get_id }, + ); + let unpack = get_trait_impl_method( + typ, + quote { crate::protocol::traits::Packable }, + quote { unpack }, + ); + + let compute_note_hash = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteHash }, + quote { compute_note_hash }, + ); + + let if_or_else_if = if i == 0 { + quote { if } + } else { + quote { else if } + }; + + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( + quote { + $if_or_else_if note_type_id == $get_note_type_id() { + // As an extra safety check we make sure that the packed_note BoundedVec has the expected + // length, since we're about to interpret its raw storage as a fixed-size array by calling the + // unpack function on it. + let expected_len = <$typ as $crate::protocol::traits::Packable>::N; + let actual_len = packed_note.len(); + if actual_len != expected_len { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", + [expected_len as Field, actual_len as Field, note_type_id], + ); + Option::none() + } else { + let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); + + Option::some($compute_note_hash(note, owner, storage_slot, randomness)) + } + } + }, + ); + } + + let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); + + quote { + /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed). + /// + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHash` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_hash( + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + storage_slot: Field, + note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + randomness: Field, + ) -> Option { + $if_note_type_id_match_statements + else { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Unknown note type id {0}. Skipping note.", + [note_type_id], + ); + Option::none() + } + } + } + } +} + +comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted { + if NOTES.len() == 0 { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. + quote { + /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_nullifier( + _unique_note_hash: Field, + _packed_note: BoundedVec, + _owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + panic(f"This contract does not use private notes") + } + } + } else { + // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the + // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we + // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // compute the inner nullifier (non-siloed). + + let mut if_note_type_id_match_statements_list = @[]; + for i in 0..NOTES.len() { + let typ = NOTES.get(i); + + let get_note_type_id = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteType }, + quote { get_id }, + ); + let unpack = get_trait_impl_method( + typ, + quote { crate::protocol::traits::Packable }, + quote { unpack }, + ); + + let compute_nullifier_unconstrained = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteHash }, + quote { compute_nullifier_unconstrained }, + ); + + let if_or_else_if = if i == 0 { + quote { if } + } else { + quote { else if } + }; + + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( + quote { + $if_or_else_if note_type_id == $get_note_type_id() { + // As an extra safety check we make sure that the packed_note BoundedVec has the expected + // length, since we're about to interpret its raw storage as a fixed-size array by calling the + // unpack function on it. + let expected_len = <$typ as $crate::protocol::traits::Packable>::N; + let actual_len = packed_note.len(); + if actual_len != expected_len { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", + [expected_len as Field, actual_len as Field, note_type_id], + ); + Option::none() + } else { + let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); + + // The message discovery process finds settled notes, that is, notes that were created in + // prior transactions and are therefore already part of the note hash tree. The note hash + // for nullification is hence the unique note hash. + $compute_nullifier_unconstrained(note, owner, unique_note_hash) + } + } + }, + ); + } + + let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); + + quote { + /// Computes a note's inner nullifier (non-siloed) given its unique note hash, preimage and extra data. + /// + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_nullifier( + unique_note_hash: Field, + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + $if_note_type_id_match_statements + else { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Unknown note type id {0}. Skipping note.", + [note_type_id], + ); + Option::none() + } + } + } + } +} diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index 904cbe830e5c..eca4d338ed91 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -84,14 +84,14 @@ randomness */ Field) -> Option; /// for each contract called `_compute_note_nullifier`. It dispatches on `note_type_id` similarly to /// [`ComputeNoteHash`], then calls the note's /// [`compute_nullifier_unconstrained`](crate::note::note_interface::NoteHash::compute_nullifier_unconstrained) method. -pub type ComputeNoteNullifier = unconstrained fn(/* unique_note_hash */Field, /* packed_note */ BoundedVec, /* - * owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* - contract_address */ AztecAddress, /* randomness */ Field) -> Option; +pub type ComputeNoteNullifier = unconstrained fn(/* unique_note_hash */Field, /* packed_note */ BoundedVec, +/* owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, +/* randomness */ Field) -> Option; /// Deprecated: use [`ComputeNoteHash`] and [`ComputeNoteNullifier`] instead. -pub type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */BoundedVec, /* - * owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* - contract_address */ AztecAddress, /* randomness */ Field, /* note nonce */ Field) -> Option; +pub type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */BoundedVec, +/* owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, +/*randomness */ Field, /* note nonce */ Field) -> Option; /// A handler for custom messages. /// diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr index 42d2634b6004..eab5007b66bb 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr @@ -101,9 +101,9 @@ pub(crate) unconstrained fn attempt_note_nonce_discovery( // associated app-siloed nullifer hiding secret key). // https://linear.app/aztec-labs/issue/F-265/store-external-notes aztecnr_warn_log_format!( - "Unable to compute nullifier of discovered note with note type id {0} and owner {1}, skipping PXE insertion", + "Unable to compute nullifier of unique note {0} with note type id {1} and owner {2}, skipping PXE insertion", )( - [note_type_id, owner.to_field()], + [unique_note_hash_for_i, note_type_id, owner.to_field()], ); } else { // Note that while we did check that the note hash is the preimage of a unique note hash, we @@ -256,7 +256,13 @@ mod test { #[test] unconstrained fn failed_nullifier_computation_is_ignored() { - let unique_note_hashes_in_tx = BoundedVec::from_array([random()]); + let note_index_in_tx = 2; + let note_and_data = construct_note(VALUE, note_index_in_tx); + + let mut unique_note_hashes_in_tx = BoundedVec::from_array([ + random(), random(), random(), random(), random(), random(), random(), + ]); + unique_note_hashes_in_tx.set(note_index_in_tx, note_and_data.unique_note_hash); let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, @@ -268,7 +274,7 @@ mod test { STORAGE_SLOT, RANDOMNESS, MockNote::get_id(), - BoundedVec::new(), + BoundedVec::from_array(note_and_data.note.pack()), ); assert_eq(discovered_notes.len(), 0); diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr index d88d123824a8..ccef7768325d 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -777,9 +777,8 @@ impl TestEnvironment { )); // We also need to provide an implementation for the `compute_note_hash` and `compute_note_nullifier` - // functions, - // which would typically be created by the macros for the different notes in a given contract. Here we build - // variants specialized for `Note`. + // functions, which would typically be created by the macros for the different notes in a given contract. Here + // we build variants specialized for `Note`. let compute_note_hash: ComputeNoteHash = |packed_note, owner, storage_slot, note_type_id, _contract_address, randomness| { assert_eq(note_type_id, Note::get_id()); assert_eq(packed_note.len(), ::N); @@ -898,9 +897,8 @@ impl TestEnvironment { )); // We also need to provide an implementation for the `compute_note_hash` and `compute_note_nullifier` - // functions, - // which would typically be created by the macros for the different notes in a given contract. Here we build - // empty ones, since they will never be invoked as this is an event and not a note message. + // functions, which would typically be created by the macros for the different notes in a given contract. Here + // we build empty ones, since they will never be invoked as this is an event and not a note message. let compute_note_hash: ComputeNoteHash = |_packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { panic( f"Unexpected compute_note_hash invocation in TestEnvironment::discover_event", From 12f0e9ba7b6c320937f486c5583a7f3f2b0fa37f Mon Sep 17 00:00:00 2001 From: thunkar Date: Wed, 18 Mar 2026 05:48:53 +0000 Subject: [PATCH 9/9] typo --- docs/docs-developers/docs/resources/migration_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index dec6445cab3d..130d4fe115b4 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -37,7 +37,7 @@ Most contracts are not affected, as the macro-generated `sync_state` and `proces ); ``` -**Impact**: Contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. Aditionally, if you had a custom `_compute_note_hash_and_nullifier` function then compilation will now fail as you'll need to also produce the corresponding `_compute_note_hash` and `_compute_note_nullifier` functions. +**Impact**: Contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. Additionally, if you had a custom `_compute_note_hash_and_nullifier` function then compilation will now fail as you'll need to also produce the corresponding `_compute_note_hash` and `_compute_note_nullifier` functions. ### Private initialization nullifier now includes `init_hash`