Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: process note logs in aztec-nr #10651

Merged
merged 30 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a124c6a
Sketching out initial approach
nventuro Dec 12, 2024
ec92180
Success!
nventuro Dec 13, 2024
2f52d31
IT LIVES
nventuro Dec 13, 2024
a443446
Misc doc improvements
nventuro Dec 13, 2024
dae83d7
Some more minor comments
nventuro Dec 13, 2024
40d2dae
Remove old ts code
nventuro Dec 14, 2024
893ad80
noir formatting
nventuro Dec 14, 2024
4c88865
Merge branch 'master' into nv/process_note_logs
nventuro Jan 8, 2025
206444f
It works!
nventuro Jan 9, 2025
51a7f0b
Add some docs
nventuro Jan 9, 2025
212219a
Merge branch 'master' into nv/process_note_logs
nventuro Jan 9, 2025
646f5ff
Handle no note contracts
nventuro Jan 9, 2025
b771c98
Fix macro
nventuro Jan 9, 2025
ebf7412
Merge branch 'master' into nv/process_note_logs
nventuro Jan 9, 2025
eccd8b6
Fix import
nventuro Jan 9, 2025
da8408f
Remove extra file
nventuro Jan 10, 2025
d5fe202
Apply suggestions from code review
nventuro Jan 10, 2025
96af47d
Rename foreach
nventuro Jan 10, 2025
48fe292
Move files around
nventuro Jan 10, 2025
7f46d5a
Merge branch 'master' into nv/process_note_logs
nventuro Jan 10, 2025
30cbc8a
If I have to nargo fmt one more time
nventuro Jan 10, 2025
5205cc4
Oh god
nventuro Jan 10, 2025
76bbd1b
zzz
nventuro Jan 10, 2025
d44ec2e
kill me now
nventuro Jan 10, 2025
8b7d508
Add node methods to txe node
nventuro Jan 13, 2025
ff0127c
Merge branch 'master' into nv/process_note_logs
nventuro Jan 13, 2025
8f56981
Add sim prov
nventuro Jan 13, 2025
72ea7c4
Fix build error
nventuro Jan 13, 2025
36c29e8
fix: simulator oracle test
benesjan Jan 15, 2025
d8b24ab
Merge branch 'master' into nv/process_note_logs
benesjan Jan 16, 2025
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
120 changes: 120 additions & 0 deletions noir-projects/aztec-nr/aztec/src/macros/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ pub comptime fn aztec(m: Module) -> Quoted {
for f in unconstrained_functions {
transform_unconstrained(f);
}

let compute_note_hash_and_optionally_a_nullifier =
generate_compute_note_hash_and_optionally_a_nullifier();
let process_logs = generate_process_log();
let note_exports = generate_note_exports();
let public_dispatch = generate_public_dispatch(m);
let sync_notes = generate_sync_notes();

quote {
$note_exports
$interface
$compute_note_hash_and_optionally_a_nullifier
$process_logs
$public_dispatch
$sync_notes
}
Expand Down Expand Up @@ -165,6 +169,122 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted {
}
}

comptime fn generate_process_log() -> Quoted {
// This mandatory function processes a log emitted by the contract. This is currently used to recover note contents
// and deliver the note to PXE.
// The bulk of the work of this function is done by aztec::oracle::management::do_process_log, so all we need to do
// is call that function. However, one of its parameters is a lambda function that computes note hash a nullifier
nventuro marked this conversation as resolved.
Show resolved Hide resolved
// given note contents and metadata (e.g. note type id), given that this is behavior is contract-specific (as it
nventuro marked this conversation as resolved.
Show resolved Hide resolved
// depends on the note types implemented by each contract).
// The job of this macro is therefore to implement this lambda function and then call `do_process_log` with it.

// A typical implementation of the lambda looks something like this:
// ```
// |serialized_note_content: BoundedVec<Field, MAX_NOTE_SERIALIZED_LEN>, note_header: NoteHeader, note_type_id: Field| {
// let hashes = if note_type_id == MyNoteType::get_note_type_id() {
// assert(serialized_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH);
// dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier(
// MyNoteType::deserialize_content,
// note_header,
// true,
// serialized_note_content.storage(),
// )
// } else {
// panic(f"Unknown note type id {note_type_id}")
// };
//
// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier {
// note_hash: hashes[0],
// unique_note_hash: hashes[1],
// inner_nullifier: hashes[3],
// })
// }
// ```
//
// We create this implementation by iterating over the different note types, creating an `if` or `else if` clause
// for each of them and calling `compute_note_hash_and_optionally_a_nullifier` with the note's deserialization
// function, and finally produce the required `NoteHashesAndNullifier` object.

let notes = NOTES.entries();

let mut if_note_type_id_match_statements_list = &[];
for i in 0..notes.len() {
let (typ, (_, serialized_note_length, _, _)) = notes[i];

let if_or_else_if = if i == 0 {
quote { if }
} else {
quote { else if }
};

if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back(
quote {
$if_or_else_if note_type_id == $typ::get_note_type_id() {
// As an extra safety check we make sure that the serialized_note_content bounded vec has the
// expected length, to avoid scenarios in which compute_note_hash_and_optionally_a_nullifier
// silently trims the end if the log were to be longer.
let expected_len = $serialized_note_length;
let actual_len = serialized_note_content.len();
assert(
actual_len == expected_len,
f"Expected note content of length {expected_len} but got {actual_len} for note type id {note_type_id}"
);

aztec::note::utils::compute_note_hash_and_optionally_a_nullifier($typ::deserialize_content, note_header, true, serialized_note_content.storage())
}
},
);
}

let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {});

let body = if notes.len() > 0 {
quote {
// Because this unconstrained function is injected after the contract is processed by the macros, it'll not
// be modified by the macros that alter unconstrained functions. As such, we need to manually inject the
// unconstrained execution context since it will not be available otherwise.
let context = dep::aztec::context::unconstrained_context::UnconstrainedContext::new();

dep::aztec::oracle::management::do_process_log(
context,
log_plaintext,
tx_hash,
unique_note_hashes_in_tx,
recipient,
|serialized_note_content: BoundedVec<Field, _>, note_header, note_type_id| {
let hashes = $if_note_type_id_match_statements
else {
panic(f"Unknown note type id {note_type_id}")
};

Option::some(
dep::aztec::oracle::management::NoteHashesAndNullifier {
note_hash: hashes[0],
unique_note_hash: hashes[1],
inner_nullifier: hashes[3],
},
)
}
);
}
} else {
quote {
panic(f"No notes defined")
}
};

quote {
unconstrained fn process_log(
log_plaintext: BoundedVec<Field, dep::aztec::protocol_types::constants::PRIVATE_LOG_SIZE_IN_FIELDS>,
tx_hash: Field,
unique_note_hashes_in_tx: BoundedVec<Field, dep::aztec::protocol_types::constants::MAX_NOTE_HASHES_PER_TX>,
recipient: aztec::protocol_types::address::AztecAddress,
) {
$body
}
}
}

comptime fn generate_note_exports() -> Quoted {
let notes = NOTES.values();
// Second value in each tuple is `note_serialized_len` and that is ignored here because it's only used when
Expand Down
33 changes: 18 additions & 15 deletions noir-projects/aztec-nr/aztec/src/note/note_interface.nr
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,44 @@ where
}

pub trait NullifiableNote {
// This function MUST be called with the correct note hash for consumption! It will otherwise silently fail and
// compute an incorrect value.
// The reason why we receive this as an argument instead of computing it ourselves directly is because the
// caller will typically already have computed this note hash, and we can reuse that value to reduce the total
// gate count of the circuit.
/// Returns the non-siloed nullifier, which will be later siloed by contract address by the kernels before being
/// committed to the state tree.
///
/// This function MUST be called with the correct note hash for consumption! It will otherwise silently fail and
/// compute an incorrect value. The reason why we receive this as an argument instead of computing it ourselves
/// directly is because the caller will typically already have computed this note hash, and we can reuse that value
/// to reduce the total gate count of the circuit.
///
/// This function receives the context since nullifier computation typically involves proving nullifying keys, and
/// we require the kernel's assistance to do this in order to prevent having to reveal private keys to application
/// circuits.
fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field;

// Unlike compute_nullifier, this function does not take a note hash since it'll only be invoked in unconstrained
// contexts, where there is no gate count.
/// 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.
unconstrained fn compute_nullifier_without_context(self) -> Field;
}

// docs:start:note_interface
// Autogenerated by the #[note] macro

pub trait NoteInterface<let N: u32> {
// Autogenerated by the #[note] macro
fn serialize_content(self) -> [Field; N];

// Autogenerated by the #[note] macro
fn deserialize_content(fields: [Field; N]) -> Self;

// Autogenerated by the #[note] macro
fn get_header(self) -> NoteHeader;

// Autogenerated by the #[note] macro
fn set_header(&mut self, header: NoteHeader) -> ();

// Autogenerated by the #[note] macro
fn get_note_type_id() -> Field;

// Autogenerated by the #[note] macro
fn to_be_bytes(self, storage_slot: Field) -> [u8; N * 32 + 64];

// Autogenerated by the #[note] macro
/// Returns the non-siloed note hash, i.e. the inner hash computed by the contract during private execution. Note
/// hashes are later siloed by contract address and nonce by the kernels before being committed to the state tree.
///
/// This should be a commitment to the note contents, including the storage slot (for indexing) and some random
/// value (to prevent brute force trial-hashing attacks).
fn compute_note_hash(self) -> Field;
}
// docs:end:note_interface
164 changes: 164 additions & 0 deletions noir-projects/aztec-nr/aztec/src/oracle/management.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use std::static_assert;

use crate::{
context::unconstrained_context::UnconstrainedContext, note::note_header::NoteHeader,
utils::array,
};
use dep::protocol_types::{
address::AztecAddress,
constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS},
hash::compute_note_hash_nonce,
};

// We reserve two fields in the note log that are not part of the note content: one for the storage slot, and one for
// the note type id.
global NOTE_LOG_RESERVED_FIELDS: u32 = 2;
global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_LOG_RESERVED_FIELDS;

pub struct NoteHashesAndNullifier {
pub note_hash: Field,
pub unique_note_hash: Field,
pub inner_nullifier: Field,
}

fn for_each_bounded_vec<T, let MaxLen: u32, Env>(
vec: BoundedVec<T, MaxLen>,
f: fn[Env](T, u32) -> (),
) {
for i in 0..MaxLen {
if i < vec.len() {
f(vec.get_unchecked(i), i);
}
}
}

/// Processes a log given its plaintext by trying to find notes encoded in it. This process involves the discovery of
/// the nonce of any such notes, which requires knowledge of the transaction hash in which the notes would've been
/// created, along with the list of unique note hashes in said transaction.
///
/// Additionally, this requires a `compute_note_hash_and_nullifier` lambda that is able to compute these values for any
/// note in the contract given their contents. A typical implementation of such a function would look like this:
///
/// ```
/// |serialized_note_content, note_header, note_type_id| {
/// let hashes = if note_type_id == MyNoteType::get_note_type_id() {
/// assert(serialized_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH);
/// dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier(
/// MyNoteType::deserialize_content,
/// note_header,
/// true,
/// serialized_note_content.storage(),
/// )
/// } else {
/// panic(f"Unknown note type id {note_type_id}")
/// };
///
/// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier {
/// note_hash: hashes[0],
/// unique_note_hash: hashes[1],
/// inner_nullifier: hashes[3],
/// })
/// }
/// ```
pub unconstrained fn do_process_log<Env>(
nventuro marked this conversation as resolved.
Show resolved Hide resolved
context: UnconstrainedContext,
log_plaintext: BoundedVec<Field, PRIVATE_LOG_SIZE_IN_FIELDS>,
tx_hash: Field,
unique_note_hashes_in_tx: BoundedVec<Field, MAX_NOTE_HASHES_PER_TX>,
recipient: AztecAddress,
compute_note_hash_and_nullifier: fn[Env](BoundedVec<Field, MAX_NOTE_SERIALIZED_LEN>, NoteHeader, Field) -> Option<NoteHashesAndNullifier>,
) {
let (storage_slot, note_type_id, serialized_note_content) =
destructure_log_plaintext(log_plaintext);

// We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash
for_each_bounded_vec(
nventuro marked this conversation as resolved.
Show resolved Hide resolved
unique_note_hashes_in_tx,
|expected_unique_note_hash, i| {
let candidate_nonce = compute_note_hash_nonce(tx_hash, i);

let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot);

// TODO: handle failed note_hash_and_nullifier computation
nventuro marked this conversation as resolved.
Show resolved Hide resolved
let hashes = compute_note_hash_and_nullifier(
serialized_note_content,
header,
note_type_id,
)
.unwrap();

if hashes.unique_note_hash == expected_unique_note_hash {
// TODO(#10726): push these into a vec to deliver all at once instead of having one oracle call per note
deliver_note(
context.this_address(), // TODO(#10727): allow other contracts to deliver notes
storage_slot,
candidate_nonce,
serialized_note_content,
hashes.note_hash,
hashes.inner_nullifier,
tx_hash,
recipient,
);

// We don't exit the loop - it is possible (though rare) for the exact same note content to be present
// multiple times in the same transaction with different nonces. This typically doesn't happen due to
// notes containing random values in order to hide their contents.
}
},
);
}

unconstrained fn destructure_log_plaintext(
log_plaintext: BoundedVec<Field, PRIVATE_LOG_SIZE_IN_FIELDS>,
) -> (Field, Field, BoundedVec<Field, MAX_NOTE_SERIALIZED_LEN>) {
assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS);

static_assert(
NOTE_LOG_RESERVED_FIELDS == 2,
"unepxected value for NOTE_LOG_RESERVED_FIELDS",
);
let storage_slot = log_plaintext.get(0);
let note_type_id = log_plaintext.get(1);

let serialized_note_content = array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS);

(storage_slot, note_type_id, serialized_note_content)
}

unconstrained fn deliver_note(
contract_address: AztecAddress,
storage_slot: Field,
nonce: Field,
content: BoundedVec<Field, MAX_NOTE_SERIALIZED_LEN>,
note_hash: Field,
nullifier: Field,
tx_hash: Field,
recipient: AztecAddress,
) {
// TODO(#10728): do something instead of failing (e.g. not advance tagging indices)
assert(
deliver_note_oracle(
contract_address,
storage_slot,
nonce,
content,
note_hash,
nullifier,
tx_hash,
recipient,
),
"Failed to deliver note",
);
}

#[oracle(deliverNote)]
unconstrained fn deliver_note_oracle(
contract_address: AztecAddress,
storage_slot: Field,
nonce: Field,
content: BoundedVec<Field, MAX_NOTE_SERIALIZED_LEN>,
note_hash: Field,
nullifier: Field,
tx_hash: Field,
recipient: AztecAddress,
) -> bool {}
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/oracle/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod get_public_data_witness;
pub mod get_membership_witness;
pub mod keys;
pub mod key_validation_request;
pub mod management;
nventuro marked this conversation as resolved.
Show resolved Hide resolved
pub mod random;
pub mod enqueue_public_function_call;
pub mod block_header;
Expand Down
2 changes: 2 additions & 0 deletions noir-projects/aztec-nr/aztec/src/utils/array/mod.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod collapse;
mod subarray;
mod subbvec;

pub use collapse::collapse;
pub use subarray::subarray;
pub use subbvec::subbvec;
Loading
Loading