From a124c6a36d38153f187cb5c8287ff4d0de1a017d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 12 Dec 2024 13:37:22 +0000 Subject: [PATCH 01/24] Sketching out initial approach --- .../aztec-nr/aztec/src/note/note_interface.nr | 35 +++-- .../aztec-nr/aztec/src/oracle/management.nr | 142 ++++++++++++++++++ .../aztec-nr/aztec/src/oracle/mod.nr | 1 + .../aztec-nr/aztec/src/utils/array/mod.nr | 2 + .../aztec/src/utils/array/subarray.nr | 24 +-- .../aztec-nr/aztec/src/utils/array/subbvec.nr | 95 ++++++++++++ .../crates/types/src/hash.nr | 2 +- .../crates/types/src/meta/mod.nr | 3 +- .../pxe/src/simulator_oracle/index.ts | 105 ++++++++++++- .../simulator/src/acvm/oracle/oracle.ts | 27 ++++ .../simulator/src/acvm/oracle/typed_oracle.ts | 13 ++ .../simulator/src/client/db_oracle.ts | 11 ++ .../simulator/src/client/view_data_oracle.ts | 18 +++ 13 files changed, 447 insertions(+), 31 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/management.nr create mode 100644 noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr 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 9e96ac4edb84..a049d061ff0e 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -1,6 +1,6 @@ use crate::context::PrivateContext; use crate::note::note_header::NoteHeader; -use dep::protocol_types::traits::{Empty, Serialize}; +use dep::protocol_types::traits::Empty; pub trait NoteProperties { fn properties() -> T; @@ -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/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr new file mode 100644 index 000000000000..121bb9090e0f --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/management.nr @@ -0,0 +1,142 @@ +use std::static_assert; + +use crate::{ + context::unconstrained_context::UnconstrainedContext, note::note_header::NoteHeader, + utils::array, +}; +use dep::protocol_types::{ + address::AztecAddress, + constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, + hash::compute_note_hash_nonce, +}; + +global NOTE_LOG_RESERVED_FIELDS: u32 = 2; +global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_LOG_RESERVED_FIELDS; + +pub struct NoteHashesAndNullifier { + note_hash: Field, + siloed_note_hash: Field, + inner_nullifier: Field, +} + +fn for_each_bounded_vec( + vec: BoundedVec, + f: fn[Env](T, u32) -> (), +) { + for i in 0..MaxLen { + if i < vec.len() { + f(vec.get_unchecked(i), i); + } + } +} + +// fn foo( +// serialized_content: BoundedVec, +// note_header: NoteHeader, +// note_type_id: Field, +// ) -> NoteHashesAndNullifier { +// let hashes = if note_type_id == 2 { +// assert(serialized_content.len() == ValueNote.serialization_length()); +// crate::note::utils::compute_note_hash_and_optionally_a_nullifier( +// ValueNote::deserialize, +// note_header, +// true, +// serialized_content.storage(), +// ) +// } else if note_type_id == 3 { +// assert(serialized_content.len() == AddressNote.serialization_length()); +// crate::note::utils::compute_note_hash_and_optionally_a_nullifier( +// AddressNote::deserialize, +// note_header, +// true, +// serialized_content.storage(), +// ) +// } else { +// panic(f"Unknown note type id {note_type_id}") +// }; + +// NoteHashesAndNullifier { +// note_hash: hashes[0], +// siloed_note_hash: hashes[2], +// inner_nullifier: hashes[3], +// } +// } + +pub unconstrained fn process_log( + context: UnconstrainedContext, + payload: BoundedVec, + tx_hash: Field, + siloed_note_hashes_in_tx: BoundedVec, + recipient: AztecAddress, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, NoteHeader, Field) -> NoteHashesAndNullifier, +) { + assert(payload.len() >= NOTE_LOG_RESERVED_FIELDS); + + static_assert( + NOTE_LOG_RESERVED_FIELDS == 2, + "unepxected value for NOTE_LOG_RESERVED_FIELDS", + ); + let storage_slot = payload.get(0); + let note_type_id = payload.get(1); + + let serialized_content: BoundedVec<_, MAX_NOTE_SERIALIZED_LEN> = + array::subbvec(payload, NOTE_LOG_RESERVED_FIELDS); + + for_each_bounded_vec( + siloed_note_hashes_in_tx, + |siloed_note_hash, i| { + let nonce = compute_note_hash_nonce(tx_hash, i); + + let header = NoteHeader::new(context.this_address(), nonce, storage_slot); + + let hashes = compute_note_hash_and_nullifier(serialized_content, header, note_type_id); + if siloed_note_hash == hashes.siloed_note_hash { + deliver_note( + context.this_address(), // PXE will reject any address that is not ourselves anyway + storage_slot, + nonce, + serialized_content, + hashes.note_hash, + hashes.inner_nullifier, + tx_hash, + recipient, + ); + } + }, + ); +} + +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, +) { + // TODO: do something instead of failing (e.g. not advance tagging indices) + assert(deliver_note_oracle( + contract_address, + storage_slot, + nonce, + content, + note_hash, + nullifier, + tx_hash, + recipient, + ), "Failed to deliver note"); +} + +#[oracle(deliverNote)] +unconstrained fn deliver_note_oracle( + contract_address: AztecAddress, + storage_slot: Field, + nonce: Field, + content: BoundedVec, + note_hash: Field, + nullifier: Field, + tx_hash: Field, + recipient: AztecAddress, +) -> bool {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 7fbf9f64b8b0..a4700c430641 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -12,6 +12,7 @@ pub mod get_public_data_witness; pub mod get_membership_witness; pub mod keys; pub mod key_validation_request; +pub mod management; pub mod get_sibling_path; pub mod random; pub mod enqueue_public_function_call; 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..e370b15c0fc9 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..023c7def5b28 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr @@ -0,0 +1,95 @@ +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); + } +} \ No newline at end of file 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 6f7914d48251..f6e2c4206a67 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/hash.nr @@ -42,7 +42,7 @@ pub fn private_functions_root_from_siblings( ) } -fn compute_note_hash_nonce(tx_hash: Field, note_index_in_tx: u32) -> Field { +pub 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 poseidon2_hash_with_separator( [tx_hash, note_index_in_tx as Field], 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 156d1c2c414f..a7484cbbb6d8 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/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index b6866e9a28c8..b4a257458f07 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -5,10 +5,12 @@ import { type L2Block, type L2BlockNumber, MerkleTreeId, + Note, type NoteStatus, type NullifierMembershipWitness, type PublicDataWitness, type TxEffect, + TxHash, type TxScopedL2Log, getNonNullifiedL1ToL2MessageWitness, } from '@aztec/circuit-types'; @@ -26,7 +28,8 @@ import { computeAddressSecret, computeTaggingSecret, } from '@aztec/circuits.js'; -import { type FunctionArtifact, getFunctionArtifact } from '@aztec/foundation/abi'; +import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/circuits.js/hash'; +import { type FunctionArtifact, NoteSelector, getFunctionArtifact } from '@aztec/foundation/abi'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { createLogger } from '@aztec/foundation/log'; import { type KeyStore } from '@aztec/key-store'; @@ -34,7 +37,7 @@ import { MessageLoadOracleInputs } from '@aztec/simulator/acvm'; import { type AcirSimulator, type DBOracle } from '@aztec/simulator/client'; import { type ContractDataOracle } from '../contract_data_oracle/index.js'; -import { type IncomingNoteDao } from '../database/incoming_note_dao.js'; +import { IncomingNoteDao } from '../database/incoming_note_dao.js'; import { type PxeDatabase } from '../database/index.js'; import { produceNoteDaos } from '../note_decryption_utils/produce_note_daos.js'; import { getAcirSimulator } from '../simulator/index.js'; @@ -651,4 +654,102 @@ export class SimulatorOracle implements DBOracle { }); }); } + + 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 for contract ${noteDao.contractAddress} at slot ${ + noteDao.storageSlot + } with nullifier ${noteDao.siloedNullifier.toString()}`, + ); + + const scopedSiloedNullifier = await this.produceScopedSiloedNullifier(noteDao.siloedNullifier); + + if (scopedSiloedNullifier !== undefined) { + await this.db.removeNullifiedNotes([scopedSiloedNullifier], recipient.toAddressPoint()); + this.log.verbose( + `Removed note for contract ${noteDao.contractAddress} at slot ${ + noteDao.storageSlot + } with nullifier ${noteDao.siloedNullifier.toString()}`, + ); + } + } + + 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(TxHash.fromField(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 siloedNoteHash = siloNoteHash(contractAddress, computeUniqueNoteHash(nonce, noteHash)); + const siloedNullifier = siloNullifier(contractAddress, nullifier); + + const siloedNoteHashTreeIndex = ( + await this.aztecNode.findLeavesIndexes(blockNumber!, MerkleTreeId.NOTE_HASH_TREE, [siloedNoteHash]) + )[0]; + if (siloedNoteHashTreeIndex === undefined) { + throw new Error( + `Note hash ${noteHash} (siloed as ${siloedNoteHash}) is not present on the tree at block ${blockNumber} (from tx ${txHash})`, + ); + } + + return new IncomingNoteDao( + new Note(content), + contractAddress, + storageSlot, + NoteSelector.empty(), // todo: remove + new TxHash(txHash.toBuffer()), // todo: unwrap + blockNumber!, + blockHash!.toString(), + nonce, + noteHash, + siloedNullifier, + siloedNoteHashTreeIndex, + recipient.toAddressPoint(), + ); + } + + async produceScopedSiloedNullifier(siloedNullifier: Fr): Promise | undefined> { + const siloedNullifierTreeIndex = ( + await this.aztecNode.findNullifiersIndexesWithBlock('latest', [siloedNullifier]) + )[0]; + + if (siloedNullifierTreeIndex !== undefined) { + return { + data: siloedNullifier, + l2BlockNumber: siloedNullifierTreeIndex.l2BlockNumber, + l2BlockHash: siloedNullifierTreeIndex.l2BlockHash, + }; + } + } } diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 99d5d5f29f2c..715c7ffbfb4a 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -393,4 +393,31 @@ export class Oracle { async syncNotes() { await this.typedOracle.syncNotes(); } + + async deliverNote( + [contractAddress]: ACVMField[], + [storageSlot]: ACVMField[], + [nonce]: ACVMField[], + content: ACVMField[], + [noteHash]: ACVMField[], + [nullifier]: ACVMField[], + [txHash]: ACVMField[], + [recipient]: ACVMField[], + ): Promise { + // TODO: 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), + content.map(fromACVMField), + fromACVMField(noteHash), + fromACVMField(nullifier), + fromACVMField(txHash), + AztecAddress.fromString(recipient), + ); + + return toACVMField(true); + } } diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 2505a0478b02..713c170403cd 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -248,4 +248,17 @@ export abstract class TypedOracle { syncNotes(): Promise { 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'); + } } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 6702810c86a3..8c8a19a2460a 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -242,4 +242,15 @@ export interface DBOracle extends CommitmentsDB { * @param recipient - The recipient of the logs. */ processTaggedLogs(logs: TxScopedL2Log[], recipient: AztecAddress): Promise; + + deliverNote( + contractAddress: AztecAddress, + storageSlot: Fr, + nonce: Fr, + content: Fr[], + noteHash: Fr, + nullifier: Fr, + txHash: Fr, + recipient: AztecAddress, + ): Promise; } diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 358a32711863..384c56c2d42a 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -318,4 +318,22 @@ export class ViewDataOracle extends TypedOracle { await this.db.processTaggedLogs(taggedLogs, AztecAddress.fromString(recipient)); } } + + public override async deliverNote( + contractAddress: AztecAddress, + storageSlot: Fr, + nonce: Fr, + content: Fr[], + noteHash: Fr, + nullifier: Fr, + txHash: Fr, + recipient: AztecAddress, + ) { + // TODO: allow other contracts to deliver notes + if (this.contractAddress != contractAddress) { + throw new Error(''); + } + + await this.db.deliverNote(contractAddress, storageSlot, nonce, content, noteHash, nullifier, txHash, recipient); + } } From ec9218072a046646a2cb1447dd5f724717c71901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 13 Dec 2024 19:21:20 +0000 Subject: [PATCH 02/24] Success! --- .../aztec-nr/aztec/src/oracle/management.nr | 32 ++--- .../schnorr_account_contract/src/main.nr | 35 ++++++ .../contracts/token_contract/src/main.nr | 35 ++++++ .../contracts/token_contract/src/test.nr | 24 ++-- .../transfer_to_private.test.ts | 2 +- .../add_public_values_to_payload.ts | 2 +- .../pxe/src/simulator_oracle/index.ts | 109 +++++++++++++++--- .../simulator/src/acvm/deserialize.ts | 12 ++ .../simulator/src/acvm/oracle/oracle.ts | 5 +- .../simulator/src/client/private_execution.ts | 2 +- .../simulator/src/client/simulator.ts | 2 +- .../src/client/unconstrained_execution.ts | 5 +- .../simulator/src/client/view_data_oracle.ts | 4 +- yarn-project/txe/src/oracle/txe_oracle.ts | 13 +++ 14 files changed, 231 insertions(+), 51 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr index 121bb9090e0f..5830ed3072f1 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/management.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/management.nr @@ -14,9 +14,9 @@ global NOTE_LOG_RESERVED_FIELDS: u32 = 2; global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_LOG_RESERVED_FIELDS; pub struct NoteHashesAndNullifier { - note_hash: Field, - siloed_note_hash: Field, - inner_nullifier: Field, + pub note_hash: Field, + pub siloed_note_hash: Field, + pub inner_nullifier: Field, } fn for_each_bounded_vec( @@ -31,25 +31,25 @@ fn for_each_bounded_vec( } // fn foo( -// serialized_content: BoundedVec, +// serialized_note_content: BoundedVec, // note_header: NoteHeader, // note_type_id: Field, // ) -> NoteHashesAndNullifier { // let hashes = if note_type_id == 2 { -// assert(serialized_content.len() == ValueNote.serialization_length()); +// assert(serialized_note_content.len() == ValueNote.serialization_length()); // crate::note::utils::compute_note_hash_and_optionally_a_nullifier( // ValueNote::deserialize, // note_header, // true, -// serialized_content.storage(), +// serialized_note_content.storage(), // ) // } else if note_type_id == 3 { -// assert(serialized_content.len() == AddressNote.serialization_length()); +// assert(serialized_note_content.len() == AddressNote.serialization_length()); // crate::note::utils::compute_note_hash_and_optionally_a_nullifier( // AddressNote::deserialize, // note_header, // true, -// serialized_content.storage(), +// serialized_note_content.storage(), // ) // } else { // panic(f"Unknown note type id {note_type_id}") @@ -64,23 +64,23 @@ fn for_each_bounded_vec( pub unconstrained fn process_log( context: UnconstrainedContext, - payload: BoundedVec, + log_plaintext: BoundedVec, tx_hash: Field, siloed_note_hashes_in_tx: BoundedVec, recipient: AztecAddress, compute_note_hash_and_nullifier: fn[Env](BoundedVec, NoteHeader, Field) -> NoteHashesAndNullifier, ) { - assert(payload.len() >= NOTE_LOG_RESERVED_FIELDS); + assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS); static_assert( NOTE_LOG_RESERVED_FIELDS == 2, "unepxected value for NOTE_LOG_RESERVED_FIELDS", ); - let storage_slot = payload.get(0); - let note_type_id = payload.get(1); + let storage_slot = log_plaintext.get(0); + let note_type_id = log_plaintext.get(1); - let serialized_content: BoundedVec<_, MAX_NOTE_SERIALIZED_LEN> = - array::subbvec(payload, NOTE_LOG_RESERVED_FIELDS); + let serialized_note_content: BoundedVec<_, MAX_NOTE_SERIALIZED_LEN> = + array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS); for_each_bounded_vec( siloed_note_hashes_in_tx, @@ -89,13 +89,13 @@ pub unconstrained fn process_log( let header = NoteHeader::new(context.this_address(), nonce, storage_slot); - let hashes = compute_note_hash_and_nullifier(serialized_content, header, note_type_id); + let hashes = compute_note_hash_and_nullifier(serialized_note_content, header, note_type_id); if siloed_note_hash == hashes.siloed_note_hash { deliver_note( context.this_address(), // PXE will reject any address that is not ourselves anyway storage_slot, nonce, - serialized_content, + serialized_note_content, hashes.note_hash, hashes.inner_nullifier, tx_hash, diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index 6fea7719fdf6..4af35c26878e 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -22,6 +22,7 @@ contract SchnorrAccount { use dep::aztec::prelude::{AztecAddress, PrivateContext, PrivateImmutable}; use crate::public_key_note::PublicKeyNote; + use dep::aztec::protocol_types::constants::{PRIVATE_LOG_SIZE_IN_FIELDS, MAX_NOTE_HASHES_PER_TX}; #[storage] struct Storage { @@ -120,4 +121,38 @@ contract SchnorrAccount { !is_spent & valid_in_private } + + unconstrained fn process_logs( + log_plaintext: BoundedVec, + tx_hash: Field, + siloed_note_hashes_in_tx: BoundedVec, + recipient: AztecAddress, + ) { + dep::aztec::oracle::management::process_log( + context, + log_plaintext, + tx_hash, + siloed_note_hashes_in_tx, + recipient, + |serialized_note_content: BoundedVec, note_header, note_type_id| { + let hashes = if note_type_id == PublicKeyNote::get_note_type_id() { + assert(serialized_note_content.len() == 3); + dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( + PublicKeyNote::deserialize_content, + note_header, + true, + serialized_note_content.storage(), + ) + } else { + panic(f"Unknown note type id {note_type_id}") + }; + + dep::aztec::oracle::management::NoteHashesAndNullifier { + note_hash: hashes[0], + siloed_note_hash: hashes[2], + inner_nullifier: hashes[3], + } + }, + ); + } } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 642244cf2510..ea4de130f09b 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -46,6 +46,7 @@ contract Token { // docs:end:import_authwit use crate::types::balance_set::BalanceSet; + use dep::aztec::protocol_types::constants::{PRIVATE_LOG_SIZE_IN_FIELDS, MAX_NOTE_HASHES_PER_TX}; // docs:end::imports @@ -748,6 +749,40 @@ contract Token { storage.balances.at(owner).balance_of().to_field() } // docs:end:balance_of_private + + unconstrained fn process_logs( + log_plaintext: BoundedVec, + tx_hash: Field, + siloed_note_hashes_in_tx: BoundedVec, + recipient: AztecAddress, + ) { + dep::aztec::oracle::management::process_log( + context, + log_plaintext, + tx_hash, + siloed_note_hashes_in_tx, + recipient, + |serialized_note_content: BoundedVec, note_header, note_type_id| { + let hashes = if note_type_id == UintNote::get_note_type_id() { + assert(serialized_note_content.len() == 4); + dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( + UintNote::deserialize_content, + note_header, + true, + serialized_note_content.storage(), + ) + } else { + panic(f"Unknown note type id {note_type_id}") + }; + + dep::aztec::oracle::management::NoteHashesAndNullifier { + note_hash: hashes[0], + siloed_note_hash: hashes[2], + inner_nullifier: hashes[3], + } + }, + ); + } } // docs:end:token_all diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr index 1ed2b177ced8..645de92ee2c2 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr @@ -1,12 +1,12 @@ -mod access_control; -mod burn_private; -mod burn_public; -mod mint_to_public; -mod reading_constants; -mod refunds; -mod transfer; -mod transfer_in_private; -mod transfer_in_public; -mod transfer_to_private; -mod transfer_to_public; -mod utils; +// mod access_control; +// mod burn_private; +// mod burn_public; +// mod mint_to_public; +// mod reading_constants; +// mod refunds; +// mod transfer; +// mod transfer_in_private; +// mod transfer_in_public; +// mod transfer_to_private; +// mod transfer_to_public; +// mod utils; diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts index c0f6c1368b2e..5fd9623d50d0 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts @@ -21,7 +21,7 @@ describe('e2e_token_contract transfer_to_private', () => { await t.tokenSim.check(); }); - it('to self', async () => { + it.only('to self', async () => { const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); const amount = balancePub / 2n; expect(amount).toBeGreaterThan(0n); 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/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index b4a257458f07..cd2745efd810 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -1,5 +1,6 @@ import { type AztecNode, + type FunctionCall, type InBlock, L1NotePayload, type L2Block, @@ -15,28 +16,36 @@ import { getNonNullifiedL1ToL2MessageWitness, } from '@aztec/circuit-types'; import { - type AztecAddress, + AztecAddress, type BlockHeader, 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, computeTaggingSecret, } from '@aztec/circuits.js'; import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/circuits.js/hash'; -import { type FunctionArtifact, NoteSelector, getFunctionArtifact } from '@aztec/foundation/abi'; +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'; import { MessageLoadOracleInputs } from '@aztec/simulator/acvm'; import { type AcirSimulator, type DBOracle } from '@aztec/simulator/client'; -import { type ContractDataOracle } from '../contract_data_oracle/index.js'; +import { ContractDataOracle } from '../contract_data_oracle/index.js'; import { IncomingNoteDao } from '../database/incoming_note_dao.js'; import { type PxeDatabase } from '../database/index.js'; import { produceNoteDaos } from '../note_decryption_utils/produce_note_daos.js'; @@ -422,6 +431,8 @@ export class SimulatorOracle implements DBOracle { maxBlockNumber: number, scopes?: AztecAddress[], ): Promise> { + this.log.verbose("Searching for tagged logs", { contract: contractAddress }); + const recipients = scopes ? scopes : await this.keyStore.getAccounts(); const result = new Map(); const contractName = await this.contractDataOracle.getDebugContractName(contractAddress); @@ -619,8 +630,31 @@ export class SimulatorOracle implements DBOracle { logs: TxScopedL2Log[], recipient: AztecAddress, simulator?: AcirSimulator, + useNewFlow = true, ): Promise { const { incomingNotes } = await this.#decryptTaggedLogs(logs, recipient, simulator); + + if (useNewFlow) { + for (const note of incomingNotes) { + const plaintext = [note.storageSlot, note.noteTypeId.toField(), ...note.note.items]; + + const txEffect = await this.aztecNode.getTxEffect(note.txHash); + if (!txEffect) { + throw new Error(`Could not find tx effect for tx hash ${note.txHash}`); + } + + await this.callProcessLogs( + note.contractAddress, + plaintext, + note.txHash, + txEffect.data.noteHashes, + recipient, + simulator, + ); + } + return; + } + if (incomingNotes.length) { await this.db.addNotes(incomingNotes, recipient); incomingNotes.forEach(noteDao => { @@ -677,21 +711,21 @@ export class SimulatorOracle implements DBOracle { ); await this.db.addNotes([noteDao], recipient); - this.log.verbose( - `Added note for contract ${noteDao.contractAddress} at slot ${ - noteDao.storageSlot - } with nullifier ${noteDao.siloedNullifier.toString()}`, - ); + this.log.verbose('Added note', { + contract: noteDao.contractAddress, + slot: noteDao.storageSlot, + nullifier: noteDao.siloedNullifier.toString, + }); const scopedSiloedNullifier = await this.produceScopedSiloedNullifier(noteDao.siloedNullifier); if (scopedSiloedNullifier !== undefined) { await this.db.removeNullifiedNotes([scopedSiloedNullifier], recipient.toAddressPoint()); - this.log.verbose( - `Removed note for contract ${noteDao.contractAddress} at slot ${ - noteDao.storageSlot - } with nullifier ${noteDao.siloedNullifier.toString()}`, - ); + this.log.verbose('Removed just-added note as nullified', { + contract: noteDao.contractAddress, + slot: noteDao.storageSlot, + nullifier: noteDao.siloedNullifier.toString, + }); } } @@ -752,4 +786,51 @@ export class SimulatorOracle implements DBOracle { }; } } + + async callProcessLogs( + contractAddress: AztecAddress, + logPlaintext: Fr[], + txHash: TxHash, + noteHashes: Fr[], + recipient: AztecAddress, + simulator?: AcirSimulator, + ) { + const artifact: FunctionArtifact | undefined = await new ContractDataOracle(this.db).getFunctionArtifactByName( + contractAddress, + 'process_logs', + ); + if (!artifact) { + throw new Error( + `Mandatory implementation of "process_logs" 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), + recipient, + ]), + returnTypes: artifact.returnTypes, + }; + + await ( + simulator ?? getAcirSimulator(this.db, this.aztecNode, this.keyStore, this.contractDataOracle) + ).runUnconstrained( + execRequest, + artifact, + contractAddress, + [], // empty scope as this call should not require access to private information + ); + } +} + +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/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 715c7ffbfb4a..939e5a3b5824 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -4,7 +4,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; 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'; @@ -399,6 +399,7 @@ export class Oracle { [storageSlot]: ACVMField[], [nonce]: ACVMField[], content: ACVMField[], + [contentLength]: ACVMField[], [noteHash]: ACVMField[], [nullifier]: ACVMField[], [txHash]: ACVMField[], @@ -411,7 +412,7 @@ export class Oracle { AztecAddress.fromString(contractAddress), fromACVMField(storageSlot), fromACVMField(nonce), - content.map(fromACVMField), + fromBoundedVec(content, contentLength), fromACVMField(noteHash), fromACVMField(nullifier), fromACVMField(txHash), diff --git a/yarn-project/simulator/src/client/private_execution.ts b/yarn-project/simulator/src/client/private_execution.ts index 199d395696bc..8c63e00b57cc 100644 --- a/yarn-project/simulator/src/client/private_execution.ts +++ b/yarn-project/simulator/src/client/private_execution.ts @@ -27,7 +27,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 9bcdc07bd7dd..e42b4060b098 100644 --- a/yarn-project/simulator/src/client/simulator.ts +++ b/yarn-project/simulator/src/client/simulator.ts @@ -179,7 +179,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 b9066fd05002..d8e813a180f8 100644 --- a/yarn-project/simulator/src/client/unconstrained_execution.ts +++ b/yarn-project/simulator/src/client/unconstrained_execution.ts @@ -20,7 +20,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 384c56c2d42a..5bfbc39d0edb 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -330,8 +330,8 @@ export class ViewDataOracle extends TypedOracle { recipient: AztecAddress, ) { // TODO: allow other contracts to deliver notes - if (this.contractAddress != contractAddress) { - throw new Error(''); + 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); diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 35340639972f..4fce665c9846 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -928,6 +928,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 { From 2f52d3192953b7a1c545fc717dfe0735b368c02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 13 Dec 2024 19:44:52 +0000 Subject: [PATCH 03/24] IT LIVES --- .../pxe/src/simulator_oracle/index.ts | 90 +++---------------- 1 file changed, 14 insertions(+), 76 deletions(-) diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index cd2745efd810..c0997a8328ce 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -630,63 +630,26 @@ export class SimulatorOracle implements DBOracle { logs: TxScopedL2Log[], recipient: AztecAddress, simulator?: AcirSimulator, - useNewFlow = true, ): Promise { const { incomingNotes } = await this.#decryptTaggedLogs(logs, recipient, simulator); + for (const note of incomingNotes) { + const plaintext = [note.storageSlot, note.noteTypeId.toField(), ...note.note.items]; - if (useNewFlow) { - for (const note of incomingNotes) { - const plaintext = [note.storageSlot, note.noteTypeId.toField(), ...note.note.items]; - - const txEffect = await this.aztecNode.getTxEffect(note.txHash); - if (!txEffect) { - throw new Error(`Could not find tx effect for tx hash ${note.txHash}`); - } - - await this.callProcessLogs( - note.contractAddress, - plaintext, - note.txHash, - txEffect.data.noteHashes, - recipient, - simulator, - ); + const txEffect = await this.aztecNode.getTxEffect(note.txHash); + if (!txEffect) { + throw new Error(`Could not find tx effect for tx hash ${note.txHash}`); } - return; - } - if (incomingNotes.length) { - await this.db.addNotes(incomingNotes, recipient); - incomingNotes.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(), - }); - }); + await this.callProcessLogs( + note.contractAddress, + plaintext, + note.txHash, + txEffect.data.noteHashes, + recipient, + simulator, + ); } - const nullifiedNotes: IncomingNoteDao[] = []; - const currentNotesForRecipient = await this.db.getIncomingNotes({ owner: recipient }); - const nullifiersToCheck = currentNotesForRecipient.map(note => note.siloedNullifier); - const currentBlockNumber = await this.getBlockNumber(); - const nullifierIndexes = await this.aztecNode.findNullifiersIndexesWithBlock(currentBlockNumber, nullifiersToCheck); - - const foundNullifiers = nullifiersToCheck - .map((nullifier, i) => { - if (nullifierIndexes[i] !== undefined) { - return { ...nullifierIndexes[i], ...{ data: nullifier } } as InBlock; - } - }) - .filter(nullifier => nullifier !== undefined) as InBlock[]; - - await this.db.removeNullifiedNotes(foundNullifiers, recipient.toAddressPoint()); - nullifiedNotes.forEach(noteDao => { - this.log.verbose(`Removed note for contract ${noteDao.contractAddress} at slot ${noteDao.storageSlot}`, { - contract: noteDao.contractAddress, - slot: noteDao.storageSlot, - nullifier: noteDao.siloedNullifier.toString(), - }); - }); + return; } public async deliverNote( @@ -716,17 +679,6 @@ export class SimulatorOracle implements DBOracle { slot: noteDao.storageSlot, nullifier: noteDao.siloedNullifier.toString, }); - - const scopedSiloedNullifier = await this.produceScopedSiloedNullifier(noteDao.siloedNullifier); - - if (scopedSiloedNullifier !== undefined) { - await this.db.removeNullifiedNotes([scopedSiloedNullifier], recipient.toAddressPoint()); - this.log.verbose('Removed just-added note as nullified', { - contract: noteDao.contractAddress, - slot: noteDao.storageSlot, - nullifier: noteDao.siloedNullifier.toString, - }); - } } async produceNoteDao( @@ -773,20 +725,6 @@ export class SimulatorOracle implements DBOracle { ); } - async produceScopedSiloedNullifier(siloedNullifier: Fr): Promise | undefined> { - const siloedNullifierTreeIndex = ( - await this.aztecNode.findNullifiersIndexesWithBlock('latest', [siloedNullifier]) - )[0]; - - if (siloedNullifierTreeIndex !== undefined) { - return { - data: siloedNullifier, - l2BlockNumber: siloedNullifierTreeIndex.l2BlockNumber, - l2BlockHash: siloedNullifierTreeIndex.l2BlockHash, - }; - } - } - async callProcessLogs( contractAddress: AztecAddress, logPlaintext: Fr[], From a443446a3bc3176d7617f26c427bf7316d8ab8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 13 Dec 2024 20:23:19 +0000 Subject: [PATCH 04/24] Misc doc improvements --- .../aztec-nr/aztec/src/oracle/management.nr | 143 ++++++++++-------- .../schnorr_account_contract/src/main.nr | 4 +- .../contracts/token_contract/src/main.nr | 4 +- .../contracts/token_contract/src/test.nr | 24 +-- .../transfer_to_private.test.ts | 2 +- .../simulator/src/acvm/oracle/oracle.ts | 6 +- .../simulator/src/client/db_oracle.ts | 13 ++ .../simulator/src/client/view_data_oracle.ts | 2 +- 8 files changed, 114 insertions(+), 84 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr index 5830ed3072f1..d149ae5a2d0d 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/management.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/management.nr @@ -10,6 +10,8 @@ use dep::protocol_types::{ hash::compute_note_hash_nonce, }; +// We reserve two fields in the note log that are not part of the note content: one for the storage slot, and one for +// the note type id. global NOTE_LOG_RESERVED_FIELDS: u32 = 2; global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_LOG_RESERVED_FIELDS; @@ -30,83 +32,95 @@ fn for_each_bounded_vec( } } -// fn foo( -// serialized_note_content: BoundedVec, -// note_header: NoteHeader, -// note_type_id: Field, -// ) -> NoteHashesAndNullifier { -// let hashes = if note_type_id == 2 { -// assert(serialized_note_content.len() == ValueNote.serialization_length()); -// crate::note::utils::compute_note_hash_and_optionally_a_nullifier( -// ValueNote::deserialize, -// note_header, -// true, -// serialized_note_content.storage(), -// ) -// } else if note_type_id == 3 { -// assert(serialized_note_content.len() == AddressNote.serialization_length()); -// crate::note::utils::compute_note_hash_and_optionally_a_nullifier( -// AddressNote::deserialize, -// note_header, -// true, -// serialized_note_content.storage(), -// ) -// } else { -// panic(f"Unknown note type id {note_type_id}") -// }; - -// NoteHashesAndNullifier { -// note_hash: hashes[0], -// siloed_note_hash: hashes[2], -// inner_nullifier: hashes[3], -// } -// } - +/// 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 siloed 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], +/// siloed_note_hash: hashes[2], +/// inner_nullifier: hashes[3], +/// }) +/// } +/// ``` pub unconstrained fn process_log( context: UnconstrainedContext, log_plaintext: BoundedVec, tx_hash: Field, siloed_note_hashes_in_tx: BoundedVec, recipient: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, NoteHeader, Field) -> NoteHashesAndNullifier, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, NoteHeader, Field) -> Option, ) { - assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS); - - static_assert( - NOTE_LOG_RESERVED_FIELDS == 2, - "unepxected value for NOTE_LOG_RESERVED_FIELDS", - ); - let storage_slot = log_plaintext.get(0); - let note_type_id = log_plaintext.get(1); - - let serialized_note_content: BoundedVec<_, MAX_NOTE_SERIALIZED_LEN> = - array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS); + 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 siloed note hashes from tx_hash for_each_bounded_vec( siloed_note_hashes_in_tx, - |siloed_note_hash, i| { - let nonce = compute_note_hash_nonce(tx_hash, i); + |expected_siloed_note_hash, i| { + let candidate_nonce = compute_note_hash_nonce(tx_hash, i); - let header = NoteHeader::new(context.this_address(), nonce, storage_slot); + let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot); - let hashes = compute_note_hash_and_nullifier(serialized_note_content, header, note_type_id); - if siloed_note_hash == hashes.siloed_note_hash { + // TODO: handle failed note_hash_and_nullifier computation + let hashes = + compute_note_hash_and_nullifier(serialized_note_content, header, note_type_id).unwrap(); + + if hashes.siloed_note_hash == expected_siloed_note_hash { + // TODO(#10726): push these into a vec to deliver all at once instead of having one oracle call per note deliver_note( - context.this_address(), // PXE will reject any address that is not ourselves anyway + context.this_address(), // TODO(#10727): allow other contracts to deliver notes storage_slot, - nonce, + candidate_nonce, serialized_note_content, hashes.note_hash, hashes.inner_nullifier, tx_hash, recipient, ); + + // We don't exit the loop - it is possible (though rare) for the same note content to be present + // multiple times in the same transaction with different nonces. } }, ); } -pub unconstrained fn deliver_note( +unconstrained fn destructure_log_plaintext( + log_plaintext: BoundedVec, +) -> (Field, Field, BoundedVec) { + assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS); + + static_assert( + NOTE_LOG_RESERVED_FIELDS == 2, + "unepxected value for NOTE_LOG_RESERVED_FIELDS", + ); + let storage_slot = log_plaintext.get(0); + let note_type_id = log_plaintext.get(1); + + let serialized_note_content = array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS); + + (storage_slot, note_type_id, serialized_note_content) +} + +unconstrained fn deliver_note( contract_address: AztecAddress, storage_slot: Field, nonce: Field, @@ -116,17 +130,20 @@ pub unconstrained fn deliver_note( tx_hash: Field, recipient: AztecAddress, ) { - // TODO: do something instead of failing (e.g. not advance tagging indices) - assert(deliver_note_oracle( - contract_address, - storage_slot, - nonce, - content, - note_hash, - nullifier, - tx_hash, - recipient, - ), "Failed to deliver note"); + // TODO(#10728): do something instead of failing (e.g. not advance tagging indices) + assert( + deliver_note_oracle( + contract_address, + storage_slot, + nonce, + content, + note_hash, + nullifier, + tx_hash, + recipient, + ), + "Failed to deliver note", + ); } #[oracle(deliverNote)] diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index 4af35c26878e..7e4d340e2b6a 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -147,11 +147,11 @@ contract SchnorrAccount { panic(f"Unknown note type id {note_type_id}") }; - dep::aztec::oracle::management::NoteHashesAndNullifier { + Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { note_hash: hashes[0], siloed_note_hash: hashes[2], inner_nullifier: hashes[3], - } + }) }, ); } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index ea4de130f09b..11fe370d1ed5 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -775,11 +775,11 @@ contract Token { panic(f"Unknown note type id {note_type_id}") }; - dep::aztec::oracle::management::NoteHashesAndNullifier { + Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { note_hash: hashes[0], siloed_note_hash: hashes[2], inner_nullifier: hashes[3], - } + }) }, ); } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr index 645de92ee2c2..1ed2b177ced8 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr @@ -1,12 +1,12 @@ -// mod access_control; -// mod burn_private; -// mod burn_public; -// mod mint_to_public; -// mod reading_constants; -// mod refunds; -// mod transfer; -// mod transfer_in_private; -// mod transfer_in_public; -// mod transfer_to_private; -// mod transfer_to_public; -// mod utils; +mod access_control; +mod burn_private; +mod burn_public; +mod mint_to_public; +mod reading_constants; +mod refunds; +mod transfer; +mod transfer_in_private; +mod transfer_in_public; +mod transfer_to_private; +mod transfer_to_public; +mod utils; diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts index 5fd9623d50d0..c0f6c1368b2e 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts @@ -21,7 +21,7 @@ describe('e2e_token_contract transfer_to_private', () => { await t.tokenSim.check(); }); - it.only('to self', async () => { + it('to self', async () => { const balancePub = await asset.methods.balance_of_public(accounts[0].address).simulate(); const amount = balancePub / 2n; expect(amount).toBeGreaterThan(0n); diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 939e5a3b5824..f75c8f624173 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -405,9 +405,9 @@ export class Oracle { [txHash]: ACVMField[], [recipient]: ACVMField[], ): Promise { - // TODO: 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. + // 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), diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 8c8a19a2460a..2a3f8ce8cc7a 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -243,6 +243,19 @@ 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 be 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, diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 5bfbc39d0edb..eac4678b6d7e 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -329,7 +329,7 @@ export class ViewDataOracle extends TypedOracle { txHash: Fr, recipient: AztecAddress, ) { - // TODO: allow other contracts to deliver notes + // 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}`); } From dae83d79b09959c9f0bf8a7d4f2ed5e8de42fc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 13 Dec 2024 20:26:13 +0000 Subject: [PATCH 05/24] Some more minor comments --- yarn-project/pxe/src/simulator_oracle/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index c0997a8328ce..1c946a2ba794 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -632,6 +632,11 @@ export class SimulatorOracle implements DBOracle { simulator?: AcirSimulator, ): Promise { const { incomingNotes } = await this.#decryptTaggedLogs(logs, recipient, simulator); + + // We've produced the full IncomingNoteDao, 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 note of incomingNotes) { const plaintext = [note.storageSlot, note.noteTypeId.toField(), ...note.note.items]; @@ -640,6 +645,7 @@ export class SimulatorOracle implements DBOracle { throw new Error(`Could not find tx effect for tx hash ${note.txHash}`); } + // This will trigger calls to the deliverNote oracle await this.callProcessLogs( note.contractAddress, plaintext, From 40d2dae75f46d6405c192a977b3cd9b9b75cabe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Sat, 14 Dec 2024 14:48:47 +0000 Subject: [PATCH 06/24] Remove old ts code --- .../pxe/src/database/incoming_note_dao.ts | 30 +------ .../pxe/src/database/outgoing_note_dao.ts | 29 +----- .../brute_force_note_info.ts | 90 ------------------- .../pxe/src/note_decryption_utils/index.ts | 2 - .../produce_note_daos.ts | 67 -------------- .../produce_note_daos_for_key.ts | 57 ------------ .../pxe/src/simulator_oracle/index.ts | 86 +++++++----------- 7 files changed, 33 insertions(+), 328 deletions(-) delete mode 100644 yarn-project/pxe/src/note_decryption_utils/brute_force_note_info.ts delete mode 100644 yarn-project/pxe/src/note_decryption_utils/index.ts delete mode 100644 yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts delete mode 100644 yarn-project/pxe/src/note_decryption_utils/produce_note_daos_for_key.ts diff --git a/yarn-project/pxe/src/database/incoming_note_dao.ts b/yarn-project/pxe/src/database/incoming_note_dao.ts index 2c9c62821cb7..3287ec9230f5 100644 --- a/yarn-project/pxe/src/database/incoming_note_dao.ts +++ b/yarn-project/pxe/src/database/incoming_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/acvm'; -import { type NoteInfo } from '../note_decryption_utils/index.js'; - /** * A note with contextual data which was decrypted as incoming. */ @@ -44,32 +42,6 @@ export class IncomingNoteDao implements NoteData { public addressPoint: PublicKey, ) {} - static fromPayloadAndNoteInfo( - note: Note, - payload: L1NotePayload, - noteInfo: NoteInfo, - l2BlockNumber: number, - l2BlockHash: string, - dataStartIndexForTx: number, - addressPoint: PublicKey, - ) { - const noteHashIndexInTheWholeTree = BigInt(dataStartIndexForTx + noteInfo.noteHashIndex); - return new IncomingNoteDao( - note, - payload.contractAddress, - payload.storageSlot, - payload.noteTypeId, - noteInfo.txHash, - l2BlockNumber, - l2BlockHash, - noteInfo.nonce, - noteInfo.noteHash, - noteInfo.siloedNullifier, - noteHashIndexInTheWholeTree, - addressPoint, - ); - } - 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 386b23ecd573..6fce4779ff04 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/brute_force_note_info.ts b/yarn-project/pxe/src/note_decryption_utils/brute_force_note_info.ts deleted file mode 100644 index 3632949065d0..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 siloedNoteHashes - 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, - siloedNoteHashes: Fr[], - txHash: TxHash, - 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 siloedNoteHash: Fr | undefined; - let innerNullifier: Fr | undefined; - const firstNullifier = Fr.fromBuffer(txHash.toBuffer()); - - for (; noteHashIndex < siloedNoteHashes.length; ++noteHashIndex) { - if (excludedIndices.has(noteHashIndex)) { - continue; - } - - const siloedNoteHashFromTxEffect = siloedNoteHashes[noteHashIndex]; - if (siloedNoteHashFromTxEffect.equals(Fr.ZERO)) { - break; - } - - const expectedNonce = computeNoteHashNonce(firstNullifier, noteHashIndex); - ({ noteHash, siloedNoteHash, innerNullifier } = await simulator.computeNoteHashAndOptionallyANullifier( - contractAddress, - expectedNonce, - storageSlot, - noteTypeId, - computeNullifier, - note, - )); - - if (siloedNoteHashFromTxEffect.equals(siloedNoteHash)) { - 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 ca05f03cfa82..000000000000 --- a/yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts +++ /dev/null @@ -1,67 +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 { IncomingNoteDao } from '../database/incoming_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, - l2BlockNumber: number, - l2BlockHash: string, - noteHashes: Fr[], - dataStartIndexForTx: number, - excludedIndices: Set, - logger: Logger, -): Promise<{ incomingNote: IncomingNoteDao | undefined }> { - if (!addressPoint) { - throw new Error('addressPoint is undefined. Cannot create note.'); - } - - let incomingNote: IncomingNoteDao | undefined; - - if (addressPoint) { - incomingNote = await produceNoteDaosForKey( - simulator, - db, - addressPoint, - payload, - txHash, - l2BlockNumber, - l2BlockHash, - noteHashes, - dataStartIndexForTx, - excludedIndices, - logger, - IncomingNoteDao.fromPayloadAndNoteInfo, - ); - } - - return { - incomingNote, - }; -} 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 291d9efd80d9..000000000000 --- a/yarn-project/pxe/src/note_decryption_utils/produce_note_daos_for_key.ts +++ /dev/null @@ -1,57 +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, - 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, - 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 1c946a2ba794..e442abc1f40f 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -1,7 +1,7 @@ import { type AztecNode, type FunctionCall, - type InBlock, + L1NotePayload, type L2Block, type L2BlockNumber, @@ -10,13 +10,12 @@ import { type NoteStatus, type NullifierMembershipWitness, type PublicDataWitness, - type TxEffect, - TxHash, + TxHash, type TxScopedL2Log, getNonNullifiedL1ToL2MessageWitness, } from '@aztec/circuit-types'; import { - AztecAddress, + type AztecAddress, type BlockHeader, type CompleteAddress, type ContractInstance, @@ -48,8 +47,8 @@ import { type AcirSimulator, type DBOracle } from '@aztec/simulator/client'; import { ContractDataOracle } from '../contract_data_oracle/index.js'; import { IncomingNoteDao } from '../database/incoming_note_dao.js'; import { type PxeDatabase } from '../database/index.js'; -import { produceNoteDaos } from '../note_decryption_utils/produce_note_daos.js'; import { getAcirSimulator } from '../simulator/index.js'; +import { getOrderedNoteItems } from '../note_decryption_utils/add_public_values_to_payload.js'; /** * A data oracle that provides information needed for simulating a transaction. @@ -558,10 +557,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, @@ -571,54 +569,30 @@ 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 incomingNotes: IncomingNoteDao[] = []; + const decrypted = []; - const txEffectsCache = new Map | undefined>(); for (const scopedLog of scopedLogs) { - const incomingNotePayload = scopedLog.isFromPublic + const payload = scopedLog.isFromPublic ? L1NotePayload.decryptAsIncomingFromPublic(scopedLog.logData, addressSecret) : L1NotePayload.decryptAsIncoming(PrivateLog.fromBuffer(scopedLog.logData), addressSecret); - if (incomingNotePayload) { - const payload = incomingNotePayload; - - 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; - } - - txEffectsCache.set(scopedLog.txHash.toString(), txEffect); - - if (!excludedIndices.has(scopedLog.txHash.toString())) { - excludedIndices.set(scopedLog.txHash.toString(), new Set()); - } - const { incomingNote } = 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.contractDataOracle), - this.db, - incomingNotePayload ? recipient.toAddressPoint() : undefined, - payload!, - txEffect.data.txHash, - txEffect.l2BlockNumber, - txEffect.l2BlockHash, - txEffect.data.noteHashes, - scopedLog.dataStartIndexForTx, - excludedIndices.get(scopedLog.txHash.toString())!, - this.log, - ); + if (!payload) { + this.log.verbose("Unable to decrypt log"); + continue; + } - if (incomingNote) { - incomingNotes.push(incomingNote); - } + if (!excludedIndices.has(scopedLog.txHash.toString())) { + excludedIndices.set(scopedLog.txHash.toString(), new Set()); } + + const note = await getOrderedNoteItems(this.db, payload); + const plaintext = [payload.storageSlot, payload.noteTypeId.toField(), ...note.items]; + + decrypted.push({ plaintext , txHash: scopedLog.txHash , contractAddress: payload.contractAddress }); } - return { incomingNotes }; + + return decrypted; } /** @@ -631,25 +605,26 @@ export class SimulatorOracle implements DBOracle { recipient: AztecAddress, simulator?: AcirSimulator, ): Promise { - const { incomingNotes } = await this.#decryptTaggedLogs(logs, recipient, simulator); + const decryptedLogs = await this.#decryptTaggedLogs(logs, recipient); // We've produced the full IncomingNoteDao, 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 note of incomingNotes) { - const plaintext = [note.storageSlot, note.noteTypeId.toField(), ...note.note.items]; - - const txEffect = await this.aztecNode.getTxEffect(note.txHash); + 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 ${note.txHash}`); + throw new Error(`Could not find tx effect for tx hash ${decryptedLog.txHash}`); } // This will trigger calls to the deliverNote oracle await this.callProcessLogs( - note.contractAddress, - plaintext, - note.txHash, + decryptedLog.contractAddress, + decryptedLog.plaintext, + decryptedLog.txHash, txEffect.data.noteHashes, recipient, simulator, @@ -658,6 +633,7 @@ export class SimulatorOracle implements DBOracle { return; } + // Called when notes are delivered, usually as a result to a call to the process_logs contract function public async deliverNote( contractAddress: AztecAddress, storageSlot: Fr, From 893ad80267c987ac32dbe7df39a204de3df32de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Sat, 14 Dec 2024 14:49:06 +0000 Subject: [PATCH 07/24] noir formatting --- .../aztec-nr/aztec/src/oracle/management.nr | 8 ++++++-- .../aztec-nr/aztec/src/utils/array/subarray.nr | 2 +- .../aztec-nr/aztec/src/utils/array/subbvec.nr | 11 ++++------- .../contracts/schnorr_account_contract/src/main.nr | 14 ++++++++------ .../contracts/token_contract/src/main.nr | 14 ++++++++------ 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr index d149ae5a2d0d..1d2a61c30275 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/management.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/management.nr @@ -80,8 +80,12 @@ pub unconstrained fn process_log( let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot); // TODO: handle failed note_hash_and_nullifier computation - let hashes = - compute_note_hash_and_nullifier(serialized_note_content, header, note_type_id).unwrap(); + let hashes = compute_note_hash_and_nullifier( + serialized_note_content, + header, + note_type_id, + ) + .unwrap(); if hashes.siloed_note_hash == expected_siloed_note_hash { // TODO(#10726): push these into a vec to deliver all at once instead of having one oracle call per note 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 e370b15c0fc9..d7e963738b82 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/subarray.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/subarray.nr @@ -48,7 +48,7 @@ mod test { assert_eq(subarray([1, 2, 3, 4, 5], 1), [2]); } - #[test(should_fail_with="DST_LEN too large for offset")] + #[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 index 023c7def5b28..f08bed659423 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr @@ -23,10 +23,7 @@ pub fn subbvec( // 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(vec.storage(), offset), vec.len() - offset) } mod test { @@ -76,7 +73,7 @@ mod test { let _: BoundedVec<_, 1> = subbvec(bvec, 2); } - #[test(should_fail_with="DST_LEN too large for offset")] + #[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]); @@ -84,7 +81,7 @@ mod test { let _: BoundedVec<_, 11> = subbvec(bvec, 0); } - #[test(should_fail_with="DST_LEN too large for offset")] + #[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]); @@ -92,4 +89,4 @@ mod test { // which is less than 7. let _: BoundedVec<_, 7> = subbvec(bvec, 4); } -} \ No newline at end of file +} diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index 7e4d340e2b6a..8ed8d38f3311 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -22,7 +22,7 @@ contract SchnorrAccount { use dep::aztec::prelude::{AztecAddress, PrivateContext, PrivateImmutable}; use crate::public_key_note::PublicKeyNote; - use dep::aztec::protocol_types::constants::{PRIVATE_LOG_SIZE_IN_FIELDS, MAX_NOTE_HASHES_PER_TX}; + use dep::aztec::protocol_types::constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}; #[storage] struct Storage { @@ -147,11 +147,13 @@ contract SchnorrAccount { panic(f"Unknown note type id {note_type_id}") }; - Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { - note_hash: hashes[0], - siloed_note_hash: hashes[2], - inner_nullifier: hashes[3], - }) + Option::some( + dep::aztec::oracle::management::NoteHashesAndNullifier { + note_hash: hashes[0], + siloed_note_hash: hashes[2], + inner_nullifier: hashes[3], + }, + ) }, ); } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 11fe370d1ed5..a0eb0ad53135 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -46,7 +46,7 @@ contract Token { // docs:end:import_authwit use crate::types::balance_set::BalanceSet; - use dep::aztec::protocol_types::constants::{PRIVATE_LOG_SIZE_IN_FIELDS, MAX_NOTE_HASHES_PER_TX}; + use dep::aztec::protocol_types::constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}; // docs:end::imports @@ -775,11 +775,13 @@ contract Token { panic(f"Unknown note type id {note_type_id}") }; - Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { - note_hash: hashes[0], - siloed_note_hash: hashes[2], - inner_nullifier: hashes[3], - }) + Option::some( + dep::aztec::oracle::management::NoteHashesAndNullifier { + note_hash: hashes[0], + siloed_note_hash: hashes[2], + inner_nullifier: hashes[3], + }, + ) }, ); } From 206444f4e51e8a4f950c2dcfa594d0987cf2a43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 9 Jan 2025 19:01:43 +0000 Subject: [PATCH 08/24] It works! --- .../aztec-nr/aztec/src/macros/mod.nr | 61 +++++++++++++++++++ .../aztec-nr/aztec/src/oracle/management.nr | 22 ++++--- .../schnorr_account_contract/src/main.nr | 37 ----------- .../contracts/token_contract/src/main.nr | 37 ----------- .../pxe/src/simulator_oracle/index.ts | 14 +++-- 5 files changed, 81 insertions(+), 90 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 883a20283269..568193b21be0 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_logs(); 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 } @@ -165,6 +169,63 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { } } +comptime fn generate_process_logs() -> Quoted { + let mut if_note_type_id_match_statements_list = &[]; + + let notes = NOTES.entries(); + 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() { + assert_eq(serialized_note_content.len(), $serialized_note_length); + 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 {}); + + quote { + unconstrained fn process_logs( + log_plaintext: BoundedVec, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + recipient: AztecAddress, + ) { + let context = dep::aztec::context::unconstrained_context::UnconstrainedContext::new(); + + dep::aztec::oracle::management::process_log( + context, + log_plaintext, + tx_hash, + unique_note_hashes_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::oracle::management::NoteHashesAndNullifier { + note_hash: hashes[0], + unique_note_hash: hashes[1], + inner_nullifier: hashes[3], + }, + ) + } + ); + } + } +} + 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/oracle/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr index 1d2a61c30275..e11cfc4eca47 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/management.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/management.nr @@ -17,7 +17,7 @@ global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_LOG_RESE pub struct NoteHashesAndNullifier { pub note_hash: Field, - pub siloed_note_hash: Field, + pub unique_note_hash: Field, pub inner_nullifier: Field, } @@ -34,7 +34,7 @@ fn for_each_bounded_vec( /// 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 siloed note hashes in said transaction. +/// 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: @@ -55,7 +55,7 @@ fn for_each_bounded_vec( /// /// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { /// note_hash: hashes[0], -/// siloed_note_hash: hashes[2], +/// unique_note_hash: hashes[1], /// inner_nullifier: hashes[3], /// }) /// } @@ -64,17 +64,17 @@ pub unconstrained fn process_log( context: UnconstrainedContext, log_plaintext: BoundedVec, tx_hash: Field, - siloed_note_hashes_in_tx: BoundedVec, + unique_note_hashes_in_tx: BoundedVec, 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 siloed note hashes from tx_hash + // We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash for_each_bounded_vec( - siloed_note_hashes_in_tx, - |expected_siloed_note_hash, i| { + unique_note_hashes_in_tx, + |expected_unique_note_hash, i| { let candidate_nonce = compute_note_hash_nonce(tx_hash, i); let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot); @@ -87,7 +87,8 @@ pub unconstrained fn process_log( ) .unwrap(); - if hashes.siloed_note_hash == expected_siloed_note_hash { + + if hashes.unique_note_hash == expected_unique_note_hash { // TODO(#10726): push these into a vec to deliver all at once instead of having one oracle call per note deliver_note( context.this_address(), // TODO(#10727): allow other contracts to deliver notes @@ -100,8 +101,9 @@ pub unconstrained fn process_log( recipient, ); - // We don't exit the loop - it is possible (though rare) for the same note content to be present - // multiple times in the same transaction with different nonces. + // 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. } }, ); diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index f94d75639cac..09a935f95e3c 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -22,7 +22,6 @@ contract SchnorrAccount { use dep::aztec::prelude::{AztecAddress, PrivateContext, PrivateImmutable}; use crate::public_key_note::PublicKeyNote; - use dep::aztec::protocol_types::constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}; #[storage] struct Storage { @@ -121,40 +120,4 @@ contract SchnorrAccount { !is_spent & valid_in_private } - - unconstrained fn process_logs( - log_plaintext: BoundedVec, - tx_hash: Field, - siloed_note_hashes_in_tx: BoundedVec, - recipient: AztecAddress, - ) { - dep::aztec::oracle::management::process_log( - context, - log_plaintext, - tx_hash, - siloed_note_hashes_in_tx, - recipient, - |serialized_note_content: BoundedVec, note_header, note_type_id| { - let hashes = if note_type_id == PublicKeyNote::get_note_type_id() { - assert(serialized_note_content.len() == 3); - dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( - PublicKeyNote::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], - siloed_note_hash: hashes[2], - inner_nullifier: hashes[3], - }, - ) - }, - ); - } } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 3638e9098e3a..f1ec6266f7e1 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -46,7 +46,6 @@ contract Token { // docs:end:import_authwit use crate::types::balance_set::BalanceSet; - use dep::aztec::protocol_types::constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}; // docs:end::imports @@ -745,42 +744,6 @@ contract Token { storage.balances.at(owner).balance_of().to_field() } // docs:end:balance_of_private - - unconstrained fn process_logs( - log_plaintext: BoundedVec, - tx_hash: Field, - siloed_note_hashes_in_tx: BoundedVec, - recipient: AztecAddress, - ) { - dep::aztec::oracle::management::process_log( - context, - log_plaintext, - tx_hash, - siloed_note_hashes_in_tx, - recipient, - |serialized_note_content: BoundedVec, note_header, note_type_id| { - let hashes = if note_type_id == UintNote::get_note_type_id() { - assert(serialized_note_content.len() == 4); - dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( - UintNote::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], - siloed_note_hash: hashes[2], - inner_nullifier: hashes[3], - }, - ) - }, - ); - } } // docs:end:token_all diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 47a3f2cae24c..06b45a896224 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -675,6 +675,8 @@ export class SimulatorOracle implements DBOracle { } 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.getIncomingNotes({ contractAddress, owner: recipient }); const nullifiersToCheck = currentNotesForRecipient.map(note => note.siloedNullifier); @@ -715,15 +717,15 @@ export class SimulatorOracle implements DBOracle { } const { blockNumber, blockHash } = receipt; - const siloedNoteHash = siloNoteHash(contractAddress, computeUniqueNoteHash(nonce, noteHash)); + const uniqueNoteHash = computeUniqueNoteHash(nonce, siloNoteHash(contractAddress, noteHash)); const siloedNullifier = siloNullifier(contractAddress, nullifier); - const siloedNoteHashTreeIndex = ( - await this.aztecNode.findLeavesIndexes(blockNumber!, MerkleTreeId.NOTE_HASH_TREE, [siloedNoteHash]) + const uniqueNoteHashTreeIndex = ( + await this.aztecNode.findLeavesIndexes(blockNumber!, MerkleTreeId.NOTE_HASH_TREE, [uniqueNoteHash]) )[0]; - if (siloedNoteHashTreeIndex === undefined) { + if (uniqueNoteHashTreeIndex === undefined) { throw new Error( - `Note hash ${noteHash} (siloed as ${siloedNoteHash}) is not present on the tree at block ${blockNumber} (from tx ${txHash})`, + `Note hash ${noteHash} (uniqued as ${uniqueNoteHash}) is not present on the tree at block ${blockNumber} (from tx ${txHash})`, ); } @@ -738,7 +740,7 @@ export class SimulatorOracle implements DBOracle { nonce, noteHash, siloedNullifier, - siloedNoteHashTreeIndex, + uniqueNoteHashTreeIndex, recipient.toAddressPoint(), ); } From 51a7f0bb4fac363f05893e8f56d1a77d2791152d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 9 Jan 2025 19:23:21 +0000 Subject: [PATCH 09/24] Add some docs --- .../aztec-nr/aztec/src/macros/mod.nr | 61 +++++++++++++++++-- .../aztec-nr/aztec/src/oracle/management.nr | 3 +- .../pxe/src/simulator_oracle/index.ts | 10 +-- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 568193b21be0..1a3eabf6e9ae 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -28,7 +28,7 @@ pub comptime fn aztec(m: Module) -> Quoted { let compute_note_hash_and_optionally_a_nullifier = generate_compute_note_hash_and_optionally_a_nullifier(); - let process_logs = generate_process_logs(); + 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(); @@ -169,21 +169,67 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { } } -comptime fn generate_process_logs() -> Quoted { - let mut if_note_type_id_match_statements_list = &[]; +comptime fn generate_process_log() -> Quoted { + // This mandatory function processes a log emitted by the contract. This is currently used to recover note contents + // and deliver the note to PXE. + // The bulk of the work of this function is done by aztec::oracle::management::do_process_log, so all we need to do + // is call that function. However, one of its parameters is a lambda function that computes note hash a nullifier + // given note contents and metadata (e.g. note type id), given that this is 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::oracle::management::NoteHashesAndNullifier { + // note_hash: hashes[0], + // unique_note_hash: hashes[1], + // inner_nullifier: hashes[3], + // }) + // } + // ``` + // + // We create this implementation by iterating over the different note types, creating an `if` or `else if` clause + // for each of them and calling `compute_note_hash_and_optionally_a_nullifier` with the note's deserialization + // function, and finally produce the required `NoteHashesAndNullifier` object. let notes = NOTES.entries(); + + let mut if_note_type_id_match_statements_list = &[]; for i in 0..notes.len() { let (typ, (_, serialized_note_length, _, _)) = notes[i]; + let if_or_else_if = if i == 0 { quote { if } } else { quote { else if } }; + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( quote { $if_or_else_if note_type_id == $typ::get_note_type_id() { - assert_eq(serialized_note_content.len(), $serialized_note_length); + // 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()) } }, @@ -193,15 +239,18 @@ comptime fn generate_process_logs() -> Quoted { let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); quote { - unconstrained fn process_logs( + unconstrained fn process_log( log_plaintext: BoundedVec, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, recipient: AztecAddress, ) { + // Because this unconstrained function is injected after the contract is processed by the macros, it'll not + // be modified by the macros that alter unconstrained functions. As such, we need to manually inject the + // unconstrained execution context since it will not be available otherwise. let context = dep::aztec::context::unconstrained_context::UnconstrainedContext::new(); - dep::aztec::oracle::management::process_log( + dep::aztec::oracle::management::do_process_log( context, log_plaintext, tx_hash, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr index e11cfc4eca47..18396f304f1a 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/management.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/management.nr @@ -60,7 +60,7 @@ fn for_each_bounded_vec( /// }) /// } /// ``` -pub unconstrained fn process_log( +pub unconstrained fn do_process_log( context: UnconstrainedContext, log_plaintext: BoundedVec, tx_hash: Field, @@ -87,7 +87,6 @@ pub unconstrained fn process_log( ) .unwrap(); - if hashes.unique_note_hash == expected_unique_note_hash { // TODO(#10726): push these into a vec to deliver all at once instead of having one oracle call per note deliver_note( diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 06b45a896224..ea72e90b9ef4 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -632,7 +632,7 @@ export class SimulatorOracle implements DBOracle { } // This will trigger calls to the deliverNote oracle - await this.callProcessLogs( + await this.callProcessLog( decryptedLog.contractAddress, decryptedLog.plaintext, decryptedLog.txHash, @@ -644,7 +644,7 @@ export class SimulatorOracle implements DBOracle { return; } - // Called when notes are delivered, usually as a result to a call to the process_logs contract function + // 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, @@ -745,7 +745,7 @@ export class SimulatorOracle implements DBOracle { ); } - async callProcessLogs( + async callProcessLog( contractAddress: AztecAddress, logPlaintext: Fr[], txHash: TxHash, @@ -755,11 +755,11 @@ export class SimulatorOracle implements DBOracle { ) { const artifact: FunctionArtifact | undefined = await new ContractDataOracle(this.db).getFunctionArtifactByName( contractAddress, - 'process_logs', + 'process_log', ); if (!artifact) { throw new Error( - `Mandatory implementation of "process_logs" missing in noir contract ${contractAddress.toString()}.`, + `Mandatory implementation of "process_log" missing in noir contract ${contractAddress.toString()}.`, ); } From 646f5ff85b4f37acc14c52e4e75e3ca03f312b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 9 Jan 2025 20:26:09 +0000 Subject: [PATCH 10/24] Handle no note contracts --- .../aztec-nr/aztec/src/macros/mod.nr | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 1a3eabf6e9ae..f9849b47aa6a 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -238,13 +238,8 @@ comptime fn generate_process_log() -> Quoted { let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); - quote { - unconstrained fn process_log( - log_plaintext: BoundedVec, - tx_hash: Field, - unique_note_hashes_in_tx: BoundedVec, - recipient: AztecAddress, - ) { + 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. @@ -272,6 +267,19 @@ comptime fn generate_process_log() -> Quoted { } ); } + } else { + panic(f"No notes defined") + }; + + quote { + unconstrained fn process_log( + log_plaintext: BoundedVec, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + recipient: AztecAddress, + ) { + $body + } } } From b771c9830fb2519eba6d17aee986f8bbaa6c17ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 9 Jan 2025 20:40:28 +0000 Subject: [PATCH 11/24] Fix macro --- noir-projects/aztec-nr/aztec/src/macros/mod.nr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index f9849b47aa6a..311f130a12f9 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -268,7 +268,9 @@ comptime fn generate_process_log() -> Quoted { ); } } else { - panic(f"No notes defined") + quote { + panic(f"No notes defined") + } }; quote { From eccd8b6a9dee752372888ca9e24d139d0815af91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 9 Jan 2025 21:34:12 +0000 Subject: [PATCH 12/24] Fix import --- noir-projects/aztec-nr/aztec/src/macros/mod.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 311f130a12f9..6157b212e50b 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -278,7 +278,7 @@ comptime fn generate_process_log() -> Quoted { log_plaintext: BoundedVec, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, - recipient: AztecAddress, + recipient: aztec::protocol_types::address::AztecAddress, ) { $body } From da8408f29f84de17798bd1a96eaab6bf21c749c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 13:53:37 +0000 Subject: [PATCH 13/24] Remove extra file --- .../produce_note_daos.ts | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts 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 7e1f94abe03d..000000000000 --- a/yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts +++ /dev/null @@ -1,67 +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, - 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, - l2BlockNumber, - l2BlockHash, - noteHashes, - dataStartIndexForTx, - excludedIndices, - logger, - NoteDao.fromPayloadAndNoteInfo, - ); - } - - return { - note, - }; -} From d5fe20223813cb2c862cb0fab3cd802ac205d954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 10:55:31 -0300 Subject: [PATCH 14/24] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Beneš --- noir-projects/aztec-nr/aztec/src/macros/mod.nr | 4 ++-- yarn-project/simulator/src/client/db_oracle.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 6157b212e50b..d5f223268c3d 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -173,8 +173,8 @@ comptime fn generate_process_log() -> Quoted { // This mandatory function processes a log emitted by the contract. This is currently used to recover note contents // and deliver the note to PXE. // The bulk of the work of this function is done by aztec::oracle::management::do_process_log, so all we need to do - // is call that function. However, one of its parameters is a lambda function that computes note hash a nullifier - // given note contents and metadata (e.g. note type id), given that this is behavior is contract-specific (as it + // 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. diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 0688f2682cb9..5f564eab62f5 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -234,7 +234,7 @@ 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 be requested via the `getNotes` + * 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) From 96af47d24159d7c5dc59e193c0d87b9d5e839af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 13:57:18 +0000 Subject: [PATCH 15/24] Rename foreach --- noir-projects/aztec-nr/aztec/src/oracle/management.nr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr index 18396f304f1a..ef5364debc3b 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/management.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/management.nr @@ -21,7 +21,7 @@ pub struct NoteHashesAndNullifier { pub inner_nullifier: Field, } -fn for_each_bounded_vec( +fn for_each_in_bounded_vec( vec: BoundedVec, f: fn[Env](T, u32) -> (), ) { @@ -72,7 +72,7 @@ pub unconstrained fn do_process_log( destructure_log_plaintext(log_plaintext); // We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash - for_each_bounded_vec( + for_each_in_bounded_vec( unique_note_hashes_in_tx, |expected_unique_note_hash, i| { let candidate_nonce = compute_note_hash_nonce(tx_hash, i); From 48fe292f37a640f4c3426fbe5a227544afb91abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 15:09:42 +0000 Subject: [PATCH 16/24] Move files around --- .../aztec-nr/aztec/src/macros/mod.nr | 8 +- .../aztec-nr/aztec/src/note/discovery/mod.nr | 134 ++++++++++++++++++ noir-projects/aztec-nr/aztec/src/note/mod.nr | 1 + .../aztec-nr/aztec/src/oracle/mod.nr | 2 +- .../aztec/src/oracle/note_discovery.nr | 49 +++++++ 5 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index d5f223268c3d..55cd2d6fedab 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -172,7 +172,7 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { comptime fn generate_process_log() -> Quoted { // This mandatory function processes a log emitted by the contract. This is currently used to recover note contents // and deliver the note to PXE. - // The bulk of the work of this function is done by aztec::oracle::management::do_process_log, so all we need to do + // 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). @@ -193,7 +193,7 @@ comptime fn generate_process_log() -> Quoted { // panic(f"Unknown note type id {note_type_id}") // }; // - // Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { + // Option::some(dep::aztec::note::discovery::NoteHashesAndNullifier { // note_hash: hashes[0], // unique_note_hash: hashes[1], // inner_nullifier: hashes[3], @@ -245,7 +245,7 @@ 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::oracle::management::do_process_log( + dep::aztec::note::discovery::do_process_log( context, log_plaintext, tx_hash, @@ -258,7 +258,7 @@ comptime fn generate_process_log() -> Quoted { }; Option::some( - dep::aztec::oracle::management::NoteHashesAndNullifier { + dep::aztec::note::discovery::NoteHashesAndNullifier { note_hash: hashes[0], unique_note_hash: hashes[1], inner_nullifier: hashes[3], 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..a544a5eb3c0f --- /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, + 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(tx_hash, 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/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 48a1977afd42..59df017d2e3d 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -12,7 +12,7 @@ pub mod get_public_data_witness; pub mod get_membership_witness; pub mod keys; pub mod key_validation_request; -pub mod management; +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..36f34fcc8506 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr @@ -0,0 +1,49 @@ +use dep::protocol_types::address::AztecAddress; +use crate::note::discovery::MAX_NOTE_SERIALIZED_LEN; + +/// 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 {} From 30cbc8acc4555101c3a4ead9f1443afc948cc68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 15:37:59 +0000 Subject: [PATCH 17/24] If I have to nargo fmt one more time --- noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr | 3 +-- noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr index 7b16c2921d8a..1f0c0316832f 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -2,8 +2,7 @@ use std::static_assert; use crate::{ context::unconstrained_context::UnconstrainedContext, note::note_header::NoteHeader, - oracle::note_discovery::deliver_note, - utils::array, + oracle::note_discovery::deliver_note, utils::array, }; use dep::protocol_types::{ 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 36f34fcc8506..8d4c2848991b 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,5 @@ -use dep::protocol_types::address::AztecAddress; 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. From 5205cc47792a54fa1b975225543e628a076e9173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 15:47:45 +0000 Subject: [PATCH 18/24] Oh god --- noir-projects/aztec-nr/aztec/src/macros/mod.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 30675d497623..2f951652348c 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -254,7 +254,7 @@ comptime fn generate_process_log() -> Quoted { log_plaintext, tx_hash, unique_note_hashes_in_tx, - first_nullifier_in_tx + first_nullifier_in_tx, recipient, |serialized_note_content: BoundedVec, note_header, note_type_id| { let hashes = $if_note_type_id_match_statements From 76bbd1befd6336fe0b87cb92c532065676826422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 16:15:52 +0000 Subject: [PATCH 19/24] zzz --- .../brute_force_note_info.ts | 90 ------------------- .../produce_note_daos.ts | 69 -------------- .../produce_note_daos_for_key.ts | 59 ------------ 3 files changed, 218 deletions(-) delete mode 100644 yarn-project/pxe/src/note_decryption_utils/brute_force_note_info.ts delete mode 100644 yarn-project/pxe/src/note_decryption_utils/produce_note_daos.ts delete mode 100644 yarn-project/pxe/src/note_decryption_utils/produce_note_daos_for_key.ts 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/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; -} From d44ec2e69c973b6be69564674e101925a130a2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 10 Jan 2025 16:17:26 +0000 Subject: [PATCH 20/24] kill me now --- .../aztec-nr/aztec/src/oracle/management.nr | 164 ------------------ 1 file changed, 164 deletions(-) delete mode 100644 noir-projects/aztec-nr/aztec/src/oracle/management.nr diff --git a/noir-projects/aztec-nr/aztec/src/oracle/management.nr b/noir-projects/aztec-nr/aztec/src/oracle/management.nr deleted file mode 100644 index ef5364debc3b..000000000000 --- a/noir-projects/aztec-nr/aztec/src/oracle/management.nr +++ /dev/null @@ -1,164 +0,0 @@ -use std::static_assert; - -use crate::{ - context::unconstrained_context::UnconstrainedContext, note::note_header::NoteHeader, - utils::array, -}; -use dep::protocol_types::{ - address::AztecAddress, - constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, - hash::compute_note_hash_nonce, -}; - -// We reserve two fields in the note log that are not part of the note content: one for the storage slot, and one for -// the note type id. -global NOTE_LOG_RESERVED_FIELDS: u32 = 2; -global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_LOG_RESERVED_FIELDS; - -pub struct NoteHashesAndNullifier { - pub note_hash: Field, - pub unique_note_hash: Field, - pub inner_nullifier: Field, -} - -fn for_each_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); - } - } -} - -/// 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, - 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(tx_hash, i); - - let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot); - - // TODO: 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 - deliver_note( - context.this_address(), // TODO(#10727): allow other contracts to deliver notes - storage_slot, - candidate_nonce, - serialized_note_content, - hashes.note_hash, - hashes.inner_nullifier, - tx_hash, - recipient, - ); - - // We don't exit the loop - it is possible (though rare) for the exact same note content to be present - // multiple times in the same transaction with different nonces. This typically doesn't happen due to - // notes containing random values in order to hide their contents. - } - }, - ); -} - -unconstrained fn destructure_log_plaintext( - log_plaintext: BoundedVec, -) -> (Field, Field, BoundedVec) { - assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS); - - static_assert( - NOTE_LOG_RESERVED_FIELDS == 2, - "unepxected value for NOTE_LOG_RESERVED_FIELDS", - ); - let storage_slot = log_plaintext.get(0); - let note_type_id = log_plaintext.get(1); - - let serialized_note_content = array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS); - - (storage_slot, note_type_id, serialized_note_content) -} - -unconstrained fn deliver_note( - contract_address: AztecAddress, - storage_slot: Field, - nonce: Field, - content: BoundedVec, - note_hash: Field, - nullifier: Field, - tx_hash: Field, - recipient: AztecAddress, -) { - // TODO(#10728): do something instead of failing (e.g. not advance tagging indices) - assert( - deliver_note_oracle( - contract_address, - storage_slot, - nonce, - content, - note_hash, - nullifier, - tx_hash, - recipient, - ), - "Failed to deliver note", - ); -} - -#[oracle(deliverNote)] -unconstrained fn deliver_note_oracle( - contract_address: AztecAddress, - storage_slot: Field, - nonce: Field, - content: BoundedVec, - note_hash: Field, - nullifier: Field, - tx_hash: Field, - recipient: AztecAddress, -) -> bool {} From 8b7d508372862a82859fee82ab940eed7ebee54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 13 Jan 2025 19:09:50 +0000 Subject: [PATCH 21/24] Add node methods to txe node --- yarn-project/txe/src/node/txe_node.ts | 88 +++++++++++++++++------ yarn-project/txe/src/oracle/txe_oracle.ts | 37 +++++----- 2 files changed, 87 insertions(+), 38 deletions(-) diff --git a/yarn-project/txe/src/node/txe_node.ts b/yarn-project/txe/src/node/txe_node.ts index f9d63c2fb3cd..9b9a6c836f86 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 { type 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 3c5a9de48eda..35a2e1d436c1 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -103,9 +103,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 siloedNullifiersFromPrivate: Set = new Set(); @@ -114,7 +111,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; debug: LogFn; @@ -128,6 +128,9 @@ export class TXE implements TypedOracle { ) { 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(this.contractDataOracle, txeDatabase, keyStore, this.node); @@ -145,12 +148,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() { @@ -221,8 +224,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( @@ -399,11 +402,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, @@ -813,8 +816,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); From 8f56981a84399ce351fd319d6764f4df903072bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 13 Jan 2025 19:53:12 +0000 Subject: [PATCH 22/24] Add sim prov --- yarn-project/pxe/src/simulator_oracle/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 4c71c71e196f..247e2496c0cd 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -794,7 +794,8 @@ export class SimulatorOracle implements DBOracle { }; await ( - simulator ?? getAcirSimulator(this.db, this.aztecNode, this.keyStore, this.contractDataOracle) + simulator ?? + getAcirSimulator(this.db, this.aztecNode, this.keyStore, this.simulationProvider, this.contractDataOracle) ).runUnconstrained( execRequest, artifact, From 72ea7c46afe973fcff72dff9a3bb6f5ff5371f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 13 Jan 2025 20:12:22 +0000 Subject: [PATCH 23/24] Fix build error --- yarn-project/txe/src/node/txe_node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/txe/src/node/txe_node.ts b/yarn-project/txe/src/node/txe_node.ts index 9b9a6c836f86..458a42db1824 100644 --- a/yarn-project/txe/src/node/txe_node.ts +++ b/yarn-project/txe/src/node/txe_node.ts @@ -42,7 +42,7 @@ import { import { type L1ContractAddresses } from '@aztec/ethereum'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; -import { type MerkleTreeSnapshotOperationsFacade, type MerkleTrees } from '@aztec/world-state'; +import { MerkleTreeSnapshotOperationsFacade, type MerkleTrees } from '@aztec/world-state'; export class TXENode implements AztecNode { #logsByTags = new Map(); From 36c29e88c4df69e6052df71d7e82350ee2bc9211 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 15 Jan 2025 23:09:36 +0000 Subject: [PATCH 24/24] fix: simulator oracle test --- .../simulator_oracle/simulator_oracle.test.ts | 125 +++++------------- 1 file changed, 36 insertions(+), 89 deletions(-) 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),