From fd87d6046dd63b7289d23dcdc438b5f8a397945f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 5 Mar 2026 21:57:08 +0000 Subject: [PATCH 1/8] feat: add note hash and nullifier helper functions with domain separation Add `compute_note_hash` and `compute_note_nullifier` helpers in `note/utils.nr` that enforce fixed positions for `storage_slot` and `note_hash_for_nullification` respectively, preventing collisions across different note implementations. Also set `DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER = 1465998995` and add its corresponding test in `constants_tests.nr`. Update all note nullifier computations across the codebase to use `compute_note_nullifier`, and improve related documentation. Co-Authored-By: Claude Opus 4.6 --- .../docs/resources/migration_notes.md | 4 ++ .../aztec-nr/aztec/src/macros/notes.nr | 14 ++--- .../aztec-nr/aztec/src/note/utils.nr | 58 +++++++++++++++--- .../aztec/src/state_vars/single_use_claim.nr | 10 +-- .../aztec/src/test/mocks/mock_note.nr | 24 +++----- .../aztec-nr/uint-note/src/uint_note.nr | 17 ++---- .../app/nft_contract/src/types/nft_note.nr | 16 ++--- .../app/orderbook_contract/src/main.nr | 6 +- .../src/types/transparent_note.nr | 18 ++---- .../src/main.nr | 1 + .../src/main.nr | 1 + .../contracts/test/spam_contract/src/main.nr | 15 ++--- .../test/test_contract/src/test_note.nr | 11 ++-- .../crates/types/src/constants.nr | 61 +++++++++++++++++-- .../crates/types/src/constants_tests.nr | 13 ++-- 15 files changed, 164 insertions(+), 105 deletions(-) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index cfc21d84d7a2..abf0167269a3 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,10 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained + +This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous. If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. + ### [Aztec.nr] Removed `get_random_bytes` The `get_random_bytes` unconstrained function has been removed from `aztec::utils::random`. If you were using it, you can replace it with direct calls to the `random` oracle from `aztec::oracle::random` and convert to bytes yourself. diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes.nr b/noir-projects/aztec-nr/aztec/src/macros/notes.nr index b2895f9fc3c9..2836182df312 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes.nr @@ -78,8 +78,8 @@ comptime fn generate_note_hash_trait_impl(s: TypeDefinition) -> Quoted { quote { impl aztec::note::note_interface::NoteHash for $name { fn compute_note_hash(self, owner: aztec::protocol::address::AztecAddress, storage_slot: Field, randomness: Field) -> Field { - let inputs = aztec::protocol::traits::Packable::pack(self).concat( [aztec::protocol::traits::ToField::to_field(owner), storage_slot, randomness]); - aztec::protocol::hash::poseidon2_hash_with_separator(inputs, aztec::protocol::constants::DOM_SEP__NOTE_HASH) + let data = aztec::protocol::traits::Packable::pack(self).concat([aztec::protocol::traits::ToField::to_field(owner), randomness]); + aztec::note::utils::compute_note_hash(storage_slot, data) } fn compute_nullifier( @@ -93,10 +93,7 @@ comptime fn generate_note_hash_trait_impl(s: TypeDefinition) -> Quoted { // in the quote to avoid "trait not in scope" compiler warnings. let owner_npk_m_hash = aztec::protocol::traits::Hash::hash(owner_npk_m); let secret = context.request_nhk_app(owner_npk_m_hash); - aztec::protocol::hash::poseidon2_hash_with_separator( - [note_hash_for_nullification, secret], - aztec::protocol::constants::DOM_SEP__NOTE_NULLIFIER as Field, - ) + aztec::note::utils::compute_note_nullifier(note_hash_for_nullification, [secret]) } unconstrained fn compute_nullifier_unconstrained( @@ -113,10 +110,7 @@ comptime fn generate_note_hash_trait_impl(s: TypeDefinition) -> Quoted { // in the quote to avoid "trait not in scope" compiler warnings. let owner_npk_m_hash = aztec::protocol::traits::Hash::hash(owner_npk_m); let secret = aztec::keys::getters::get_nhk_app(owner_npk_m_hash); - aztec::protocol::hash::poseidon2_hash_with_separator( - [note_hash_for_nullification, secret], - aztec::protocol::constants::DOM_SEP__NOTE_NULLIFIER as Field, - ) + aztec::note::utils::compute_note_nullifier(note_hash_for_nullification, [secret]) }) } } diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index c649b2fb0455..0626999c7a3f 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -1,6 +1,43 @@ use crate::{context::NoteExistenceRequest, note::{ConfirmedNote, HintedNote, note_interface::NoteHash}}; -use crate::protocol::hash::{compute_siloed_note_hash, compute_unique_note_hash}; +use crate::protocol::{ + constants::{DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_NULLIFIER}, + hash::{compute_siloed_note_hash, compute_unique_note_hash, poseidon2_hash_with_separator}, +}; + +/// Computes a domain-separated note hash. +/// +/// Receives the `storage_slot` of the [`crate::state_vars::StateVariable`] that holds the note, plus any arbitrary +/// note `data`. This typically includes randomness, owner, and domain specific values (e.g. numeric amount, address, +/// id, etc.). +/// +/// Usage of this function guarantees that different state variables will never produce colliding note hashes, even if +/// their underlying notes have different implementations. +pub fn compute_note_hash(storage_slot: Field, data: [Field; N]) -> Field { + // All state variables have different storage slots, so by placing this at a fixed first position in the preimage + // we + // prevent collisions. + poseidon2_hash_with_separator([storage_slot].concat(data), DOM_SEP__NOTE_HASH) +} + +/// Computes a domain-separated note nullifier. +/// +/// Receives the `note_hash_for_nullification` of the note (usually returned by +/// [`compute_confirmed_note_hash_for_nullification`]), plus any arbitrary note `data`. This typically includes +/// secrets, +/// such as the app-siloed nullifier hiding key of the note's owner. +/// +/// Usage of this function guarantees that different state variables will never produce colliding note nullifiers, even +/// if their underlying notes have different implementations. +pub fn compute_note_nullifier(note_hash_for_nullification: Field, data: [Field; N]) -> Field { + // All notes have different note hashes for nullification (i.e. transient or settled), so by placing this at a + // fixed + // first position in the preimage we prevent collisions. + poseidon2_hash_with_separator( + [note_hash_for_nullification].concat(data), + DOM_SEP__NOTE_NULLIFIER, + ) +} /// Returns the [`NoteExistenceRequest`] used to prove a note exists. pub fn compute_note_existence_request(hinted_note: HintedNote) -> NoteExistenceRequest @@ -26,21 +63,28 @@ where } } -/// Returns the note hash that must be used to compute a note's nullifier when calling `NoteHash::compute_nullifier` or -/// `NoteHash::compute_nullifier_unconstrained`. -pub fn compute_note_hash_for_nullification(hinted_note: HintedNote) -> Field +/// Unconstrained variant of [`compute_confirmed_note_hash_for_nullification`]. +pub unconstrained fn compute_note_hash_for_nullification(hinted_note: HintedNote) -> Field where Note: NoteHash, { + // Creating a ConfirmedNote like we do here is typically unsafe, as we've not confirmed existence. We can do it + // here + // because this is an unconstrained function, so the returned value should not make its way to a constrained + // function. This lets us reuse the `compute_confirmed_note_hash_for_nullification` implementation. compute_confirmed_note_hash_for_nullification(ConfirmedNote::new( hinted_note, compute_note_existence_request(hinted_note).note_hash(), )) } -/// Same as `compute_note_hash_for_nullification`, except it takes the note hash used in a read request (i.e. what -/// `compute_note_existence_request` would return). This is useful in scenarios where that hash has already been -/// computed to reduce constraints by reusing this value. +/// Returns the note hash to use when computing its nullifier. +/// +/// The `note_hash_for_nullification` parameter [`NoteHash::compute_nullifier`] takes depends on the note's stage, e.g. +/// settled notes use the unique note hash, but pending notes cannot as they have no nonce. This function returns the +/// correct note hash to use. +/// +/// Use [`compute_note_hash_for_nullification`] when computing this value in unconstrained functions. pub fn compute_confirmed_note_hash_for_nullification(confirmed_note: ConfirmedNote) -> Field { // There is just one instance in which the note hash for nullification does not match the note hash used for a read // request, which is when dealing with pending previous phase notes. These had their existence proven using their diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/single_use_claim.nr b/noir-projects/aztec-nr/aztec/src/state_vars/single_use_claim.nr index 2edf233a6f9c..98c5fb0a38bc 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/single_use_claim.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/single_use_claim.nr @@ -1,5 +1,6 @@ use crate::protocol::{ - address::AztecAddress, constants::DOM_SEP__NOTE_HASH, hash::poseidon2_hash_with_separator, traits::Hash, + address::AztecAddress, constants::DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, hash::poseidon2_hash_with_separator, + traits::Hash, }; use crate::{ @@ -62,7 +63,6 @@ mod test; /// /// Public effects you emit alongside a claim (e.g. a public function call to update a tally) may still let observers /// infer who likely exercised the claim, so consider that when designing flows. -/// ``` pub struct SingleUseClaim { context: Context, storage_slot: Field, @@ -82,8 +82,10 @@ impl SingleUseClaim { /// This function is primarily used internally by functions [`SingleUseClaim::claim`], /// [`SingleUseClaim::assert_claimed`] and [`SingleUseClaim::has_claimed`] to coherently write and read state. fn compute_nullifier(self, owner_nhk_app: Field) -> Field { - // TODO(F-180): make sure we follow the nullifier convention - poseidon2_hash_with_separator([owner_nhk_app, self.storage_slot], DOM_SEP__NOTE_HASH) + poseidon2_hash_with_separator( + [owner_nhk_app, self.storage_slot], + DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, + ) } } diff --git a/noir-projects/aztec-nr/aztec/src/test/mocks/mock_note.nr b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_note.nr index fabceb778c14..bee037c40061 100644 --- a/noir-projects/aztec-nr/aztec/src/test/mocks/mock_note.nr +++ b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_note.nr @@ -5,11 +5,9 @@ use crate::{ note::{HintedNote, note_interface::{NoteHash, NoteType}, note_metadata::NoteMetadata}, }; -use crate::protocol::{ - address::AztecAddress, - constants::{DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_NULLIFIER}, - hash::poseidon2_hash_with_separator, - traits::{Packable, ToField}, +use crate::{ + note::utils::{compute_note_hash, compute_note_nullifier}, + protocol::{address::AztecAddress, traits::{Packable, ToField}}, }; #[derive(Eq, Packable)] @@ -26,8 +24,8 @@ impl NoteType for MockNote { impl NoteHash for MockNote { fn compute_note_hash(self, owner: AztecAddress, storage_slot: Field, randomness: Field) -> Field { - let input = self.pack().concat([owner.to_field(), storage_slot, randomness]); - poseidon2_hash_with_separator(input, DOM_SEP__NOTE_HASH) + let data = self.pack().concat([owner.to_field(), randomness]); + compute_note_hash(storage_slot, data) } fn compute_nullifier( @@ -38,10 +36,7 @@ impl NoteHash for MockNote { ) -> Field { // We don't use any kind of secret here since this is only a mock note and having it here would make tests more // cumbersome - poseidon2_hash_with_separator( - [note_hash_for_nullification], - DOM_SEP__NOTE_NULLIFIER as Field, - ) + compute_note_nullifier(note_hash_for_nullification, []) } unconstrained fn compute_nullifier_unconstrained( @@ -51,12 +46,7 @@ impl NoteHash for MockNote { ) -> Option { // We don't use any kind of secret here since this is only a mock note and having it here would make tests more // cumbersome - Option::some( - poseidon2_hash_with_separator( - [note_hash_for_nullification], - DOM_SEP__NOTE_NULLIFIER as Field, - ), - ) + Option::some(compute_note_nullifier(note_hash_for_nullification, [])) } } diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 0fb84e2515c5..00d62a9ea45f 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -4,14 +4,11 @@ use aztec::{ keys::getters::{get_nhk_app, get_public_keys, try_get_public_keys}, macros::notes::custom_note, messages::logs::partial_note::compute_partial_note_private_content_log, - note::note_interface::{NoteHash, NoteType}, + note::{note_interface::{NoteHash, NoteType}, utils::compute_note_nullifier}, oracle::random::random, protocol::{ address::AztecAddress, - constants::{ - DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_NULLIFIER, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, - PRIVATE_LOG_SIZE_IN_FIELDS, - }, + constants::{DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, PRIVATE_LOG_SIZE_IN_FIELDS}, hash::{compute_siloed_nullifier, poseidon2_hash_with_separator}, traits::{Deserialize, FromField, Hash, Packable, Serialize, ToField}, }, @@ -65,10 +62,7 @@ impl NoteHash for UintNote { let owner_npk_m = get_public_keys(owner).npk_m; let owner_npk_m_hash = owner_npk_m.hash(); let secret = context.request_nhk_app(owner_npk_m_hash); - poseidon2_hash_with_separator( - [note_hash_for_nullification, secret], - DOM_SEP__NOTE_NULLIFIER, - ) + compute_note_nullifier(note_hash_for_nullification, [secret]) } unconstrained fn compute_nullifier_unconstrained( @@ -80,10 +74,7 @@ impl NoteHash for UintNote { let owner_npk_m = public_keys.npk_m; let owner_npk_m_hash = owner_npk_m.hash(); let secret = get_nhk_app(owner_npk_m_hash); - poseidon2_hash_with_separator( - [note_hash_for_nullification, secret], - DOM_SEP__NOTE_NULLIFIER, - ) + compute_note_nullifier(note_hash_for_nullification, [secret]) }) } } diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr index 52c59fba9592..0e780fd41c34 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/types/nft_note.nr @@ -3,13 +3,11 @@ use aztec::{ keys::getters::{get_nhk_app, get_public_keys, try_get_public_keys}, macros::notes::custom_note, messages::logs::partial_note::compute_partial_note_private_content_log, - note::note_interface::{NoteHash, NoteType}, + note::{note_interface::{NoteHash, NoteType}, utils::compute_note_nullifier}, oracle::random::random, protocol::{ address::AztecAddress, - constants::{ - DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_NULLIFIER, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, - }, + constants::{DOM_SEP__NOTE_HASH, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT}, hash::poseidon2_hash_with_separator, traits::{Deserialize, Hash, Packable, Serialize, ToField}, }, @@ -66,10 +64,7 @@ impl NoteHash for NFTNote { let owner_npk_m = get_public_keys(owner).npk_m; let owner_npk_m_hash = owner_npk_m.hash(); let secret = context.request_nhk_app(owner_npk_m_hash); - poseidon2_hash_with_separator( - [note_hash_for_nullification, secret], - DOM_SEP__NOTE_NULLIFIER, - ) + compute_note_nullifier(note_hash_for_nullification, [secret]) } unconstrained fn compute_nullifier_unconstrained( @@ -81,10 +76,7 @@ impl NoteHash for NFTNote { let owner_npk_m = public_keys.npk_m; let owner_npk_m_hash = owner_npk_m.hash(); let secret = get_nhk_app(owner_npk_m_hash); - poseidon2_hash_with_separator( - [note_hash_for_nullification, secret], - DOM_SEP__NOTE_NULLIFIER, - ) + compute_note_nullifier(note_hash_for_nullification, [secret]) }) } } diff --git a/noir-projects/noir-contracts/contracts/app/orderbook_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/orderbook_contract/src/main.nr index 55757d6289d0..201954b1c4af 100644 --- a/noir-projects/noir-contracts/contracts/app/orderbook_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/orderbook_contract/src/main.nr @@ -145,9 +145,9 @@ pub contract Orderbook { // Nullify the order such that it cannot be fulfilled again. We emit a nullifier instead of deleting the order // from public storage because we get no refund for resetting public storage to zero and just emitting - // a nullifier is cheaper (1 Field in DA instead of multiple Fields for the order). We use the `order_id` - // itself as the nullifier because this contract does not work with notes and hence there is no risk of - // colliding with a real note nullifier. + // a nullifier is cheaper (1 Field in DA instead of multiple Fields for the order). + // TODO(F-399): pushing a raw nullifier with no domain separator like we do here is unsafe: we should instead + // use something like a singleton `SingleUseClaim`. self.context.push_nullifier(order_id); // Enqueue the fulfillment to finalize both partial notes diff --git a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/types/transparent_note.nr b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/types/transparent_note.nr index 805e63a72ccd..85f941e9fcdb 100644 --- a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/types/transparent_note.nr +++ b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/types/transparent_note.nr @@ -1,13 +1,8 @@ use aztec::{ context::PrivateContext, macros::notes::custom_note, - note::note_interface::NoteHash, - protocol::{ - address::AztecAddress, - constants::{DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_NULLIFIER}, - hash::poseidon2_hash_with_separator, - traits::Packable, - }, + note::{note_interface::NoteHash, utils::{compute_note_hash, compute_note_nullifier}}, + protocol::{address::AztecAddress, traits::Packable}, }; use std::mem::zeroed; @@ -29,8 +24,8 @@ impl NoteHash for TransparentNote { storage_slot: Field, randomness: Field, ) -> Field { - let inputs = self.pack().concat([storage_slot, randomness]); - poseidon2_hash_with_separator(inputs, DOM_SEP__NOTE_HASH) + let data = self.pack().concat([randomness]); + compute_note_hash(storage_slot, data) } // Computing a nullifier in a transparent note is not guarded by making secret a part of the nullifier preimage (as @@ -47,10 +42,7 @@ impl NoteHash for TransparentNote { _owner: AztecAddress, note_hash_for_nullification: Field, ) -> Field { - poseidon2_hash_with_separator( - [note_hash_for_nullification], - DOM_SEP__NOTE_NULLIFIER as Field, - ) + compute_note_nullifier(note_hash_for_nullification, []) } unconstrained fn compute_nullifier_unconstrained( diff --git a/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr index 629497482dca..89791e564fff 100644 --- a/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/protocol/contract_class_registry_contract/src/main.nr @@ -93,6 +93,7 @@ pub contract ContractClassRegistry { // Emit the contract class id as a nullifier: // - to demonstrate that this contract class hasn't been published before // - to enable apps to read that this contract class has been published. + // We use no domain separators because these are the only nullifiers this contract uses. context.push_nullifier(contract_class_id.to_field()); // Broadcast class info including public bytecode diff --git a/noir-projects/noir-contracts/contracts/protocol/contract_instance_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/protocol/contract_instance_registry_contract/src/main.nr index eb48b949d35a..bc9a5eed0b94 100644 --- a/noir-projects/noir-contracts/contracts/protocol/contract_instance_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/protocol/contract_instance_registry_contract/src/main.nr @@ -139,6 +139,7 @@ pub contract ContractInstanceRegistry { let address = AztecAddress::compute(public_keys, partial_address); // Emit address as nullifier: prevents duplicate deployment and proves publication. + // We use no domain separators because these are the only nullifiers this contract uses. context.push_nullifier(address.to_field()); // Broadcast deployment event. Uses non-standard serialization (2 fields per point, diff --git a/noir-projects/noir-contracts/contracts/test/spam_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/spam_contract/src/main.nr index 9d26399f290e..7519a67fd2f2 100644 --- a/noir-projects/noir-contracts/contracts/test/spam_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/spam_contract/src/main.nr @@ -6,12 +6,10 @@ pub contract Spam { use aztec::{ macros::{functions::{external, only_self}, storage::storage}, messages::message_delivery::MessageDelivery, - protocol::{ - constants::{ - DOM_SEP__NOTE_NULLIFIER, MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIERS_PER_CALL, - MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, - }, - hash::poseidon2_hash_with_separator, + note::utils::compute_note_nullifier, + protocol::constants::{ + MAX_NOTE_HASHES_PER_CALL, MAX_NULLIFIERS_PER_CALL, + MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, }, state_vars::{Map, Owned, PublicMutable}, }; @@ -34,10 +32,7 @@ pub contract Spam { for i in 0..MAX_NULLIFIERS_PER_CALL { if (i < nullifier_count) { - self.context.push_nullifier(poseidon2_hash_with_separator( - [nullifier_seed, i as Field], - DOM_SEP__NOTE_NULLIFIER as Field, - )); + self.context.push_nullifier(compute_note_nullifier(nullifier_seed, [i as Field])); } } diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/test_note.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/test_note.nr index a0d4d6b00dad..a7a7a6260547 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/test_note.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/test_note.nr @@ -1,11 +1,8 @@ use aztec::{ context::PrivateContext, macros::notes::custom_note, - note::note_interface::NoteHash, - protocol::{ - address::AztecAddress, constants::DOM_SEP__NOTE_HASH, hash::poseidon2_hash_with_separator, - traits::Packable, - }, + note::{note_interface::NoteHash, utils::compute_note_hash}, + protocol::{address::AztecAddress, traits::Packable}, }; /// A note used only for testing purposes. @@ -24,8 +21,8 @@ impl NoteHash for TestNote { ) -> Field { // The note is inserted into the state in the Test contract so we provide a real compute_note_hash // implementation. - let inputs = self.pack().concat([storage_slot, randomness]); - poseidon2_hash_with_separator(inputs, DOM_SEP__NOTE_HASH) + let data = self.pack().concat([randomness]); + compute_note_hash(storage_slot, data) } fn compute_nullifier( diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 5ab1d49109e4..61e914ee75b2 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -634,16 +634,49 @@ pub global MAX_PUBLIC_CALLS_TO_UNIQUE_CONTRACT_CLASS_IDS: u32 = 21; // See ./constants_tests.nr for how the values are generated. // --------------------------------------------------------------- -// Note hash generator index which can be used by custom implementations of NoteHash::compute_note_hash +/// Domain separator for note hashes. +/// +/// This is not technically a protocol constant as note hashes are computed by each contract. pub global DOM_SEP__NOTE_HASH: u32 = 116501019; -pub global DOM_SEP__NOTE_HASH_NONCE: u32 = 1721808740; -pub global DOM_SEP__UNIQUE_NOTE_HASH: u32 = 226850429; + +/// Domain separator for siloed note hashes. +/// +/// Used by [`crate::hash::compute_siloed_note_hash`]. pub global DOM_SEP__SILOED_NOTE_HASH: u32 = 3361878420; +/// Domain separator for unique note hashes. +/// +/// Used by [`crate::hash::compute_unique_note_hash`]. +pub global DOM_SEP__UNIQUE_NOTE_HASH: u32 = 226850429; + +/// Domain separator for nonces. +/// +/// Used by [`crate::hash::compute_note_hash_nonce`]. +pub global DOM_SEP__NOTE_HASH_NONCE: u32 = 1721808740; + +/// Domain separator for `SingleUseClaim` nullifiers. +/// +/// This is not technically a protocol constant as these nullifiers are computed by each contract. +pub global DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER: u32 = 1465998995; + +/// Domain separator for note nullifiers. +/// +/// This is not technically a protocol constant as note nullifiers are computed by each contract. pub global DOM_SEP__NOTE_NULLIFIER: u32 = 50789342; -pub global DOM_SEP__MESSAGE_NULLIFIER: u32 = 3754509616; + +/// Domain separator for siloed nullifiers. +/// +/// Used by [`crate::hash::compute_siloed_nullifier`]. pub global DOM_SEP__SILOED_NULLIFIER: u32 = 57496191; +/// Domain separator for L1 to L2 message nullifiers. +/// +/// This is not technically a protocol constant as message nullifiers are computed by each contract. +pub global DOM_SEP__MESSAGE_NULLIFIER: u32 = 3754509616; + +/// Domain separator for private log tags. +/// +/// Used by [`crate::hash::compute_siloed_private_log_first_field`]. pub global DOM_SEP__PRIVATE_LOG_FIRST_FIELD: u32 = 2769976252; pub global DOM_SEP__PUBLIC_LEAF_SLOT: u32 = 1247650290; @@ -710,11 +743,29 @@ pub global DOM_SEP__CIPHERTEXT_FIELD_MASK: u32 = 1870492847; pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423; // --------------------------------------------------------------- -// TODO: consider moving these to aztec-nr +// TODO(F-397): move these to aztec-nr, along with note hash, note nullifier, message nullifier and single use claim +// nullifier. +/// Domain separator for state variable initialization. +/// +/// Should not be reused for a given storage slot. pub global DOM_SEP__INITIALIZATION_NULLIFIER: u32 = 1653084894; + +/// Domain separator for L1 to L2 message secret hashes. pub global DOM_SEP__SECRET_HASH: u32 = 4199652938; + +/// Domain separator for transaction nullifiers. +/// +/// Used to produce cancellable (replaceable) transactions. +/// +/// This is not technically a protocol constant as cancellable transactions are an account contract feature. pub global DOM_SEP__TX_NULLIFIER: u32 = 1025801951; + +/// Domain separator for account contract payloads. +/// +/// Used to check for authorization to execute a payload. +/// +/// This is not technically a protocol constant as payload authorization is an account contract feature. pub global DOM_SEP__SIGNATURE_PAYLOAD: u32 = 463525807; // --------------------------------------------------------------- diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr index 6f7c9dee1cc1..a3a9f80fe9d5 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr @@ -17,9 +17,10 @@ use crate::{ DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_KEYS_HASH, DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH, DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, DOM_SEP__SILOED_NOTE_HASH, - DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, - DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST, DOM_SEP__UNIQUE_NOTE_HASH, - NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS, TX_START_PREFIX, + DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, DOM_SEP__SYMMETRIC_KEY, + DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST, + DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS, + TX_START_PREFIX, }, hash::poseidon2_hash_bytes, traits::{FromField, ToField}, @@ -129,7 +130,7 @@ impl HashedValueTester::new(); + let mut tester = HashedValueTester::<51, 44>::new(); // ----------------- // Domain separators @@ -139,6 +140,10 @@ fn hashed_values_match_derived() { tester.assert_dom_sep_matches_derived(DOM_SEP__UNIQUE_NOTE_HASH, "unique_note_hash"); tester.assert_dom_sep_matches_derived(DOM_SEP__SILOED_NOTE_HASH, "siloed_note_hash"); tester.assert_dom_sep_matches_derived(DOM_SEP__NOTE_NULLIFIER, "note_nullifier"); + tester.assert_dom_sep_matches_derived( + DOM_SEP__SINGLE_USE_CLAIM_NULLIFIER, + "single_use_claim_nullifier", + ); tester.assert_dom_sep_matches_derived(DOM_SEP__SILOED_NULLIFIER, "siloed_nullifier"); tester.assert_dom_sep_matches_derived( DOM_SEP__PRIVATE_LOG_FIRST_FIELD, From 3741abc92e523a1d8de742c2b19fbee865c332cc Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 6 Mar 2026 07:07:34 +0000 Subject: [PATCH 2/8] fmt fix --- noir-projects/aztec-nr/aztec/src/note/utils.nr | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 0626999c7a3f..9146be00905c 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -7,7 +7,7 @@ use crate::protocol::{ /// Computes a domain-separated note hash. /// -/// Receives the `storage_slot` of the [`crate::state_vars::StateVariable`] that holds the note, plus any arbitrary +/// Receives the `storage_slot` of the [`crate::state_vars::StateVariable`] that holds the note, plus any arbitrary /// note `data`. This typically includes randomness, owner, and domain specific values (e.g. numeric amount, address, /// id, etc.). /// @@ -15,8 +15,7 @@ use crate::protocol::{ /// their underlying notes have different implementations. pub fn compute_note_hash(storage_slot: Field, data: [Field; N]) -> Field { // All state variables have different storage slots, so by placing this at a fixed first position in the preimage - // we - // prevent collisions. + // we prevent collisions. poseidon2_hash_with_separator([storage_slot].concat(data), DOM_SEP__NOTE_HASH) } @@ -24,15 +23,13 @@ pub fn compute_note_hash(storage_slot: Field, data: [Field; N]) -> F /// /// Receives the `note_hash_for_nullification` of the note (usually returned by /// [`compute_confirmed_note_hash_for_nullification`]), plus any arbitrary note `data`. This typically includes -/// secrets, -/// such as the app-siloed nullifier hiding key of the note's owner. +/// secrets, such as the app-siloed nullifier hiding key of the note's owner. /// /// Usage of this function guarantees that different state variables will never produce colliding note nullifiers, even /// if their underlying notes have different implementations. pub fn compute_note_nullifier(note_hash_for_nullification: Field, data: [Field; N]) -> Field { // All notes have different note hashes for nullification (i.e. transient or settled), so by placing this at a - // fixed - // first position in the preimage we prevent collisions. + // fixed first position in the preimage we prevent collisions. poseidon2_hash_with_separator( [note_hash_for_nullification].concat(data), DOM_SEP__NOTE_NULLIFIER, @@ -69,8 +66,7 @@ where Note: NoteHash, { // Creating a ConfirmedNote like we do here is typically unsafe, as we've not confirmed existence. We can do it - // here - // because this is an unconstrained function, so the returned value should not make its way to a constrained + // here because this is an unconstrained function, so the returned value should not make its way to a constrained // function. This lets us reuse the `compute_confirmed_note_hash_for_nullification` implementation. compute_confirmed_note_hash_for_nullification(ConfirmedNote::new( hinted_note, From 1e7a89a40e0d9fe881b47a48e0300f86d54f50a0 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 6 Mar 2026 07:10:40 +0000 Subject: [PATCH 3/8] claim contract fix --- .../contracts/app/claim_contract/src/main.nr | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr index 3d022fc8b76f..79257cb8b8a9 100644 --- a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr @@ -7,7 +7,10 @@ pub contract Claim { // docs:end:history_import use aztec::{ macros::{functions::{external, initializer}, storage::storage}, - note::{HintedNote, note_interface::NoteHash, utils::compute_note_hash_for_nullification}, + note::{ + HintedNote, note_interface::NoteHash, + utils::compute_confirmed_note_hash_for_nullification, + }, protocol::address::AztecAddress, state_vars::PublicImmutable, }; @@ -48,7 +51,7 @@ pub contract Claim { // 3) Prove that the note hash exists in the note hash tree // docs:start:prove_note_inclusion let header = self.context.get_anchor_block_header(); - let _ = assert_note_existed_by(header, hinted_note); + let confirmed_note = assert_note_existed_by(header, hinted_note); // docs:end:prove_note_inclusion // 4) Compute and emit a nullifier which is unique to the note and this contract to ensure the reward can be @@ -58,7 +61,8 @@ pub contract Claim { // the address of a contract it was emitted from. // TODO(#7775): manually computing the hash and passing it to compute_nullifier func is not great as note could // handle it on its own or we could make assert_note_existed_by return note_hash_for_nullification. - let note_hash_for_nullification = compute_note_hash_for_nullification(hinted_note); + let note_hash_for_nullification = + compute_confirmed_note_hash_for_nullification(confirmed_note); let nullifier = hinted_note.note.compute_nullifier( self.context, hinted_note.owner, From eca7df015a2be0f7e3898a24aa3b22befb5112d8 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 6 Mar 2026 07:28:21 +0000 Subject: [PATCH 4/8] fmt fix 2 --- noir-projects/aztec-nr/aztec/src/note/utils.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 9146be00905c..0e8008d64651 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -7,7 +7,7 @@ use crate::protocol::{ /// Computes a domain-separated note hash. /// -/// Receives the `storage_slot` of the [`crate::state_vars::StateVariable`] that holds the note, plus any arbitrary +/// Receives the `storage_slot` of the [`crate::state_vars::StateVariable`] that holds the note, plus any arbitrary /// note `data`. This typically includes randomness, owner, and domain specific values (e.g. numeric amount, address, /// id, etc.). /// From 9f28df85b247b187ecda00f90ef8802b80910231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 6 Mar 2026 18:10:35 -0300 Subject: [PATCH 5/8] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Beneš --- 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 abf0167269a3..dc360c059de4 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -11,7 +11,7 @@ Aztec is in active development. Each version may introduce breaking changes that ### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained -This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous. If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. +This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ComfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. ### [Aztec.nr] Removed `get_random_bytes` From 71fcd26a753a03dd1354f3a0b35b9614bfd4f642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 6 Mar 2026 18:11:49 -0300 Subject: [PATCH 6/8] Update migration_notes.md --- docs/docs-developers/docs/resources/migration_notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index dc360c059de4..22bbd76bb819 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -13,6 +13,12 @@ Aztec is in active development. Each version may introduce breaking changes that This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ComfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. +### [Aztec.nr] Changes to standard note hash computation + +Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation. + +This change requires no input from your side unless you were testing or relying on hardcoded note hashes. + ### [Aztec.nr] Removed `get_random_bytes` The `get_random_bytes` unconstrained function has been removed from `aztec::utils::random`. If you were using it, you can replace it with direct calls to the `random` oracle from `aztec::oracle::random` and convert to bytes yourself. From 75ea615cacc684f94ebcf71d4caa0f4a5a80554d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 6 Mar 2026 21:54:58 +0000 Subject: [PATCH 7/8] fix test --- .../oracle/private_execution.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 02e5f378c460..3ce458889e16 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -275,7 +275,7 @@ describe('Private Execution test suite', () => { const computeNoteHash = (note: Note, owner: AztecAddress, storageSlot: Fr, randomness: Fr) => { // We're assuming here that the note hash function is the default one injected by the #[note] macro. return poseidon2HashWithSeparator( - [...note.items, owner.toField(), storageSlot, randomness], + [storageSlot, ...note.items, owner.toField(), randomness], DomainSeparator.NOTE_HASH, ); }; From ff9f79e1e9e29d32ee131382978d9ba099fea44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 6 Mar 2026 20:14:52 -0300 Subject: [PATCH 8/8] fix 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 ae05a40f3958..9a342de45a2f 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -11,7 +11,7 @@ Aztec is in active development. Each version may introduce breaking changes that ### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained -This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ComfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. +This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ConfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. ### [Aztec.nr] Changes to standard note hash computation