Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions noir-projects/aztec-nr/address-note/src/address_note.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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);
}
Expand Down
9 changes: 4 additions & 5 deletions noir-projects/aztec-nr/aztec/src/note/lifecycle.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/note/mod.nr
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/note/note_emission.nr
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
pub struct NoteEmission<Note> {
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<Note> NoteEmission<Note> {
Expand Down
18 changes: 10 additions & 8 deletions noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ where
// is check that the metadata is correct, and that the note exists.
let retrieved_note = unsafe { get_note_internal::<Note, N>(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.",
Expand Down Expand Up @@ -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.",
Expand Down
14 changes: 11 additions & 3 deletions noir-projects/aztec-nr/aztec/src/note/note_interface.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// transaction, which therefore has a nonce. This is typically fine, since this function will mostly be used in
/// transaction, which therefore has a nonce (nonces are injected in the kernel tail circuit after pending note squashing). This is typically fine, since this function will mostly be used in

I think readers would generally have no clue why this implies that it has a nonce.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some reflection I think we just simplify this fn so that it also takes a note hash. I'll do that once both our PRs are 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,
Expand Down
182 changes: 182 additions & 0 deletions noir-projects/aztec-nr/aztec/src/note/note_metadata.nr
Original file line number Diff line number Diff line change
@@ -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<PendingSamePhaseNoteMetadata> for NoteMetadata {
fn from(_value: PendingSamePhaseNoteMetadata) -> Self {
NoteMetadata::from_raw_data(true, std::mem::zeroed())
}
}

impl From<PendingPreviousPhaseNoteMetadata> for NoteMetadata {
fn from(value: PendingPreviousPhaseNoteMetadata) -> Self {
NoteMetadata::from_raw_data(true, value.nonce())
}
}

impl From<SettledNoteMetadata> 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
}
}
35 changes: 14 additions & 21 deletions noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr
Original file line number Diff line number Diff line change
@@ -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<NOTE> {
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<NOTE> Eq for RetrievedNote<NOTE>
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<NOTE, let N: u32> Serialize<N + 3> for RetrievedNote<NOTE>
impl<NOTE, let N: u32> Serialize<N + 1 + 2> for RetrievedNote<NOTE>
where
NOTE: Serialize<N>,
{
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(),
)
}
}
Loading
Loading