diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index aa6884090881..2f951652348c 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -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 } @@ -169,6 +173,124 @@ 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::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 note contents 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: + // ``` + // |serialized_note_content: BoundedVec, 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::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 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::note::discovery::do_process_log( + context, + log_plaintext, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + |serialized_note_content: BoundedVec, 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::note::discovery::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, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + 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 diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr new file mode 100644 index 000000000000..1f0c0316832f --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -0,0 +1,134 @@ +use std::static_assert; + +use crate::{ + context::unconstrained_context::UnconstrainedContext, note::note_header::NoteHeader, + 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 note content: one for the storage slot, and one for +// the note type id. +global NOTE_LOG_RESERVED_FIELDS: u32 = 2; +pub 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, +} + +/// 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( + 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, NoteHeader, Field) -> Option, +) { + 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_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); + + let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot); + + // TODO(#11157): handle failed note_hash_and_nullifier computation + 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 + + assert( + 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, + ), + "Failed to deliver note", + ); + + // 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, 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, + "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) +} + +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 6ada1a1fabfd..593a00b03adc 100644 --- a/noir-projects/aztec-nr/aztec/src/note/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/mod.nr @@ -1,4 +1,5 @@ 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 d3f15cced885..a049d061ff0e 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -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 { - // 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 diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 642cde6b960a..e960d38f5af6 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -11,6 +11,7 @@ pub mod get_public_data_witness; pub mod get_membership_witness; pub mod keys; pub mod key_validation_request; +pub mod note_discovery; pub mod random; pub mod enqueue_public_function_call; pub mod block_header; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr new file mode 100644 index 000000000000..8d4c2848991b --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr @@ -0,0 +1,49 @@ +use crate::note::discovery::MAX_NOTE_SERIALIZED_LEN; +use dep::protocol_types::address::AztecAddress; + +/// Informs PXE of a note's existence so that it can later 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. +/// +/// The note's `content` is what `getNotes` will later return. PXE indexes notes by `storage_slot`, so this value is +/// typically used to filter notes that correspond to different state variables. `note_hash` and `nullifier` are the +/// inner hashes, i.e. the raw hashes returned by `NoteInterface::compute_note_hash` and +/// `NullifiableNote::compute_nullifier`. PXE will verify that the siloed unique note hash was inserted into the tree at +/// `tx_hash`, and will store the nullifier to later check for nullification. +/// +/// `recipient` is the account to which the note was sent to. Other accounts will not be able to access this note (e.g. +/// other accounts will not be able to see one another's token balance notes, even in the same PXE) unless authorized. +/// +/// Returns true if the note was sucessfully delivered and added to PXE's database. +pub unconstrained fn deliver_note( + contract_address: AztecAddress, + storage_slot: Field, + nonce: Field, + content: BoundedVec, + note_hash: Field, + nullifier: Field, + tx_hash: Field, + recipient: AztecAddress, +) -> bool { + deliver_note_oracle( + contract_address, + storage_slot, + nonce, + content, + note_hash, + nullifier, + tx_hash, + recipient, + ) +} + +#[oracle(deliverNote)] +unconstrained fn deliver_note_oracle( + contract_address: AztecAddress, + storage_slot: Field, + nonce: Field, + content: BoundedVec, + note_hash: Field, + nullifier: Field, + tx_hash: Field, + recipient: AztecAddress, +) -> bool {} 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 832615e787c2..ef46a00a5a24 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr @@ -1,5 +1,7 @@ mod collapse; mod subarray; +mod subbvec; pub use collapse::collapse; pub use subarray::subarray; +pub use subbvec::subbvec; diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/subarray.nr b/noir-projects/aztec-nr/aztec/src/utils/array/subarray.nr index fc4b75671858..d7e963738b82 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/subarray.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/subarray.nr @@ -1,18 +1,20 @@ -/// Returns `DST_LEN` elements from a source array, starting at `offset`. `DST_LEN` must be large enough to hold all of -/// the elements past `offset`. +/// Returns `DST_LEN` elements from a source array, starting at `offset`. `DST_LEN` must not be larger than the number +/// of elements past `offset`. /// -/// Example: +/// Examples: /// ``` /// let foo: [Field; 2] = subarray([1, 2, 3, 4, 5], 2); /// assert_eq(foo, [3, 4]); +/// +/// let bar: [Field; 5] = subarray([1, 2, 3, 4, 5], 2); // fails - we can't return 5 elements since only 3 remain /// ``` -pub fn subarray( - src: [Field; SRC_LEN], +pub fn subarray( + src: [T; SRC_LEN], offset: u32, -) -> [Field; DST_LEN] { - assert(offset + DST_LEN <= SRC_LEN, "offset too large"); +) -> [T; DST_LEN] { + assert(offset + DST_LEN <= SRC_LEN, "DST_LEN too large for offset"); - let mut dst: [Field; DST_LEN] = std::mem::zeroed(); + let mut dst: [T; DST_LEN] = std::mem::zeroed(); for i in 0..DST_LEN { dst[i] = src[i + offset]; } @@ -26,14 +28,14 @@ mod test { #[test] unconstrained fn subarray_into_empty() { // In all of these cases we're setting DST_LEN to be 0, so we always get back an emtpy array. - assert_eq(subarray([], 0), []); + assert_eq(subarray::([], 0), []); assert_eq(subarray([1, 2, 3, 4, 5], 0), []); assert_eq(subarray([1, 2, 3, 4, 5], 2), []); } #[test] unconstrained fn subarray_complete() { - assert_eq(subarray([], 0), []); + assert_eq(subarray::([], 0), []); assert_eq(subarray([1, 2, 3, 4, 5], 0), [1, 2, 3, 4, 5]); } @@ -46,7 +48,7 @@ mod test { assert_eq(subarray([1, 2, 3, 4, 5], 1), [2]); } - #[test(should_fail)] + #[test(should_fail_with = "DST_LEN too large for offset")] unconstrained fn subarray_offset_too_large() { // With an offset of 1 we can only request up to 4 elements let _: [_; 5] = subarray([1, 2, 3, 4, 5], 1); diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr new file mode 100644 index 000000000000..f08bed659423 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr @@ -0,0 +1,92 @@ +use crate::utils::array; + +/// Returns `DST_MAX_LEN` elements from a source BoundedVec, starting at `offset`. `offset` must not be larger than the +/// original length, and `DST_LEN` must not be larger than the total number of elements past `offset` (including the +/// zeroed elements past `len()`). +/// +/// Only elements at the beginning of the vector can be removed: it is not possible to also remove elements at the end +/// of the vector by passing a value for `DST_LEN` that is smaller than `len() - offset`. +/// +/// Examples: +/// ``` +/// let foo = BoundedVec::<_, 10>::from_array([1, 2, 3, 4, 5]); +/// assert_eq(subbvec(foo, 2), BoundedVec::<_, 8>::from_array([3, 4, 5])); +/// +/// let bar: BoundedVec<_, 1> = subbvec(foo, 2); // fails - we can't return just 1 element since 3 remain +/// let baz: BoundedVec<_, 10> = subbvec(foo, 3); // fails - we can't return 10 elements since only 7 remain +/// ``` +pub fn subbvec( + vec: 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) +} + +mod test { + use super::subbvec; + + #[test] + unconstrained fn subbvec_empty() { + let bvec = BoundedVec::::from_array([]); + assert_eq(subbvec(bvec, 0), bvec); + } + + #[test] + unconstrained fn subbvec_complete() { + let bvec = BoundedVec::<_, 10>::from_array([1, 2, 3, 4, 5]); + assert_eq(subbvec(bvec, 0), bvec); + + let smaller_capacity = BoundedVec::<_, 5>::from_array([1, 2, 3, 4, 5]); + assert_eq(subbvec(bvec, 0), smaller_capacity); + } + + #[test] + unconstrained fn subbvec_partial() { + let bvec = BoundedVec::<_, 10>::from_array([1, 2, 3, 4, 5]); + + assert_eq(subbvec(bvec, 2), BoundedVec::<_, 8>::from_array([3, 4, 5])); + assert_eq(subbvec(bvec, 2), BoundedVec::<_, 3>::from_array([3, 4, 5])); + } + + #[test] + unconstrained fn subbvec_into_empty() { + let bvec: BoundedVec<_, 10> = BoundedVec::from_array([1, 2, 3, 4, 5]); + assert_eq(subbvec(bvec, 5), BoundedVec::<_, 5>::from_array([])); + } + + #[test(should_fail)] + unconstrained fn subbvec_offset_past_len() { + let bvec = BoundedVec::<_, 10>::from_array([1, 2, 3, 4, 5]); + let _: BoundedVec<_, 1> = subbvec(bvec, 6); + } + + #[test(should_fail)] + unconstrained fn subbvec_insufficient_dst_len() { + let bvec = BoundedVec::<_, 10>::from_array([1, 2, 3, 4, 5]); + + // We're not providing enough space to hold all of the items inside the original BoundedVec. subbvec can cause + // for the capacity to reduce, but not the length (other than by len - offset). + let _: BoundedVec<_, 1> = subbvec(bvec, 2); + } + + #[test(should_fail_with = "DST_LEN too large for offset")] + unconstrained fn subbvec_dst_len_causes_enlarge() { + let bvec = BoundedVec::<_, 10>::from_array([1, 2, 3, 4, 5]); + + // subbvec does not supprt capacity increases + let _: BoundedVec<_, 11> = subbvec(bvec, 0); + } + + #[test(should_fail_with = "DST_LEN too large for offset")] + unconstrained fn subbvec_dst_len_too_large_for_offset() { + let bvec = BoundedVec::<_, 10>::from_array([1, 2, 3, 4, 5]); + + // This effectively requests a capacity increase, since there'd be just one element plus the 5 empty slots, + // which is less than 7. + let _: BoundedVec<_, 7> = subbvec(bvec, 4); + } +} diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr index 27cf2901a82e..215b48c7cdf9 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr @@ -42,10 +42,11 @@ pub fn private_functions_root_from_siblings( ) } -fn compute_note_hash_nonce(tx_hash: Field, note_index_in_tx: u32) -> Field { - // Hashing tx hash with note index in tx is guaranteed to be unique +pub fn compute_note_hash_nonce(first_nullifier_in_tx: Field, note_index_in_tx: u32) -> Field { + // Hashing the first nullifier with note index in tx is guaranteed to be unique (because all nullifiers are also + // unique). poseidon2_hash_with_separator( - [tx_hash, note_index_in_tx as Field], + [first_nullifier_in_tx, note_index_in_tx as Field], GENERATOR_INDEX__NOTE_HASH_NONCE, ) } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr b/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr index 5176086a2e1d..15feefea1791 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/meta/mod.nr @@ -96,7 +96,8 @@ pub comptime fn flatten_to_fields(name: Quoted, typ: Type, omit: [Quoted]) -> ([ let mut fields = &[]; let mut aux_vars = &[]; - if omit.all(|to_omit| to_omit != name) { + // Proceed if none of the omit rules omis this name + if !omit.any(|to_omit| to_omit == name) { if typ.is_field() { // For field we just add the value to fields fields = fields.push_back(name); diff --git a/yarn-project/pxe/src/database/note_dao.ts b/yarn-project/pxe/src/database/note_dao.ts index af8357a7e33c..7c9e07d4c81a 100644 --- a/yarn-project/pxe/src/database/note_dao.ts +++ b/yarn-project/pxe/src/database/note_dao.ts @@ -1,12 +1,10 @@ -import { type L1NotePayload, Note, TxHash, randomTxHash } from '@aztec/circuit-types'; +import { Note, TxHash, randomTxHash } from '@aztec/circuit-types'; import { AztecAddress, Fr, Point, type PublicKey } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type NoteData } from '@aztec/simulator/client'; -import { type NoteInfo } from '../note_decryption_utils/index.js'; - /** * A Note Data Access Object, representing a note that was comitted to the note hash tree, holding all of the * information required to use it during execution and manage its state. @@ -61,32 +59,6 @@ export class NoteDao implements NoteData { public noteTypeId: NoteSelector, ) {} - static fromPayloadAndNoteInfo( - note: Note, - payload: L1NotePayload, - noteInfo: NoteInfo, - l2BlockNumber: number, - l2BlockHash: string, - dataStartIndexForTx: number, - addressPoint: PublicKey, - ) { - const noteHashIndexInTheWholeTree = BigInt(dataStartIndexForTx + noteInfo.noteHashIndex); - return new NoteDao( - note, - payload.contractAddress, - payload.storageSlot, - noteInfo.nonce, - noteInfo.noteHash, - noteInfo.siloedNullifier, - noteInfo.txHash, - l2BlockNumber, - l2BlockHash, - noteHashIndexInTheWholeTree, - addressPoint, - payload.noteTypeId, - ); - } - toBuffer(): Buffer { return serializeToBuffer([ this.note, diff --git a/yarn-project/pxe/src/database/outgoing_note_dao.ts b/yarn-project/pxe/src/database/outgoing_note_dao.ts index 50fae33c5244..9cf40726d85b 100644 --- a/yarn-project/pxe/src/database/outgoing_note_dao.ts +++ b/yarn-project/pxe/src/database/outgoing_note_dao.ts @@ -1,11 +1,9 @@ -import { type L1NotePayload, Note, TxHash, randomTxHash } from '@aztec/circuit-types'; +import { Note, TxHash, randomTxHash } from '@aztec/circuit-types'; import { AztecAddress, Fr, Point, type PublicKey } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { type NoteInfo } from '../note_decryption_utils/index.js'; - /** * A note with contextual data which was decrypted as outgoing. */ @@ -38,31 +36,6 @@ export class OutgoingNoteDao { public ovpkM: PublicKey, ) {} - static fromPayloadAndNoteInfo( - note: Note, - payload: L1NotePayload, - noteInfo: NoteInfo, - l2BlockNumber: number, - l2BlockHash: string, - dataStartIndexForTx: number, - ovpkM: PublicKey, - ) { - const noteHashIndexInTheWholeTree = BigInt(dataStartIndexForTx + noteInfo.noteHashIndex); - return new OutgoingNoteDao( - note, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - noteInfo.txHash, - l2BlockNumber, - l2BlockHash, - noteInfo.nonce, - noteInfo.noteHash, - noteHashIndexInTheWholeTree, - ovpkM, - ); - } - toBuffer(): Buffer { return serializeToBuffer([ this.note, diff --git a/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts b/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts index 4d36c3a46e36..1ec8073784f0 100644 --- a/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts +++ b/yarn-project/pxe/src/note_decryption_utils/add_public_values_to_payload.ts @@ -41,7 +41,7 @@ export async function getOrderedNoteItems( noteFields.sort((a, b) => a.index - b.index); // Now we insert the public fields into the note based on its indices defined in the ABI. - const modifiedNoteItems = privateNoteValues; + const modifiedNoteItems = [...privateNoteValues]; let indexInPublicValues = 0; for (let i = 0; i < noteFields.length; i++) { const noteField = noteFields[i]; diff --git a/yarn-project/pxe/src/note_decryption_utils/brute_force_note_info.ts b/yarn-project/pxe/src/note_decryption_utils/brute_force_note_info.ts deleted file mode 100644 index abbae919f825..000000000000 --- a/yarn-project/pxe/src/note_decryption_utils/brute_force_note_info.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { type Note, type TxHash } from '@aztec/circuit-types'; -import { type AztecAddress } from '@aztec/circuits.js'; -import { computeNoteHashNonce, siloNullifier } from '@aztec/circuits.js/hash'; -import { type NoteSelector } from '@aztec/foundation/abi'; -import { Fr } from '@aztec/foundation/fields'; -import { type AcirSimulator } from '@aztec/simulator/client'; - -export interface NoteInfo { - noteHashIndex: number; - nonce: Fr; - noteHash: Fr; - siloedNullifier: Fr; - txHash: TxHash; -} - -/** - * Finds nonce, index, inner hash and siloed nullifier for a given note. - * @dev Finds the index in the note hash tree by computing the note hash with different nonce and see which hash for - * the current tx matches this value. - * @remarks This method assists in identifying spent notes in the note hash tree. - * @param uniqueNoteHashes - Note hashes in the tx. One of them should correspond to the note we are looking for - * @param txHash - Hash of a tx the note was emitted in. - * @param contractAddress - Address of the contract the note was emitted in. - * @param storageSlot - Storage slot of the note. - * @param noteTypeId - Type of the note. - * @param note - Note items. - * @param excludedIndices - Indices that have been assigned a note in the same tx. Notes in a tx can have the same - * l1NotePayload. We need to find a different index for each replicate. - * @param computeNullifier - A flag indicating whether to compute the nullifier or just return 0. - * @returns Nonce, index, inner hash and siloed nullifier for a given note. - * @throws If cannot find the nonce for the note. - */ -export async function bruteForceNoteInfo( - simulator: AcirSimulator, - uniqueNoteHashes: Fr[], - txHash: TxHash, - firstNullifier: Fr, - contractAddress: AztecAddress, - storageSlot: Fr, - noteTypeId: NoteSelector, - note: Note, - excludedIndices: Set, - computeNullifier: boolean, -): Promise { - let noteHashIndex = 0; - let nonce: Fr | undefined; - let noteHash: Fr | undefined; - let uniqueNoteHash: Fr | undefined; - let innerNullifier: Fr | undefined; - - for (; noteHashIndex < uniqueNoteHashes.length; ++noteHashIndex) { - if (excludedIndices.has(noteHashIndex)) { - continue; - } - - const uniqueNoteHashFromTxEffect = uniqueNoteHashes[noteHashIndex]; - if (uniqueNoteHashFromTxEffect.equals(Fr.ZERO)) { - break; - } - - const expectedNonce = computeNoteHashNonce(firstNullifier, noteHashIndex); - ({ noteHash, uniqueNoteHash, innerNullifier } = await simulator.computeNoteHashAndOptionallyANullifier( - contractAddress, - expectedNonce, - storageSlot, - noteTypeId, - computeNullifier, - note, - )); - - if (uniqueNoteHashFromTxEffect.equals(uniqueNoteHash)) { - nonce = expectedNonce; - break; - } - } - - if (!nonce) { - // NB: this used to warn the user that a decrypted log didn't match any notes. - // This was previously fine as we didn't chop transient note logs, but now we do (#1641 complete). - throw new Error('Cannot find a matching note hash for the note.'); - } - - return { - noteHashIndex, - nonce, - noteHash: noteHash!, - siloedNullifier: siloNullifier(contractAddress, innerNullifier!), - txHash, - }; -} diff --git a/yarn-project/pxe/src/note_decryption_utils/index.ts b/yarn-project/pxe/src/note_decryption_utils/index.ts deleted file mode 100644 index b272abb2e5c0..000000000000 --- a/yarn-project/pxe/src/note_decryption_utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { produceNoteDaos } from './produce_note_daos.js'; -export { NoteInfo } from './brute_force_note_info.js'; diff --git a/yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts b/yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts deleted file mode 100644 index 242b34b39fff..000000000000 --- a/yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { type L1NotePayload, type PublicKey, type TxHash } from '@aztec/circuit-types'; -import { type Fr } from '@aztec/foundation/fields'; -import { type Logger } from '@aztec/foundation/log'; -import { type AcirSimulator } from '@aztec/simulator/client'; - -import { NoteDao } from '../database/note_dao.js'; -import { type PxeDatabase } from '../database/pxe_database.js'; -import { produceNoteDaosForKey } from './produce_note_daos_for_key.js'; - -/** - * Decodes a note from a transaction that we know was intended for us. - * Throws if we do not yet have the contract corresponding to the note in our database. - * Accepts a set of excluded indices, which are indices that have been assigned a note in the same tx. - * Inserts the index of the note into the excludedIndices set if the note is successfully decoded. - * - * @param simulator - An instance of AcirSimulator. - * @param db - An instance of PxeDatabase. - * @param addressPoint - The public counterpart to the address secret, which is used in the decryption of incoming note logs. - * @param payload - An instance of l1NotePayload. - * @param txHash - The hash of the transaction that created the note. Equivalent to the first nullifier of the transaction. - * @param noteHashes - New note hashes in this transaction, one of which belongs to this note. - * @param dataStartIndexForTx - The next available leaf index for the note hash tree for this transaction. - * @param excludedIndices - Indices that have been assigned a note in the same tx. Notes in a tx can have the same l1NotePayload, we need to find a different index for each replicate. - * @param logger - An instance of Logger. - * @param unencryptedLogs - Unencrypted logs for the transaction (used to complete partial notes). - * @returns An object containing the incoming notes. - */ -export async function produceNoteDaos( - simulator: AcirSimulator, - db: PxeDatabase, - addressPoint: PublicKey | undefined, - payload: L1NotePayload, - txHash: TxHash, - firstNullifier: Fr, - l2BlockNumber: number, - l2BlockHash: string, - noteHashes: Fr[], - dataStartIndexForTx: number, - excludedIndices: Set, - logger: Logger, -): Promise<{ note: NoteDao | undefined }> { - if (!addressPoint) { - throw new Error('addressPoint is undefined. Cannot create note.'); - } - - let note: NoteDao | undefined; - - if (addressPoint) { - note = await produceNoteDaosForKey( - simulator, - db, - addressPoint, - payload, - txHash, - firstNullifier, - l2BlockNumber, - l2BlockHash, - noteHashes, - dataStartIndexForTx, - excludedIndices, - logger, - NoteDao.fromPayloadAndNoteInfo, - ); - } - - return { - note, - }; -} diff --git a/yarn-project/pxe/src/note_decryption_utils/produce_note_daos_for_key.ts b/yarn-project/pxe/src/note_decryption_utils/produce_note_daos_for_key.ts deleted file mode 100644 index ef78cf7a76b6..000000000000 --- a/yarn-project/pxe/src/note_decryption_utils/produce_note_daos_for_key.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { type L1NotePayload, type Note, type TxHash } from '@aztec/circuit-types'; -import { type Fr, type PublicKey } from '@aztec/circuits.js'; -import { type Logger } from '@aztec/foundation/log'; -import { type AcirSimulator } from '@aztec/simulator/client'; - -import { type PxeDatabase } from '../database/pxe_database.js'; -import { getOrderedNoteItems } from './add_public_values_to_payload.js'; -import { type NoteInfo, bruteForceNoteInfo } from './brute_force_note_info.js'; - -export async function produceNoteDaosForKey( - simulator: AcirSimulator, - db: PxeDatabase, - pkM: PublicKey, - payload: L1NotePayload, - txHash: TxHash, - firstNullifier: Fr, - l2BlockNumber: number, - l2BlockHash: string, - noteHashes: Fr[], - dataStartIndexForTx: number, - excludedIndices: Set, - logger: Logger, - daoConstructor: ( - note: Note, - payload: L1NotePayload, - noteInfo: NoteInfo, - l2BlockNumber: number, - l2BlockHash: string, - dataStartIndexForTx: number, - pkM: PublicKey, - ) => T, -): Promise { - let noteDao: T | undefined; - - try { - // We get the note by merging publicly and privately delivered note values. - const note = await getOrderedNoteItems(db, payload); - - const noteInfo = await bruteForceNoteInfo( - simulator, - noteHashes, - txHash, - firstNullifier, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - note, - excludedIndices, - true, // For incoming we compute a nullifier (recipient of incoming is the party that nullifies). - ); - excludedIndices?.add(noteInfo.noteHashIndex); - - noteDao = daoConstructor(note, payload, noteInfo, l2BlockNumber, l2BlockHash, dataStartIndexForTx, pkM); - } catch (e) { - logger.error(`Could not process note because of "${e}". Discarding note...`); - } - - return noteDao; -} diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index e73318798699..247e2496c0cd 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -1,14 +1,16 @@ import { type AztecNode, + type FunctionCall, type InBlock, L1NotePayload, type L2Block, type L2BlockNumber, MerkleTreeId, + Note, type NoteStatus, type NullifierMembershipWitness, type PublicDataWitness, - type TxEffect, + TxHash, type TxScopedL2Log, getNonNullifiedL1ToL2MessageWitness, } from '@aztec/circuit-types'; @@ -18,15 +20,24 @@ import { type CompleteAddress, type ContractInstance, Fr, - type FunctionSelector, + FunctionSelector, IndexedTaggingSecret, type KeyValidationRequest, type L1_TO_L2_MSG_TREE_HEIGHT, + MAX_NOTE_HASHES_PER_TX, + PRIVATE_LOG_SIZE_IN_FIELDS, PrivateLog, computeAddressSecret, computeTaggingSecretPoint, } from '@aztec/circuits.js'; -import { type FunctionArtifact, getFunctionArtifact } from '@aztec/foundation/abi'; +import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/circuits.js/hash'; +import { + type FunctionArtifact, + FunctionType, + NoteSelector, + encodeArguments, + getFunctionArtifact, +} from '@aztec/foundation/abi'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { createLogger } from '@aztec/foundation/log'; import { type KeyStore } from '@aztec/key-store'; @@ -37,10 +48,10 @@ import { type SimulationProvider, } from '@aztec/simulator/client'; -import { type ContractDataOracle } from '../contract_data_oracle/index.js'; +import { ContractDataOracle } from '../contract_data_oracle/index.js'; import { type PxeDatabase } from '../database/index.js'; -import { type NoteDao } from '../database/note_dao.js'; -import { produceNoteDaos } from '../note_decryption_utils/produce_note_daos.js'; +import { NoteDao } from '../database/note_dao.js'; +import { getOrderedNoteItems } from '../note_decryption_utils/add_public_values_to_payload.js'; import { getAcirSimulator } from '../simulator/index.js'; import { WINDOW_HALF_SIZE, getIndexedTaggingSecretsForTheWindow, getInitialIndexesMap } from './tagging_utils.js'; @@ -431,6 +442,8 @@ export class SimulatorOracle implements DBOracle { maxBlockNumber: number, scopes?: AztecAddress[], ): Promise> { + this.log.verbose('Searching for tagged logs', { contract: contractAddress }); + // Ideally this algorithm would be implemented in noir, exposing its building blocks as oracles. // However it is impossible at the moment due to the language not supporting nested slices. // This nesting is necessary because for a given set of tags we don't @@ -568,10 +581,9 @@ export class SimulatorOracle implements DBOracle { * Decrypts logs tagged for a recipient and returns them. * @param scopedLogs - The logs to decrypt. * @param recipient - The recipient of the logs. - * @param simulator - The simulator to use for decryption. * @returns The decrypted notes. */ - async #decryptTaggedLogs(scopedLogs: TxScopedL2Log[], recipient: AztecAddress, simulator?: AcirSimulator) { + async #decryptTaggedLogs(scopedLogs: TxScopedL2Log[], recipient: AztecAddress) { const recipientCompleteAddress = await this.getCompleteAddress(recipient); const ivskM = await this.keyStore.getMasterSecretKey( recipientCompleteAddress.publicKeys.masterIncomingViewingPublicKey, @@ -581,56 +593,29 @@ export class SimulatorOracle implements DBOracle { // Since we could have notes with the same index for different txs, we need // to keep track of them scoping by txHash const excludedIndices: Map> = new Map(); - const notes: NoteDao[] = []; - - const txEffectsCache = new Map | undefined>(); + const decrypted = []; for (const scopedLog of scopedLogs) { - const notePayload = scopedLog.isFromPublic + const payload = scopedLog.isFromPublic ? L1NotePayload.decryptAsIncomingFromPublic(scopedLog.logData, addressSecret) : L1NotePayload.decryptAsIncoming(PrivateLog.fromBuffer(scopedLog.logData), addressSecret); - if (notePayload) { - const payload = notePayload; - - const txEffect = - txEffectsCache.get(scopedLog.txHash.toString()) ?? (await this.aztecNode.getTxEffect(scopedLog.txHash)); - - if (!txEffect) { - this.log.warn(`No tx effect found for ${scopedLog.txHash} while decrypting tagged logs`); - continue; - } + if (!payload) { + this.log.verbose('Unable to decrypt log'); + continue; + } - txEffectsCache.set(scopedLog.txHash.toString(), txEffect); + if (!excludedIndices.has(scopedLog.txHash.toString())) { + excludedIndices.set(scopedLog.txHash.toString(), new Set()); + } - if (!excludedIndices.has(scopedLog.txHash.toString())) { - excludedIndices.set(scopedLog.txHash.toString(), new Set()); - } - const { note } = await produceNoteDaos( - // I don't like this at all, but we need a simulator to run `computeNoteHashAndOptionallyANullifier`. This generates - // a chicken-and-egg problem due to this oracle requiring a simulator, which in turn requires this oracle. Furthermore, since jest doesn't allow - // mocking ESM exports, we have to pollute the method even more by providing a simulator parameter so tests can inject a fake one. - simulator ?? - getAcirSimulator(this.db, this.aztecNode, this.keyStore, this.simulationProvider, this.contractDataOracle), - this.db, - notePayload ? recipient.toAddressPoint() : undefined, - payload!, - txEffect.data.txHash, - txEffect.data.nullifiers[0], - txEffect.l2BlockNumber, - txEffect.l2BlockHash, - txEffect.data.noteHashes, - scopedLog.dataStartIndexForTx, - excludedIndices.get(scopedLog.txHash.toString())!, - this.log, - ); + const note = await getOrderedNoteItems(this.db, payload); + const plaintext = [payload.storageSlot, payload.noteTypeId.toField(), ...note.items]; - if (note) { - notes.push(note); - } - } + decrypted.push({ plaintext, txHash: scopedLog.txHash, contractAddress: payload.contractAddress }); } - return { notes }; + + return decrypted; } /** @@ -643,20 +628,68 @@ export class SimulatorOracle implements DBOracle { recipient: AztecAddress, simulator?: AcirSimulator, ): Promise { - const { notes } = await this.#decryptTaggedLogs(logs, recipient, simulator); - if (notes.length) { - await this.db.addNotes(notes, recipient); - notes.forEach(noteDao => { - this.log.verbose(`Added incoming note for contract ${noteDao.contractAddress} at slot ${noteDao.storageSlot}`, { - contract: noteDao.contractAddress, - slot: noteDao.storageSlot, - nullifier: noteDao.siloedNullifier.toString(), - }); - }); + const decryptedLogs = await this.#decryptTaggedLogs(logs, recipient); + + // We've produced the full NoteDao, which we'd be able to simply insert into the database. However, this is + // only a temporary measure as we migrate from the PXE-driven discovery into the new contract-driven approach. We + // discard most of the work done up to this point and reconstruct the note plaintext to then hand over to the + // contract for further processing. + for (const decryptedLog of decryptedLogs) { + // Log processing requires the note hashes in the tx in which the note was created. We are now assuming that the + // note was included in the same block in which the log was delivered - note that partial notes will not work this + // way. + const txEffect = await this.aztecNode.getTxEffect(decryptedLog.txHash); + if (!txEffect) { + throw new Error(`Could not find tx effect for tx hash ${decryptedLog.txHash}`); + } + + // This will trigger calls to the deliverNote oracle + await this.callProcessLog( + decryptedLog.contractAddress, + decryptedLog.plaintext, + decryptedLog.txHash, + txEffect.data.noteHashes, + txEffect.data.nullifiers[0], + recipient, + simulator, + ); } + return; + } + + // Called when notes are delivered, usually as a result to a call to the process_log contract function + public async deliverNote( + contractAddress: AztecAddress, + storageSlot: Fr, + nonce: Fr, + content: Fr[], + noteHash: Fr, + nullifier: Fr, + txHash: Fr, + recipient: AztecAddress, + ): Promise { + const noteDao = await this.produceNoteDao( + contractAddress, + storageSlot, + nonce, + content, + noteHash, + nullifier, + txHash, + recipient, + ); + + await this.db.addNotes([noteDao], recipient); + this.log.verbose('Added note', { + contract: noteDao.contractAddress, + slot: noteDao.storageSlot, + nullifier: noteDao.siloedNullifier.toString, + }); } public async removeNullifiedNotes(contractAddress: AztecAddress) { + this.log.verbose('Removing nullified notes', { contract: contractAddress }); + for (const recipient of await this.keyStore.getAccounts()) { const currentNotesForRecipient = await this.db.getNotes({ contractAddress, owner: recipient }); const nullifiersToCheck = currentNotesForRecipient.map(note => note.siloedNullifier); @@ -681,6 +714,96 @@ export class SimulatorOracle implements DBOracle { } } + async produceNoteDao( + contractAddress: AztecAddress, + storageSlot: Fr, + nonce: Fr, + content: Fr[], + noteHash: Fr, + nullifier: Fr, + txHash: Fr, + recipient: AztecAddress, + ): Promise { + const receipt = await this.aztecNode.getTxReceipt(new TxHash(txHash)); + if (receipt === undefined) { + throw new Error(`Failed to fetch tx receipt for tx hash ${txHash} when searching for note hashes`); + } + const { blockNumber, blockHash } = receipt; + + const uniqueNoteHash = computeUniqueNoteHash(nonce, siloNoteHash(contractAddress, noteHash)); + const siloedNullifier = siloNullifier(contractAddress, nullifier); + + const uniqueNoteHashTreeIndex = ( + await this.aztecNode.findLeavesIndexes(blockNumber!, MerkleTreeId.NOTE_HASH_TREE, [uniqueNoteHash]) + )[0]; + if (uniqueNoteHashTreeIndex === undefined) { + throw new Error( + `Note hash ${noteHash} (uniqued as ${uniqueNoteHash}) is not present on the tree at block ${blockNumber} (from tx ${txHash})`, + ); + } + + return new NoteDao( + new Note(content), + contractAddress, + storageSlot, + nonce, + noteHash, + siloedNullifier, + new TxHash(txHash), + blockNumber!, + blockHash!.toString(), + uniqueNoteHashTreeIndex, + recipient.toAddressPoint(), + NoteSelector.empty(), // todo: remove + ); + } + + async callProcessLog( + contractAddress: AztecAddress, + logPlaintext: Fr[], + txHash: TxHash, + noteHashes: Fr[], + firstNullifier: Fr, + recipient: AztecAddress, + simulator?: AcirSimulator, + ) { + const artifact: FunctionArtifact | undefined = await new ContractDataOracle(this.db).getFunctionArtifactByName( + contractAddress, + 'process_log', + ); + if (!artifact) { + throw new Error( + `Mandatory implementation of "process_log" missing in noir contract ${contractAddress.toString()}.`, + ); + } + + const execRequest: FunctionCall = { + name: artifact.name, + to: contractAddress, + selector: FunctionSelector.fromNameAndParameters(artifact), + type: FunctionType.UNCONSTRAINED, + isStatic: artifact.isStatic, + args: encodeArguments(artifact, [ + toBoundedVec(logPlaintext, PRIVATE_LOG_SIZE_IN_FIELDS), + txHash.toString(), + toBoundedVec(noteHashes, MAX_NOTE_HASHES_PER_TX), + firstNullifier, + recipient, + ]), + returnTypes: artifact.returnTypes, + }; + + await ( + simulator ?? + getAcirSimulator(this.db, this.aztecNode, this.keyStore, this.simulationProvider, this.contractDataOracle) + ).runUnconstrained( + execRequest, + artifact, + contractAddress, + [], // empty scope as this call should not require access to private information + ); + } + /** * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped * to a specific `contract`. @@ -703,3 +826,7 @@ export class SimulatorOracle implements DBOracle { return this.db.load(contract, key); } } + +function toBoundedVec(array: Fr[], maxLength: number) { + return { storage: array.concat(Array(maxLength - array.length).fill(new Fr(0))), len: array.length }; +} diff --git a/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts index 6d7037a7de8d..3f7d3d19eabb 100644 --- a/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts +++ b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts @@ -7,6 +7,8 @@ import { type TxEffect, TxHash, TxScopedL2Log, + randomContractArtifact, + randomContractInstanceWithAddress, randomInBlock, wrapInBlock, } from '@aztec/circuit-types'; @@ -23,6 +25,7 @@ import { computeTaggingSecretPoint, deriveKeys, } from '@aztec/circuits.js'; +import { type FunctionArtifact, FunctionType } from '@aztec/foundation/abi'; import { pedersenHash, poseidon2Hash } from '@aztec/foundation/crypto'; import { KeyStore } from '@aztec/key-store'; import { openTmpStore } from '@aztec/kv-store/lmdb'; @@ -34,7 +37,6 @@ import times from 'lodash.times'; import { type PxeDatabase } from '../database/index.js'; import { KVPxeDatabase } from '../database/kv_pxe_database.js'; -import { type NoteDao } from '../database/note_dao.js'; import { ContractDataOracle } from '../index.js'; import { SimulatorOracle } from './index.js'; import { WINDOW_HALF_SIZE } from './tagging_utils.js'; @@ -466,21 +468,41 @@ describe('Simulator oracle', () => { let getNotesSpy: any; let removeNullifiedNotesSpy: any; let simulator: MockProxy; + let runUnconstrainedSpy: any; + + let processLogFuncArtifact: FunctionArtifact; + + beforeEach(async () => { + // Set up process_log function artifact --> it is never executed as simulator.runUnconstrained(...) is mocked + processLogFuncArtifact = { + name: 'process_log', + functionType: FunctionType.UNCONSTRAINED, + isInternal: false, + parameters: [], + returnTypes: [], + errorTypes: {}, + isInitializer: false, + isStatic: false, + bytecode: Buffer.alloc(0), + debugSymbols: '', + }; + + // Set up contract instance and artifact + const contractInstance = randomContractInstanceWithAddress(); + const contractArtifact = randomContractArtifact(); + contractArtifact.functions = [processLogFuncArtifact]; + await database.addContractInstance(contractInstance); + await database.addContractArtifact(contractInstance.contractClassId, contractArtifact); + contractAddress = contractInstance.address; - beforeEach(() => { addNotesSpy = jest.spyOn(database, 'addNotes'); getNotesSpy = jest.spyOn(database, 'getNotes'); removeNullifiedNotesSpy = jest.spyOn(database, 'removeNullifiedNotes'); removeNullifiedNotesSpy.mockImplementation(() => Promise.resolve([])); simulator = mock(); - simulator.computeNoteHashAndOptionallyANullifier.mockImplementation((...args: any) => - Promise.resolve({ - noteHash: Fr.random(), - uniqueNoteHash: pedersenHash(args[5].items), // args[5] is note - siloedNoteHash: Fr.random(), - innerNullifier: Fr.random(), - }), - ); + simulator.runUnconstrained.mockImplementation(() => Promise.resolve({})); + + runUnconstrainedSpy = jest.spyOn(simulator, 'runUnconstrained'); }); afterEach(() => { @@ -544,32 +566,7 @@ describe('Simulator oracle', () => { ); return taggedLogs; } - - it('should store an incoming note that belongs to us', async () => { - const request = new MockNoteRequest( - getRandomNoteLogPayload(Fr.random(), contractAddress), - 4, - 0, - 2, - recipient.address, - ); - const taggedLogs = mockTaggedLogs([request]); - - await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); - - expect(addNotesSpy).toHaveBeenCalledTimes(1); - expect(addNotesSpy).toHaveBeenCalledWith( - [ - expect.objectContaining({ - ...request.snippetOfNoteDao, - index: request.indexWithinNoteHashTree, - }), - ], - recipient.address, - ); - }, 25_000); - - it('should store multiple notes that belong to us', async () => { + it('should call processLog on multiple notes', async () => { const requests = [ new MockNoteRequest(getRandomNoteLogPayload(Fr.random(), contractAddress), 1, 1, 1, recipient.address), new MockNoteRequest( @@ -594,25 +591,9 @@ describe('Simulator oracle', () => { await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); - expect(addNotesSpy).toHaveBeenCalledTimes(1); - expect(addNotesSpy).toHaveBeenCalledWith( - // Incoming should contain notes from requests 0, 2, 4 because in those requests we set owner address point. - [ - expect.objectContaining({ - ...requests[0].snippetOfNoteDao, - index: requests[0].indexWithinNoteHashTree, - }), - expect.objectContaining({ - ...requests[2].snippetOfNoteDao, - index: requests[2].indexWithinNoteHashTree, - }), - expect.objectContaining({ - ...requests[4].snippetOfNoteDao, - index: requests[4].indexWithinNoteHashTree, - }), - ], - recipient.address, - ); + // We test that a call to `processLog` is made with the correct function artifact and contract address + expect(runUnconstrainedSpy).toHaveBeenCalledTimes(3); + expect(runUnconstrainedSpy).toHaveBeenCalledWith(expect.anything(), processLogFuncArtifact, contractAddress, []); }, 30_000); it('should not store notes that do not belong to us', async () => { @@ -629,40 +610,6 @@ describe('Simulator oracle', () => { expect(addNotesSpy).toHaveBeenCalledTimes(0); }); - it('should be able to recover two note payloads containing the same note', async () => { - const note = getRandomNoteLogPayload(Fr.random(), contractAddress); - const note2 = getRandomNoteLogPayload(Fr.random(), contractAddress); - // All note payloads except one have the same contract address, storage slot, and the actual note. - const requests = [ - new MockNoteRequest(note, 3, 0, 0, recipient.address), - new MockNoteRequest(note, 4, 0, 2, recipient.address), - new MockNoteRequest(note, 4, 2, 0, recipient.address), - new MockNoteRequest(note2, 5, 2, 1, recipient.address), - new MockNoteRequest(note, 6, 2, 3, recipient.address), - ]; - - const taggedLogs = mockTaggedLogs(requests); - - await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); - - // Check notes - { - const addedNotes: NoteDao[] = addNotesSpy.mock.calls[0][0]; - expect(addedNotes.map(dao => dao)).toEqual([ - expect.objectContaining({ ...requests[0].snippetOfNoteDao, index: requests[0].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[1].snippetOfNoteDao, index: requests[1].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[2].snippetOfNoteDao, index: requests[2].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[3].snippetOfNoteDao, index: requests[3].indexWithinNoteHashTree }), - expect.objectContaining({ ...requests[4].snippetOfNoteDao, index: requests[4].indexWithinNoteHashTree }), - ]); - - // Check that every note has a different nonce. - const nonceSet = new Set(); - addedNotes.forEach(info => nonceSet.add(info.nonce.value)); - expect(nonceSet.size).toBe(requests.length); - } - }); - it('should remove nullified notes', async () => { const requests = [ new MockNoteRequest(getRandomNoteLogPayload(Fr.random(), contractAddress), 1, 1, 1, recipient.address), diff --git a/yarn-project/simulator/src/acvm/deserialize.ts b/yarn-project/simulator/src/acvm/deserialize.ts index 5936d381a370..57ad9fe8d0df 100644 --- a/yarn-project/simulator/src/acvm/deserialize.ts +++ b/yarn-project/simulator/src/acvm/deserialize.ts @@ -29,6 +29,18 @@ export function frToBoolean(fr: Fr): boolean { return fr.toBigInt() === BigInt(1); } +/** + * Converts a Noir BoundedVec of Fields into an Fr array. Note that BoundedVecs are structs, and therefore translated as + * two separate ACVMField arrays. + * + * @param storage The array with the BoundedVec's storage (i.e. BoundedVec::storage()) + * @param length The length of the BoundedVec (i.e. BoundedVec::len()) + * @returns An array with the same content as the Noir version. Elements past the length are discarded. + */ +export function fromBoundedVec(storage: ACVMField[], length: ACVMField): Fr[] { + return storage.slice(0, frToNumber(fromACVMField(length))).map(fromACVMField); +} + /** * Transforms a witness map to its field elements. * @param witness - The witness to extract from. diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index f8f7474757d7..3b0332619ba4 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -5,7 +5,7 @@ import { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import { type ACVMField } from '../acvm_types.js'; -import { frToBoolean, frToNumber, fromACVMField } from '../deserialize.js'; +import { frToBoolean, frToNumber, fromACVMField, fromBoundedVec } from '../deserialize.js'; import { toACVMField } from '../serialize.js'; import { type TypedOracle } from './typed_oracle.js'; @@ -384,6 +384,34 @@ export class Oracle { await this.typedOracle.syncNotes(); } + async deliverNote( + [contractAddress]: ACVMField[], + [storageSlot]: ACVMField[], + [nonce]: ACVMField[], + content: ACVMField[], + [contentLength]: ACVMField[], + [noteHash]: ACVMField[], + [nullifier]: ACVMField[], + [txHash]: ACVMField[], + [recipient]: ACVMField[], + ): Promise { + // TODO(#10728): try-catch this block and return false if we get an exception so that the contract can decide what + // to do if a note fails delivery (e.g. not increment the tagging index, or add it to some pending work list). + // Delivery might fail due to temporary issues, such as poor node connectivity. + await this.typedOracle.deliverNote( + AztecAddress.fromString(contractAddress), + fromACVMField(storageSlot), + fromACVMField(nonce), + fromBoundedVec(content, contentLength), + fromACVMField(noteHash), + fromACVMField(nullifier), + fromACVMField(txHash), + AztecAddress.fromString(recipient), + ); + + return toACVMField(true); + } + async store([contract]: ACVMField[], [key]: ACVMField[], values: ACVMField[]) { const processedContract = AztecAddress.fromField(fromACVMField(contract)); const processedKey = fromACVMField(key); diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 979b8d89753f..c5db2cfa9a74 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -237,6 +237,19 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('syncNotes'); } + deliverNote( + _contractAddress: AztecAddress, + _storageSlot: Fr, + _nonce: Fr, + _content: Fr[], + _noteHash: Fr, + _nullifier: Fr, + _txHash: Fr, + _recipient: AztecAddress, + ): Promise { + throw new OracleMethodNotAvailableError('deliverNote'); + } + store(_contract: AztecAddress, _key: Fr, _values: Fr[]): Promise { throw new OracleMethodNotAvailableError('store'); } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 1cc2e5650098..5f564eab62f5 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -233,6 +233,30 @@ export interface DBOracle extends CommitmentsDB { */ processTaggedLogs(logs: TxScopedL2Log[], recipient: AztecAddress): Promise; + /** + * Delivers the preimage and metadata of a committed note so that it can be later requested via the `getNotes` + * oracle. + * + * @param contractAddress - The address of the contract that created the note (i.e. the siloing contract) + * @param storageSlot - The storage slot of the note - used for indexing in `getNotes` + * @param nonce - The nonce of the note used by the kernel to compute the unique note hash + * @param content - The note's content: this is the primary item to return in `getNotes` + * @param noteHash - The non-unique non-siloed note hash + * @param nullifier - The inner (non-siloed) note nullifier + * @param txHash - The transaction in which the note was added to the note hash tree + * @param recipient - The account that discovered the note + */ + deliverNote( + contractAddress: AztecAddress, + storageSlot: Fr, + nonce: Fr, + content: Fr[], + noteHash: Fr, + nullifier: Fr, + txHash: Fr, + recipient: AztecAddress, + ): 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.ts b/yarn-project/simulator/src/client/private_execution.ts index fd28d1edd4b9..ff15487be8fb 100644 --- a/yarn-project/simulator/src/client/private_execution.ts +++ b/yarn-project/simulator/src/client/private_execution.ts @@ -29,7 +29,7 @@ export async function executePrivateFunction( log = createLogger('simulator:private_execution'), ): Promise { const functionName = await context.getDebugFunctionName(); - log.verbose(`Executing private function ${functionName}@${contractAddress}`); + log.verbose(`Executing private function ${functionName}`, { contract: contractAddress }); const acir = artifact.bytecode; const initialWitness = context.getInitialWitness(artifact); const acvmCallback = new Oracle(context); diff --git a/yarn-project/simulator/src/client/simulator.ts b/yarn-project/simulator/src/client/simulator.ts index 1750bbf89aa3..9a77707fcd55 100644 --- a/yarn-project/simulator/src/client/simulator.ts +++ b/yarn-project/simulator/src/client/simulator.ts @@ -192,7 +192,7 @@ export class AcirSimulator { const execRequest: FunctionCall = { name: artifact.name, to: contractAddress, - selector: FunctionSelector.empty(), + selector: FunctionSelector.fromNameAndParameters(artifact), type: FunctionType.UNCONSTRAINED, isStatic: artifact.isStatic, args: encodeArguments(artifact, [ diff --git a/yarn-project/simulator/src/client/unconstrained_execution.ts b/yarn-project/simulator/src/client/unconstrained_execution.ts index f29bf3c311f2..d5b6036991ff 100644 --- a/yarn-project/simulator/src/client/unconstrained_execution.ts +++ b/yarn-project/simulator/src/client/unconstrained_execution.ts @@ -22,7 +22,10 @@ export async function executeUnconstrainedFunction( args: Fr[], log = createLogger('simulator:unconstrained_execution'), ): Promise { - log.verbose(`Executing unconstrained function ${contractAddress}:${functionSelector}(${artifact.name})`); + log.verbose(`Executing unconstrained function ${artifact.name}`, { + contract: contractAddress, + selector: functionSelector, + }); const acir = artifact.bytecode; const initialWitness = toACVMWitness(0, args); diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 90c276814346..7f6d10bacfc0 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -310,6 +310,24 @@ export class ViewDataOracle extends TypedOracle { await this.db.removeNullifiedNotes(this.contractAddress); } + public override async deliverNote( + contractAddress: AztecAddress, + storageSlot: Fr, + nonce: Fr, + content: Fr[], + noteHash: Fr, + nullifier: Fr, + txHash: Fr, + recipient: AztecAddress, + ) { + // TODO(#10727): allow other contracts to deliver notes + if (!this.contractAddress.equals(contractAddress)) { + throw new Error(`Got a note delivery request from ${contractAddress}, expected ${this.contractAddress}`); + } + + await this.db.deliverNote(contractAddress, storageSlot, nonce, content, noteHash, nullifier, txHash, recipient); + } + public override store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { if (!contract.equals(this.contractAddress)) { // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract diff --git a/yarn-project/txe/src/node/txe_node.ts b/yarn-project/txe/src/node/txe_node.ts index f9d63c2fb3cd..458a42db1824 100644 --- a/yarn-project/txe/src/node/txe_node.ts +++ b/yarn-project/txe/src/node/txe_node.ts @@ -5,6 +5,7 @@ import { type GetUnencryptedLogsResponse, type InBlock, type L2Block, + L2BlockHash, type L2BlockNumber, type L2Tips, type LogFilter, @@ -18,7 +19,7 @@ import { type Tx, type TxEffect, TxHash, - type TxReceipt, + TxReceipt, TxScopedL2Log, type TxValidationResult, type UnencryptedL2Log, @@ -39,27 +40,32 @@ import { type ProtocolContractAddresses, } from '@aztec/circuits.js'; import { type L1ContractAddresses } from '@aztec/ethereum'; +import { poseidon2Hash } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; +import { MerkleTreeSnapshotOperationsFacade, type MerkleTrees } from '@aztec/world-state'; export class TXENode implements AztecNode { #logsByTags = new Map(); - #txEffectsByTxHash = new Map | undefined>(); + #txEffectsByTxHash = new Map>(); + #txReceiptsByTxHash = new Map(); #blockNumberToNullifiers = new Map(); #noteIndex = 0; - #blockNumber: number; #logger = createLogger('aztec:txe_node'); - constructor(blockNumber: number) { - this.#blockNumber = blockNumber; - } + constructor( + private blockNumber: number, + private version: number, + private chainId: number, + private trees: MerkleTrees, + ) {} /** * Fetches the current block number. * @returns The block number. */ getBlockNumber(): Promise { - return Promise.resolve(this.#blockNumber); + return Promise.resolve(this.blockNumber); } /** @@ -67,7 +73,7 @@ export class TXENode implements AztecNode { * @param - The block number to set. */ setBlockNumber(blockNumber: number) { - this.#blockNumber = blockNumber; + this.blockNumber = blockNumber; } /** @@ -76,23 +82,42 @@ export class TXENode implements AztecNode { * @returns The requested tx effect. */ getTxEffect(txHash: TxHash): Promise | undefined> { - const txEffect = this.#txEffectsByTxHash.get(new Fr(txHash.toBuffer()).toString()); + const txEffect = this.#txEffectsByTxHash.get(txHash.toString()); return Promise.resolve(txEffect); } /** - * Sets a tx effect for a given block number. + * Sets a tx effect and receipt for a given block number. * @param blockNumber - The block number that this tx effect resides. * @param txHash - The transaction hash of the transaction. * @param effect - The tx effect to set. */ setTxEffect(blockNumber: number, txHash: TxHash, effect: TxEffect) { - this.#txEffectsByTxHash.set(new Fr(txHash.toBuffer()).toString(), { - l2BlockHash: blockNumber.toString(), + // We are not creating real blocks on which membership proofs can be constructed - we instead define its hash as + // simply the hash of the block number. + const blockHash = poseidon2Hash([blockNumber]); + + this.#txEffectsByTxHash.set(txHash.toString(), { + l2BlockHash: blockHash.toString(), l2BlockNumber: blockNumber, data: effect, }); + + // We also set the receipt since we want to be able to serve `getTxReceipt` - we don't care about most values here, + // but we do need to be able to retrieve the block number of a given txHash. + this.#txReceiptsByTxHash.set( + txHash.toString(), + new TxReceipt( + txHash, + TxReceipt.statusFromRevertCode(effect.revertCode), + '', + undefined, + new L2BlockHash(blockHash.toBuffer()), + blockNumber, + undefined, + ), + ); } /** @@ -234,12 +259,28 @@ export class TXENode implements AztecNode { * @param leafValue - The values to search for * @returns The indexes of the given leaves in the given tree or undefined if not found. */ - findLeavesIndexes( - _blockNumber: L2BlockNumber, - _treeId: MerkleTreeId, - _leafValues: Fr[], + async findLeavesIndexes( + blockNumber: L2BlockNumber, + treeId: MerkleTreeId, + leafValues: Fr[], ): Promise<(bigint | undefined)[]> { - throw new Error('TXE Node method findLeavesIndexes not implemented'); + // Temporary workaround to be able to respond this query: the trees are currently stored in the TXE oracle, but we + // hold a reference to them. + // We should likely migrate this so that the trees are owned by the node. + + if (blockNumber == 'latest') { + blockNumber = await this.getBlockNumber(); + } + + const db = + blockNumber === (await this.getBlockNumber()) + ? await this.trees.getLatest() + : new MerkleTreeSnapshotOperationsFacade(this.trees, blockNumber); + + return await db.findLeafIndices( + treeId, + leafValues.map(x => x.toBuffer()), + ); } /** @@ -420,7 +461,7 @@ export class TXENode implements AztecNode { * @returns The rollup version. */ getVersion(): Promise { - throw new Error('TXE Node method getVersion not implemented'); + return Promise.resolve(this.version); } /** @@ -428,7 +469,7 @@ export class TXENode implements AztecNode { * @returns The chain id. */ getChainId(): Promise { - throw new Error('TXE Node method getChainId not implemented'); + return Promise.resolve(this.chainId); } /** @@ -490,8 +531,13 @@ export class TXENode implements AztecNode { * @param txHash - The transaction hash. * @returns A receipt of the transaction. */ - getTxReceipt(_txHash: TxHash): Promise { - throw new Error('TXE Node method getTxReceipt not implemented'); + getTxReceipt(txHash: TxHash): Promise { + const txEffect = this.#txReceiptsByTxHash.get(txHash.toString()); + if (!txEffect) { + throw new Error('Unknown txHash'); + } + + return Promise.resolve(txEffect); } /** diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index ee5a3d457f61..f7d3ec93916c 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -105,9 +105,6 @@ export class TXE implements TypedOracle { private contractDataOracle: ContractDataOracle; private simulatorOracle: SimulatorOracle; - private version: Fr = Fr.ONE; - private chainId: Fr = Fr.ONE; - private uniqueNoteHashesFromPublic: Fr[] = []; private siloedNullifiersFromPublic: Fr[] = []; private privateLogs: PrivateLog[] = []; @@ -115,7 +112,10 @@ export class TXE implements TypedOracle { private committedBlocks = new Set(); - private node = new TXENode(this.blockNumber); + private VERSION = 1; + private CHAIN_ID = 1; + + private node: TXENode; private simulationProvider = new WASMSimulator(); @@ -133,6 +133,9 @@ export class TXE implements TypedOracle { this.noteCache = new ExecutionNoteCache(this.getTxRequestHash()); this.contractDataOracle = new ContractDataOracle(txeDatabase); this.contractAddress = AztecAddress.random(); + + this.node = new TXENode(this.blockNumber, this.VERSION, this.CHAIN_ID, this.trees); + // Default msg_sender (for entrypoints) is now Fr.max_value rather than 0 addr (see #7190 & #7404) this.msgSender = AztecAddress.fromField(Fr.MAX_FIELD_VALUE); this.simulatorOracle = new SimulatorOracle( @@ -156,12 +159,12 @@ export class TXE implements TypedOracle { return db; } - getChainId() { - return Promise.resolve(this.chainId); + getChainId(): Promise { + return Promise.resolve(this.node.getChainId().then(id => new Fr(id))); } - getVersion() { - return Promise.resolve(this.version); + getVersion(): Promise { + return Promise.resolve(this.node.getVersion().then(v => new Fr(v))); } getMsgSender() { @@ -232,8 +235,8 @@ export class TXE implements TypedOracle { const stateReference = await db.getStateReference(); const inputs = PrivateContextInputs.empty(); - inputs.txContext.chainId = this.chainId; - inputs.txContext.version = this.version; + inputs.txContext.chainId = new Fr(await this.node.getChainId()); + inputs.txContext.version = new Fr(await this.node.getVersion()); inputs.historicalHeader.globalVariables.blockNumber = new Fr(blockNumber); inputs.historicalHeader.state = stateReference; inputs.historicalHeader.lastArchive.root = Fr.fromBuffer( @@ -406,11 +409,11 @@ export class TXE implements TypedOracle { return [new Fr(index), ...siblingPath.toFields()]; } - async getSiblingPath(blockNumber: number, treeId: MerkleTreeId, leafIndex: Fr) { - const committedDb = new MerkleTreeSnapshotOperationsFacade(this.trees, blockNumber); - const result = await committedDb.getSiblingPath(treeId, leafIndex.toBigInt()); - return result.toFields(); - } + // async getSiblingPath(blockNumber: number, treeId: MerkleTreeId, leafIndex: Fr) { + // const committedDb = new MerkleTreeSnapshotOperationsFacade(this.trees, blockNumber); + // const result = await committedDb.getSiblingPath(treeId, leafIndex.toBigInt()); + // return result.toFields(); + // } async getNullifierMembershipWitness( blockNumber: number, @@ -810,8 +813,8 @@ export class TXE implements TypedOracle { const worldStateDb = new TXEWorldStateDB(db, new TXEPublicContractDataSource(this)); const globalVariables = GlobalVariables.empty(); - globalVariables.chainId = this.chainId; - globalVariables.version = this.version; + globalVariables.chainId = new Fr(await this.node.getChainId()); + globalVariables.version = new Fr(await this.node.getVersion()); globalVariables.blockNumber = new Fr(this.blockNumber); globalVariables.gasFees = new GasFees(1, 1); @@ -974,6 +977,19 @@ export class TXE implements TypedOracle { return Promise.resolve(); } + deliverNote( + _contractAddress: AztecAddress, + _storageSlot: Fr, + _nonce: Fr, + _content: Fr[], + _noteHash: Fr, + _nullifier: Fr, + _txHash: Fr, + _recipient: AztecAddress, + ): Promise { + throw new Error('deliverNote'); + } + // AVM oracles async avmOpcodeCall(targetContractAddress: AztecAddress, args: Fr[], isStaticCall: boolean): Promise {