diff --git a/src/nft_contract/src/types/nft_note.nr b/src/nft_contract/src/types/nft_note.nr index ebfb9e5d..40d3cdcc 100644 --- a/src/nft_contract/src/types/nft_note.nr +++ b/src/nft_contract/src/types/nft_note.nr @@ -6,12 +6,15 @@ use aztec::{ delivery::{do_private_message_delivery, MessageDelivery}, logs::partial_note::encode_partial_note_private_message, }, - note::{note_interface::{NoteHash, NoteType}, utils::compute_note_nullifier}, + note::{ + note_interface::{NoteHash, NoteType}, + utils::{compute_note_hash, compute_note_nullifier}, + }, oracle::random::random, protocol::{ address::AztecAddress, constants::{ - DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, + DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__PARTIAL_NOTE_COMMITMENT, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, }, hash::{compute_log_tag, poseidon2_hash_with_separator}, @@ -49,7 +52,7 @@ impl NoteHash for NFTNote { // values, so that notes all behave the same way regardless of how they were created. To achieve this, we // perform both steps of the partial note computation. - // First we create the partial note from a commitment to the private content (including storage slot). + // First we create the partial note from a commitment to the private content. let partial_note = PartialNFTNote { commitment: compute_partial_commitment(owner, randomness) }; @@ -151,7 +154,10 @@ impl NFTNote { /// Computes a commitment to the private content of a partial NFTNote, i.e. the fields that will remain private. All /// other note fields will be made public. fn compute_partial_commitment(owner: AztecAddress, randomness: Field) -> Field { - poseidon2_hash_with_separator([owner.to_field(), randomness], DOM_SEP__NOTE_HASH) + poseidon2_hash_with_separator( + [owner.to_field(), randomness], + DOM_SEP__PARTIAL_NOTE_COMMITMENT, + ) } #[derive(Packable)] @@ -165,8 +171,9 @@ impl NoteType for NFTPartialNotePrivateLogContent { } } -/// A partial instance of a NFTNote. This value represents a private commitment to the owner, randomness and storage -/// slot, but the token id field has not yet been set. A partial note can be completed in public with the `complete` +/// A partial instance of a NFTNote. This value represents a private commitment to the owner and randomness (the storage +/// slot is folded into the completed note hash, not the commitment), but the token id field has not yet been set. A +/// partial note can be completed in public with the `complete` /// function (revealing the token id to the public), resulting in a NFTNote that can be used like any other one (except /// of course that its token id is known). #[derive(Packable, Serialize, Deserialize)] @@ -224,12 +231,38 @@ impl PartialNFTNote { } fn compute_complete_note_hash(self, storage_slot: Field, token_id: Field) -> Field { - // Here we finalize the note hash by including the (public) storage slot and token id into the partial note - // commitment. Note that we use the same separator as we used for the first round of poseidon - this is not - // an issue. - poseidon2_hash_with_separator( - [self.commitment, storage_slot, token_id], - DOM_SEP__NOTE_HASH, - ) + // Finalize the note hash by folding the (public) storage slot and token id into the partial note commitment. + // `compute_note_hash` fixes the storage slot at the first position of the preimage (preventing cross-slot + // collisions) and appends the remaining fields in order. + compute_note_hash(storage_slot, [self.commitment, token_id]) + } +} + +mod test { + use super::{compute_partial_commitment, NFTNote, PartialNFTNote}; + use aztec::{ + note::note_interface::NoteHash, + protocol::{address::AztecAddress, traits::FromField}, + }; + + global token_id: Field = 17; + global randomness: Field = 42; + global owner: AztecAddress = AztecAddress::from_field(50); + global storage_slot: Field = 13; + + #[test] + fn note_hash_matches_completed_partial_note_hash() { + // A NFTNote must have the same note hash as a PartialNFTNote created and then completed with the same private + // values. This requires the same hash function in both flows, with the fields in the same order — the invariant + // that lets a directly-created note and a completed partial note be used interchangeably. + let note = NFTNote { token_id }; + let note_hash = note.compute_note_hash(owner, storage_slot, randomness); + + let partial_note = + PartialNFTNote { commitment: compute_partial_commitment(owner, randomness) }; + let completed_partial_note_hash = + partial_note.compute_complete_note_hash(storage_slot, token_id); + + assert_eq(note_hash, completed_partial_note_hash); } }