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 140d7a610f4a..77d6adf051a4 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 @@ -5,8 +5,9 @@ use crate::{ encoding::MAX_MESSAGE_CONTENT_LEN, logs::partial_note::{decode_partial_note_private_message, MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN}, processing::{ - enqueue_note_for_validation, get_pending_partial_notes_completion_logs, - log_retrieval_response::LogRetrievalResponse, + enqueue_note_for_validation, + get_pending_partial_notes_completion_logs, + log_retrieval_response::{LogRetrievalResponse, MAX_LOG_CONTENT_LEN}, }, }, utils::array, @@ -25,7 +26,6 @@ pub(crate) global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT: Fie #[derive(Serialize, Deserialize)] pub(crate) struct DeliveredPendingPartialNote { pub(crate) owner: AztecAddress, - pub(crate) storage_slot: Field, pub(crate) randomness: Field, pub(crate) note_completion_log_tag: Field, pub(crate) note_type_id: Field, @@ -41,12 +41,11 @@ pub(crate) unconstrained fn process_partial_note_private_msg( ) { // We store the information of the partial note we found in a persistent capsule in PXE, so that we can later // search for the public log that will complete it. - let (owner, storage_slot, randomness, note_completion_log_tag, note_type_id, packed_private_note_content) = + let (owner, randomness, note_completion_log_tag, note_type_id, packed_private_note_content) = decode_partial_note_private_message(msg_metadata, msg_content); let pending = DeliveredPendingPartialNote { owner, - storage_slot, randomness, note_completion_log_tag, note_type_id, @@ -105,12 +104,17 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( ]); let log = maybe_log.unwrap(); - // Public fields are assumed to all be placed at the end of the packed representation, so we combine the - // private and public packed fields (i.e. the contents of the private message and public log plaintext to - // get the complete packed content. + // The first field in the completion log payload is the storage slot, followed by the public note + // content fields. + let storage_slot = log.log_payload.get(0); + let public_note_content: BoundedVec = array::subbvec(log.log_payload, 1); + + // Public fields are assumed to all be placed at the end of the packed representation, so we combine + // the private and public packed fields (i.e. the contents of the private message and public log + // plaintext) to get the complete packed content. let complete_packed_note = array::append( pending_partial_note.packed_private_note_content, - log.log_payload, + public_note_content, ); let discovered_notes = attempt_note_nonce_discovery( @@ -119,7 +123,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( compute_note_hash_and_nullifier, contract_address, pending_partial_note.owner, - pending_partial_note.storage_slot, + storage_slot, pending_partial_note.randomness, pending_partial_note.note_type_id, complete_packed_note, @@ -142,7 +146,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( enqueue_note_for_validation( contract_address, pending_partial_note.owner, - pending_partial_note.storage_slot, + storage_slot, pending_partial_note.randomness, discovered_note.note_nonce, complete_packed_note, diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr index b39e0a809fbc..1e72dd815110 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/partial_note.nr @@ -15,11 +15,10 @@ use crate::protocol::{ }; /// The number of fields in a private note message content that are not the note's packed representation. -pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN: u32 = 4; +pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN: u32 = 3; pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_OWNER_INDEX: u32 = 0; -pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_STORAGE_SLOT_INDEX: u32 = 1; -pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RANDOMNESS_INDEX: u32 = 2; -pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NOTE_COMPLETION_LOG_TAG_INDEX: u32 = 3; +pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RANDOMNESS_INDEX: u32 = 1; +pub(crate) global PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NOTE_COMPLETION_LOG_TAG_INDEX: u32 = 2; /// Partial notes have a maximum packed length of their private fields bound by extra content in their private message /// (e.g. the storage slot, note completion log tag, etc.). @@ -30,7 +29,6 @@ pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 = pub fn compute_partial_note_private_content_log( partial_note_private_content: PartialNotePrivateContent, owner: AztecAddress, - storage_slot: Field, randomness: Field, recipient: AztecAddress, note_completion_log_tag: Field, @@ -41,7 +39,6 @@ where let message_plaintext = encode_partial_note_private_message( partial_note_private_content, owner, - storage_slot, randomness, note_completion_log_tag, ); @@ -56,7 +53,6 @@ where pub fn encode_partial_note_private_message( partial_note_private_content: PartialNotePrivateContent, owner: AztecAddress, - storage_slot: Field, randomness: Field, note_completion_log_tag: Field, ) -> [Field; PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN + ::N + MESSAGE_EXPANDED_METADATA_LEN] @@ -68,14 +64,13 @@ where // If PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NON_NOTE_FIELDS_LEN is changed, causing the assertion below to fail, then // the encoding below must be updated as well. std::static_assert( - PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN == 4, + PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN == 3, "unexpected value for PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NON_NOTE_FIELDS_LEN", ); let mut msg_content = [0; PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN + ::N]; msg_content[PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_OWNER_INDEX] = owner.to_field(); - msg_content[PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_STORAGE_SLOT_INDEX] = storage_slot; msg_content[PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RANDOMNESS_INDEX] = randomness; msg_content[PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NOTE_COMPLETION_LOG_TAG_INDEX] = note_completion_log_tag; @@ -102,7 +97,7 @@ where pub(crate) unconstrained fn decode_partial_note_private_message( msg_metadata: u64, msg_content: BoundedVec, -) -> (AztecAddress, Field, Field, Field, Field, BoundedVec) { +) -> (AztecAddress, Field, Field, Field, BoundedVec) { let note_type_id = msg_metadata as Field; // TODO: make note type id not be a full field // The following ensures that the message content contains at least the minimum number of fields required for a @@ -116,16 +111,15 @@ pub(crate) unconstrained fn decode_partial_note_private_message( // If PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NON_NOTE_FIELDS_LEN is changed, causing the assertion below to fail, then // the destructuring of the partial note private message encoding below must be updated as well. std::static_assert( - PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN == 4, + PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN == 3, "unexpected value for PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NON_NOTE_FIELDS_LEN", ); - // We currently have four fields that are not the partial note's packed representation, which are the owner, the - // storage slot, the randomness, and the note completion log tag. + // We currently have three fields that are not the partial note's packed representation, which are the owner, the + // randomness, and the note completion log tag. let owner = AztecAddress::from_field( msg_content.get(PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_OWNER_INDEX), ); - let storage_slot = msg_content.get(PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_STORAGE_SLOT_INDEX); let randomness = msg_content.get(PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RANDOMNESS_INDEX); let note_completion_log_tag = msg_content.get(PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_NOTE_COMPLETION_LOG_TAG_INDEX); @@ -134,7 +128,7 @@ pub(crate) unconstrained fn decode_partial_note_private_message( PARTIAL_NOTE_PRIVATE_MSG_PLAINTEXT_RESERVED_FIELDS_LEN, ); - (owner, storage_slot, randomness, note_completion_log_tag, note_type_id, packed_private_note_content) + (owner, randomness, note_completion_log_tag, note_type_id, packed_private_note_content) } mod test { @@ -151,7 +145,6 @@ mod test { global VALUE: Field = 7; global OWNER: AztecAddress = AztecAddress::from_field(8); - global STORAGE_SLOT: Field = 9; global RANDOMNESS: Field = 10; global NOTE_COMPLETION_LOG_TAG: Field = 11; @@ -160,24 +153,17 @@ mod test { // Note that here we use MockNote as the private fields of a partial note let note = MockNote::new(VALUE).build_note(); - let message_plaintext = encode_partial_note_private_message( - note, - OWNER, - STORAGE_SLOT, - RANDOMNESS, - NOTE_COMPLETION_LOG_TAG, - ); + let message_plaintext = encode_partial_note_private_message(note, OWNER, RANDOMNESS, NOTE_COMPLETION_LOG_TAG); let (msg_type_id, msg_metadata, msg_content) = decode_message(BoundedVec::from_array(message_plaintext)); assert_eq(msg_type_id, PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID); - let (owner, storage_slot, randomness, note_completion_log_tag, note_type_id, packed_note) = + let (owner, randomness, note_completion_log_tag, note_type_id, packed_note) = decode_partial_note_private_message(msg_metadata, msg_content); assert_eq(note_type_id, MockNote::get_id()); assert_eq(owner, OWNER); - assert_eq(storage_slot, STORAGE_SLOT); assert_eq(randomness, RANDOMNESS); assert_eq(note_completion_log_tag, NOTE_COMPLETION_LOG_TAG); assert_eq(packed_note, BoundedVec::from_array(note.pack())); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/log_retrieval_response.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/log_retrieval_response.nr index e478bf139964..1d177689b57e 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/log_retrieval_response.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/log_retrieval_response.nr @@ -3,7 +3,7 @@ use crate::protocol::{constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_CIPHERTEXT pub global MAX_PUBLIC_LOG_LEN_FOR_NOTE_COMPLETION: u32 = MAX_NOTE_PACKED_LEN; -global MAX_LOG_CONTENT_LEN: u32 = std::cmp::max( +pub(crate) global MAX_LOG_CONTENT_LEN: u32 = std::cmp::max( MAX_PUBLIC_LOG_LEN_FOR_NOTE_COMPLETION, PRIVATE_LOG_CIPHERTEXT_LEN, ); 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 00d62a9ea45f..0c9ea6fcd8e8 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -15,10 +15,10 @@ use aztec::{ }; // UintNote supports partial notes, i.e. the ability to create an incomplete note in private, hiding certain values -// (the owner, storage slot and randomness), and then completing the note in public with the ones missing (the amount). -// Partial notes are being actively developed and are not currently fully supported via macros, and so we rely on the -// #[custom_note] macro to implement it manually, resulting in some boilerplate. This is expected to be unnecessary -// once macro support is expanded. +// (the owner and randomness), and then completing the note in public with the ones missing (the storage slot and +// amount). Partial notes are being actively developed and are not currently fully supported via macros, and so we +// rely on the #[custom_note] macro to implement it manually, resulting in some boilerplate. This is expected to be +// unnecessary once macro support is expanded. /// A private note representing a numeric value associated to an account (e.g. a token balance). // docs:start:uint_note_def @@ -42,11 +42,11 @@ impl NoteHash for UintNote { // same 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). - let partial_note = PartialUintNote { commitment: compute_partial_commitment(owner, storage_slot, randomness) }; + // First we create the partial note from a commitment to the private content. + let partial_note = PartialUintNote { commitment: compute_partial_commitment(owner, randomness) }; // Then compute the completion note hash. In a real partial note this step would be performed in public. - partial_note.compute_complete_note_hash(self.value) + partial_note.compute_complete_note_hash(storage_slot, self.value) } // docs:end:compute_note_hash @@ -80,7 +80,7 @@ impl NoteHash for UintNote { } impl UintNote { - /// Creates a partial note that will hide the owner and storage slot but not the value, since the note will be + /// Creates a partial note that will hide the owner but not the value or storage slot, since the note will be /// later completed in public. This is a powerful technique for scenarios in which the value cannot be known in /// private (e.g. because it depends on some public state, such as a DEX). /// @@ -95,7 +95,6 @@ impl UintNote { /// note. `recipient` will typically be the same as `owner`. pub fn partial( owner: AztecAddress, - storage_slot: Field, context: &mut PrivateContext, recipient: AztecAddress, completer: AztecAddress, @@ -107,7 +106,7 @@ impl UintNote { let randomness = unsafe { random() }; // We create a commitment to the private data, which we then use to construct the log we send to the recipient. - let commitment = compute_partial_commitment(owner, storage_slot, randomness); + let commitment = compute_partial_commitment(owner, randomness); // Our partial note log encoding scheme includes a field with the tag of the public completion log, and we use // the commitment as the tag. This is good for multiple reasons: @@ -120,7 +119,6 @@ impl UintNote { let encrypted_log = compute_partial_note_private_content_log( private_log_content, owner, - storage_slot, randomness, recipient, commitment, @@ -145,11 +143,8 @@ impl UintNote { /// Computes a commitment to the private content of a partial UintNote, i.e. the fields that will remain private. All /// other note fields will be made public. // docs:start:compute_partial_commitment -fn compute_partial_commitment(owner: AztecAddress, storage_slot: Field, randomness: Field) -> Field { - poseidon2_hash_with_separator( - [owner.to_field(), storage_slot, randomness], - DOM_SEP__NOTE_HASH, - ) +fn compute_partial_commitment(owner: AztecAddress, randomness: Field) -> Field { + poseidon2_hash_with_separator([owner.to_field(), randomness], DOM_SEP__NOTE_HASH) } // docs:end:compute_partial_commitment @@ -164,10 +159,10 @@ impl NoteType for UintPartialNotePrivateLogContent { } } -/// A partial instance of a UintNote. This value represents a private commitment to the owner, randomness and storage -/// slot, but the value field has not yet been set. A partial note can be completed in public with the `complete` -/// function (revealing the value to the public), resulting in a UintNote that can be used like any other one (except -/// of course that its value is known). +/// A partial instance of a UintNote. This value represents a private commitment to the owner and randomness, but the +/// storage slot and value fields have not yet been set. A partial note can be completed in public with the `complete` +/// function (revealing the storage slot and value to the public), resulting in a UintNote that can be used like any +/// other one (except of course that its value is known). // docs:start:partial_uint_note_def #[derive(Packable, Serialize, Deserialize, Eq)] pub struct PartialUintNote { @@ -175,11 +170,11 @@ pub struct PartialUintNote { } // docs:end:partial_uint_note_def -global NOTE_COMPLETION_LOG_LENGTH: u32 = 2; +global NOTE_COMPLETION_LOG_LENGTH: u32 = 3; impl PartialUintNote { /// Completes the partial note, creating a new note that can be used like any other UintNote. - pub fn complete(self, context: PublicContext, completer: AztecAddress, value: u128) { + pub fn complete(self, context: PublicContext, completer: AztecAddress, storage_slot: Field, value: u128) { // A note with a value of zero is valid, but we cannot currently complete a partial note with such a value // because this will result in the completion log having its last field set to 0. Public logs currently do not // track their length, and so trailing zeros are simply trimmed. This results in the completion log missing its @@ -197,18 +192,24 @@ impl PartialUintNote { ); // We need to do two things: - // - emit a public log containing the public fields (the value). The contract will later find it by searching - // for the expected tag (which is simply the partial note commitment). + // - emit a public log containing the public fields (the storage slot and value). The contract will later find + // it by searching for the expected tag (which is simply the partial note commitment). // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_public_log(self.compute_note_completion_log(value)); - context.push_note_hash(self.compute_complete_note_hash(value)); + context.emit_public_log(self.compute_note_completion_log(storage_slot, value)); + context.push_note_hash(self.compute_complete_note_hash(storage_slot, value)); } /// Completes the partial note, creating a new note that can be used like any other UintNote. Same as `complete` /// function but works from private context. - pub fn complete_from_private(self, context: &mut PrivateContext, completer: AztecAddress, value: u128) { + pub fn complete_from_private( + self, + context: &mut PrivateContext, + completer: AztecAddress, + storage_slot: Field, + value: u128, + ) { // We verify that the partial note we're completing is valid (i.e. completer is correct, it uses the correct // state variable's storage slot, and it is internally consistent). let validity_commitment = self.compute_validity_commitment(completer); @@ -222,16 +223,17 @@ impl PartialUintNote { ); // We need to do two things: - // - emit an unencrypted log containing the public fields (the value) via the private log channel. The - // contract will later find it by searching for the expected tag (which is simply the partial note commitment). + // - emit an unencrypted log containing the public fields (the storage slot and value) via the private log + // channel. The contract will later find it by searching for the expected tag (which is simply the partial note + // commitment). // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. context.emit_private_log( - self.compute_note_completion_log_padded_for_private_log(value), + self.compute_note_completion_log_padded_for_private_log(storage_slot, value), NOTE_COMPLETION_LOG_LENGTH, ); - context.push_note_hash(self.compute_complete_note_hash(value)); + context.push_note_hash(self.compute_complete_note_hash(storage_slot, value)); } /// Computes a validity commitment for this partial note. The commitment cryptographically binds the note's private @@ -246,23 +248,31 @@ impl PartialUintNote { ) } - fn compute_note_completion_log(self, value: u128) -> [Field; NOTE_COMPLETION_LOG_LENGTH] { + fn compute_note_completion_log(self, storage_slot: Field, value: u128) -> [Field; NOTE_COMPLETION_LOG_LENGTH] { // The first field of this log must be the tag that the recipient of the partial note private field logs // expects, which is equal to the partial note commitment. - [self.commitment, value.to_field()] + [self.commitment, storage_slot, value.to_field()] } - fn compute_note_completion_log_padded_for_private_log(self, value: u128) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] { - let note_completion_log = self.compute_note_completion_log(value); + fn compute_note_completion_log_padded_for_private_log( + self, + storage_slot: Field, + value: u128, + ) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] { + let note_completion_log = self.compute_note_completion_log(storage_slot, value); let padding = [0; PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_COMPLETION_LOG_LENGTH]; note_completion_log.concat(padding) } // docs:start:compute_complete_note_hash - fn compute_complete_note_hash(self, value: u128) -> Field { - // Here we finalize the note hash by including the (public) value into the partial note commitment. Note that - // we use the same generator index as we used for the first round of poseidon - this is not an issue. - poseidon2_hash_with_separator([self.commitment, value.to_field()], DOM_SEP__NOTE_HASH) + fn compute_complete_note_hash(self, storage_slot: Field, value: u128) -> Field { + // Here we finalize the note hash by including the (public) storage slot and value 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, value.to_field()], + DOM_SEP__NOTE_HASH, + ) } // docs:end:compute_complete_note_hash } @@ -296,8 +306,8 @@ mod test { let note = UintNote { value }; let note_hash = note.compute_note_hash(owner, storage_slot, randomness); - let partial_note = PartialUintNote { commitment: compute_partial_commitment(owner, storage_slot, randomness) }; - let completed_partial_note_hash = partial_note.compute_complete_note_hash(value); + let partial_note = PartialUintNote { commitment: compute_partial_commitment(owner, randomness) }; + let completed_partial_note_hash = partial_note.compute_complete_note_hash(storage_slot, value); assert_eq(note_hash, completed_partial_note_hash); } diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr index 9a79bf339513..b70fa9734f17 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr @@ -21,7 +21,7 @@ pub contract NFT { note_interface::NoteProperties, note_viewer_options::NoteViewerOptions, }, protocol::{address::AztecAddress, traits::ToField}, - state_vars::{Map, Owned, PrivateSet, PublicImmutable, PublicMutable}, + state_vars::{Map, Owned, PrivateSet, PublicImmutable, PublicMutable, StateVariable}, utils::comparison::Comparator, }; use compressed_string::FieldCompressedString; @@ -167,13 +167,7 @@ pub contract NFT { #[internal("private")] fn _prepare_private_balance_increase(to: AztecAddress) -> PartialNFTNote { // We setup a partial note with unpopulated/zero token id for 'to'. - let partial_note = NFTNote::partial( - to, - self.storage.private_nfts.at(to).storage_slot, - self.context, - to, - self.msg_sender(), - ); + let partial_note = NFTNote::partial(to, self.context, to, self.msg_sender()); partial_note } @@ -222,7 +216,12 @@ pub contract NFT { public_owners_storage.write(AztecAddress::zero()); // We finalize the transfer by completing the partial note. - partial_note.complete(self.context, from_and_completer, token_id); + partial_note.complete( + self.context, + from_and_completer, + self.storage.private_nfts.get_storage_slot(), + token_id, + ); } /** 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 0e780fd41c34..a2dad7d1e64d 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 @@ -14,7 +14,7 @@ use aztec::{ }; // NFTNote supports partial notes, i.e. the ability to create an incomplete note in private, hiding certain values (the -// owner, storage slot and randomness), and then completing the note in public with the ones missing (the token id). +// owner and randomness), and then completing the note in public with the ones missing (the storage slot and token id). // Partial notes are being actively developed and are not currently fully supported via macros, and so we rely on the // #[custom_note] macro to implement it manually, resulting in some boilerplate. This is expected to be unnecessary once // macro support is expanded. @@ -43,13 +43,12 @@ 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). - let partial_note = PartialNFTNote { - commitment: compute_partial_commitment(owner, storage_slot, randomness), - }; + // First we create the partial note from a commitment to the private content. + let partial_note = + PartialNFTNote { commitment: compute_partial_commitment(owner, randomness) }; // Then compute the completion note hash. In a real partial note this step would be performed in public. - partial_note.compute_complete_note_hash(self.token_id) + partial_note.compute_complete_note_hash(storage_slot, self.token_id) } // The nullifiers are nothing special - this is just the canonical implementation that would be injected by the @@ -82,7 +81,7 @@ impl NoteHash for NFTNote { } impl NFTNote { - /// Creates a partial note that will hide the owner and storage slot but not the token id, since the note will be + /// Creates a partial note that will hide the owner but not the token id or storage slot, since the note will be /// later completed in public. This is a powerful technique for scenarios in which the token id cannot be known in /// private (e.g. because it depends on some public state, such as a DEX). /// @@ -97,7 +96,6 @@ impl NFTNote { /// discover the note. `recipient` will typically be the same as `owner`. pub fn partial( owner: AztecAddress, - storage_slot: Field, context: &mut PrivateContext, recipient: AztecAddress, completer: AztecAddress, @@ -109,7 +107,7 @@ impl NFTNote { let randomness = unsafe { random() }; // We create a commitment to the private data, which we then use to construct the log we send to the recipient. - let commitment = compute_partial_commitment(owner, storage_slot, randomness); + let commitment = compute_partial_commitment(owner, randomness); // Our partial note log encoding scheme includes a field with the tag of the public completion log, and we use // the commitment as the tag. This is good for multiple reasons: @@ -122,7 +120,6 @@ impl NFTNote { let encrypted_log = compute_partial_note_private_content_log( private_log_content, owner, - storage_slot, randomness, recipient, commitment, @@ -146,15 +143,8 @@ 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, - storage_slot: Field, - randomness: Field, -) -> Field { - poseidon2_hash_with_separator( - [owner.to_field(), storage_slot, randomness], - DOM_SEP__NOTE_HASH, - ) +fn compute_partial_commitment(owner: AztecAddress, randomness: Field) -> Field { + poseidon2_hash_with_separator([owner.to_field(), randomness], DOM_SEP__NOTE_HASH) } #[derive(Packable)] @@ -168,10 +158,10 @@ 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` -/// 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). +/// A partial instance of a NFTNote. This value represents a private commitment to the owner and randomness, but the +/// storage slot and token id fields have not yet been set. A partial note can be completed in public with the +/// `complete` function (revealing the storage slot and 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)] pub struct PartialNFTNote { commitment: Field, @@ -179,7 +169,13 @@ pub struct PartialNFTNote { impl PartialNFTNote { /// Completes the partial note, creating a new note that can be used like any other NFTNote. - pub fn complete(self, context: PublicContext, completer: AztecAddress, token_id: Field) { + pub fn complete( + self, + context: PublicContext, + completer: AztecAddress, + storage_slot: Field, + token_id: Field, + ) { // A note with a value of zero is valid, but we cannot currently complete a partial note with such a value // because this will result in the completion log having its last field set to 0. Public logs currently do not // track their length, and so trailing zeros are simply trimmed. This results in the completion log missing its @@ -198,13 +194,13 @@ impl PartialNFTNote { ); // We need to do two things: - // - emit a public log containing the public fields (the token id). The contract will later find it by - // searching for the expected tag (which is simply the partial note commitment). + // - emit a public log containing the public fields (the storage slot and token id). The contract will later + // find it by searching for the expected tag (which is simply the partial note commitment). // - insert the completion note hash (i.e. the hash of the note) into the note hash tree. This is typically // only done in private to hide the preimage of the hash that is inserted, but completed partial notes are // inserted in public as the public values are provided and the note hash computed. - context.emit_public_log(self.compute_note_completion_log(token_id)); - context.push_note_hash(self.compute_complete_note_hash(token_id)); + context.emit_public_log(self.compute_note_completion_log(storage_slot, token_id)); + context.push_note_hash(self.compute_complete_note_hash(storage_slot, token_id)); } /// Computes a validity commitment for this partial note. The commitment cryptographically binds the note's private @@ -219,16 +215,21 @@ impl PartialNFTNote { ) } - fn compute_note_completion_log(self, token_id: Field) -> [Field; 2] { + fn compute_note_completion_log(self, storage_slot: Field, token_id: Field) -> [Field; 3] { // The first field of this log must be the tag that the recipient of the partial note private field logs - // expects, which is equal to the partial note commitment. - [self.commitment, token_id] + // expects, which is equal to the partial note commitment. The storage slot is included as the first public + // value so that note discovery can extract it. + [self.commitment, storage_slot, token_id] } - fn compute_complete_note_hash(self, token_id: Field) -> Field { - // Here we finalize the note hash by including the (public) token id into the partial note commitment. Note that - // we use the same generator index as we used for the first round of poseidon - this is not an issue. - poseidon2_hash_with_separator([self.commitment, token_id], DOM_SEP__NOTE_HASH) + 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, + ) } } @@ -253,10 +254,10 @@ mod test { 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, storage_slot, randomness), - }; - let completed_partial_note_hash = partial_note.compute_complete_note_hash(token_id); + 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); } diff --git a/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr index ae350f4fd577..4ffcdfcd6dcf 100644 --- a/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr @@ -170,13 +170,7 @@ pub contract SimpleToken { #[internal("private")] fn _prepare_private_balance_increase(to: AztecAddress) -> PartialUintNote { - let partial_note = UintNote::partial( - to, - self.storage.balances.get_storage_slot(), - self.context, - to, - self.msg_sender(), - ); + let partial_note = UintNote::partial(to, self.context, to, self.msg_sender()); partial_note } @@ -208,7 +202,12 @@ pub contract SimpleToken { let from_balance = balance_storage.read().sub(amount); balance_storage.write(from_balance); - partial_note.complete(self.context, from_and_completer, amount); + partial_note.complete( + self.context, + from_and_completer, + self.storage.balances.get_storage_slot(), + amount, + ); } #[external("private")] @@ -241,7 +240,12 @@ pub contract SimpleToken { let supply = self.storage.total_supply.read().add(amount); self.storage.total_supply.write(supply); - partial_note.complete(self.context, completer, amount); + partial_note.complete( + self.context, + completer, + self.storage.balances.get_storage_slot(), + amount, + ); } #[external("public")] diff --git a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr index c4378f376926..7f77a2c3101c 100644 --- a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr @@ -355,13 +355,7 @@ pub contract Token { // docs:start:prepare_private_balance_increase #[internal("private")] fn _prepare_private_balance_increase(to: AztecAddress) -> PartialUintNote { - let partial_note = UintNote::partial( - to, - self.storage.balances.get_storage_slot(), - self.context, - to, - self.msg_sender(), - ); + let partial_note = UintNote::partial(to, self.context, to, self.msg_sender()); partial_note } @@ -402,7 +396,12 @@ pub contract Token { // First we subtract the `amount` from the private balance of `from` self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); - partial_note.complete_from_private(self.context, self.msg_sender(), amount); + partial_note.complete_from_private( + self.context, + self.msg_sender(), + self.storage.balances.get_storage_slot(), + amount, + ); } /// This is a wrapper around `_finalize_transfer_to_private` placed here so that a call @@ -435,7 +434,12 @@ pub contract Token { balance_storage.write(from_balance); // We finalize the transfer by completing the partial note. - partial_note.complete(self.context, from_and_completer, amount); + partial_note.complete( + self.context, + from_and_completer, + self.storage.balances.get_storage_slot(), + amount, + ); } // docs:end:finalize_transfer_to_private @@ -495,7 +499,12 @@ pub contract Token { self.storage.total_supply.write(supply); // We finalize the transfer by completing the partial note. - partial_note.complete(self.context, completer, amount); + partial_note.complete( + self.context, + completer, + self.storage.balances.get_storage_slot(), + amount, + ); } #[external("public")] diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr index 4077277f1ea8..d1767fe7a1eb 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr @@ -151,13 +151,13 @@ pub contract Test { } #[external("private")] - fn call_create_partial_note(owner: AztecAddress, storage_slot: Field) -> PartialUintNote { - UintNote::partial(owner, storage_slot, self.context, owner, self.address) + fn call_create_partial_note(owner: AztecAddress) -> PartialUintNote { + UintNote::partial(owner, self.context, owner, self.address) } #[external("public")] - fn call_complete_partial_note(partial_note: PartialUintNote, value: u128) { - partial_note.complete(self.context, self.address, value); + fn call_complete_partial_note(partial_note: PartialUintNote, storage_slot: Field, value: u128) { + partial_note.complete(self.context, self.address, storage_slot, value); } #[external("private")] @@ -166,10 +166,9 @@ pub contract Test { storage_slot: Field, value: u128, ) { - let partial_note = - UintNote::partial(owner, storage_slot, self.context, owner, self.address); + let partial_note = UintNote::partial(owner, self.context, owner, self.address); - self.enqueue_self.call_complete_partial_note(partial_note, value); + self.enqueue_self.call_complete_partial_note(partial_note, storage_slot, value); } #[external("private")] diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/test/note_delivery.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/test/note_delivery.nr index 3d57d7bc51c2..dc3eb139591c 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/test/note_delivery.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/test/note_delivery.nr @@ -134,10 +134,12 @@ unconstrained fn create_partial_note_in_two_txs_and_read_in_private() { let (env, contract_address, sender, recipient) = setup(); let test_contract = Test::at(contract_address); - let partial_note = - env.call_private(sender, test_contract.call_create_partial_note(recipient, STORAGE_SLOT)); + let partial_note = env.call_private(sender, test_contract.call_create_partial_note(recipient)); - env.call_public(sender, test_contract.call_complete_partial_note(partial_note, VALUE)); + env.call_public( + sender, + test_contract.call_complete_partial_note(partial_note, STORAGE_SLOT, VALUE), + ); // The 'from' param is irrelevant in this case let retrieved = env.call_private( @@ -152,10 +154,12 @@ unconstrained fn create_partial_note_in_two_txs_and_read_in_utility() { let (env, contract_address, sender, recipient) = setup(); let test_contract = Test::at(contract_address); - let partial_note = - env.call_private(sender, test_contract.call_create_partial_note(recipient, STORAGE_SLOT)); + let partial_note = env.call_private(sender, test_contract.call_create_partial_note(recipient)); - env.call_public(sender, test_contract.call_complete_partial_note(partial_note, VALUE)); + env.call_public( + sender, + test_contract.call_complete_partial_note(partial_note, STORAGE_SLOT, VALUE), + ); let retrieved = env.execute_utility(test_contract.call_view_notes( recipient, diff --git a/noir-projects/protocol-fuzzer/contracts/side_effect_contract/src/main.nr b/noir-projects/protocol-fuzzer/contracts/side_effect_contract/src/main.nr index 97a301a9c01c..5ac9de10a710 100644 --- a/noir-projects/protocol-fuzzer/contracts/side_effect_contract/src/main.nr +++ b/noir-projects/protocol-fuzzer/contracts/side_effect_contract/src/main.nr @@ -85,10 +85,9 @@ pub contract SideEffect { value: u128, ) { assert(value != 0, "note value must be non-zero"); - let partial_note = - UintNote::partial(owner, storage_slot, self.context, owner, self.address); + let partial_note = UintNote::partial(owner, self.context, owner, self.address); - self.enqueue_self.call_complete_partial_note(partial_note, value); + self.enqueue_self.call_complete_partial_note(partial_note, storage_slot, value); } // This function creates a nullifier for an existing note hash. If the linked @@ -191,7 +190,7 @@ pub contract SideEffect { } #[external("public")] - fn call_complete_partial_note(partial_note: PartialUintNote, value: u128) { - partial_note.complete(self.context, self.address, value); + fn call_complete_partial_note(partial_note: PartialUintNote, storage_slot: Field, value: u128) { + partial_note.complete(self.context, self.address, storage_slot, value); } }