diff --git a/noir-projects/aztec-nr/address-note/src/address_note.nr b/noir-projects/aztec-nr/address-note/src/address_note.nr index d3e8fc4f5861..93251b61a2b1 100644 --- a/noir-projects/aztec-nr/address-note/src/address_note.nr +++ b/noir-projects/aztec-nr/address-note/src/address_note.nr @@ -3,8 +3,8 @@ use dep::aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::note, note::{ - note_interface::NullifiableNote, retrieved_note::RetrievedNote, - utils::compute_note_hash_for_nullify, + note_interface::NullifiableNote, note_metadata::SettledNoteMetadata, + retrieved_note::RetrievedNote, utils::compute_note_hash_for_nullify, }, oracle::random::random, protocol_types::{ @@ -45,9 +45,11 @@ impl NullifiableNote for AddressNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr index 0cb4bd36d129..0a4df5183c3e 100644 --- a/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr +++ b/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr @@ -6,7 +6,7 @@ use crate::{ note::{ note_interface::{NoteInterface, NullifiableNote}, retrieved_note::RetrievedNote, - utils::compute_siloed_nullifier, + utils::compute_siloed_note_nullifier, }, oracle::get_nullifier_membership_witness::get_nullifier_membership_witness, }; @@ -64,7 +64,7 @@ impl ProveNoteIsNullified for BlockHeader { where Note: NoteInterface + NullifiableNote, { - let nullifier = compute_siloed_nullifier(retrieved_note, storage_slot, context); + let nullifier = compute_siloed_note_nullifier(retrieved_note, storage_slot, context); self.prove_nullifier_inclusion(nullifier); } diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr index 755f2ee63533..1e68f7f6b79a 100644 --- a/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr +++ b/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr @@ -3,7 +3,7 @@ use crate::{ note::{ note_interface::{NoteInterface, NullifiableNote}, retrieved_note::RetrievedNote, - utils::compute_siloed_nullifier, + utils::compute_siloed_note_nullifier, }, oracle::get_nullifier_membership_witness::get_low_nullifier_membership_witness, }; @@ -77,7 +77,7 @@ impl ProveNoteNotNullified for BlockHeader { where Note: NoteInterface + NullifiableNote, { - let nullifier = compute_siloed_nullifier(retrieved_note, storage_slot, context); + let nullifier = compute_siloed_note_nullifier(retrieved_note, storage_slot, context); self.prove_nullifier_non_inclusion(nullifier); } diff --git a/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr b/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr index b6db493b8642..2e9a7c4544e9 100644 --- a/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr +++ b/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr @@ -3,7 +3,7 @@ use crate::note::{ note_emission::NoteEmission, note_interface::{NoteInterface, NullifiableNote}, retrieved_note::RetrievedNote, - utils::{compute_note_hash_for_nullify_internal, compute_note_hash_for_read_request}, + utils::{compute_note_hash_for_nullify_from_read_request, compute_note_hash_for_read_request}, }; use crate::oracle::notes::notify_created_note; use protocol_types::traits::Packable; @@ -58,11 +58,10 @@ where Note: NoteInterface + NullifiableNote, { let note_hash_for_nullify = - compute_note_hash_for_nullify_internal(retrieved_note, note_hash_for_read_request); + compute_note_hash_for_nullify_from_read_request(retrieved_note, note_hash_for_read_request); let nullifier = retrieved_note.note.compute_nullifier(context, note_hash_for_nullify); - let note_hash_counter = retrieved_note.note_hash_counter; - let notification_note_hash = if (note_hash_counter == 0) { + let note_hash = if retrieved_note.metadata.is_settled() { // Counter is zero, so we're nullifying a settled note and we don't populate the note_hash with real value. 0 } else { @@ -74,5 +73,5 @@ where note_hash_for_nullify }; - context.push_nullifier_for_note_hash(nullifier, notification_note_hash) + context.push_nullifier_for_note_hash(nullifier, note_hash) } diff --git a/noir-projects/aztec-nr/aztec/src/note/mod.nr b/noir-projects/aztec-nr/aztec/src/note/mod.nr index 1dd9cc818167..ab9c50a007f6 100644 --- a/noir-projects/aztec-nr/aztec/src/note/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/mod.nr @@ -1,5 +1,6 @@ pub mod constants; pub mod lifecycle; +pub mod note_metadata; pub mod note_getter; pub mod note_getter_options; pub mod note_interface; diff --git a/noir-projects/aztec-nr/aztec/src/note/note_emission.nr b/noir-projects/aztec-nr/aztec/src/note/note_emission.nr index e3644b48b335..6f92bfbc95ff 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_emission.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_emission.nr @@ -5,7 +5,7 @@ pub struct NoteEmission { pub note: Note, pub storage_slot: Field, - pub note_hash_counter: u32, // a note_hash_counter of 0 means non-transient + pub note_hash_counter: u32, // a note_hash_counter of 0 means settled } impl NoteEmission { diff --git a/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr b/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr index 3e3bd3a1850a..08143796c153 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr @@ -82,11 +82,11 @@ where // is check that the metadata is correct, and that the note exists. let retrieved_note = unsafe { get_note_internal::(storage_slot) }; - // For non-transient notes, the contract address is implicitly checked since the hash returned from - // `compute_note_hash_for_read_request(...)` is siloed and kernels verify the siloing during note - // read request validation. For transient notes, however, we return an "unsiloed" hash, so we need to check - // that the contract address returned from the oracle matches. Since branching in circuits is expensive, we - // perform this check on all the note types. + // For settled notes, the contract address is implicitly checked since the hash returned from + // `compute_note_hash_for_read_request` is siloed and kernels verify the siloing during note read request + // validation. Pending notes however are read with the unsiloed note hash, so we need to check that the contract + // address returned from the oracle matches. Since branching in circuits is expensive, we perform this check on all + // note types. assert( retrieved_note.contract_address.eq(context.this_address()), "Note contract address mismatch.", @@ -154,9 +154,11 @@ where if i < notes.len() { let retrieved_note = notes.get_unchecked(i); - // TODO: For non-transient notes, the contract address appears to be implicitly checked since we return - // a siloed note hash from the `compute_note_hash_for_read_request(...)` function. Investigate whether this - // check is needed for transient notes and if not, remove it. + // For settled notes, the contract address is implicitly checked since the hash returned from + // `compute_note_hash_for_read_request` is siloed and kernels verify the siloing during note read request + // validation. Pending notes however are read with the unsiloed note hash, so we need to check that the + // contract address returned from the oracle matches. Since branching in circuits is expensive, we perform + // this check on all note types. assert( retrieved_note.contract_address.eq(context.this_address()), "Note contract address mismatch.", diff --git a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr index 4ede75c5d58f..4d58d126319b 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -29,9 +29,17 @@ pub trait NullifiableNote { /// circuits. fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field; - /// Same as compute_nullifier, but unconstrained. This version does not take a note hash because it'll only be - /// invoked in unconstrained contexts, where there is no gate count. Note that it also takes a contract address: - /// this is because nullifier computation typically involves contract siloed nullifying keys. + /// Like `compute_nullifier`, this function also computes a note's nullifier, but there are some key differences. + /// + /// First and most importantly, this function is unconstrained: there are no guarantees on the returned value being + /// correct. Because of that it doesn't need to take a context (since it won't perform any kernel key validation + /// requests). + /// + /// Second, it always computes the nullifier for a **settled** note, i.e. a note that has been created in a previous + /// transaction, which therefore has a nonce. This is typically fine, since this function will mostly be used in + /// unconstrained execution contexts, which exist outside of any transaction and therefore have no concept of + /// pending notes, only settled. This also causes this function to not need to take a note hash for nullification, + /// since it will just compute the unique note hash internally using the provided nonce. unconstrained fn compute_nullifier_without_context( self, storage_slot: Field, diff --git a/noir-projects/aztec-nr/aztec/src/note/note_metadata.nr b/noir-projects/aztec-nr/aztec/src/note/note_metadata.nr new file mode 100644 index 000000000000..fa88bd493f1c --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/note/note_metadata.nr @@ -0,0 +1,182 @@ +use protocol_types::traits::Serialize; + +// There's temporarily quite a bit of boilerplate here because Noir does not yet support enums. This file will +// eventually be simplified into something closer to: +// +// pub enum NoteMetadata { +// PendingSamePhase{ note_hash_counter: u32 }, +// PendingOtherPhase{ note_hash_counter: u32, nonce: Field }, +// Settled{ nonce: Field }, +// } +// +// For now, we have `NoteMetadata` acting as a sort of tagged union. + +struct NoteStageEnum { + /// A note that was created in the transaction that is currently being executed, during the current execution phase, + /// i.e. non-revertible or revertible. + /// + /// These notes are not yet in the note hash tree, though they will be inserted unless nullified in this transaction + /// (becoming a transient note). + PENDING_SAME_PHASE: u8, + /// A note that was created in the transaction that is currently being executed, during the previous execution + /// phase. Because there are only two phases and their order is always the same (first non-revertible and then + /// revertible) this implies that the note was created in the non-revertible phase, and that the current phase is + /// the revertible phase. + /// + /// These notes are not yet in the note hash tree, though they will be inserted **even if nullified in this + /// transaction**. This means that they must be nullified as if they were settled (i.e. using the unique note hash) + /// in order to avoid double spends once they become settled. + PENDING_PREVIOUS_PHASE: u8, + /// A note that was created in a prior transaction and is therefore already in the note hash tree. + SETTLED: u8, +} + +global NoteStage: NoteStageEnum = + NoteStageEnum { PENDING_SAME_PHASE: 1, PENDING_PREVIOUS_PHASE: 2, SETTLED: 3 }; + +/// The metadata required to both prove a note's existence and destroy it, by computing the correct note hash for kernel +/// read requests, as well as the correct nullifier to avoid double-spends. +/// +/// This represents a note in any of the three valid stages (pending same phase, pending previous phase, or settled). In +/// order to access the underlying fields callers must first find the appropriate stage (e.g. via `is_settled()`) and +/// then convert this into the appropriate type (e.g. via `to_settled()`). +#[derive(Eq, Serialize)] +pub struct NoteMetadata { + stage: u8, + maybe_nonce: Field, +} + +impl NoteMetadata { + /// Constructs a `NoteMetadata` object from optional note hash counter and nonce. Both a zero note hash counter and + /// a zero nonce are invalid, so those are used to signal non-existent values. + pub fn from_raw_data(nonzero_note_hash_counter: bool, maybe_nonce: Field) -> Self { + if nonzero_note_hash_counter { + if maybe_nonce == 0 { + Self { stage: NoteStage.PENDING_SAME_PHASE, maybe_nonce } + } else { + Self { stage: NoteStage.PENDING_PREVIOUS_PHASE, maybe_nonce } + } + } else if maybe_nonce != 0 { + Self { stage: NoteStage.SETTLED, maybe_nonce } + } else { + panic( + f"Note has a zero note hash counter and no nonce - existence cannot be proven", + ) + } + } + + /// Returns true if the note is pending **and** from the same phase, i.e. if it's been created in the current + /// transaction during the current execution phase (either non-revertible or revertible). + pub fn is_pending_same_phase(self) -> bool { + self.stage == NoteStage.PENDING_SAME_PHASE + } + + /// Returns true if the note is pending **and** from the previous phase, i.e. if it's been created in the current + /// transaction during an execution phase prior to the current one. Because private execution only has two phases + /// with strict ordering, this implies that the note was created in the non-revertible phase, and that the current + /// phase is the revertible phase. + pub fn is_pending_previous_phase(self) -> bool { + self.stage == NoteStage.PENDING_PREVIOUS_PHASE + } + + /// Returns true if the note is settled, i.e. if it's been created in a prior transaction and is therefore already + /// in the note hash tree. + pub fn is_settled(self) -> bool { + self.stage == NoteStage.SETTLED + } + + /// Asserts that the metadata is that of a pending note from the same phase and converts it accordingly. + pub fn to_pending_same_phase(self) -> PendingSamePhaseNoteMetadata { + assert_eq(self.stage, NoteStage.PENDING_SAME_PHASE); + PendingSamePhaseNoteMetadata::new() + } + + /// Asserts that the metadata is that of a pending note from a previous phase and converts it accordingly. + pub fn to_pending_previous_phase(self) -> PendingPreviousPhaseNoteMetadata { + assert_eq(self.stage, NoteStage.PENDING_PREVIOUS_PHASE); + PendingPreviousPhaseNoteMetadata::new(self.maybe_nonce) + } + + /// Asserts that the metadata is that of a settled note and converts it accordingly. + pub fn to_settled(self) -> SettledNoteMetadata { + assert_eq(self.stage, NoteStage.SETTLED); + SettledNoteMetadata::new(self.maybe_nonce) + } +} + +impl From for NoteMetadata { + fn from(_value: PendingSamePhaseNoteMetadata) -> Self { + NoteMetadata::from_raw_data(true, std::mem::zeroed()) + } +} + +impl From for NoteMetadata { + fn from(value: PendingPreviousPhaseNoteMetadata) -> Self { + NoteMetadata::from_raw_data(true, value.nonce()) + } +} + +impl From for NoteMetadata { + fn from(value: SettledNoteMetadata) -> Self { + NoteMetadata::from_raw_data(false, value.nonce()) + } +} + +/// The metadata required to both prove a note's existence and destroy it, by computing the correct note hash for kernel +/// read requests, as well as the correct nullifier to avoid double-spends. +/// +/// This represents a pending same phase note, i.e. a note that was created in the transaction that is currently being +/// executed during the current execution phase (either non-revertible or revertible). +pub struct PendingSamePhaseNoteMetadata { + // This struct contains no fields since there is no metadata associated with a pending same phase note: it has no + // nonce (since it may get squashed by a nullifier emitted in the same phase), and while it does have a note hash + // counter we cannot constrain its value (and don't need to - only that it is non-zero). +} + +impl PendingSamePhaseNoteMetadata { + pub fn new() -> Self { + Self {} + } +} + +/// The metadata required to both prove a note's existence and destroy it, by computing the correct note hash for kernel +/// read requests, as well as the correct nullifier to avoid double-spends. +/// +/// This represents a pending previous phase note, i.e. a note that was created in the transaction that is currently +/// being executed, during the previous execution phase. Because there are only two phases and their order is always the +/// same (first non-revertible and then revertible) this implies that the note was created in the non-revertible phase, +/// and that the current phase is the revertible phase. +pub struct PendingPreviousPhaseNoteMetadata { + nonce: Field, + // This struct does not contain a note hash counter, even though one exists for this note, because we cannot + // constrain its value (and don't need to - only that it is non-zero). +} + +impl PendingPreviousPhaseNoteMetadata { + pub fn new(nonce: Field) -> Self { + Self { nonce } + } + + pub fn nonce(self) -> Field { + self.nonce + } +} + +/// The metadata required to both prove a note's existence and destroy it, by computing the correct note hash for kernel +/// read requests, as well as the correct nullifier to avoid double-spends. +/// +/// This represents a settled note, i.e. a note that was created in a prior transaction and is therefore already in the +/// note hash tree. +pub struct SettledNoteMetadata { + nonce: Field, +} + +impl SettledNoteMetadata { + pub fn new(nonce: Field) -> Self { + Self { nonce } + } + + pub fn nonce(self) -> Field { + self.nonce + } +} diff --git a/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr b/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr index ae0e75fc6c1b..16c4586788f2 100644 --- a/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr +++ b/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr @@ -1,34 +1,27 @@ -use protocol_types::{address::AztecAddress, traits::Serialize, utils::arrays::array_concat}; +use crate::note::note_metadata::NoteMetadata; +use protocol_types::{ + address::AztecAddress, + traits::{Serialize, ToField}, + utils::arrays::array_concat, +}; -/// A container of a note and the metadata required to constrain its existence, regardless of whether the note is -/// historical (created in a previous transaction) or transient (created in the current transaction). +/// A container of a note and the metadata required to prove its existence, regardless of whether the note is +/// pending (created in the current transaction) or settled (created in a previous transaction). +#[derive(Eq)] pub struct RetrievedNote { pub note: NOTE, pub contract_address: AztecAddress, - pub nonce: Field, - pub note_hash_counter: u32, // a note_hash_counter of 0 means non-transient + pub metadata: NoteMetadata, } -impl Eq for RetrievedNote -where - NOTE: Eq, -{ - fn eq(self, other: Self) -> bool { - (self.note == other.note) - & (self.contract_address == other.contract_address) - & (self.nonce == other.nonce) - & (self.note_hash_counter == other.note_hash_counter) - } -} - -impl Serialize for RetrievedNote +impl Serialize for RetrievedNote where NOTE: Serialize, { - fn serialize(self) -> [Field; N + 3] { + fn serialize(self) -> [Field; N + 1 + 2] { array_concat( - self.note.serialize(), - [self.contract_address.to_field(), self.nonce, self.note_hash_counter as Field], + array_concat(self.note.serialize(), [self.contract_address.to_field()]), + self.metadata.serialize(), ) } } diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index ac9d2b39bcc0..9b67cf8728bb 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -4,112 +4,86 @@ use crate::{ }; use dep::protocol_types::hash::{ - compute_siloed_note_hash, compute_siloed_nullifier as compute_siloed_nullifier_from_preimage, - compute_unique_note_hash, + compute_siloed_note_hash, compute_siloed_nullifier, compute_unique_note_hash, }; -pub fn compute_siloed_nullifier( +/// Returns the note hash that must be used to issue a private kernel read request for a note. +pub fn compute_note_hash_for_read_request( retrieved_note: RetrievedNote, storage_slot: Field, - context: &mut PrivateContext, ) -> Field where - Note: NoteInterface + NullifiableNote, + Note: NoteInterface, { - let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); - let inner_nullifier = retrieved_note.note.compute_nullifier(context, note_hash_for_nullify); + let note_hash = retrieved_note.note.compute_note_hash(storage_slot); - compute_siloed_nullifier_from_preimage(retrieved_note.contract_address, inner_nullifier) + if retrieved_note.metadata.is_settled() { + // Settled notes are read by siloing with contract address and nonce (resulting in the final unique note hash, + // which is already in the note hash tree). + let siloed_note_hash = compute_siloed_note_hash(retrieved_note.contract_address, note_hash); + compute_unique_note_hash( + retrieved_note.metadata.to_settled().nonce(), + siloed_note_hash, + ) + } else { + // Pending notes (both same phase and previous phase ones) re read by their non-siloed hash (not even by + // contract address), which is what is stored in the new note hashes array (at the position hinted by note hash + // counter). + note_hash + } } -// TODO(#7775): make this not impossible to understand -pub fn compute_note_hash_for_read_request( +/// Returns the note hash that must be used to compute a note's nullifier. +pub fn compute_note_hash_for_nullify( retrieved_note: RetrievedNote, storage_slot: Field, ) -> Field where - Note: NoteInterface, + Note: NoteInterface + NullifiableNote, { - let note_hash = retrieved_note.note.compute_note_hash(storage_slot); - - // If same tx note, read request always uses the normal note hash - if retrieved_note.note_hash_counter != 0 { - note_hash - } else { - // If the note comes from a different tx, we need to compute the note hash that reached the tree - compute_unique_note_hash( - retrieved_note.nonce, - compute_siloed_note_hash(retrieved_note.contract_address, note_hash), - ) - } + compute_note_hash_for_nullify_from_read_request( + retrieved_note, + compute_note_hash_for_read_request(retrieved_note, storage_slot), + ) } -// TODO(#7775): make this not impossible to understand -pub fn compute_note_hash_for_nullify_internal( +/// Same as `compute_note_hash_for_nullify`, except it takes the note hash used in a read request (i.e. what +/// `compute_note_hash_for_read_request` would return). This is useful in scenarios where that hash has already been +/// computed to reduce constraints by reusing this value. +pub fn compute_note_hash_for_nullify_from_read_request( retrieved_note: RetrievedNote, note_hash_for_read_request: Field, ) -> Field { - if (retrieved_note.note_hash_counter != 0) & (retrieved_note.nonce != 0) { - // Non-revertible note, nullified by a revertible nullifier, we need to nullify the note hash that will reach the tree + // 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 + // non-siloed note hash along with the note hash counter (like all pending notes), but since they will be + // uncondtionally inserted in the note hash tree (since they cannot be squashed) they must be nullified using the + // *unique* note hash. + // If we didn't, it'd be possible to emit a second different nullifier for the same note in a follow up transaction, + // once the note is settled, resulting in a double spend. + + if retrieved_note.metadata.is_pending_previous_phase() { let siloed_note_hash = compute_siloed_note_hash(retrieved_note.contract_address, note_hash_for_read_request); + let nonce = retrieved_note.metadata.to_pending_previous_phase().nonce(); - compute_unique_note_hash(retrieved_note.nonce, siloed_note_hash) + compute_unique_note_hash(nonce, siloed_note_hash) } else { note_hash_for_read_request } } -// TODO(#7775): nuke this commented out code - kept it around as it contains comments which might be helpful when tackling #7775 -// pub fn compute_note_hash_for_nullify(note: Note) -> Field where Note: NoteInterface { -// let header = note.get_header(); -// // There are 3 cases for reading a note intended for consumption: -// // 1. The note was inserted in this transaction, is revertible, or is not nullified by a revertible nullifier in -// // the same transaction: (note_hash_counter != 0) & (nonce == 0) -// // 2. The note was inserted in this transaction, is non-revertible, and is nullified by a revertible nullifier in -// // the same transaction: (note_hash_counter != 0) & (nonce != 0) -// // 3. The note was inserted in a previous transaction: (note_hash_counter == 0) & (nonce != 0) - -// let note_hash = note.compute_note_hiding_point().x; - -// if header.nonce == 0 { -// // Case 1. -// // If a note is transient, we just read the note_hash (kernel will hash it with nonce and silo by contract address). -// note_hash -// } else { -// // Case 2: If a note is non-revertible, and is nullified by a revertible nullifier, we cannot squash them in the -// // private reset circuit. Because if the tx reverts, we will have to keep the note hash and throw away the -// // nullifier. -// // And if the tx does not revert, both will be emitted. In which case, the nullifier must be created in the app -// // from the siloed note hash. -// // The kernel circuit will check that a nullifier with non-zero note_nonce is linked to a note hash, whose -// // siloed note hash matches the note hash specified in the nullifier. - -// // Case 3: If a note is not from the current transaction, that means we are reading a settled note (from -// // tree) created in a previous TX. So we need the siloed_note_hash which has already been hashed with -// // nonce and then contract address. This hash will match the existing leaf in the note hash -// // tree, so the kernel can just perform a membership check directly on this hash/leaf. -// let unique_note_hash = compute_unique_note_hash(header.nonce, note_hash); -// compute_siloed_note_hash(header.contract_address, unique_note_hash) -// // IMPORTANT NOTE ON REDUNDANT SILOING BY CONTRACT ADDRESS: The note hash computed above is -// // "siloed" by contract address. When a note hash is computed solely for the purpose of -// // nullification, it is not strictly necessary to silo the note hash before computing -// // its nullifier. In other words, it is NOT NECESSARY for protocol security that a nullifier -// // be computed from a siloed note hash. After all, persistable note hashes and nullifiers are -// // siloed by the kernel circuit. That being said, the siloed note hash computed above CAN be -// // used for nullifier computation, and this achieves the (arguably unnecessary) property that -// // nullifiers are computed from a note hash's fully-computed note hash tree leaf. -// } -// } - -pub fn compute_note_hash_for_nullify( +/// Computes a note's siloed nullifier, i.e. the one that will be inserted into the nullifier tree. +pub fn compute_siloed_note_nullifier( retrieved_note: RetrievedNote, storage_slot: Field, + context: &mut PrivateContext, ) -> Field where Note: NoteInterface + NullifiableNote, { - let note_hash_for_read_request = - compute_note_hash_for_read_request(retrieved_note, storage_slot); - compute_note_hash_for_nullify_internal(retrieved_note, note_hash_for_read_request) + let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); + let inner_nullifier = retrieved_note.note.compute_nullifier(context, note_hash_for_nullify); + + compute_siloed_nullifier(retrieved_note.contract_address, inner_nullifier) } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index 141e66231fba..b8c319e9b0ce 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -1,4 +1,9 @@ -use crate::{note::{note_interface::NoteInterface, retrieved_note::RetrievedNote}, utils::array}; +use crate::{ + note::{ + note_interface::NoteInterface, note_metadata::NoteMetadata, retrieved_note::RetrievedNote, + }, + utils::array, +}; use dep::protocol_types::{ address::AztecAddress, @@ -191,12 +196,16 @@ where let extra_preimage_length: u32 = 2; // nonce & note_hash_counter. let read_offset: u32 = return_header_length + i * (N + extra_preimage_length); - let nonce = fields[read_offset]; - let note_hash_counter = fields[read_offset + 1] as u32; + let maybe_nonce = fields[read_offset]; + let maybe_note_hash_counter = fields[read_offset + 1] as u32; let packed_note = array::subarray(fields, read_offset + 2); let note = Note::unpack(packed_note); - let retrieved_note = RetrievedNote { note, contract_address, nonce, note_hash_counter }; + let retrieved_note = RetrievedNote { + note, + contract_address, + metadata: NoteMetadata::from_raw_data(maybe_note_hash_counter != 0, maybe_nonce), + }; placeholder_opt_notes[i] = Option::some(retrieved_note); }; 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 e28dea8fc8ed..c71d1a6a210d 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 @@ -1,8 +1,8 @@ use crate::{ context::PrivateContext, note::{ - note_interface::NoteInterface, retrieved_note::RetrievedNote, - utils::compute_note_hash_for_nullify, + note_interface::NoteInterface, note_metadata::SettledNoteMetadata, + retrieved_note::RetrievedNote, utils::compute_note_hash_for_nullify, }, }; @@ -40,9 +40,11 @@ impl NullifiableNote for MockNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; // 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 let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); @@ -91,8 +93,9 @@ impl MockNoteBuilder { RetrievedNote { note: MockNote { value: self.value }, contract_address: self.contract_address.unwrap_or(AztecAddress::zero()), - nonce: 0, - note_hash_counter: 0, + // This is invalid note metadata, which would result in the kernel failing to prove note existence. Because + // our tests don't interact with the kernel this is not a problem. + metadata: std::mem::zeroed(), } } } 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 9ab7254fabc1..923a29de886e 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -1,7 +1,7 @@ use dep::aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::partial_note, - note::utils::compute_note_hash_for_nullify, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, oracle::random::random, prelude::{NullifiableNote, PrivateContext, RetrievedNote}, protocol_types::{ @@ -44,9 +44,11 @@ impl NullifiableNote for UintNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/aztec-nr/value-note/src/value_note.nr b/noir-projects/aztec-nr/value-note/src/value_note.nr index c70d425873c4..ba5bdf4c0699 100644 --- a/noir-projects/aztec-nr/value-note/src/value_note.nr +++ b/noir-projects/aztec-nr/value-note/src/value_note.nr @@ -3,8 +3,8 @@ use dep::aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::note, note::{ - note_interface::NullifiableNote, retrieved_note::RetrievedNote, - utils::compute_note_hash_for_nullify, + note_interface::NullifiableNote, note_metadata::SettledNoteMetadata, + retrieved_note::RetrievedNote, utils::compute_note_hash_for_nullify, }, oracle::random::random, protocol_types::{ @@ -49,9 +49,11 @@ impl NullifiableNote for ValueNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr index 3009cbf6a87e..f1ff8936dd60 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/subscription_note.nr @@ -2,7 +2,10 @@ use dep::aztec::{ hash::poseidon2_hash_with_separator, keys::getters::{get_nsk_app, get_public_keys}, macros::notes::note, - note::{retrieved_note::RetrievedNote, utils::compute_note_hash_for_nullify}, + note::{ + note_metadata::SettledNoteMetadata, retrieved_note::RetrievedNote, + utils::compute_note_hash_for_nullify, + }, oracle::random::random, prelude::{NullifiableNote, PrivateContext}, protocol_types::{address::AztecAddress, constants::GENERATOR_INDEX__NOTE_NULLIFIER}, @@ -37,9 +40,11 @@ impl NullifiableNote for SubscriptionNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr b/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr index d7253738e053..725101d6af7c 100644 --- a/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr +++ b/noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr @@ -1,7 +1,7 @@ use dep::aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::note, - note::utils::compute_note_hash_for_nullify, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, prelude::{NullifiableNote, PrivateContext, RetrievedNote}, protocol_types::{ address::AztecAddress, constants::GENERATOR_INDEX__NOTE_NULLIFIER, @@ -50,9 +50,11 @@ impl NullifiableNote for CardNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/src/lib.nr b/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/src/lib.nr index a0c507b24590..d5928253c422 100644 --- a/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/ecdsa_public_key_note/src/lib.nr @@ -1,7 +1,7 @@ use dep::aztec::prelude::{NullifiableNote, PrivateContext, RetrievedNote}; use dep::aztec::{ - note::utils::compute_note_hash_for_nullify, keys::getters::{get_nsk_app, get_public_keys}, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, keys::getters::{get_nsk_app, get_public_keys}, protocol_types::{address::AztecAddress, constants::GENERATOR_INDEX__NOTE_NULLIFIER, hash::poseidon2_hash_with_separator, traits::Packable}, macros::notes::note }; @@ -30,8 +30,7 @@ impl NullifiableNote for EcdsaPublicKeyNote { } unconstrained fn compute_nullifier_without_context(self, storage_slot: Field, contract_address: AztecAddress, note_nonce: Field) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { note: self, contract_address, metadata: SettledNoteMetadata::new(note_nonce).into() }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); @@ -98,4 +97,4 @@ impl Packable for EcdsaPublicKeyNote { EcdsaPublicKeyNote { x, y, owner: AztecAddress::from_field(packed_note[4]) } } -} \ No newline at end of file +} diff --git a/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr b/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr index ec7f01efca1d..93e62ee246b2 100644 --- a/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/inclusion_proofs_contract/src/main.nr @@ -82,8 +82,8 @@ pub contract InclusionProofs { let retrieved_note = RetrievedNote { note: ValueNote::new(1, owner), contract_address: AztecAddress::zero(), - nonce: 0, - note_hash_counter: 0, + // This can never result in a succesful proof because the kernel will receive no note hash counter nor nonce + metadata: std::mem::zeroed(), }; let note_storage_slot = 2; diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr index 90469e127669..ee101fc87954 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr @@ -1,7 +1,7 @@ use dep::aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::partial_note, - note::utils::compute_note_hash_for_nullify, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, oracle::random::random, prelude::{NullifiableNote, PrivateContext, RetrievedNote}, protocol_types::{ @@ -45,9 +45,11 @@ impl NullifiableNote for NFTNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr index 24e0971d1dc8..1e7b41ac7a70 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/public_key_note.nr @@ -1,7 +1,7 @@ use aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::note, - note::utils::compute_note_hash_for_nullify, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, prelude::{NullifiableNote, PrivateContext, RetrievedNote}, protocol_types::{ address::AztecAddress, constants::GENERATOR_INDEX__NOTE_NULLIFIER, @@ -38,9 +38,11 @@ impl NullifiableNote for PublicKeyNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/noir-contracts/contracts/spam_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/spam_contract/src/types/token_note.nr index 78962d7029ed..8f3422f1e456 100644 --- a/noir-projects/noir-contracts/contracts/spam_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/spam_contract/src/types/token_note.nr @@ -1,7 +1,7 @@ use dep::aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::note, - note::utils::compute_note_hash_for_nullify, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, oracle::random::random, prelude::{NullifiableNote, PrivateContext, RetrievedNote}, protocol_types::{ @@ -49,9 +49,11 @@ impl NullifiableNote for TokenNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr index 4efc3de70867..5bd7e611cbc9 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/token_note.nr @@ -1,7 +1,7 @@ use dep::aztec::{ keys::getters::{get_nsk_app, get_public_keys}, macros::notes::note, - note::utils::compute_note_hash_for_nullify, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, oracle::random::random, prelude::{NullifiableNote, PrivateContext, RetrievedNote}, protocol_types::{ @@ -47,9 +47,11 @@ impl NullifiableNote for TokenNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash(); let secret = get_nsk_app(owner_npk_m_hash); diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr index 2497ce428a30..16146ca67b4a 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/transparent_note.nr @@ -1,6 +1,6 @@ use dep::aztec::{ macros::notes::note, - note::utils::compute_note_hash_for_nullify, + note::{note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullify}, prelude::{NullifiableNote, PrivateContext, RetrievedNote}, protocol_types::{ address::AztecAddress, constants::GENERATOR_INDEX__NOTE_NULLIFIER, @@ -47,9 +47,11 @@ impl NullifiableNote for TransparentNote { contract_address: AztecAddress, note_nonce: Field, ) -> Field { - // We set the note_hash_counter to 0 as the note is assumed to be committed (and hence not transient). - let retrieved_note = - RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; + let retrieved_note = RetrievedNote { + note: self, + contract_address, + metadata: SettledNoteMetadata::new(note_nonce).into(), + }; let note_hash_for_nullify = compute_note_hash_for_nullify(retrieved_note, storage_slot); // compute_nullifier ignores context so we can reuse it here self.compute_nullifier(zeroed(), note_hash_for_nullify) diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index 7c95d6c9e2a3..56e1c7a8de24 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -137,10 +137,11 @@ describe('e2e_crowdfunding_and_claim', () => { }, // eslint-disable-next-line camelcase contract_address: uniqueNote.contractAddress, - // eslint-disable-next-line camelcase - nonce: uniqueNote.nonce, - // eslint-disable-next-line camelcase - note_hash_counter: 0, // set as 0 as note is not transient + metadata: { + stage: 3, // aztec::note::note_metadata::NoteStage::SETTLED + // eslint-disable-next-line camelcase + maybe_nonce: uniqueNote.nonce, + }, }; }; diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index f33797411616..f0f4a60ffb57 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -842,7 +842,7 @@ describe('Private Execution test suite', () => { { contractAddress, storageSlot, - nonce: Fr.ZERO, + nonce: Fr.random(), note, noteHash: Fr.ZERO, siloedNullifier: Fr.random(),