diff --git a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr new file mode 100644 index 000000000000..0988d95ead0e --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr @@ -0,0 +1,77 @@ +use dep::protocol_types::{ + address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, debug_log::debug_log, +}; + +pub mod private_logs; +pub mod partial_notes; +pub mod nonce_discovery; + +/// We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and +/// one for the combined log and note type ID. +global NOTE_PRIVATE_LOG_RESERVED_FIELDS: u32 = 2; + +/// The maximum length of the packed representation of a note's contents. This is limited by private log size and extra +/// fields in the log (e.g. the combined log and note type ID). +// TODO (#11634): we're assuming here that the entire log is plaintext, which is not true due to headers, encryption +// padding, etc. Notes can't actually be this large. +pub global MAX_NOTE_PACKED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; + +pub struct NoteHashAndNullifier { + /// The result of NoteInterface::compute_note_hash + pub note_hash: Field, + /// The result of NullifiableNote::compute_nullifier_without_context + pub inner_nullifier: Field, +} + +/// A function which takes a note's packed content, address of the emitting contract, nonce, storage slot and note type +/// ID and attempts to compute its note hash (not siloed by nonce nor address) and inner nullifier (not siloed by +/// address). +/// +/// This function must be user-provided as its implementation requires knowledge of how note type IDs are allocated in a +/// contract. A typical implementation would look like this: +/// +/// ``` +/// |packed_note_content, contract_address, nonce, storage_slot, note_type_id| { +/// if note_type_id == MyNoteType::get_note_type_id() { +/// assert(packed_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); +/// let hashes = dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( +/// MyNoteType::unpack_content, +/// note_header, +/// true, +/// packed_note_content.storage(), +/// ) +/// +/// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { +/// note_hash: hashes[0], +/// inner_nullifier: hashes[3], +/// }) +/// } else if note_type_id == MyOtherNoteType::get_note_type_id() { +/// ... // Similar to above but calling MyOtherNoteType::unpack_content +/// } else { +/// Option::none() // Unknown note type ID +/// }; +/// } +/// ``` +type ComputeNoteHashAndNullifier = fn[Env](/* packed_note_content */BoundedVec, /* contract_address */ AztecAddress, /* nonce */ Field, /* storage_slot */ Field, /* note_type_id */ Field) -> Option; + +/// Performs the note discovery process, in which private and public logs are downloaded and inspected to find private +/// notes, partial notes, and their completion. This is the mechanism via which PXE learns of new notes. +/// +/// Receives the address of the contract on which discovery is performed (i.e. the contract that emitted the notes) +/// along with its `compute_note_hash_and_nullifier` function. +pub unconstrained fn discover_new_notes( + contract_address: AztecAddress, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, +) { + debug_log("Performing note discovery"); + + private_logs::fetch_and_process_private_tagged_logs( + contract_address, + compute_note_hash_and_nullifier, + ); + + partial_notes::fetch_and_process_public_partial_note_completion_logs( + contract_address, + compute_note_hash_and_nullifier, + ); +} diff --git a/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr new file mode 100644 index 000000000000..a96985796bc9 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr @@ -0,0 +1,94 @@ +use crate::{discovery::{MAX_NOTE_PACKED_LEN, NoteHashAndNullifier}, utils::array}; + +use dep::protocol_types::{ + address::AztecAddress, + constants::MAX_NOTE_HASHES_PER_TX, + debug_log::debug_log_format, + hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, + traits::ToField, +}; + +/// A struct with the discovered information of a complete note, required for delivery to PXE. Note that this is *not* +/// the complete note information, since it does not include content, storage slot, etc. +pub struct DiscoveredNoteInfo { + pub nonce: Field, + pub note_hash: Field, + pub inner_nullifier: Field, +} + +/// Searches for note nonces that will result in a note that was emitted in a transaction. While rare, it is possible +/// for multiple notes to have the exact same packed content and storage slot but different nonces, resulting in +/// different unique note hashes. Because of this this function returns a *vector* of discovered notes, though in most +/// cases it will contain a single element. +/// +/// Due to how nonces are computed, this function requires knowledge of the transaction in which the note was created, +/// more specifically the list of all unique note hashes in it plus the value of its first nullifier. +pub unconstrained fn attempt_note_nonce_discovery( + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + contract_address: AztecAddress, + storage_slot: Field, + note_type_id: Field, + packed_note_content: BoundedVec, +) -> BoundedVec { + let discovered_notes = &mut BoundedVec::new(); + + debug_log_format( + "Attempting note discovery on {0} potential notes on contract {1} for storage slot {2}", + [unique_note_hashes_in_tx.len() as Field, contract_address.to_field(), storage_slot], + ); + + // We need to find nonces (typically just one) that result in a note hash that, once siloed into a unique note hash, + // is one of the note hashes created by the transaction. + array::for_each_in_bounded_vec( + unique_note_hashes_in_tx, + |expected_unique_note_hash, i| { + // Nonces are computed by hashing the first nullifier in the transaction with the index of the note in the + // new note hashes array. We therefore know for each note in every transaction what its nonce is. + let candidate_nonce = compute_note_hash_nonce(first_nullifier_in_tx, i); + + // Given nonce, note content and metadata, we can compute the note hash and silo it to check if it matches + // the note hash at the array index we're currently processing. + // TODO(#11157): handle failed note_hash_and_nullifier computation + let hashes = compute_note_hash_and_nullifier( + packed_note_content, + contract_address, + candidate_nonce, + storage_slot, + note_type_id, + ) + .expect(f"Failed to compute a note hash for note type {note_type_id}"); + + let siloed_note_hash = compute_siloed_note_hash(contract_address, hashes.note_hash); + let unique_note_hash = compute_unique_note_hash(candidate_nonce, siloed_note_hash); + + if unique_note_hash == expected_unique_note_hash { + // Note that while we did check that the note hash is the preimage of the expected unique note hash, we + // perform no validations on the nullifier - we fundamentally cannot, since only the application knows + // how to compute nullifiers. We simply trust it to have provided the correct one: if it hasn't, then + // PXE may fail to realize that a given note has been nullified already, and calls to the application + // could result in invalid transactions (with duplicate nullifiers). This is not a concern because an + // application already has more direct means of making a call to it fail the transaction. + discovered_notes.push( + DiscoveredNoteInfo { + nonce: candidate_nonce, + note_hash: hashes.note_hash, + inner_nullifier: hashes.inner_nullifier, + }, + ); + + // 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. + } + }, + ); + + debug_log_format( + "Discovered a total of {0} notes", + [discovered_notes.len() as Field], + ); + + *discovered_notes +} diff --git a/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr new file mode 100644 index 000000000000..bddf62ea6687 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr @@ -0,0 +1,153 @@ +use crate::{ + capsules::CapsuleArray, + discovery::{ + ComputeNoteHashAndNullifier, + nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, + private_logs::MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN, + }, + oracle::note_discovery::{deliver_note, get_log_by_tag}, + utils::array, +}; + +use dep::protocol_types::{ + address::AztecAddress, + constants::PUBLIC_LOG_DATA_SIZE_IN_FIELDS, + debug_log::debug_log_format, + traits::{Deserialize, Serialize, ToField}, +}; + +/// The slot in the PXE capsules where we store a `CapsuleArray` of `DeliveredPendingPartialNote`. +// TODO(#11630): come up with some sort of slot allocation scheme. +pub global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT: Field = 77; + +/// Public logs contain an extra field at the beginning with the address of the contract that emitted them, and partial +/// notes emit their completion tag in the log, resulting in the first two fields in the public log not being part of +/// the packed public content. +// TODO(#10273): improve how contract log siloing is handled +pub global NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG: u32 = 2; + +/// The maximum length of the packed representation of public fields in a partial note. This is limited by public log +/// size and extra fields in the log (e.g. the tag). +pub global MAX_PUBLIC_PARTIAL_NOTE_PACKED_CONTENT_LENGTH: u32 = + PUBLIC_LOG_DATA_SIZE_IN_FIELDS - NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG; + +/// A partial note that was delivered but is still pending completion. Contains the information necessary to find the +/// log that will complete it and lead to a note being discovered and delivered. +#[derive(Serialize, Deserialize)] +pub(crate) struct DeliveredPendingPartialNote { + pub(crate) note_completion_log_tag: Field, + pub(crate) storage_slot: Field, + pub(crate) note_type_id: Field, + pub(crate) packed_private_note_content: BoundedVec, + pub(crate) recipient: AztecAddress, +} + +/// Searches for public logs that would result in the completion of pending partial notes, ultimately resulting in the +/// notes being delivered to PXE if completed. +pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( + contract_address: AztecAddress, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, +) { + let pending_partial_notes = CapsuleArray::at( + contract_address, + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, + ); + + debug_log_format( + "{} pending partial notes", + [pending_partial_notes.len() as Field], + ); + + let mut i = &mut 0; + whyle( + || *i < pending_partial_notes.len(), + || { + let pending_partial_note: DeliveredPendingPartialNote = pending_partial_notes.get(*i); + + let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); + if maybe_log.is_none() { + debug_log_format( + "Found no completion logs for partial note #{}", + [(*i) as Field], + ); + *i += 1 as u32; + // Note that we're not removing the pending partial note from the PXE DB, so we will continue searching + // for this tagged log when performing note discovery in the future until we either find it or the entry + // is somehow removed from the PXE DB. + } else { + debug_log_format("Completion log found for partial note #{}", [(*i) as Field]); + let log = maybe_log.unwrap(); + + // Public logs have an extra field at the beginning with the contract address, which we use to verify + // that we're getting the logs from the expected contract. + // TODO(#10273): improve how contract log siloing is handled + assert_eq( + log.log_content.get(0), + contract_address.to_field(), + "Got a public log emitted by a different contract", + ); + + // 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 log sans the extra fields) to get the + // complete packed content. + let packed_public_note_content: BoundedVec<_, MAX_PUBLIC_PARTIAL_NOTE_PACKED_CONTENT_LENGTH> = + array::subbvec(log.log_content, NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG); + let complete_packed_note_content = array::append( + pending_partial_note.packed_private_note_content, + packed_public_note_content, + ); + + let discovered_notes = attempt_note_nonce_discovery( + log.unique_note_hashes_in_tx, + log.first_nullifier_in_tx, + compute_note_hash_and_nullifier, + contract_address, + pending_partial_note.storage_slot, + pending_partial_note.note_type_id, + complete_packed_note_content, + ); + + debug_log_format( + "Discovered {0} notes for partial note {1}", + [discovered_notes.len() as Field, (*i) as Field], + ); + + array::for_each_in_bounded_vec( + discovered_notes, + |discovered_note: DiscoveredNoteInfo, _| { + // TODO:(#10728): decide how to handle notes that fail delivery. This could be due to e.g. a + // temporary node connectivity issue - is simply throwing good enough here? + assert( + deliver_note( + contract_address, + pending_partial_note.storage_slot, + discovered_note.nonce, + complete_packed_note_content, + discovered_note.note_hash, + discovered_note.inner_nullifier, + log.tx_hash, + pending_partial_note.recipient, + ), + "Failed to deliver note", + ); + }, + ); + + // Because there is only a single log for a given tag, once we've processed the tagged log then we + // simply delete the pending work entry, regardless of whether it was actually completed or not. + // TODO(#11627): only remove the pending entry if we actually process a log that results in the note + // being completed. + pending_partial_notes.remove(*i); + } + }, + ); +} + +/// Custom version of a while loop, calls `body` repeatedly until `condition` returns false. To be removed once Noir +/// supports looping in unconstrained code. +fn whyle(condition: fn[Env]() -> bool, body: fn[Env2]() -> ()) { + if condition() { + body(); + whyle(condition, body); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr new file mode 100644 index 000000000000..52a28eb155ba --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -0,0 +1,197 @@ +use std::static_assert; + +use crate::{ + capsules::CapsuleArray, + oracle::note_discovery::{deliver_note, sync_notes}, + utils::array, +}; + +use dep::protocol_types::{ + address::AztecAddress, + constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, + debug_log::{debug_log, debug_log_format}, +}; + +use crate::discovery::{ + ComputeNoteHashAndNullifier, + MAX_NOTE_PACKED_LEN, + nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, + NOTE_PRIVATE_LOG_RESERVED_FIELDS, + partial_notes::{ + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, DeliveredPendingPartialNote, + }, +}; + +pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; +/// Partial notes have a maximum packed length of their private fields bound by extra content in their private log (i.e. +/// the note completion log tag). +pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 = + MAX_NOTE_PACKED_LEN - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN; + +/// Searches for private logs that signal new private notes that are then delivered to PXE, or new partial notes that +/// are stored in the PXE capsules so that `fetch_and_process_public_partial_note_completion_logs` can later search for +/// public logs that will complete them. +pub unconstrained fn fetch_and_process_private_tagged_logs( + _contract_address: AztecAddress, + _compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, +) { + // We will eventually fetch tagged logs, decrypt and process them here, but for now we simply call the `syncNotes` + // oracle. This has PXE perform tag synchronization, log download, decryption, and finally calls to the the + // `process_log` contract function with the decrypted payload, which will in turn call `do_process_log` with a + // decrypted log, letting us continue the work outside of PXE. + sync_notes(); +} + +/// Processes a log's plaintext, searching for private notes or partial notes. Private notes result in nonce discovery +/// being performed prior to delivery, which requires knowledge of the transaction hash in which the notes would've been +/// created (typically the same transaction in which the log was emitted), along with the list of unique note hashes in +/// said transaction and the `compute_note_hash_and_nullifier` function. +pub unconstrained fn do_process_log( + contract_address: AztecAddress, + log_plaintext: BoundedVec, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, +) { + // The first thing to do is to determine what type of private log we're processing. We currently just have two log + // types: 0 for private notes and 1 for partial notes. This will likely be expanded and improved upon in the future + // to also handle events, etc. + + let (storage_slot, note_type_id, log_type_id, log_payload) = + destructure_log_plaintext(log_plaintext); + + if log_type_id == 0 { + debug_log("Processing private note log"); + + process_private_note_log( + contract_address, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + compute_note_hash_and_nullifier, + storage_slot, + note_type_id, + log_payload, + ); + } else if log_type_id == 1 { + debug_log("Processing partial note private log"); + + process_partial_note_private_log( + contract_address, + storage_slot, + note_type_id, + log_payload, + recipient, + ); + } else { + panic(f"Unknown log type id {log_type_id}"); + } +} + +unconstrained fn destructure_log_plaintext( + log_plaintext: BoundedVec, +) -> (Field, Field, Field, BoundedVec) { + assert(log_plaintext.len() >= NOTE_PRIVATE_LOG_RESERVED_FIELDS); + + // If NOTE_PRIVATE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for + // `storage_slot` and `note_type_id` must be updated as well. + static_assert( + NOTE_PRIVATE_LOG_RESERVED_FIELDS == 2, + "unexpected value for NOTE_PRIVATE_LOG_RESERVED_FIELDS", + ); + let storage_slot = log_plaintext.get(0); + + // We currently identify log types by packing the log type ID and note type ID into a single field, called the + // combined type ID. We can do this because the note type ID is only 7 bits long, and so use an 8th bit to + // distinguish private note logs and partial note logs. + // This abuses the fact that the encoding of both of these logs is extremely similar, and will need improving and + // more formalization once we introduce other disimilar log types, such as events. Ideally we'd be able to leverage + // enums and tagged unions to achieve this goal. + let combined_type_id = log_plaintext.get(1); + let note_type_id = ((combined_type_id as u64) % 128) as Field; + let log_type_id = ((combined_type_id as u64) / 128) as Field; + + let log_payload = array::subbvec(log_plaintext, NOTE_PRIVATE_LOG_RESERVED_FIELDS); + + (storage_slot, note_type_id, log_type_id, log_payload) +} + +unconstrained fn process_private_note_log( + contract_address: AztecAddress, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + storage_slot: Field, + note_type_id: Field, + packed_note_content: BoundedVec, +) { + let discovered_notes = attempt_note_nonce_discovery( + unique_note_hashes_in_tx, + first_nullifier_in_tx, + compute_note_hash_and_nullifier, + contract_address, + storage_slot, + note_type_id, + packed_note_content, + ); + + debug_log_format( + "Discovered {0} notes from a private log", + [discovered_notes.len() as Field], + ); + + array::for_each_in_bounded_vec( + discovered_notes, + |discovered_note: DiscoveredNoteInfo, _| { + // TODO:(#10728): handle notes that fail delivery. This could be due to e.g. a temporary node connectivity + // issue, and we should perhaps not have marked the tag index as taken. + assert( + deliver_note( + contract_address, + storage_slot, + discovered_note.nonce, + packed_note_content, + discovered_note.note_hash, + discovered_note.inner_nullifier, + tx_hash, + recipient, + ), + "Failed to deliver note", + ); + }, + ); +} + +unconstrained fn process_partial_note_private_log( + contract_address: AztecAddress, + storage_slot: Field, + note_type_id: Field, + log_payload: BoundedVec, + recipient: AztecAddress, +) { + // We store the information of the partial note we found so that we can later search for the public log that will + // complete it. The tag is the first value in the payload, with the packed note content taking up the rest of it. + static_assert( + PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN == 1, + "unexpected value for PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN", + ); + + let pending = DeliveredPendingPartialNote { + note_completion_log_tag: log_payload.get(0), + storage_slot, + note_type_id, + packed_private_note_content: array::subbvec(log_payload, 1), + recipient, + }; + + CapsuleArray::at( + contract_address, + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, + ) + .push(pending); +} diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 1748a6ee90ab..cd9db81afc4f 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -1,5 +1,6 @@ mod context; mod deploy; +mod discovery; mod hash; mod history; mod keys; diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr index b7ab1a005707..0cf94a054b7e 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/utils.nr @@ -1,5 +1,6 @@ use crate::macros::{ functions::{abi_export::create_fn_abi_export, call_interface_stubs::stub_fn, stub_registry}, + notes::NOTES, utils::{ add_to_hasher, fn_has_noinitcheck, get_fn_visibility, is_fn_initializer, is_fn_internal, is_fn_private, is_fn_view, modify_fn_body, module_has_initializer, module_has_storage, @@ -94,6 +95,14 @@ pub(crate) comptime fn transform_private(f: FunctionDefinition) -> Quoted { quote {} }; + // All private functions perform note discovery, since they may need to access notes. This is slightly inefficient + // and could be improved by only doing it once we actually attempt to read any. + let note_discovery_call = if NOTES.len() > 0 { + create_note_discovery_call() + } else { + quote {} + }; + // Finally, we need to change the return type to be `PrivateCircuitPublicInputs`, which is what the Private Kernel // circuit expects. let return_value_var_name = quote { macro__returned__values }; @@ -145,6 +154,7 @@ pub(crate) comptime fn transform_private(f: FunctionDefinition) -> Quoted { $internal_check $view_check $storage_init + $note_discovery_call }; let to_append = quote { @@ -273,9 +283,19 @@ pub(crate) comptime fn transform_unconstrained(f: FunctionDefinition) { } else { quote {} }; + + // All unconstrained functions perform note discovery, since they may need to access notes. This is slightly + // inefficient and could be improved by only doing it once we actually attempt to read any. + let note_discovery_call = if NOTES.len() > 0 { + create_note_discovery_call() + } else { + quote {} + }; + let to_prepend = quote { $context_creation $storage_init + $note_discovery_call }; let body = f.body().as_block().unwrap(); let modified_body = modify_fn_body(body, to_prepend, quote {}); @@ -318,3 +338,28 @@ comptime fn create_init_check(f: FunctionDefinition) -> Quoted { f"dep::aztec::macros::functions::initialization_utils::assert_is_initialized_{fn_visibility}(&mut context);" .quoted_contents() } + +/// Injects a call to `aztec::discovery::discover_new_notes`, causing for new notes to be added to PXE and made +/// available for the current execution. +comptime fn create_note_discovery_call() -> Quoted { + quote { + /// Safety: note discovery returns nothing and is performed solely for its side-effects. It is therefore always + /// safe to call. + unsafe { + dep::aztec::discovery::discover_new_notes( + context.this_address(), + |packed_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { + // _compute_note_hash_and_optionally_a_nullifier is a contract library method injected by `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` + let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, packed_note_content); + + Option::some( + aztec::discovery::NoteHashAndNullifier { + note_hash: hashes[0], + inner_nullifier: hashes[3], + }, + ) + }, + ) + }; + } +} diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 82df585a9187..0497fbda4c0c 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -12,6 +12,8 @@ use storage::STORAGE_LAYOUT_NAME; use dispatch::generate_public_dispatch; use utils::{get_trait_impl_method, module_has_storage}; +use crate::discovery::MAX_NOTE_PACKED_LEN; + /// Marks a contract as an Aztec contract, generating the interfaces for its functions and notes, as well as injecting /// the `compute_note_hash_and_optionally_a_nullifier` function PXE requires in order to validate notes. /// Note: This is a module annotation, so the returned quote gets injected inside the module (contract) itself. @@ -24,9 +26,11 @@ pub comptime fn aztec(m: Module) -> Quoted { transform_unconstrained(f); } + let contract_library_method_compute_note_hash_and_optionally_a_nullifier = + generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier(); let compute_note_hash_and_optionally_a_nullifier = generate_compute_note_hash_and_optionally_a_nullifier(); - let process_logs = generate_process_log(); + let process_log = generate_process_log(); let note_exports = generate_note_exports(); let public_dispatch = generate_public_dispatch(m); let sync_notes = generate_sync_notes(); @@ -34,8 +38,9 @@ pub comptime fn aztec(m: Module) -> Quoted { quote { $note_exports $interface + $contract_library_method_compute_note_hash_and_optionally_a_nullifier $compute_note_hash_and_optionally_a_nullifier - $process_logs + $process_log $public_dispatch $sync_notes } @@ -109,9 +114,13 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } -comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { - let mut max_note_packed_len: u32 = 0; +/// Generates a contract library method called `_compute_note_hash_and_optionally_a_nullifier` which is used for note +/// discovery (to create the `aztec::discovery::ComputeNoteHashAndNullifier` function) and to implement the +/// `compute_note_hash_and_nullifier` unconstrained contract function. +comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier() -> Quoted { let notes = NOTES.entries(); + + let mut max_note_packed_len: u32 = 0; if notes.len() > 0 { max_note_packed_len = notes.fold( 0, @@ -124,10 +133,20 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { }, ); - let mut if_statements_list = &[]; + if max_note_packed_len > MAX_NOTE_PACKED_LEN { + panic( + f"One of the notes has packed len {max_note_packed_len} but the maximum is {MAX_NOTE_PACKED_LEN}", + ); + } + // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the + // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we + // know it we call `aztec::note::utils::compute_note_hash_and_optionally_a_nullifier` (which is the one that + // actually does the work) with the correct `unpack()` function. + + let mut if_note_type_id_match_statements_list = &[]; for i in 0..notes.len() { - let (typ, (_, _, _, _)) = notes[i]; + let (typ, (_, packed_note_length, _, _)) = notes[i]; let get_note_type_id = get_trait_impl_method( typ, @@ -145,32 +164,85 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { } else { quote { else if } }; - if_statements_list = if_statements_list.push_back( + + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( quote { - $if_or_else_if note_type_id == $get_note_type_id() { - aztec::note::utils::compute_note_hash_and_optionally_a_nullifier($unpack, contract_address, nonce, compute_nullifier, storage_slot, packed_note) - } - }, + $if_or_else_if note_type_id == $get_note_type_id() { + // As an extra safety check we make sure that the packed_note 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 = $packed_note_length; + let actual_len = packed_note.len(); + assert( + actual_len == expected_len, + f"Expected packed note 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($unpack, contract_address, nonce, storage_slot, compute_nullifier, packed_note.storage()) + } + }, ); } - let if_statements = if_statements_list.join(quote {}); + let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); quote { - unconstrained fn compute_note_hash_and_optionally_a_nullifier( + #[contract_library_method] + unconstrained fn _compute_note_hash_and_optionally_a_nullifier( contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field, compute_nullifier: bool, - packed_note: [Field; $max_note_packed_len], + packed_note: BoundedVec, ) -> pub [Field; 4] { - $if_statements + $if_note_type_id_match_statements else { - panic(f"Unknown note type ID: {note_type_id}") + panic(f"Unknown note type ID") } } } + } else { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. + quote { + #[contract_library_method] + unconstrained fn _compute_note_hash_and_optionally_a_nullifier( + _contract_address: aztec::protocol_types::address::AztecAddress, + _nonce: Field, + _storage_slot: Field, + _note_type_id: Field, + _compute_nullifier: bool, + _packed_note: BoundedVec, + ) -> pub [Field; 4] { + panic(f"This contract does not use private notes") + } + } + } +} + +comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { + // For historical reasons we keep this function returning four fields (even though the caller should likely perform + // note hash siloing on their own and not trust this). The contract library method `_compute_note_hash...` is + // affected by this. + // TODO(#11638): In the future we might remove these things as we rely less and less on this function, and then + // change the `_compute_note_hash...` contract library method to be of type + // `aztec::discovery::ComputeNoteHashAndNullifier`, simplifying other macros by removing the need to create + // intermediate lambdas that adapt their interfaces. + + if NOTES.entries().len() > 0 { + quote { + unconstrained fn compute_note_hash_and_optionally_a_nullifier( + contract_address: aztec::protocol_types::address::AztecAddress, + nonce: Field, + storage_slot: Field, + note_type_id: Field, + compute_nullifier: bool, + packed_note: BoundedVec, + ) -> pub [Field; 4] { + _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, compute_nullifier, packed_note) + } + } } else { quote { unconstrained fn compute_note_hash_and_optionally_a_nullifier( @@ -179,7 +251,7 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { _storage_slot: Field, _note_type_id: Field, _compute_nullifier: bool, - _packed_note: [Field; $max_note_packed_len], + _packed_note: BoundedVec, ) -> pub [Field; 4] { panic(f"No notes defined") } @@ -188,87 +260,16 @@ 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 a note and - // deliver it to PXE. - // The bulk of the work of this function is done by aztec::note::discovery::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 and nullifier - // given packed note and metadata (e.g. note type id), since this behavior is contract-specific (as it - // 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: - // ``` - // |packed_note: BoundedVec, contract_address: AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { - // let hashes = if note_type_id == MyNoteType::get_note_type_id() { - // assert(packed_note.len() == MY_NOTE_TYPE_PACKED_LENGTH); - // dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( - // MyNoteType::unpack, - // contract_address, - // nonce, - // true, - // storage_slot, - // packed_note.storage(), - // ) - // } else { - // panic(f"Unknown note type id {note_type_id}") - // }; - // - // Option::some(dep::aztec::note::discovery::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 `unpack` function, - // and finally produce the required `NoteHashesAndNullifier` object. - + // This mandatory function processes a log emitted by the contract. This is currently used to process private logs + // and perform note discovery of either private notes or partial notes. + // The bulk of the work of this function is done by aztec::discovery::do_process_log, so all we need to do is call + // that function. We use the contract library method injected by + // `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` in order to create the required + // `aztec::discovery::ComputeNoteHashAndNullifier` function. + + // We'll produce the entire body of the function in one go and then insert it into the function. let notes = NOTES.entries(); - let mut if_note_type_id_match_statements_list = &[]; - for i in 0..notes.len() { - let (typ, (_, packed_note_length, _, _)) = notes[i]; - - let get_note_type_id = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteInterface }, - quote { get_note_type_id }, - ); - let unpack = get_trait_impl_method( - typ, - quote { crate::protocol_types::traits::Packable<_> }, - quote { unpack }, - ); - - 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 == $get_note_type_id() { - // As an extra safety check we make sure that the packed_note 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 = $packed_note_length; - let actual_len = packed_note.len(); - assert( - actual_len == expected_len, - f"Expected packed note 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($unpack, contract_address, nonce, true, storage_slot, packed_note.storage()) - } - }, - ); - } - - let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); - if notes.len() > 0 { quote { unconstrained fn process_log( @@ -283,23 +284,22 @@ comptime fn generate_process_log() -> Quoted { // unconstrained execution context since it will not be available otherwise. let context = dep::aztec::context::unconstrained_context::UnconstrainedContext::new(); - dep::aztec::note::discovery::do_process_log( - context, + // TODO(#10727): allow other contracts to process logs and deliver notes + let contract_address = context.this_address(); + + aztec::discovery::private_logs::do_process_log( + contract_address, log_plaintext, tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, - |packed_note: BoundedVec, contract_address, nonce, storage_slot, note_type_id| { - let hashes = $if_note_type_id_match_statements - else { - panic(f"Unknown note type id {note_type_id}") - }; + |packed_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { + let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, packed_note_content); Option::some( - dep::aztec::note::discovery::NoteHashesAndNullifier { + aztec::discovery::NoteHashAndNullifier { note_hash: hashes[0], - unique_note_hash: hashes[1], inner_nullifier: hashes[3], }, ) @@ -308,6 +308,8 @@ comptime fn generate_process_log() -> Quoted { } } } else { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. quote { unconstrained fn process_log( _log_plaintext: BoundedVec, @@ -316,7 +318,7 @@ comptime fn generate_process_log() -> Quoted { _first_nullifier_in_tx: Field, _recipient: aztec::protocol_types::address::AztecAddress, ) { - panic(f"No notes defined") + panic(f"This contract does not use private notes") } } } @@ -336,7 +338,7 @@ comptime fn generate_note_exports() -> Quoted { comptime fn generate_sync_notes() -> Quoted { quote { unconstrained fn sync_notes() { - aztec::oracle::notes::sync_notes(); + aztec::oracle::note_discovery::sync_notes(); } } } diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr deleted file mode 100644 index 1416fc82324f..000000000000 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ /dev/null @@ -1,134 +0,0 @@ -use std::static_assert; - -use crate::{ - context::unconstrained_context::UnconstrainedContext, oracle::note_discovery::deliver_note, - 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 packed note: one for the storage slot, and one for -// the note type id. -global NOTE_LOG_RESERVED_FIELDS: u32 = 2; -pub global MAX_NOTE_PACKED_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, -} - -/// 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 type in the contract given the packed note. A typical implementation of such a function would look like this: -/// -/// ``` -/// |packed_note, note_header, note_type_id| { -/// let hashes = if note_type_id == MyNoteType::get_note_type_id() { -/// assert(packed_note.len() == MY_NOTE_TYPE_PACKED_LENGTH); -/// dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( -/// MyNoteType::unpack, -/// contract_address, -/// nonce, -/// storage_slot, -/// packed_note.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( - context: UnconstrainedContext, - log_plaintext: BoundedVec, - tx_hash: Field, - unique_note_hashes_in_tx: BoundedVec, - first_nullifier_in_tx: Field, - recipient: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, -) { - let (storage_slot, note_type_id, packed_note) = 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_in_bounded_vec( - unique_note_hashes_in_tx, - |expected_unique_note_hash, i| { - let candidate_nonce = compute_note_hash_nonce(first_nullifier_in_tx, i); - - // TODO(#11157): handle failed note_hash_and_nullifier computation - let hashes = compute_note_hash_and_nullifier( - packed_note, - context.this_address(), - candidate_nonce, - storage_slot, - 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 - - assert( - deliver_note( - context.this_address(), // TODO(#10727): allow other contracts to deliver notes - storage_slot, - candidate_nonce, - packed_note, - hashes.note_hash, - hashes.inner_nullifier, - tx_hash, - recipient, - ), - "Failed to deliver note", - ); - - // We don't exit the loop - it is possible (though rare) for the exact same packed note 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 protect against note hash preimage attacks. - } - }, - ); -} - -unconstrained fn destructure_log_plaintext( - log_plaintext: BoundedVec, -) -> (Field, Field, BoundedVec) { - assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS); - - // If NOTE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for - // `storage_slot` and `note_type_id` must be updated as well. - static_assert( - NOTE_LOG_RESERVED_FIELDS == 2, - "unexpected value for NOTE_LOG_RESERVED_FIELDS", - ); - let storage_slot = log_plaintext.get(0); - let note_type_id = log_plaintext.get(1); - - let packed_note = array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS); - - (storage_slot, note_type_id, packed_note) -} - -fn for_each_in_bounded_vec( - vec: BoundedVec, - f: fn[Env](T, u32) -> (), -) { - for i in 0..MaxLen { - if i < vec.len() { - f(vec.get_unchecked(i), i); - } - } -} diff --git a/noir-projects/aztec-nr/aztec/src/note/mod.nr b/noir-projects/aztec-nr/aztec/src/note/mod.nr index f8cc5173cc96..1dd9cc818167 100644 --- a/noir-projects/aztec-nr/aztec/src/note/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/mod.nr @@ -1,5 +1,4 @@ pub mod constants; -pub mod discovery; pub mod lifecycle; pub mod note_getter; pub mod note_getter_options; 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 57bc5e5d51e9..87eaea1d58af 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -16,8 +16,8 @@ where } pub trait NullifiableNote { - /// Returns the non-siloed nullifier, which will be later siloed by contract address by the kernels before being - /// committed to the state tree. + /// Returns the non-siloed nullifier (also called inner-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 diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 000b187df1e9..2f3e6143abdd 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -125,21 +125,23 @@ where /// Note: `packed_note_with_padding` is typically constructed by calling the `storage()` method on a `BoundedVec`. This /// function will then extract the relevant fields from the array using the `subarray` method and the actual packed /// note length `N`. +// TODO (#11638): simplify or remove this function by inlining it in the `_compute_note_hash_and_nullifier` contract +// library method that is autogenerated by macros. pub unconstrained fn compute_note_hash_and_optionally_a_nullifier( unpack_note: fn([Field; N]) -> Note, contract_address: AztecAddress, nonce: Field, - compute_nullifier: bool, storage_slot: Field, + compute_nullifier: bool, packed_note_with_padding: [Field; S], ) -> [Field; 4] where Note: NoteInterface + NullifiableNote, { - let packed_note = array::subarray(packed_note_with_padding, 0); - let note = unpack_note(packed_note); + let note = unpack_note(array::subarray(packed_note_with_padding, 0)); let note_hash = note.compute_note_hash(storage_slot); + let siloed_note_hash = compute_siloed_note_hash(contract_address, note_hash); let unique_note_hash = compute_unique_note_hash(nonce, siloed_note_hash); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr index 5b1cb8bd248a..3a7d2579301c 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr @@ -1,5 +1,17 @@ -use crate::note::discovery::MAX_NOTE_PACKED_LEN; -use dep::protocol_types::address::AztecAddress; +use crate::discovery::MAX_NOTE_PACKED_LEN; +use dep::protocol_types::{ + address::AztecAddress, + constants::{MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS}, +}; + +/// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them +/// available for later querying via the `get_notes` oracle. +pub unconstrained fn sync_notes() { + sync_notes_oracle(); +} + +#[oracle(syncNotes)] +unconstrained fn sync_notes_oracle() {} /// Informs PXE of a note's existence so that it can later be retrieved by the `getNotes` oracle. The note will be /// scoped to `contract_address`, meaning other contracts will not be able to access it unless authorized. @@ -36,6 +48,29 @@ pub unconstrained fn deliver_note( ) } +/// The contents of a public log, plus contextual information about the transaction in which the log was emitted. This +/// is the data required in order to discover notes that are being delivered in a log. +// TODO(#11639): this could also be used to fetch private logs, but the `BoundedVec` maximum length is that of a public +// log. +pub struct LogWithTxData { + pub log_content: BoundedVec, + pub tx_hash: Field, + /// The array of new note hashes created by `tx_hash` + pub unique_note_hashes_in_tx: BoundedVec, + /// The first nullifier created by `tx_hash` + pub first_nullifier_in_tx: Field, +} + +/// Fetches a log from the node that has the corresponding `tag`. The log can be either a public or a private log, and +/// the tag is the first field in the log's content. Returns `Option::none` if no such log exists. Throws if more than +/// one log with that tag exists. +/// Public logs have an extra field included at the beginning with the address of the contract that emtitted them. +// TODO(#11627): handle multiple logs with the same tag. +// TODO(#10273): improve contract siloing of logs, don't introduce an extra field. +pub unconstrained fn get_log_by_tag(tag: Field) -> Option { + get_log_by_tag_oracle(tag) +} + #[oracle(deliverNote)] unconstrained fn deliver_note_oracle( contract_address: AztecAddress, @@ -47,3 +82,6 @@ unconstrained fn deliver_note_oracle( tx_hash: Field, recipient: AztecAddress, ) -> bool {} + +#[oracle(getLogByTag)] +unconstrained fn get_log_by_tag_oracle(tag: Field) -> Option {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index c0fc57318a85..141e66231fba 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -3,7 +3,7 @@ use crate::{note::{note_interface::NoteInterface, retrieved_note::RetrievedNote} use dep::protocol_types::{ address::AztecAddress, indexed_tagging_secret::{INDEXED_TAGGING_SECRET_LENGTH, IndexedTaggingSecret}, - traits::Packable, + traits::{Deserialize, FromField, Packable}, }; /// Notifies the simulator that a note has been created, so that it can be returned in future read requests in the same @@ -32,14 +32,15 @@ pub fn notify_created_note( /// the same transaction. This note should only be removed to the non-volatile database if its nullifier is found in an /// actual block. pub fn notify_nullified_note(nullifier: Field, note_hash: Field, counter: u32) { - // Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe - // to call. + // Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to + // call. unsafe { notify_nullified_note_oracle_wrapper(nullifier, note_hash, counter) }; } /// Notifies the simulator that a non-note nullifier has been created, so that it can be used for note nonces. pub fn notify_created_nullifier(nullifier: Field) { - // This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to call. + // Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to + // call. unsafe { notify_created_nullifier_oracle_wrapper(nullifier) }; } @@ -164,7 +165,6 @@ pub unconstrained fn get_notes, { - sync_notes_oracle_wrapper(); let fields = get_notes_oracle_wrapper( storage_slot, num_selects, @@ -263,20 +263,3 @@ unconstrained fn increment_app_tagging_secret_index_as_sender_oracle( _sender: AztecAddress, _recipient: AztecAddress, ) {} - -/// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them available -/// for later querying via the `get_notes` oracle. -pub fn sync_notes() { - // Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe - // to call. - unsafe { - sync_notes_oracle_wrapper(); - } -} - -unconstrained fn sync_notes_oracle_wrapper() { - sync_notes_oracle(); -} - -#[oracle(syncNotes)] -unconstrained fn sync_notes_oracle() {} diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/append.nr b/noir-projects/aztec-nr/aztec/src/utils/array/append.nr new file mode 100644 index 000000000000..dbc370346c5d --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/array/append.nr @@ -0,0 +1,48 @@ +/// Appends two `BoundedVec`s together, returning one that contains all of the elements of the first one followed by all +/// of the elements of the second one. The resulting `BoundedVec` can have any arbitrary maximum length, but it must be +/// large enough to fit all of the elements of both the first and second vectors. +pub fn append( + a: BoundedVec, + b: BoundedVec, +) -> BoundedVec { + let mut dst = BoundedVec::new(); + + dst.extend_from_bounded_vec(a); + dst.extend_from_bounded_vec(b); + + dst +} + +mod test { + use super::append; + + #[test] + unconstrained fn append_empty_vecs() { + let a: BoundedVec<_, 3> = BoundedVec::new(); + let b: BoundedVec<_, 14> = BoundedVec::new(); + + let result: BoundedVec = append(a, b); + + assert_eq(result.len(), 0); + assert_eq(result.storage(), std::mem::zeroed()); + } + + #[test] + unconstrained fn append_non_empty_vecs() { + let a: BoundedVec<_, 3> = BoundedVec::from_array([1, 2, 3]); + let b: BoundedVec<_, 14> = BoundedVec::from_array([4, 5, 6]); + + let result: BoundedVec = append(a, b); + + assert_eq(result.len(), 6); + assert_eq(result.storage(), [1, 2, 3, 4, 5, 6, std::mem::zeroed(), std::mem::zeroed()]); + } + + #[test(should_fail_with = "out of bounds")] + unconstrained fn append_non_empty_vecs_insufficient_max_len() { + let a: BoundedVec<_, 3> = BoundedVec::from_array([1, 2, 3]); + let b: BoundedVec<_, 14> = BoundedVec::from_array([4, 5, 6]); + + let _: BoundedVec = append(a, b); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr index ef46a00a5a24..291dc9241663 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr @@ -1,7 +1,19 @@ +mod append; mod collapse; mod subarray; mod subbvec; +pub use append::append; pub use collapse::collapse; pub use subarray::subarray; pub use subbvec::subbvec; + +// This will eventually be replaced by `BoundedVec::for_each`, once that's implemented. +pub unconstrained fn for_each_in_bounded_vec( + vec: BoundedVec, + f: fn[Env](T, u32) -> (), +) { + for i in 0..vec.len() { + f(vec.get_unchecked(i), i); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr index f08bed659423..c457b7695d19 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr @@ -16,14 +16,14 @@ use crate::utils::array; /// let baz: BoundedVec<_, 10> = subbvec(foo, 3); // fails - we can't return 10 elements since only 7 remain /// ``` pub fn subbvec( - vec: BoundedVec, + bvec: BoundedVec, offset: u32, ) -> BoundedVec { // from_parts_unchecked does not verify that the elements past len are zeroed, but that is not an issue in our case // because we're constructing the new storage array as a subarray of the original one (which should have zeroed // storage past len), guaranteeing correctness. This is because `subarray` does not allow extending arrays past // their original length. - BoundedVec::from_parts_unchecked(array::subarray(vec.storage(), offset), vec.len() - offset) + BoundedVec::from_parts_unchecked(array::subarray(bvec.storage(), offset), bvec.len() - offset) } mod test { diff --git a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr index 41a6249db735..e0aaa40c630c 100644 --- a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr @@ -143,9 +143,11 @@ pub contract Counter { unconstrained fn inclusion_proofs() { let initial_value = 5; let (env, contract_address, owner) = test::setup(initial_value); + env.impersonate(contract_address); + env.advance_block_by(1); + sync_notes(); - env.impersonate(contract_address); let counter_slot = Counter::storage_layout().counters.slot; let owner_storage_slot = derive_storage_slot_in_map(counter_slot, owner); @@ -173,6 +175,7 @@ pub contract Counter { destroy_note(&mut env.private(), old_retrieved_note, owner_storage_slot); env.advance_block_by(1); + sync_notes(); env.private().get_block_header().prove_note_is_nullified( old_retrieved_note, @@ -207,6 +210,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); env.impersonate(contract_address); let counter_slot = Counter::storage_layout().counters.slot; let owner_storage_slot = derive_storage_slot_in_map(counter_slot, owner); @@ -229,6 +233,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); let notes: BoundedVec = view_notes(owner_storage_slot, options); assert(get_counter(owner) == 7); @@ -244,6 +249,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); let notes: BoundedVec = view_notes(owner_storage_slot, options); assert(notes.len() == 4); @@ -258,6 +264,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); let notes: BoundedVec = view_notes(owner_storage_slot, options); assert(get_counter(owner) == 6); diff --git a/yarn-project/circuits.js/package.json b/yarn-project/circuits.js/package.json index 407615a90fbb..60bc4a58d714 100644 --- a/yarn-project/circuits.js/package.json +++ b/yarn-project/circuits.js/package.json @@ -22,6 +22,7 @@ "./rollup": "./dest/structs/rollup/index.js", "./kernel": "./dest/structs/kernel/index.js", "./trees": "./dest/structs/trees/index.js", + "./logs": "./dest/structs/logs/index.js", "./avm": "./dest/structs/avm/index.js", "./schemas": "./dest/schemas/index.js", "./noir": "./dest/noir/index.js", diff --git a/yarn-project/circuits.js/src/structs/logs/index.ts b/yarn-project/circuits.js/src/structs/logs/index.ts new file mode 100644 index 000000000000..1c0333af575c --- /dev/null +++ b/yarn-project/circuits.js/src/structs/logs/index.ts @@ -0,0 +1 @@ +export * from './log_with_tx_data.js'; diff --git a/yarn-project/circuits.js/src/structs/logs/log_with_tx_data.ts b/yarn-project/circuits.js/src/structs/logs/log_with_tx_data.ts new file mode 100644 index 000000000000..fd1c1f67d6f2 --- /dev/null +++ b/yarn-project/circuits.js/src/structs/logs/log_with_tx_data.ts @@ -0,0 +1,36 @@ +import { MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS } from '@aztec/constants'; +import { Fr } from '@aztec/foundation/fields'; + +// TypeScript representation of the Noir aztec::oracle::note_discovery::LogWithTxData struct. This is used as a response +// for PXE's custom getLogByTag oracle. +export class LogWithTxData { + constructor( + public logContent: Fr[], + public txHash: Fr, + public uniqueNoteHashesInTx: Fr[], + public firstNullifierInTx: Fr, + ) {} + + toNoirSerialization(): (Fr | Fr[])[] { + return [ + ...toBoundedVecSerialization(this.logContent, PUBLIC_LOG_DATA_SIZE_IN_FIELDS), + this.txHash, + ...toBoundedVecSerialization(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), + this.firstNullifierInTx, + ]; + } + + static noirSerializationOfEmpty(): (Fr | Fr[])[] { + return new LogWithTxData([], new Fr(0), [], new Fr(0)).toNoirSerialization(); + } +} + +function toBoundedVecSerialization(array: Fr[], maxLength: number) { + if (array.length > maxLength) { + throw new Error( + `An array of length ${array.length} cannot be converted to a BoundedVec of max length ${maxLength}`, + ); + } + + return [array.concat(Array(maxLength - array.length).fill(new Fr(0))), new Fr(array.length)]; +} diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 35e4c8dc65b3..4ead5ddba67b 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -38,10 +38,17 @@ import { getFunctionArtifact, } from '@aztec/circuits.js/abi'; import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/circuits.js/hash'; -import { type L1_TO_L2_MSG_TREE_HEIGHT, MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; +import { LogWithTxData } from '@aztec/circuits.js/logs'; +import { + type L1_TO_L2_MSG_TREE_HEIGHT, + MAX_NOTE_HASHES_PER_TX, + PRIVATE_LOG_SIZE_IN_FIELDS, + PUBLIC_LOG_DATA_SIZE_IN_FIELDS, +} from '@aztec/constants'; import { timesParallel } from '@aztec/foundation/collection'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { createLogger } from '@aztec/foundation/log'; +import { BufferReader } from '@aztec/foundation/serialize'; import { type KeyStore } from '@aztec/key-store'; import { type AcirSimulator, @@ -699,8 +706,45 @@ export class SimulatorOracle implements DBOracle { }); } + public async getLogByTag(tag: Fr): Promise { + const logs = await this.aztecNode.getLogsByTags([tag]); + const logsForTag = logs[0]; + + this.log.debug(`Got ${logsForTag.length} logs for tag ${tag}`); + + if (logsForTag.length == 0) { + return null; + } else if (logsForTag.length > 1) { + // TODO(#11627): handle this case + throw new Error( + `Got ${logsForTag.length} logs for tag ${tag}. getLogByTag currently only supports a single log per tag`, + ); + } + + const log = logsForTag[0]; + + // getLogsByTag doesn't have all of the information that we need (notably note hashes and the first nullifier), so + // we need to make a second call to the node for `getTxEffect`. + // TODO(#9789): bundle this information in the `getLogsByTag` call. + const txEffect = await this.aztecNode.getTxEffect(log.txHash); + if (txEffect == undefined) { + throw new Error(`Unexpected: failed to retrieve tx effects for tx ${log.txHash} which is known to exist`); + } + + const reader = BufferReader.asReader(log.logData); + const logArray = reader.readArray(PUBLIC_LOG_DATA_SIZE_IN_FIELDS, Fr); + + // Public logs always take up all available fields by padding with zeroes, and the length of the originally emitted + // log is lost. Until this is improved, we simply remove all of the zero elements (which are expected to be at the + // end). + // TODO(#11636): use the actual log length. + const trimmedLog = logArray.filter(x => !x.isZero()); + + return new LogWithTxData(trimmedLog, log.txHash.hash, txEffect.data.noteHashes, txEffect.data.nullifiers[0]); + } + public async removeNullifiedNotes(contractAddress: AztecAddress) { - this.log.verbose('Removing nullified notes', { contract: contractAddress }); + this.log.verbose('Searching for nullifiers of known notes', { contract: contractAddress }); for (const recipient of await this.keyStore.getAccounts()) { const currentNotesForRecipient = await this.db.getNotes({ contractAddress, owner: recipient }); diff --git a/yarn-project/simulator/src/acvm/deserialize.ts b/yarn-project/simulator/src/acvm/deserialize.ts index 57ad9fe8d0df..af6794196a8a 100644 --- a/yarn-project/simulator/src/acvm/deserialize.ts +++ b/yarn-project/simulator/src/acvm/deserialize.ts @@ -31,7 +31,7 @@ export function frToBoolean(fr: Fr): boolean { /** * Converts a Noir BoundedVec of Fields into an Fr array. Note that BoundedVecs are structs, and therefore translated as - * two separate ACVMField arrays. + * two separate ACVMField values (an array and a single field). * * @param storage The array with the BoundedVec's storage (i.e. BoundedVec::storage()) * @param length The length of the BoundedVec (i.e. BoundedVec::len()) diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 3f1f3eaf9e79..23987761feef 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -1,11 +1,12 @@ import { MerkleTreeId, UnencryptedL2Log } from '@aztec/circuit-types'; import { FunctionSelector, NoteSelector } from '@aztec/circuits.js/abi'; +import { LogWithTxData } from '@aztec/circuits.js/logs'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; import { type ACVMField } from '../acvm_types.js'; import { frToBoolean, frToNumber, fromACVMField, fromBoundedVec } from '../deserialize.js'; -import { toACVMField } from '../serialize.js'; +import { toACVMField, toACVMFieldSingleOrArray } from '../serialize.js'; import { type TypedOracle } from './typed_oracle.js'; /** @@ -396,6 +397,16 @@ export class Oracle { return toACVMField(true); } + async getLogByTag([tag]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { + const log = await this.typedOracle.getLogByTag(fromACVMField(tag)); + + if (log == null) { + return [toACVMField(0), ...LogWithTxData.noirSerializationOfEmpty().map(toACVMFieldSingleOrArray)]; + } else { + return [toACVMField(1), ...log.toNoirSerialization().map(toACVMFieldSingleOrArray)]; + } + } + async storeCapsule([contractAddress]: ACVMField[], [slot]: ACVMField[], capsule: ACVMField[]) { await this.typedOracle.storeCapsule( AztecAddress.fromField(fromACVMField(contractAddress)), diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 5fda45816c98..580e1466a821 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -14,6 +14,7 @@ import { type KeyValidationRequest, } from '@aztec/circuits.js'; import { type FunctionSelector, type NoteSelector } from '@aztec/circuits.js/abi'; +import { type LogWithTxData } from '@aztec/circuits.js/logs'; import { type L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/constants'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; @@ -242,6 +243,10 @@ export abstract class TypedOracle { return Promise.reject(new OracleMethodNotAvailableError('deliverNote')); } + getLogByTag(_tag: Fr): Promise { + throw new OracleMethodNotAvailableError('getLogByTag'); + } + storeCapsule(_contractAddress: AztecAddress, _key: Fr, _capsule: Fr[]): Promise { return Promise.reject(new OracleMethodNotAvailableError('storeCapsule')); } diff --git a/yarn-project/simulator/src/acvm/serialize.ts b/yarn-project/simulator/src/acvm/serialize.ts index 0ae28ed6ce5c..d76f0d6438c8 100644 --- a/yarn-project/simulator/src/acvm/serialize.ts +++ b/yarn-project/simulator/src/acvm/serialize.ts @@ -39,6 +39,13 @@ export function toACVMField( return `0x${adaptBufferSize(buffer).toString('hex')}`; } +/** + * Converts a single value or an array of single values into the equivalent ACVM field representation. + */ +export function toACVMFieldSingleOrArray(value: Fr | Fr[]) { + return Array.isArray(value) ? value.map(toACVMField) : toACVMField(value); +} + /** * Inserts a list of ACVM fields to a witness. * @param witnessStartIndex - The index where to start inserting the fields. diff --git a/yarn-project/simulator/src/client/client_execution_context.ts b/yarn-project/simulator/src/client/client_execution_context.ts index c205527c0042..ad1db2d81f5b 100644 --- a/yarn-project/simulator/src/client/client_execution_context.ts +++ b/yarn-project/simulator/src/client/client_execution_context.ts @@ -281,6 +281,13 @@ export class ClientExecutionContext extends ViewDataOracle { noteHash: Fr, counter: number, ) { + this.log.debug(`Notified of new note with inner hash ${noteHash}`, { + contractAddress: this.callContext.contractAddress, + storageSlot, + noteTypeId, + counter, + }); + const note = new Note(noteItems); this.noteCache.addNewNote( { diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 811f38d08a11..1a3b30a4d10d 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -14,6 +14,7 @@ import { type KeyValidationRequest, } from '@aztec/circuits.js'; import { type FunctionArtifact, type FunctionSelector } from '@aztec/circuits.js/abi'; +import { LogWithTxData } from '@aztec/circuits.js/logs'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; import { type Fr } from '@aztec/foundation/fields'; @@ -250,6 +251,14 @@ export interface DBOracle extends CommitmentsDB { recipient: AztecAddress, ): Promise; + /** + * Searches for a log with the corresponding `tag` and returns it along with contextual transaction information. + * Returns null if no such log exists, and throws if more than one exists. + * + * @param tag - The log tag to search for. + */ + getLogByTag(tag: Fr): Promise; + /** * Removes all of a contract's notes that have been nullified from the note database. */ diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index 84ef3be96dba..ff246732ae6b 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -4,6 +4,7 @@ import { Note, PublicExecutionRequest, TxExecutionRequest, + type TxScopedL2Log, } from '@aztec/circuit-types'; import { type AztecNode, @@ -303,6 +304,9 @@ describe('Private Execution test suite', () => { return Promise.resolve(artifact); }); + oracle.syncTaggedLogs.mockImplementation((_, __, ___) => Promise.resolve(new Map())); + oracle.loadCapsule.mockImplementation((_, __) => Promise.resolve(null)); + node = mock(); node.getPublicStorageAt.mockImplementation( (_address: AztecAddress, _storageSlot: Fr, _blockNumber: L2BlockNumber) => { diff --git a/yarn-project/simulator/src/client/simulator.test.ts b/yarn-project/simulator/src/client/simulator.test.ts index 086e622b5a52..c6f4bcafb596 100644 --- a/yarn-project/simulator/src/client/simulator.test.ts +++ b/yarn-project/simulator/src/client/simulator.test.ts @@ -86,37 +86,5 @@ describe('Simulator', () => { ), ); }); - - it('throw if a note has more fields than "compute_note_hash_and_optionally_a_nullifier" can process', async () => { - const note = await createNote(); - const wrongPreimageLength = note.length - 1; - - const modifiedArtifact: FunctionArtifact = { - ...artifact, - parameters: [ - ...artifact.parameters.slice(0, -1), - { - name: 'note', - type: { - kind: 'array', - length: wrongPreimageLength, - type: { - kind: 'field', - }, - }, - visibility: 'private', - }, - ], - }; - oracle.getFunctionArtifactByName.mockResolvedValue(modifiedArtifact); - - await expect( - simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, noteTypeId, note), - ).rejects.toThrow( - new RegExp( - `"compute_note_hash_and_optionally_a_nullifier" can only handle a maximum of ${wrongPreimageLength} fields`, - ), - ); - }); }); }); diff --git a/yarn-project/simulator/src/client/simulator.ts b/yarn-project/simulator/src/client/simulator.ts index 615992c064b5..c3c64c86e7d7 100644 --- a/yarn-project/simulator/src/client/simulator.ts +++ b/yarn-project/simulator/src/client/simulator.ts @@ -2,7 +2,6 @@ import { type FunctionCall, type Note, type TxExecutionRequest } from '@aztec/ci import { type AztecNode, PrivateExecutionResult } from '@aztec/circuit-types/interfaces/client'; import { CallContext } from '@aztec/circuits.js'; import { - type ArrayType, type FunctionArtifact, FunctionSelector, FunctionType, @@ -190,14 +189,21 @@ export class AcirSimulator { ); } - const maxNoteFields = (artifact.parameters[artifact.parameters.length - 1].type as ArrayType).length; + // This constant is not exposed anywhere (because it doesn't have to - it's internal to aztec-nr). It's only here as + // a temporary stopgap until we delete this function fully. + const MAX_NOTE_PACKED_LEN = 16; + const maxNoteFields = MAX_NOTE_PACKED_LEN; + if (maxNoteFields < note.items.length) { throw new Error( `The note being processed has ${note.items.length} fields, while "compute_note_hash_and_optionally_a_nullifier" can only handle a maximum of ${maxNoteFields} fields. Please reduce the number of fields in your note.`, ); } - const extendedNoteItems = note.items.concat(Array(maxNoteFields - note.items.length).fill(Fr.ZERO)); + const noteItemsBoundedVec = { + len: note.items.length, + storage: note.items.concat(Array(maxNoteFields - note.items.length).fill(Fr.ZERO)), + }; const selector = await FunctionSelector.fromNameAndParameters(artifact); const execRequest: FunctionCall = { name: artifact.name, @@ -205,7 +211,7 @@ export class AcirSimulator { selector, type: FunctionType.UNCONSTRAINED, isStatic: artifact.isStatic, - args: encodeArguments(artifact, [contractAddress, nonce, storageSlot, noteTypeId, true, extendedNoteItems]), + args: encodeArguments(artifact, [contractAddress, nonce, storageSlot, noteTypeId, true, noteItemsBoundedVec]), returnTypes: artifact.returnTypes, }; diff --git a/yarn-project/simulator/src/client/unconstrained_execution.test.ts b/yarn-project/simulator/src/client/unconstrained_execution.test.ts index 3c1c09bc02e4..c14c53ce291c 100644 --- a/yarn-project/simulator/src/client/unconstrained_execution.test.ts +++ b/yarn-project/simulator/src/client/unconstrained_execution.test.ts @@ -1,4 +1,4 @@ -import { type FunctionCall, Note } from '@aztec/circuit-types'; +import { type FunctionCall, Note, type TxScopedL2Log } from '@aztec/circuit-types'; import { type AztecNode } from '@aztec/circuit-types/interfaces/client'; import { BlockHeader, CompleteAddress, type ContractInstance } from '@aztec/circuits.js'; import { FunctionSelector, FunctionType, encodeArguments } from '@aztec/circuits.js/abi'; @@ -80,6 +80,9 @@ describe('Unconstrained Execution test suite', () => { })), ); + oracle.syncTaggedLogs.mockImplementation((_, __, ___) => Promise.resolve(new Map())); + oracle.loadCapsule.mockImplementation((_, __) => Promise.resolve(null)); + const execRequest: FunctionCall = { name: artifact.name, to: contractAddress, diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index c7b36b3ca861..71f9530c22b1 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -14,6 +14,7 @@ import { type KeyValidationRequest, } from '@aztec/circuits.js'; import { siloNullifier } from '@aztec/circuits.js/hash'; +import { LogWithTxData } from '@aztec/circuits.js/logs'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Aes128 } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; @@ -321,6 +322,10 @@ export class ViewDataOracle extends TypedOracle { await this.db.deliverNote(contractAddress, storageSlot, nonce, content, noteHash, nullifier, txHash, recipient); } + public override getLogByTag(tag: Fr): Promise { + return this.db.getLogByTag(tag); + } + public override storeCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[]): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 65a350d3f8e3..c49c2a5a4ba9 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -49,6 +49,7 @@ import { siloNoteHash, siloNullifier, } from '@aztec/circuits.js/hash'; +import { LogWithTxData } from '@aztec/circuits.js/logs'; import { makeAppendOnlyTreeSnapshot, makeContentCommitment, @@ -1087,6 +1088,10 @@ export class TXE implements TypedOracle { throw new Error('deliverNote'); } + async getLogByTag(tag: Fr): Promise { + return await this.simulatorOracle.getLogByTag(tag); + } + // AVM oracles async avmOpcodeCall(targetContractAddress: AztecAddress, args: Fr[], isStaticCall: boolean): Promise {