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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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 3b08dcfd911c7cc6e32a951ef831669c1d85a846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 15 Jan 2025 19:28:16 +0000 Subject: [PATCH 24/39] wip --- noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../aztec-nr/aztec/src/note/discovery/mod.nr | 342 +++++++++++++++--- .../aztec-nr/aztec/src/note/note_interface.nr | 4 +- .../aztec/src/oracle/note_discovery.nr | 34 +- .../aztec-nr/aztec/src/oracle/notes.nr | 26 +- .../aztec-nr/aztec/src/oracle/pxe_db.nr | 26 +- .../aztec-nr/aztec/src/pxe_db/mod.nr | 160 ++++++++ .../aztec-nr/aztec/src/utils/array/append.nr | 11 + .../aztec-nr/aztec/src/utils/array/mod.nr | 2 + .../aztec-nr/aztec/src/utils/array/subbvec.nr | 4 +- .../pxe/src/database/kv_pxe_database.ts | 48 ++- yarn-project/pxe/src/database/pxe_database.ts | 42 ++- .../src/database/pxe_database_test_suite.ts | 137 ++++++- .../pxe/src/simulator_oracle/index.ts | 30 +- .../simulator/src/acvm/oracle/oracle.ts | 69 ++-- .../simulator/src/acvm/oracle/typed_oracle.ts | 16 +- .../simulator/src/client/db_oracle.ts | 42 ++- .../simulator/src/client/view_data_oracle.ts | 40 +- yarn-project/txe/src/oracle/txe_oracle.ts | 56 +-- .../txe/src/txe_service/txe_service.ts | 48 ++- 20 files changed, 901 insertions(+), 237 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/pxe_db/mod.nr create mode 100644 noir-projects/aztec-nr/aztec/src/utils/array/append.nr diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index beb2d4b81dc1..ebbb84faeb20 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -10,6 +10,7 @@ mod note; mod event; mod oracle; mod state_vars; +mod pxe_db; mod prelude; mod encrypted_logs; mod unencrypted_logs; 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 1f0c0316832f..40a3f7c27df9 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -1,25 +1,104 @@ use std::static_assert; use crate::{ - context::unconstrained_context::UnconstrainedContext, note::note_header::NoteHeader, - oracle::note_discovery::deliver_note, utils::array, + context::unconstrained_context::UnconstrainedContext, + oracle::note_discovery::{deliver_note, get_log_by_tag, sync_notes}, + pxe_db::DBArray, + utils::array, }; use dep::protocol_types::{ address::AztecAddress, constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, - hash::compute_note_hash_nonce, + hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, + traits::{Deserialize, FromField, Serialize, ToField}, }; -// 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; +use crate::oracle::note_discovery::PUBLIC_LOG_SIZE_IN_FIELDS; // todo: move to constants -pub struct NoteHashesAndNullifier { - pub note_hash: Field, - pub unique_note_hash: Field, - pub inner_nullifier: Field, +// We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and +// one for the note type id. +global NOTE_PRIVATE_LOG_RESERVED_FIELDS: u32 = 2; + +pub global MAX_NOTE_SERIALIZED_LEN: u32 = + PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; + +global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; +global MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN: u32 = + MAX_NOTE_SERIALIZED_LEN - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN; + +pub struct NoteHashAndNullifier { + pub note_hash: Field, // The result of NoteInterface::compute_note_hash + pub inner_nullifier: Field, // The result of NullifiableNote::compute_nullifier_without_context +} + +pub unconstrained fn discover_new_notes( + contract_address: AztecAddress, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, +) { + fetch_and_process_private_logs(contract_address, compute_note_hash_and_nullifier); + + let pending_partial_notes = DBArray::at( + contract_address, + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_SLOT, + ); + + for i in 0..pending_partial_notes.len() { + let pending_partial_note: DeliveredPendingPartialNote = pending_partial_notes.get(i); + + let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); + if maybe_log.is_some() { + let log = maybe_log.unwrap(); + + let complete_serialized_note_content = array::append( + pending_partial_note.serialized_private_note_content, + log.log_content, + ); + + let discovered_notes = attempt_note_nonce_discovery( + log.unique_note_hashes_in_tx, + log.first_nullifier_in_tx, + compute_note_hash_and_nullifier, + contract_address, + pending_partial_note.storage_slot, + pending_partial_note.note_type_id, + complete_serialized_note_content, + ); + + for_each_in_bounded_vec( + discovered_notes, + |discovered_note: DiscoveredNoteInfo, _| { + // TODO:(#10728): handle notes that fail delivery + assert( + deliver_note( + contract_address, + pending_partial_note.storage_slot, + discovered_note.nonce, + complete_serialized_note_content, + discovered_note.note_hash, + discovered_note.inner_nullifier, + log.tx_hash, + pending_partial_note.recipient, + ), + "Failed to deliver note", + ); + }, + ); + + //pending_partial_notes.pop() + } + // todo: handle que nunca aparezca el log? podria no ser nunca + } +} + +unconstrained fn fetch_and_process_private_logs( + _contract_address: AztecAddress, + _compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, +) { + // We will eventually fetch tagged logs, decrypt and process them here, but for now we simply call the `syncNotes` + // oracle, which performs tag synchronization, log download, decryption, and then calls the `process_log` function + // which the decrypted payload, at which point `do_process_log` gets executed and we continue the work ourselves. + sync_notes(); } /// Processes a log given its plaintext by trying to find notes encoded in it. This process involves the discovery of @@ -57,78 +136,239 @@ pub unconstrained fn do_process_log( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, NoteHeader, Field) -> Option, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, ) { - let (storage_slot, note_type_id, serialized_note_content) = + let (storage_slot, note_type_id, log_type_id, log_payload) = destructure_log_plaintext(log_plaintext); + if log_type_id == 0 { + process_private_note_log( + context, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + compute_note_hash_and_nullifier, + storage_slot, + note_type_id, + log_payload, + ); + } else if log_type_id == 1 { + process_partial_note_private_log( + context, + storage_slot, + note_type_id, + log_payload, + recipient, + ); + } else { + panic(f"Unknown log type id {log_type_id}"); + } +} + +unconstrained fn destructure_log_plaintext( + log_plaintext: BoundedVec, +) -> (Field, Field, Field, BoundedVec) { + assert(log_plaintext.len() >= NOTE_PRIVATE_LOG_RESERVED_FIELDS); + + // If NOTE_PRIVATE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for + // `storage_slot` and `note_type_id` must be updated as well. + static_assert( + NOTE_PRIVATE_LOG_RESERVED_FIELDS == 2, + "unepxected value for NOTE_PRIVATE_LOG_RESERVED_FIELDS", + ); + let storage_slot = log_plaintext.get(0); + + // The combined type id is the bit packing of the note type id (which is 7 bits big) and the log type id (which is + // currently a single bit, and right above it). + let combined_type_id = log_plaintext.get(1); + let note_type_id = ((combined_type_id as u64) % 128) as Field; + let log_type_id = combined_type_id / 128; + + let log_payload = array::subbvec(log_plaintext, NOTE_PRIVATE_LOG_RESERVED_FIELDS); + + (storage_slot, note_type_id, log_type_id, log_payload) +} + +unconstrained fn process_private_note_log( + context: UnconstrainedContext, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, + storage_slot: Field, + note_type_id: Field, + serialized_note_content: BoundedVec, +) { + let discovered_notes = attempt_note_nonce_discovery( + unique_note_hashes_in_tx, + first_nullifier_in_tx, + compute_note_hash_and_nullifier, + context.this_address(), + storage_slot, + note_type_id, + serialized_note_content, + ); + + for_each_in_bounded_vec( + discovered_notes, + |discovered_note: DiscoveredNoteInfo, _| { + // TODO:(#10728): handle notes that fail delivery + assert( + deliver_note( + context.this_address(), // TODO(#10727): allow other contracts to deliver notes + storage_slot, + discovered_note.nonce, + serialized_note_content, + discovered_note.note_hash, + discovered_note.inner_nullifier, + tx_hash, + recipient, + ), + "Failed to deliver note", + ); + }, + ); +} + +struct DiscoveredNoteInfo { + nonce: Field, + note_hash: Field, + inner_nullifier: Field, +} + +unconstrained fn attempt_note_nonce_discovery( + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, + contract_address: AztecAddress, + storage_slot: Field, + note_type_id: Field, + serialized_note_content: BoundedVec, +) -> BoundedVec { + let mut discovered_notes = BoundedVec::new(); + // We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash for_each_in_bounded_vec( unique_note_hashes_in_tx, |expected_unique_note_hash, i| { let candidate_nonce = compute_note_hash_nonce(first_nullifier_in_tx, i); - let header = NoteHeader::new(context.this_address(), candidate_nonce, storage_slot); - // TODO(#11157): handle failed note_hash_and_nullifier computation let hashes = compute_note_hash_and_nullifier( serialized_note_content, - header, + storage_slot, note_type_id, ) .unwrap(); - if hashes.unique_note_hash == expected_unique_note_hash { - // TODO(#10726): push these into a vec to deliver all at once instead of having one oracle call per note - - assert( - deliver_note( - context.this_address(), // TODO(#10727): allow other contracts to deliver notes - storage_slot, - candidate_nonce, - serialized_note_content, - hashes.note_hash, - hashes.inner_nullifier, - tx_hash, - recipient, - ), - "Failed to deliver note", - ); + let siloed_note_hash = compute_siloed_note_hash(contract_address, hashes.note_hash); + let unique_note_hash = compute_unique_note_hash(candidate_nonce, siloed_note_hash); + if unique_note_hash == expected_unique_note_hash { + // Note that we perform no validataions on the nullifier - we fundamentally cannot, since only the + // application knows how to compute nullifiers, and we simply trust it to have provided the correct one. + // If it hasn't, then PXE may fail to realize that a given note has been nullified already and end up + // producing invalid transactions (with duplicate nullifiers). + discovered_notes.push( + DiscoveredNoteInfo { + nonce: candidate_nonce, + note_hash: hashes.note_hash, + inner_nullifier: hashes.inner_nullifier, + }, + ); // We don't exit the loop - it is possible (though rare) for the exact same note content to be present // multiple times in the same transaction with different nonces. This typically doesn't happen due to // notes containing random values in order to hide their contents. } }, ); + + discovered_notes } -unconstrained fn destructure_log_plaintext( - log_plaintext: BoundedVec, -) -> (Field, Field, BoundedVec) { - assert(log_plaintext.len() >= NOTE_LOG_RESERVED_FIELDS); +struct DeliveredPendingPartialNote { + note_completion_log_tag: Field, + storage_slot: Field, + note_type_id: Field, + serialized_private_note_content: BoundedVec, + recipient: AztecAddress, +} - // 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); +// TODO: use `derive(Serialize)`, but we need for generics in struct fields to be handled properly before that happens. +// Currently blocked by Noir's StructDefinition::fields. +impl Serialize for DeliveredPendingPartialNote { + fn serialize(self) -> [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5] { + [ + self.note_completion_log_tag, + self.storage_slot, + self.note_type_id, + self.serialized_private_note_content.storage()[0], + self.serialized_private_note_content.storage()[1], + self.serialized_private_note_content.storage()[2], + self.serialized_private_note_content.storage()[3], + self.serialized_private_note_content.storage()[4], + self.serialized_private_note_content.storage()[5], + self.serialized_private_note_content.storage()[6], + self.serialized_private_note_content.storage()[7], + self.serialized_private_note_content.storage()[8], + self.serialized_private_note_content.storage()[9], + self.serialized_private_note_content.storage()[10], + self.serialized_private_note_content.storage()[12], + self.serialized_private_note_content.storage()[13], + self.serialized_private_note_content.storage()[14], + self.serialized_private_note_content.storage()[15], + self.serialized_private_note_content.len() as Field, + self.recipient.to_field(), + ] + } +} - let serialized_note_content = array::subbvec(log_plaintext, NOTE_LOG_RESERVED_FIELDS); +impl Deserialize for DeliveredPendingPartialNote { + fn deserialize(values: [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5]) -> Self { + Self { + note_completion_log_tag: values[0], + storage_slot: values[1], + note_type_id: values[2], + serialized_private_note_content: BoundedVec::from_parts( + array::subarray(values, 3), + values[18] as u32, + ), + recipient: AztecAddress::from_field(values[19]), + } + } +} - (storage_slot, note_type_id, serialized_note_content) +global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_SLOT: Field = 5; + +unconstrained fn process_partial_note_private_log( + context: UnconstrainedContext, + storage_slot: Field, + note_type_id: Field, + log_payload: BoundedVec, + recipient: AztecAddress, +) { + let pending = DeliveredPendingPartialNote { + note_completion_log_tag: log_payload.get(0), + storage_slot, + note_type_id, + serialized_private_note_content: array::subbvec(log_payload, 1), + recipient, + }; + + DBArray::at( + context.this_address(), + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_SLOT, + ) + .push(pending); } -fn for_each_in_bounded_vec( +unconstrained 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); - } + for i in 0..vec.len() { + f(vec.get_unchecked(i), i); } } 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 a049d061ff0e..82d3d9c41cf4 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_interface.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_interface.nr @@ -17,8 +17,8 @@ where } pub trait NullifiableNote { - /// Returns the non-siloed nullifier, which will be later siloed by contract address by the kernels before being - /// committed to the state tree. + /// Returns the non-siloed nullifier (also called inner-nullifier), which will be later siloed by contract address + /// by the kernels before being committed to the state tree. /// /// This function MUST be called with the correct note hash for consumption! It will otherwise silently fail and /// compute an incorrect value. The reason why we receive this as an argument instead of computing it ourselves diff --git a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr index 8d4c2848991b..47ffd3d61480 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,23 @@ use crate::note::discovery::MAX_NOTE_SERIALIZED_LEN; -use dep::protocol_types::address::AztecAddress; +use dep::protocol_types::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX}; + +pub global PUBLIC_LOG_SIZE_IN_FIELDS: u32 = 5; // todo replace with constant from miranda's pr. make sure to use data and not raw size (excl addr) and remove tag + +/// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them available +/// for later querying via the `get_notes` oracle. +pub fn sync_notes() { + // This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to call. + unsafe { + sync_notes_oracle_wrapper(); + } +} + +unconstrained fn sync_notes_oracle_wrapper() { + sync_notes_oracle(); +} + +#[oracle(syncNotes)] +unconstrained fn sync_notes_oracle() {} /// 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. @@ -47,3 +65,17 @@ unconstrained fn deliver_note_oracle( tx_hash: Field, recipient: AztecAddress, ) -> bool {} + +pub struct LogWithTxData { + pub log_content: BoundedVec, + pub tx_hash: Field, + pub unique_note_hashes_in_tx: BoundedVec, + pub first_nullifier_in_tx: Field, +} + +pub unconstrained fn get_log_by_tag(tag: Field) -> Option { + get_log_by_tag_oracle(tag) +} + +#[oracle(deliverNote)] +unconstrained fn get_log_by_tag_oracle(tag: Field) -> Option {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index e7534b14fb9b..6f565c1262ff 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -1,4 +1,8 @@ -use crate::{note::{note_header::NoteHeader, note_interface::NoteInterface}, utils::array}; +use crate::{ + note::{discovery::discover_new_notes, note_header::NoteHeader, note_interface::NoteInterface}, + oracle, + utils::array, +}; use dep::protocol_types::{ address::AztecAddress, @@ -154,7 +158,9 @@ pub unconstrained fn get_notes, { - sync_notes_oracle_wrapper(); + // TODO(#10727): allow other contracts to discover notes + discover_new_notes(oracle::execution::get_contract_address()); + let fields = get_notes_oracle_wrapper( storage_slot, num_selects, @@ -252,19 +258,3 @@ unconstrained fn increment_app_tagging_secret_index_as_sender_oracle( _sender: AztecAddress, _recipient: AztecAddress, ) {} - -/// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them available -/// for later querying via the `get_notes` oracle. -pub fn sync_notes() { - // This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to call. - unsafe { - sync_notes_oracle_wrapper(); - } -} - -unconstrained fn sync_notes_oracle_wrapper() { - sync_notes_oracle(); -} - -#[oracle(syncNotes)] -unconstrained fn sync_notes_oracle() {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr b/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr index 0bcf6fe5a9e4..10e091787f6b 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/pxe_db.nr @@ -1,6 +1,6 @@ use protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}}; -#[oracle(store)] +#[oracle(dbStore)] unconstrained fn store_oracle( contract_address: AztecAddress, key: Field, @@ -25,7 +25,7 @@ where /// the shape of T would affect the expected oracle response (e.g. if we were returning a struct of 3 u32 values /// then the expected response shape would be 3 single items. If instead we had a struct containing /// `u32, [Field;10], u32`, then the expected shape would be single, array, single.). -#[oracle(load)] +#[oracle(dbLoad)] unconstrained fn load_oracle( contract_address: AztecAddress, key: Field, @@ -42,6 +42,28 @@ where serialized_option.map(|arr| Deserialize::deserialize(arr)) } +#[oracle(dbDelete)] +unconstrained fn delete_oracle( + contract_address: AztecAddress, + key: Field, +) {} + +pub unconstrained fn delete(contract_address: AztecAddress, key: Field) { + delete_oracle(contract_address, key); +} + +#[oracle(dbCopy)] +unconstrained fn copy_oracle( + contract_address: AztecAddress, + src_key: Field, + dst_key: Field, + num_entries: u32, +) {} + +pub unconstrained fn copy(contract_address: AztecAddress, src_key: Field, dst_key: Field, num_entries: u32) { + copy_oracle(contract_address, src_key, dst_key, num_entries); +} + mod test { use crate::{ oracle::{pxe_db::{load, store}, random::random}, diff --git a/noir-projects/aztec-nr/aztec/src/pxe_db/mod.nr b/noir-projects/aztec-nr/aztec/src/pxe_db/mod.nr new file mode 100644 index 000000000000..15ece3e8c1b5 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/pxe_db/mod.nr @@ -0,0 +1,160 @@ +use crate::oracle::pxe_db; +use protocol_types::{address::AztecAddress, traits::{Deserialize, Serialize}}; + +pub struct DBArray { + contract_address: AztecAddress, + key: Field, +} + +impl DBArray +where + T: Serialize + Deserialize, +{ + pub unconstrained fn at(contract_address: AztecAddress, key: Field) -> Self { + Self { contract_address, key } + } + + pub unconstrained fn len(self) -> u32 { + pxe_db::load(self.contract_address, self.key).unwrap_or(0) as u32 + } + + pub unconstrained fn push(self, value: T) { + // Arrays are stored by storing their length at the key, and each element at each following key. For example, the + // array [5, 7] at key 13 would have: + // - key: 13, value: 2 + // - key: 14, value: 5 + // - key: 15, value: 7 + + let current_length = self.len(); + + // `key + 1` is where the first element is located, so we get the first free slot by adding `current_length` + pxe_db::store(self.contract_address, self.key_at(current_length), value); + + // Then we simply update the length + let new_length = current_length + 1; + pxe_db::store(self.contract_address, self.key, new_length); + } + + pub unconstrained fn get(self, index: u32) -> T { + assert(index < self.len(), "Attempted to read past the length of a DBArray"); + + pxe_db::load(self.contract_address, self.key_at(index)).unwrap() + } + + pub unconstrained fn remove(self, index: u32) { + let current_length = self.len(); + assert(index < current_length, "Attempted to delete past the length of a DBArray"); + + if index < current_length - 1 { + pxe_db::copy(self.contract_address, self.key_at(index + 1), self.key_at(index), current_length - index - 1); + } + + pxe_db::delete(self.contract_address, self.key_at(current_length - 1)); + + pxe_db::store(self.contract_address, self.key, current_length - 1); + } + + unconstrained fn key_at(self, index: u32) -> Field { + self.key + 1 + index as Field + } +} + +mod test { + use crate::test::helpers::test_environment::TestEnvironment; + use super::DBArray; + use protocol_types::address::AztecAddress; + + global KEY: Field = 1230; + + unconstrained fn setup() -> AztecAddress { + TestEnvironment::new().unkonstrained().this_address() + } + + #[test] + unconstrained fn empty_array() { + let contract_address = setup(); + + let array: DBArray = DBArray::at(contract_address, KEY); + assert_eq(array.len(), 0); + } + + #[test(should_fail_with = "Attempted to read past the length of a DBArray")] + unconstrained fn empty_array_read() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, KEY); + let _: Field = array.get(0); + } + + #[test] + unconstrained fn array_push() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, KEY); + array.push(5); + + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + } + + #[test(should_fail_with = "Attempted to read past the length of a DBArray")] + unconstrained fn read_past_len() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, KEY); + array.push(5); + + let _ = array.get(1); + } + + #[test] + unconstrained fn array_remove_last() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, KEY); + + array.push(5); + array.remove(0); + + assert_eq(array.len(), 0); + } + + #[test] + unconstrained fn array_remove_some() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, KEY); + + array.push(7); + array.push(8); + array.push(9); + + assert_eq(array.len(), 3); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 8); + assert_eq(array.get(2), 9); + + array.remove(1); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 9); + } + + #[test] + unconstrained fn array_remove_all() { + let contract_address = setup(); + + let array = DBArray::at(contract_address, KEY); + + array.push(7); + array.push(8); + array.push(9); + + array.remove(1); + array.remove(1); + array.remove(0); + + assert_eq(array.len(), 0); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/append.nr b/noir-projects/aztec-nr/aztec/src/utils/array/append.nr new file mode 100644 index 000000000000..505f84e039f7 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/array/append.nr @@ -0,0 +1,11 @@ +pub fn append( + a: BoundedVec, + b: BoundedVec, +) -> BoundedVec { + let mut dst = BoundedVec::new(); + + dst.extend_from_bounded_vec(a); + dst.extend_from_bounded_vec(b); + + dst +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr index ef46a00a5a24..6763e7a1a16b 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr @@ -1,7 +1,9 @@ +mod append; mod collapse; mod subarray; mod subbvec; +pub use append::append; pub use collapse::collapse; pub use subarray::subarray; pub use subbvec::subbvec; diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr index f08bed659423..c457b7695d19 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/subbvec.nr @@ -16,14 +16,14 @@ use crate::utils::array; /// let baz: BoundedVec<_, 10> = subbvec(foo, 3); // fails - we can't return 10 elements since only 7 remain /// ``` pub fn subbvec( - vec: BoundedVec, + bvec: BoundedVec, offset: u32, ) -> BoundedVec { // from_parts_unchecked does not verify that the elements past len are zeroed, but that is not an issue in our case // because we're constructing the new storage array as a subarray of the original one (which should have zeroed // storage past len), guaranteeing correctness. This is because `subarray` does not allow extending arrays past // their original length. - BoundedVec::from_parts_unchecked(array::subarray(vec.storage(), offset), vec.len() - offset) + BoundedVec::from_parts_unchecked(array::subarray(bvec.storage(), offset), bvec.len() - offset) } mod test { diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index 4c73644ca975..a4f3a9129bcb 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -622,17 +622,17 @@ export class KVPxeDatabase implements PxeDatabase { }); } - async store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { - const dataKey = `${contract.toString()}:${key.toString()}`; - const dataBuffer = Buffer.concat(values.map(value => value.toBuffer())); - await this.#contractStore.set(dataKey, dataBuffer); + async dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise { + await this.#contractStore.set( + dbSlotToKey(contractAddress, slot), + Buffer.concat(values.map(value => value.toBuffer())), + ); } - async load(contract: AztecAddress, key: Fr): Promise { - const dataKey = `${contract.toString()}:${key.toString()}`; - const dataBuffer = await this.#contractStore.getAsync(dataKey); + async dbLoad(contractAddress: AztecAddress, slot: Fr): Promise { + const dataBuffer = await this.#contractStore.getAsync(dbSlotToKey(contractAddress, slot)); if (!dataBuffer) { - this.debug(`Data not found for contract ${contract.toString()} and key ${key.toString()}`); + this.debug(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); return null; } const values: Fr[] = []; @@ -641,4 +641,36 @@ export class KVPxeDatabase implements PxeDatabase { } return values; } + + async dbDelete(contractAddress: AztecAddress, slot: Fr): Promise { + await this.#contractStore.delete(dbSlotToKey(contractAddress, slot)); + } + + async dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + // In order to support overlaping source and destination regions we need to check the relative positions of source + // and destination. If destination is ahead of source, then by the time we overwrite source elements using forward + // indexes we'll have already read those. On the contrary, if source is ahead of destination we need to use backward + // indexes to avoid reading elements that've been overwritten. + + const indexes = Array.from(Array(numEntries).keys()); + if (srcSlot.lt(dstSlot)) { + indexes.reverse(); + } + + for (const i of indexes) { + const currentSrcSlot = dbSlotToKey(contractAddress, srcSlot.add(new Fr(i))); + const currentDstSlot = dbSlotToKey(contractAddress, dstSlot.add(new Fr(i))); + + const toCopy = await this.#contractStore.getAsync(currentSrcSlot); + if (!toCopy) { + throw new Error(`Attempted to copy empty slot ${currentSrcSlot} for contract ${contractAddress.toString()}`); + } + + await this.#contractStore.set(currentDstSlot, toCopy); + } + } +} + +function dbSlotToKey(contractAddress: AztecAddress, slot: Fr): string { + return `${contractAddress.toString()}:${slot.toString()}`; } diff --git a/yarn-project/pxe/src/database/pxe_database.ts b/yarn-project/pxe/src/database/pxe_database.ts index cc8496aaadfc..3f1a6d041070 100644 --- a/yarn-project/pxe/src/database/pxe_database.ts +++ b/yarn-project/pxe/src/database/pxe_database.ts @@ -215,20 +215,38 @@ export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceD resetNoteSyncData(): Promise; /** - * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - An address of a contract that is requesting to store the data. - * @param key - A field element representing the key to store the data under. - * @param values - An array of field elements representing the data to store. + * Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `dbLoad`. + * @param contractAddress - The contract address to scope the data under. + * @param slot - The slot in the database in which to store the value. Slots need not be contiguous. + * @param values - The data to store. */ - store(contract: AztecAddress, key: Fr, values: Fr[]): Promise; + dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise; /** - * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - An address of a contract that is requesting to load the data. - * @param key - A field element representing the key under which to load the data.. - * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. + * Returns data previously stored via `dbStore` in the per-contract non-volatile database. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to read. + * @returns The stored data or `null` if no data is stored under the slot. */ - load(contract: AztecAddress, key: Fr): Promise; + dbLoad(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Deletes data in the per-contract non-volatile database. Does nothing if no data was present. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to delete. + */ + dbDelete(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data + * structures by avoiding repeated calls to `dbLoad` and `dbStore`. + * Supports overlapping source and destination regions (which will result in the overlapped source values being + * overwritten). All copied slots must exist in the database (i.e. have been stored and not deleted) + * + * @param contractAddress - The contract address under which the data is scoped. + * @param srcSlot - The first slot to copy from. + * @param dstSlot - The first slot to copy to. + * @param numEntries - The number of entries to copy. + */ + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise; } diff --git a/yarn-project/pxe/src/database/pxe_database_test_suite.ts b/yarn-project/pxe/src/database/pxe_database_test_suite.ts index 1a1a586c78d2..a15a56bdf3a1 100644 --- a/yarn-project/pxe/src/database/pxe_database_test_suite.ts +++ b/yarn-project/pxe/src/database/pxe_database_test_suite.ts @@ -406,7 +406,7 @@ export function describePxeDatabase(getDatabase: () => PxeDatabase) { }); }); - describe('contract store', () => { + describe('contract non-volatile database', () => { let contract: AztecAddress; beforeEach(() => { @@ -415,56 +415,155 @@ export function describePxeDatabase(getDatabase: () => PxeDatabase) { }); it('stores and loads a single value', async () => { - const key = new Fr(1); + const slot = new Fr(1); const values = [new Fr(42)]; - await database.store(contract, key, values); - const result = await database.load(contract, key); + await database.dbStore(contract, slot, values); + const result = await database.dbLoad(contract, slot); expect(result).toEqual(values); }); it('stores and loads multiple values', async () => { - const key = new Fr(1); + const slot = new Fr(1); const values = [new Fr(42), new Fr(43), new Fr(44)]; - await database.store(contract, key, values); - const result = await database.load(contract, key); + await database.dbStore(contract, slot, values); + const result = await database.dbLoad(contract, slot); expect(result).toEqual(values); }); it('overwrites existing values', async () => { - const key = new Fr(1); + const slot = new Fr(1); const initialValues = [new Fr(42)]; const newValues = [new Fr(100)]; - await database.store(contract, key, initialValues); - await database.store(contract, key, newValues); + await database.dbStore(contract, slot, initialValues); + await database.dbStore(contract, slot, newValues); - const result = await database.load(contract, key); + const result = await database.dbLoad(contract, slot); expect(result).toEqual(newValues); }); it('stores values for different contracts independently', async () => { const anotherContract = AztecAddress.random(); - const key = new Fr(1); + const slot = new Fr(1); const values1 = [new Fr(42)]; const values2 = [new Fr(100)]; - await database.store(contract, key, values1); - await database.store(anotherContract, key, values2); + await database.dbStore(contract, slot, values1); + await database.dbStore(anotherContract, slot, values2); - const result1 = await database.load(contract, key); - const result2 = await database.load(anotherContract, key); + const result1 = await database.dbLoad(contract, slot); + const result2 = await database.dbLoad(anotherContract, slot); expect(result1).toEqual(values1); expect(result2).toEqual(values2); }); - it('returns null for non-existent keys', async () => { - const key = Fr.random(); - const result = await database.load(contract, key); + it('returns null for non-existent slots', async () => { + const slot = Fr.random(); + const result = await database.dbLoad(contract, slot); expect(result).toBeNull(); }); + + it('deletes a slot', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + await database.dbStore(contract, slot, values); + await database.dbDelete(contract, slot); + + expect(await database.dbLoad(contract, slot)).toBeNull(); + }); + + it('deletes an empty slot', async () => { + const slot = new Fr(1); + await database.dbDelete(contract, slot); + + expect(await database.dbLoad(contract, slot)).toBeNull(); + }); + + it('copies a single value', async () => { + const slot = new Fr(1); + const values = [new Fr(42)]; + + await database.dbStore(contract, slot, values); + + const dstSlot = new Fr(5); + await database.dbCopy(contract, slot, dstSlot, 1); + + expect(await database.dbLoad(contract, dstSlot)).toEqual(values); + }); + + it('copies multiple non-overlapping values', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + await database.dbStore(contract, src.add(new Fr(1)), valuesArray[1]); + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(5); + await database.dbCopy(contract, src, dst, 3); + + expect(await database.dbLoad(contract, dst)).toEqual(valuesArray[0]); + expect(await database.dbLoad(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await database.dbLoad(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + }); + + it('copies overlapping values with src ahead', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + await database.dbStore(contract, src.add(new Fr(1)), valuesArray[1]); + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(2); + await database.dbCopy(contract, src, dst, 3); + + expect(await database.dbLoad(contract, dst)).toEqual(valuesArray[0]); + expect(await database.dbLoad(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await database.dbLoad(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + + // Slots 2 and 3 (src[1] and src[2]) should have been overwritten since they are also dst[0] and dst[1] + expect(await database.dbLoad(contract, src)).toEqual(valuesArray[0]); // src[0] (unchanged) + expect(await database.dbLoad(contract, src.add(new Fr(1)))).toEqual(valuesArray[0]); // dst[0] + expect(await database.dbLoad(contract, src.add(new Fr(2)))).toEqual(valuesArray[1]); // dst[1] + }); + + it('copies overlapping values with dst ahead', async () => { + const src = new Fr(5); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + await database.dbStore(contract, src.add(new Fr(1)), valuesArray[1]); + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(4); + await database.dbCopy(contract, src, dst, 3); + + expect(await database.dbLoad(contract, dst)).toEqual(valuesArray[0]); + expect(await database.dbLoad(contract, dst.add(new Fr(1)))).toEqual(valuesArray[1]); + expect(await database.dbLoad(contract, dst.add(new Fr(2)))).toEqual(valuesArray[2]); + + // Slots 5 and 6 (src[0] and src[1]) should have been overwritten since they are also dst[1] and dst[2] + expect(await database.dbLoad(contract, src)).toEqual(valuesArray[1]); // dst[1] + expect(await database.dbLoad(contract, src.add(new Fr(1)))).toEqual(valuesArray[2]); // dst[2] + expect(await database.dbLoad(contract, src.add(new Fr(2)))).toEqual(valuesArray[2]); // src[2] (unchanged) + }); + + it('copying fails if any value is empty', async () => { + const src = new Fr(1); + const valuesArray = [[new Fr(42)], [new Fr(1337)], [new Fr(13)]]; + + await database.dbStore(contract, src, valuesArray[0]); + // We skip src[1] + await database.dbStore(contract, src.add(new Fr(2)), valuesArray[2]); + + const dst = new Fr(5); + await expect(database.dbCopy(contract, src, dst, 3)).rejects.toThrow('Attempted to copy empty slot'); + }); }); }); } diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 247e2496c0cd..ba80dc2940d7 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -804,26 +804,20 @@ export class SimulatorOracle implements DBOracle { ); } - /** - * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - An address of a contract that is requesting to store the data. - * @param key - A field element representing the key to store the data under. - * @param values - An array of field elements representing the data to store. - */ - store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { - return this.db.store(contract, key, values); + dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise { + return this.db.dbStore(contractAddress, slot, values); } - /** - * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - An address of a contract that is requesting to load the data. - * @param key - A field element representing the key under which to load the data.. - * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. - */ - load(contract: AztecAddress, key: Fr): Promise { - return this.db.load(contract, key); + dbLoad(contractAddress: AztecAddress, slot: Fr): Promise { + return this.db.dbLoad(contractAddress, slot); + } + + dbDelete(contractAddress: AztecAddress, slot: Fr): Promise { + return this.db.dbDelete(contractAddress, slot); + } + + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + return this.db.dbCopy(contractAddress, srcSlot, dstSlot, numEntries); } } diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 7eb11903ee7a..025d15a3c4d8 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -2,15 +2,12 @@ import { MerkleTreeId, UnencryptedL2Log } from '@aztec/circuit-types'; import { FunctionSelector, NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; -import { createLogger } from '@aztec/foundation/log'; import { type ACVMField } from '../acvm_types.js'; import { frToBoolean, frToNumber, fromACVMField, fromBoundedVec } from '../deserialize.js'; import { toACVMField } from '../serialize.js'; import { type TypedOracle } from './typed_oracle.js'; -const logger = createLogger('simulator:acvm:oracle'); - /** * A data source that has all the apis required by Aztec.nr. */ @@ -412,34 +409,50 @@ export class Oracle { return toACVMField(true); } - async store([contract]: ACVMField[], [key]: ACVMField[], values: ACVMField[]) { - const processedContract = AztecAddress.fromField(fromACVMField(contract)); - const processedKey = fromACVMField(key); - const processedValues = values.map(fromACVMField); - logger.debug(`Storing data for key ${processedKey} in contract ${processedContract}. Data: [${processedValues}]`); - await this.typedOracle.store(processedContract, processedKey, processedValues); - } - - /** - * Load data from pxe db. - * @param contract - The contract address. - * @param key - The key to load. - * @param tSize - The size of the serialized object to return. - * @returns The data found flag and the serialized object concatenated in one array. - */ - async load([contract]: ACVMField[], [key]: ACVMField[], [tSize]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { - const processedContract = AztecAddress.fromField(fromACVMField(contract)); - const processedKey = fromACVMField(key); - const values = await this.typedOracle.load(processedContract, processedKey); + async dbStore([contractAddress]: ACVMField[], [slot]: ACVMField[], values: ACVMField[]) { + await this.typedOracle.dbStore( + AztecAddress.fromField(fromACVMField(contractAddress)), + fromACVMField(slot), + values.map(fromACVMField), + ); + } + + async dbLoad( + [contractAddress]: ACVMField[], + [slot]: ACVMField[], + [tSize]: ACVMField[], + ): Promise<(ACVMField | ACVMField[])[]> { + const values = await this.typedOracle.dbLoad( + AztecAddress.fromField(fromACVMField(contractAddress)), + fromACVMField(slot), + ); + + // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct + // with two fields: `some` (a boolean) and `value` (a field array in this case). if (values === null) { - // No data was found so we set the data-found flag to 0 and we pad with zeros get the correct return size. - const processedTSize = frToNumber(fromACVMField(tSize)); - logger.debug(`No data found for key ${processedKey} in contract ${processedContract}`); - return [toACVMField(0), Array(processedTSize).fill(toACVMField(0))]; + // No data was found so we set `some` to 0 and pad `value` with zeros get the correct return size. + return [toACVMField(0), Array(frToNumber(fromACVMField(tSize))).fill(toACVMField(0))]; } else { - // Data was found so we set the data-found flag to 1 and return it along with the data. - logger.debug(`Returning data for key ${processedKey} in contract ${processedContract}. Data: [${values}]`); + // Data was found so we set `some` to 1 and return it along with `value`. return [toACVMField(1), values.map(toACVMField)]; } } + + async dbDelete([contractAddress]: ACVMField[], [slot]: ACVMField[]) { + await this.typedOracle.dbDelete(AztecAddress.fromField(fromACVMField(contractAddress)), fromACVMField(slot)); + } + + async dbCopy( + [contractAddress]: ACVMField[], + [srcSlot]: ACVMField[], + [dstSlot]: ACVMField[], + [numEntries]: ACVMField[], + ) { + await this.typedOracle.dbCopy( + AztecAddress.fromField(fromACVMField(contractAddress)), + fromACVMField(srcSlot), + fromACVMField(dstSlot), + frToNumber(fromACVMField(numEntries)), + ); + } } diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 26a2230182d7..3a9fa60a9311 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -246,11 +246,19 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('deliverNote'); } - store(_contract: AztecAddress, _key: Fr, _values: Fr[]): Promise { - throw new OracleMethodNotAvailableError('store'); + dbStore(_contractAddress: AztecAddress, _key: Fr, _values: Fr[]): Promise { + throw new OracleMethodNotAvailableError('dbStore'); } - load(_contract: AztecAddress, _key: Fr): Promise { - throw new OracleMethodNotAvailableError('load'); + dbLoad(_contractAddress: AztecAddress, _key: Fr): Promise { + throw new OracleMethodNotAvailableError('dbLoad'); + } + + dbDelete(_contractAddress: AztecAddress, _key: Fr): Promise { + throw new OracleMethodNotAvailableError('dbDelete'); + } + + dbCopy(_contractAddress: AztecAddress, _srcKey: Fr, _dstKey: Fr, _numEntries: number): Promise { + throw new OracleMethodNotAvailableError('dbCopy'); } } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 5f564eab62f5..7b4705a59bf8 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -263,20 +263,38 @@ export interface DBOracle extends CommitmentsDB { removeNullifiedNotes(contractAddress: AztecAddress): Promise; /** - * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - An address of a contract that is requesting to store the data. - * @param key - A field element representing the key to store the data under. - * @param values - An array of field elements representing the data to store. + * Stores arbitrary information in a per-contract non-volatile database, which can later be retrieved with `dbLoad`. + * @param contractAddress - The contract address to scope the data under. + * @param slot - The slot in the database in which to store the value. Slots need not be contiguous. + * @param values - The data to store. */ - store(contract: AztecAddress, key: Fr, values: Fr[]): Promise; + dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise; /** - * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - An address of a contract that is requesting to load the data. - * @param key - A field element representing the key under which to load the data.. - * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. + * Returns data previously stored via `dbStore` in the per-contract non-volatile database. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to read. + * @returns The stored data or `null` if no data is stored under the slot. */ - load(contract: AztecAddress, key: Fr): Promise; + dbLoad(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Deletes data in the per-contract non-volatile database. Does nothing if no data was present. + * @param contractAddress - The contract address under which the data is scoped. + * @param slot - The slot in the database to delete. + */ + dbDelete(contractAddress: AztecAddress, slot: Fr): Promise; + + /** + * Copies a number of contiguous entries in the per-contract non-volatile database. This allows for efficient data + * structures by avoiding repeated calls to `dbLoad` and `dbStore`. + * Supports overlapping source and destination regions (which will result in the overlapped source values being + * overwritten). All copied slots must exist in the database (i.e. have been stored and not deleted) + * + * @param contractAddress - The contract address under which the data is scoped. + * @param srcSlot - The first slot to copy from. + * @param dstSlot - The first slot to copy to. + * @param numEntries - The number of entries to copy. + */ + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise; } diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 7f6d10bacfc0..64f2e2c5cecf 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -328,23 +328,35 @@ export class ViewDataOracle extends TypedOracle { await this.db.deliverNote(contractAddress, storageSlot, nonce, content, noteHash, nullifier, txHash, recipient); } - public override store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { - if (!contract.equals(this.contractAddress)) { - // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract - throw new Error( - `Contract address ${contract} does not match the oracle's contract address ${this.contractAddress}`, - ); + public override dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.db.store(this.contractAddress, key, values); + return this.db.dbStore(this.contractAddress, slot, values); } - public override load(contract: AztecAddress, key: Fr): Promise { - if (!contract.equals(this.contractAddress)) { - // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract - throw new Error( - `Contract address ${contract} does not match the oracle's contract address ${this.contractAddress}`, - ); + public override dbLoad(contractAddress: AztecAddress, slot: Fr): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); + } + return this.db.dbLoad(this.contractAddress, slot); + } + + public override dbDelete(contractAddress: AztecAddress, slot: Fr): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); + } + return this.db.dbDelete(this.contractAddress, slot); + } + + public override dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.db.load(this.contractAddress, key); + return this.db.dbCopy(this.contractAddress, srcSlot, dstSlot, numEntries); } } diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 8fc0b3e2cfea..eca11e5a39b1 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -1084,36 +1084,36 @@ export class TXE implements TypedOracle { return preimage.value; } - /** - * Used by contracts during execution to store arbitrary data in the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - The contract address to store the data under. - * @param key - A field element representing the key to store the data under. - * @param values - An array of field elements representing the data to store. - */ - store(contract: AztecAddress, key: Fr, values: Fr[]): Promise { - if (!contract.equals(this.contractAddress)) { - // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract - throw new Error( - `Contract address ${contract} does not match the oracle's contract address ${this.contractAddress}`, - ); + dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); } - return this.txeDatabase.store(this.contractAddress, key, values); - } - - /** - * Used by contracts during execution to load arbitrary data from the local PXE database. The data is siloed/scoped - * to a specific `contract`. - * @param contract - The contract address to load the data from. - * @param key - A field element representing the key under which to load the data.. - * @returns An array of field elements representing the stored data or `null` if no data is stored under the key. - */ - load(contract: AztecAddress, key: Fr): Promise { - if (!contract.equals(this.contractAddress)) { - // TODO(#10727): instead of this check check that this.contractAddress is allowed to process notes for contract - this.debug(`Data not found for contract ${contract.toString()} and key ${key.toString()}`); + return this.txeDatabase.dbStore(this.contractAddress, slot, values); + } + + dbLoad(contractAddress: AztecAddress, slot: Fr): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + this.debug(`Data not found for contract ${contractAddress.toString()} and slot ${slot.toString()}`); return Promise.resolve(null); } - return this.txeDatabase.load(this.contractAddress, key); + return this.txeDatabase.dbLoad(this.contractAddress, slot); + } + + dbDelete(contractAddress: AztecAddress, slot: Fr): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); + } + return this.txeDatabase.dbDelete(this.contractAddress, slot); + } + + dbCopy(contractAddress: AztecAddress, srcSlot: Fr, dstSlot: Fr, numEntries: number): Promise { + if (!contractAddress.equals(this.contractAddress)) { + // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB + throw new Error(`Contract ${contractAddress} is not allowed to access ${this.contractAddress}'s PXE DB`); + } + return this.txeDatabase.dbCopy(this.contractAddress, srcSlot, dstSlot, numEntries); } } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 0164be15d6ea..8e66b794ddb5 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -590,37 +590,49 @@ export class TXEService { return toForeignCallResult([]); } - async store(contract: ForeignCallSingle, key: ForeignCallSingle, values: ForeignCallArray) { - const processedContract = AztecAddress.fromField(fromSingle(contract)); - const processedKey = fromSingle(key); - const processedValues = fromArray(values); - await this.typedOracle.store(processedContract, processedKey, processedValues); + async dbStore(contractAddress: ForeignCallSingle, slot: ForeignCallSingle, values: ForeignCallArray) { + await this.typedOracle.dbStore( + AztecAddress.fromField(fromSingle(contractAddress)), + fromSingle(slot), + fromArray(values), + ); return toForeignCallResult([]); } - /** - * Load data from pxe db. - * @param contract - The contract address. - * @param key - The key to load. - * @param tSize - The size of the serialized object to return. - * @returns The data found flag and the serialized object concatenated in one array. - */ - async load(contract: ForeignCallSingle, key: ForeignCallSingle, tSize: ForeignCallSingle) { - const processedContract = AztecAddress.fromField(fromSingle(contract)); - const processedKey = fromSingle(key); - const values = await this.typedOracle.load(processedContract, processedKey); + async dbLoad(contractAddress: ForeignCallSingle, slot: ForeignCallSingle, tSize: ForeignCallSingle) { + const values = await this.typedOracle.dbLoad(AztecAddress.fromField(fromSingle(contractAddress)), fromSingle(slot)); // We are going to return a Noir Option struct to represent the possibility of null values. Options are a struct // with two fields: `some` (a boolean) and `value` (a field array in this case). if (values === null) { // No data was found so we set `some` to 0 and pad `value` with zeros get the correct return size. - const processedTSize = fromSingle(tSize).toNumber(); - return toForeignCallResult([toSingle(new Fr(0)), toArray(Array(processedTSize).fill(new Fr(0)))]); + return toForeignCallResult([toSingle(new Fr(0)), toArray(Array(fromSingle(tSize).toNumber()).fill(new Fr(0)))]); } else { // Data was found so we set `some` to 1 and return it along with `value`. return toForeignCallResult([toSingle(new Fr(1)), toArray(values)]); } } + async dbDelete(contractAddress: ForeignCallSingle, slot: ForeignCallSingle) { + await this.typedOracle.dbDelete(AztecAddress.fromField(fromSingle(contractAddress)), fromSingle(slot)); + return toForeignCallResult([]); + } + + async dbCopy( + contractAddress: ForeignCallSingle, + srcSlot: ForeignCallSingle, + dstSlot: ForeignCallSingle, + numEntries: ForeignCallSingle, + ) { + await this.typedOracle.dbCopy( + AztecAddress.fromField(fromSingle(contractAddress)), + fromSingle(srcSlot), + fromSingle(dstSlot), + fromSingle(numEntries).toNumber(), + ); + + return toForeignCallResult([]); + } + // AVM opcodes avmOpcodeEmitUnencryptedLog(_message: ForeignCallArray) { From 8e349723d1da89870615171497c3fe3fe146691f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 28 Jan 2025 00:48:39 +0000 Subject: [PATCH 25/39] Add oracle --- .../aztec/src/oracle/note_discovery.nr | 20 ++++++++++- yarn-project/circuits.js/src/structs/index.ts | 1 + .../src/structs/log_with_tx_data.ts | 35 +++++++++++++++++++ .../pxe/src/simulator_oracle/index.ts | 29 +++++++++++++++ .../simulator/src/acvm/oracle/oracle.ts | 15 ++++++++ .../simulator/src/acvm/oracle/typed_oracle.ts | 5 +++ .../simulator/src/client/db_oracle.ts | 3 ++ .../simulator/src/client/view_data_oracle.ts | 5 +++ yarn-project/telemetry-client/package.json | 2 +- yarn-project/txe/src/oracle/txe_oracle.ts | 5 +++ 10 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 yarn-project/circuits.js/src/structs/log_with_tx_data.ts 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 8d4c2848991b..fc3d2d4a8b93 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,8 @@ use crate::note::discovery::MAX_NOTE_SERIALIZED_LEN; -use dep::protocol_types::address::AztecAddress; +use dep::protocol_types::{ + address::AztecAddress, + constants::{MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS}, +}; /// 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. @@ -36,6 +39,18 @@ pub unconstrained fn deliver_note( ) } +pub struct LogWithTxData { + pub log_content: BoundedVec, + pub tx_hash: Field, + pub unique_note_hashes_in_tx: BoundedVec, + pub first_nullifier_in_tx: Field, +} + +// todo: return a bounded vec of these +pub unconstrained fn get_log_by_tag(tag: Field) -> Option { + get_log_by_tag_oracle(tag) +} + #[oracle(deliverNote)] unconstrained fn deliver_note_oracle( contract_address: AztecAddress, @@ -47,3 +62,6 @@ unconstrained fn deliver_note_oracle( tx_hash: Field, recipient: AztecAddress, ) -> bool {} + +#[oracle(getLogByTag)] +unconstrained fn get_log_by_tag_oracle(tag: Field) -> Option {} diff --git a/yarn-project/circuits.js/src/structs/index.ts b/yarn-project/circuits.js/src/structs/index.ts index 7a727fcff51b..c12a90db3792 100644 --- a/yarn-project/circuits.js/src/structs/index.ts +++ b/yarn-project/circuits.js/src/structs/index.ts @@ -16,6 +16,7 @@ export * from './gas_settings.js'; export * from './global_variables.js'; export * from './block_header.js'; export * from './indexed_tagging_secret.js'; +export * from './log_with_tx_data.js'; export * from './kernel/private_to_rollup_accumulated_data.js'; export * from './kernel/combined_constant_data.js'; export * from './kernel/private_to_rollup_kernel_circuit_public_inputs.js'; diff --git a/yarn-project/circuits.js/src/structs/log_with_tx_data.ts b/yarn-project/circuits.js/src/structs/log_with_tx_data.ts new file mode 100644 index 000000000000..94a86b95709a --- /dev/null +++ b/yarn-project/circuits.js/src/structs/log_with_tx_data.ts @@ -0,0 +1,35 @@ +import { Fr } from '@aztec/foundation/fields'; + +import { MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS } from '../constants.gen.js'; + +export class LogWithTxData { + constructor( + public logContent: Fr[], + public txHash: Fr, + public uniqueNoteHashesInTx: Fr[], + public firstNullifierInTx: Fr, + ) {} + + toNoirSerialization(): (Fr | Fr[])[] { + return [ + toBoundedVecStorage(this.logContent, PUBLIC_LOG_DATA_SIZE_IN_FIELDS), + this.txHash, + toBoundedVecStorage(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), + this.firstNullifierInTx, + ]; + } + + static noirSerializationOfEmpty(): (Fr | Fr[])[] { + return new LogWithTxData([], new Fr(0), [], new Fr(0)).toNoirSerialization(); + } +} + +function toBoundedVecStorage(array: Fr[], maxLength: number) { + if (array.length > maxLength) { + throw new Error( + `An array of length ${array.length} cannot be converted to a BoundedVec of max length ${maxLength}`, + ); + } + + return array.concat(Array(maxLength - array.length).fill(new Fr(0))); +} diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 1317c88eddfb..bf8fa60d1189 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -24,8 +24,10 @@ import { IndexedTaggingSecret, type KeyValidationRequest, type L1_TO_L2_MSG_TREE_HEIGHT, + LogWithTxData, MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS, + PUBLIC_LOG_DATA_SIZE_IN_FIELDS, PrivateLog, PublicLog, computeAddressSecret, @@ -41,6 +43,7 @@ import { } from '@aztec/foundation/abi'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { createLogger } from '@aztec/foundation/log'; +import { BufferReader } from '@aztec/foundation/serialize'; import { type KeyStore } from '@aztec/key-store'; import { type AcirSimulator, @@ -706,6 +709,32 @@ export class SimulatorOracle implements DBOracle { }); } + public async getLogByTag(tag: Fr): Promise { + const logs = await this.aztecNode.getLogsByTags([tag]); + const logsForTag = logs[0]; + + if (logsForTag.length == 0) { + return null; + } else if (logsForTag.length > 1) { + throw new Error(`Got ${logsForTag.length} logs for tag ${tag}. getLogByTag currently only supports a single log`); + } + + const log = logsForTag[0]; + + const txEffect = await this.aztecNode.getTxEffect(log.txHash); + if (txEffect == undefined) { + throw new Error(`Unexpected: failed to retrieve tx effects for tx ${log.txHash} which is known to exist`); + } + + const reader = BufferReader.asReader(log.logData); + return new LogWithTxData( + reader.readArray(PUBLIC_LOG_DATA_SIZE_IN_FIELDS, Fr), + log.txHash.hash, + txEffect.data.noteHashes, + txEffect.data.nullifiers[0], + ); + } + public async removeNullifiedNotes(contractAddress: AztecAddress) { this.log.verbose('Removing nullified notes', { contract: contractAddress }); diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 3d2501a3a2eb..3f7a110bfbcc 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -1,4 +1,5 @@ import { MerkleTreeId, UnencryptedL2Log } from '@aztec/circuit-types'; +import { LogWithTxData } from '@aztec/circuits.js'; import { FunctionSelector, NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; @@ -404,6 +405,16 @@ export class Oracle { return toACVMField(true); } + async getLogByTag([tag]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { + const log = await this.typedOracle.getLogByTag(fromACVMField(tag)); + + if (log == null) { + return [toACVMField(0), ...LogWithTxData.noirSerializationOfEmpty().map(toACVMFieldSingleOrArray)]; + } else { + return [toACVMField(1), ...log.toNoirSerialization().map(toACVMFieldSingleOrArray)]; + } + } + async dbStore([contractAddress]: ACVMField[], [slot]: ACVMField[], values: ACVMField[]) { await this.typedOracle.dbStore( AztecAddress.fromField(fromACVMField(contractAddress)), @@ -451,3 +462,7 @@ export class Oracle { ); } } + +function toACVMFieldSingleOrArray(value: Fr | Fr[]) { + return Array.isArray(value) ? value.map(toACVMField) : toACVMField(value); +} diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 6eac05947f37..afa1dcb66ea9 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -13,6 +13,7 @@ import { type IndexedTaggingSecret, type KeyValidationRequest, type L1_TO_L2_MSG_TREE_HEIGHT, + type LogWithTxData, } from '@aztec/circuits.js'; import { type FunctionSelector, type NoteSelector } from '@aztec/foundation/abi'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; @@ -246,6 +247,10 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('deliverNote'); } + getLogByTag(_tag: Fr): Promise { + throw new OracleMethodNotAvailableError('getLogByTag'); + } + dbStore(_contractAddress: AztecAddress, _key: Fr, _values: Fr[]): Promise { throw new OracleMethodNotAvailableError('dbStore'); } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 594af643bccb..4ebb95ae9f94 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -12,6 +12,7 @@ import { type ContractInstance, type IndexedTaggingSecret, type KeyValidationRequest, + LogWithTxData, } from '@aztec/circuits.js'; import { type FunctionArtifact, type FunctionSelector } from '@aztec/foundation/abi'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; @@ -257,6 +258,8 @@ export interface DBOracle extends CommitmentsDB { recipient: AztecAddress, ): Promise; + getLogByTag(tag: Fr): Promise; + /** * Removes all of a contract's notes that have been nullified from the note database. */ diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 64f2e2c5cecf..abb086eef99c 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -12,6 +12,7 @@ import { type ContractInstance, type IndexedTaggingSecret, type KeyValidationRequest, + type LogWithTxData, } from '@aztec/circuits.js'; import { siloNullifier } from '@aztec/circuits.js/hash'; import { AztecAddress } from '@aztec/foundation/aztec-address'; @@ -328,6 +329,10 @@ export class ViewDataOracle extends TypedOracle { await this.db.deliverNote(contractAddress, storageSlot, nonce, content, noteHash, nullifier, txHash, recipient); } + public override getLogByTag(tag: Fr): Promise { + return this.db.getLogByTag(tag); + } + public override dbStore(contractAddress: AztecAddress, slot: Fr, values: Fr[]): Promise { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB diff --git a/yarn-project/telemetry-client/package.json b/yarn-project/telemetry-client/package.json index d2f76ca021ea..887088da9179 100644 --- a/yarn-project/telemetry-client/package.json +++ b/yarn-project/telemetry-client/package.json @@ -88,4 +88,4 @@ "../../foundation/src/jest/setup.mjs" ] } -} \ No newline at end of file +} diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index a3457c914445..4feab1d07df6 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -24,6 +24,7 @@ import { IndexedTaggingSecret, type KeyValidationRequest, type L1_TO_L2_MSG_TREE_HEIGHT, + type LogWithTxData, NULLIFIER_SUBTREE_HEIGHT, type NULLIFIER_TREE_HEIGHT, type NullifierLeafPreimage, @@ -996,6 +997,10 @@ export class TXE implements TypedOracle { throw new Error('deliverNote'); } + async getLogByTag(tag: Fr): Promise { + return await this.simulatorOracle.getLogByTag(tag); + } + // AVM oracles async avmOpcodeCall(targetContractAddress: AztecAddress, args: Fr[], isStaticCall: boolean): Promise { From b5bfca638faca0ed7e154e30ebcfefc68cf11f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Tue, 28 Jan 2025 00:49:25 +0000 Subject: [PATCH 26/39] wip --- .../aztec-nr/aztec/src/encrypted_logs/mod.nr | 2 + .../aztec/src/encrypted_logs/partial.nr | 34 ++ .../aztec/src/macros/functions/mod.nr | 22 ++ .../aztec-nr/aztec/src/macros/mod.nr | 153 ++++---- .../aztec-nr/aztec/src/macros/notes/mod.nr | 2 + .../aztec-nr/aztec/src/note/discovery/mod.nr | 346 +----------------- .../src/note/discovery/nonce_discovery.nr | 78 ++++ .../aztec/src/note/discovery/partial_notes.nr | 129 +++++++ .../aztec/src/note/discovery/private_logs.nr | 200 ++++++++++ .../aztec-nr/aztec/src/note/utils.nr | 20 +- .../aztec/src/oracle/note_discovery.nr | 11 +- .../aztec-nr/aztec/src/oracle/notes.nr | 4 +- .../aztec-nr/aztec/src/utils/array/mod.nr | 9 + .../crates/types/src/lib.nr | 1 + .../crates/types/src/log_with_tx_data.nr | 6 + .../end-to-end/src/e2e_partial_stuff.test.ts | 42 +++ .../simulator/src/acvm/deserialize.ts | 2 +- .../simulator/src/acvm/oracle/oracle.ts | 23 +- .../simulator/src/acvm/oracle/typed_oracle.ts | 4 + yarn-project/simulator/src/acvm/serialize.ts | 22 ++ .../src/client/client_execution_context.ts | 7 + .../simulator/src/client/db_oracle.ts | 4 +- 22 files changed, 687 insertions(+), 434 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr create mode 100644 noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr create mode 100644 noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr create mode 100644 noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr create mode 100644 noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr create mode 100644 yarn-project/end-to-end/src/e2e_partial_stuff.test.ts diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr index edd6d4224b03..f8847939664a 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr @@ -2,3 +2,5 @@ pub mod header; pub mod payload; pub mod encrypted_note_emission; pub mod encrypted_event_emission; + +pub mod partial; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr new file mode 100644 index 000000000000..56b48fd81365 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr @@ -0,0 +1,34 @@ +use crate::context::private_context::PrivateContext; +use protocol_types::address::aztec_address::AztecAddress; + +pub fn compute_and_emit_partial_note_private_log( + context: &mut PrivateContext, + recipient: AztecAddress, + sender: AztecAddress, + log_plaintext: [u8; PLAINTEXT_LEN], +) { + // let magic_tag: Field = 42424242; + // let magic_tag_encoded: [u8; 32] = magic_tag.to_be_bytes(); + + let mut mutated_plaintext: [u8; PLAINTEXT_LEN + 32] = std::mem::zeroed(); + + for i in 0..log_plaintext.len() { + mutated_plaintext[i] = log_plaintext[i]; + } + // for i in 0..32 { + // mutated_plaintext[i] = log_plaintext[i]; + // mutated_plaintext[i + 32] = log_plaintext[i + 32]; + // } + mutated_plaintext[32 + 31] = log_plaintext[32 + 31] + 128; + + /// Safety: lol + let contract_address = unsafe { crate::oracle::execution::get_contract_address() }; + + let payload = super::payload::compute_private_log_payload( + contract_address, + recipient, + sender, + mutated_plaintext, + ); + context.emit_private_log(payload); +} diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index 3f8f5c3817b6..db9d7d962139 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -81,6 +81,21 @@ comptime fn create_init_check(f: FunctionDefinition) -> Quoted { .quoted_contents() } +comptime fn create_note_discovery_call() -> Quoted { + quote { + unsafe { dep::aztec::note::discovery::discover_new_notes(context.this_address(), |serialized_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { + let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, serialized_note_content); + + Option::some( + aztec::note::discovery::NoteHashAndNullifier { + note_hash: hashes[0], + inner_nullifier: hashes[3], + }, + ) + }) }; + } +} + /// Private functions are executed client-side and preserve privacy. pub comptime fn private(f: FunctionDefinition) -> Quoted { let fn_abi = create_fn_abi_export(f); @@ -160,6 +175,8 @@ pub comptime fn private(f: FunctionDefinition) -> Quoted { quote {} }; + let note_discovery_call = create_note_discovery_call(); + // Finally, we need to change the return type to be `PrivateCircuitPublicInputs`, which is what the Private Kernel // circuit expects. let return_value_var_name = quote { macro__returned__values }; @@ -211,6 +228,7 @@ pub comptime fn private(f: FunctionDefinition) -> Quoted { $internal_check $view_check $storage_init + $note_discovery_call }; let to_append = quote { @@ -334,9 +352,13 @@ pub comptime fn transform_unconstrained(f: FunctionDefinition) { } else { quote {} }; + + let note_discovery_call = create_note_discovery_call(); + let to_prepend = quote { $context_creation $storage_init + $note_discovery_call }; let body = f.body().as_block().unwrap(); let modified_body = modify_fn_body(body, to_prepend, quote {}); diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 2f951652348c..b72b0ac47ce8 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -26,9 +26,11 @@ pub comptime fn aztec(m: Module) -> Quoted { transform_unconstrained(f); } + let contract_library_method_compute_note_hash_and_optionally_a_nullifier = + generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier(); let compute_note_hash_and_optionally_a_nullifier = generate_compute_note_hash_and_optionally_a_nullifier(); - let process_logs = generate_process_log(); + let process_log = generate_process_log(); let note_exports = generate_note_exports(); let public_dispatch = generate_public_dispatch(m); let sync_notes = generate_sync_notes(); @@ -36,8 +38,9 @@ pub comptime fn aztec(m: Module) -> Quoted { quote { $note_exports $interface + $contract_library_method_compute_note_hash_and_optionally_a_nullifier $compute_note_hash_and_optionally_a_nullifier - $process_logs + $process_log $public_dispatch $sync_notes } @@ -111,54 +114,78 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } -comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { - let mut max_note_length: u32 = 0; +comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier() -> Quoted { let notes = NOTES.entries(); - let body = if notes.len() > 0 { - max_note_length = notes.fold( - 0, - |acc, (_, (_, len, _, _)): (Type, (StructDefinition, u32, Field, [(Quoted, u32, bool)]))| { - if len > acc { - len - } else { - acc - } - }, - ); - - let mut if_statements_list = &[]; + let body = if notes.len() == 0 { + quote { panic(f"This contract does not use private notes") } + } else { + let mut if_note_type_id_match_statements_list = &[]; for i in 0..notes.len() { - let (typ, (_, _, _, _)) = notes[i]; + let (typ, (_, serialized_note_length, _, _)) = notes[i]; + let if_or_else_if = if i == 0 { quote { if } } else { quote { else if } }; - if_statements_list = if_statements_list.push_back( + + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( quote { - $if_or_else_if note_type_id == $typ::get_note_type_id() { - aztec::note::utils::compute_note_hash_and_optionally_a_nullifier($typ::deserialize_content, note_header, compute_nullifier, serialized_note) - } - }, + $if_or_else_if note_type_id == $typ::get_note_type_id() { + // As an extra safety check we make sure that the serialized_note_content bounded vec has the + // expected length, to avoid scenarios in which compute_note_hash_and_optionally_a_nullifier + // silently trims the end if the log were to be longer. + let expected_len = $serialized_note_length; + let actual_len = serialized_note_content.len(); + assert( + actual_len == expected_len, + f"Expected note content of length {expected_len} but got {actual_len} for note type ID {note_type_id}" + ); + + aztec::note::utils::compute_note_hash_and_optionally_a_nullifier($typ::deserialize_content, contract_address, nonce, storage_slot, compute_nullifier, serialized_note_content.storage()) + } + }, ); } - let if_statements = if_statements_list.join(quote {}); + let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); quote { - let note_header = aztec::prelude::NoteHeader::new(contract_address, nonce, storage_slot); - $if_statements + $if_note_type_id_match_statements else { panic(f"Unknown note type ID") } } - } else { - quote { - panic(f"No notes defined") - } }; + quote { + #[contract_library_method] + unconstrained fn _compute_note_hash_and_optionally_a_nullifier( + contract_address: aztec::protocol_types::address::AztecAddress, + nonce: Field, + storage_slot: Field, + note_type_id: Field, + compute_nullifier: bool, + serialized_note_content: BoundedVec, + ) -> pub [Field; 4] { + $body + } + } +} + +comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { + // For historical reasons we keep this function taking an array of a length of up to the longest note, receiving a + // boolean and returning four fields. The contract library method `_compute_note_hash...` is affected by this. + // Ideally we'd eventually remove these things. + + let max_note_length = NOTES.entries().fold( + 0, + |acc, (_, (_, len, _, _)): (Type, (StructDefinition, u32, Field, [(Quoted, u32, bool)]))| { + std::cmp::max(len, acc) + }, + ); + quote { unconstrained fn compute_note_hash_and_optionally_a_nullifier( contract_address: aztec::protocol_types::address::AztecAddress, @@ -168,7 +195,7 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { compute_nullifier: bool, serialized_note: [Field; $max_note_length], ) -> pub [Field; 4] { - $body + _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, compute_nullifier, BoundedVec::from_array(serialized_note)) } } } @@ -211,78 +238,44 @@ comptime fn generate_process_log() -> Quoted { let notes = NOTES.entries(); - let mut if_note_type_id_match_statements_list = &[]; - for i in 0..notes.len() { - let (typ, (_, serialized_note_length, _, _)) = notes[i]; - - let if_or_else_if = if i == 0 { - quote { if } - } else { - quote { else if } - }; - - if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( - quote { - $if_or_else_if note_type_id == $typ::get_note_type_id() { - // As an extra safety check we make sure that the serialized_note_content bounded vec has the - // expected length, to avoid scenarios in which compute_note_hash_and_optionally_a_nullifier - // silently trims the end if the log were to be longer. - let expected_len = $serialized_note_length; - let actual_len = serialized_note_content.len(); - assert( - actual_len == expected_len, - f"Expected note content of length {expected_len} but got {actual_len} for note type id {note_type_id}" - ); - - aztec::note::utils::compute_note_hash_and_optionally_a_nullifier($typ::deserialize_content, note_header, true, serialized_note_content.storage()) - } - }, - ); - } - - let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); - - let body = if notes.len() > 0 { + let body = if notes.len() == 0 { + quote { panic(f"This contract does not use private notes") } + } else { quote { // Because this unconstrained function is injected after the contract is processed by the macros, it'll not // be modified by the macros that alter unconstrained functions. As such, we need to manually inject the // unconstrained execution context since it will not be available otherwise. - let context = dep::aztec::context::unconstrained_context::UnconstrainedContext::new(); + let context = aztec::context::unconstrained_context::UnconstrainedContext::new(); - dep::aztec::note::discovery::do_process_log( - context, + // TODO(#10727): allow other contracts to process logs and deliver notes + let contract_address = context.this_address(); + + aztec::note::discovery::private_logs::do_process_log( + contract_address, log_plaintext, tx_hash, unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, - |serialized_note_content: BoundedVec, note_header, note_type_id| { - let hashes = $if_note_type_id_match_statements - else { - panic(f"Unknown note type id {note_type_id}") - }; + |serialized_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { + let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, serialized_note_content); Option::some( - dep::aztec::note::discovery::NoteHashesAndNullifier { + aztec::note::discovery::NoteHashAndNullifier { note_hash: hashes[0], - unique_note_hash: hashes[1], inner_nullifier: hashes[3], }, ) } ); } - } else { - quote { - panic(f"No notes defined") - } }; quote { unconstrained fn process_log( - log_plaintext: BoundedVec, + log_plaintext: BoundedVec, tx_hash: Field, - unique_note_hashes_in_tx: BoundedVec, + unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: aztec::protocol_types::address::AztecAddress, ) { @@ -305,7 +298,7 @@ comptime fn generate_note_exports() -> Quoted { comptime fn generate_sync_notes() -> Quoted { quote { unconstrained fn sync_notes() { - aztec::oracle::notes::sync_notes(); + aztec::oracle::note_discovery::sync_notes(); } } } diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr index 53efed5e62ef..c86c4b7610ce 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr @@ -469,6 +469,8 @@ comptime fn generate_setup_payload( } pub fn encrypt_log(self, context: &mut PrivateContext, recipient: aztec::protocol_types::address::AztecAddress, sender: aztec::protocol_types::address::AztecAddress) -> [Field; $encrypted_log_field_length] { + aztec::encrypted_logs::partial::compute_and_emit_partial_note_private_log(context, recipient, sender, self.log_plaintext); + let encrypted_log_bytes: [u8; $encrypted_log_byte_length] = aztec::encrypted_logs::payload::compute_partial_public_log_payload( context.this_address(), recipient, 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 40a3f7c27df9..ac7a3ecdd06d 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -14,7 +14,11 @@ use dep::protocol_types::{ traits::{Deserialize, FromField, Serialize, ToField}, }; -use crate::oracle::note_discovery::PUBLIC_LOG_SIZE_IN_FIELDS; // todo: move to constants +pub global PUBLIC_LOG_SIZE_IN_FIELDS: u32 = 14; // todo: move to constants + +mod private_logs; +mod partial_notes; +mod nonce_discovery; // We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and // one for the note type id. @@ -23,8 +27,8 @@ global NOTE_PRIVATE_LOG_RESERVED_FIELDS: u32 = 2; pub global MAX_NOTE_SERIALIZED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; -global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; -global MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN: u32 = +pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; +pub global MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN: u32 = MAX_NOTE_SERIALIZED_LEN - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN; pub struct NoteHashAndNullifier { @@ -34,341 +38,15 @@ pub struct NoteHashAndNullifier { pub unconstrained fn discover_new_notes( contract_address: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, ) { - fetch_and_process_private_logs(contract_address, compute_note_hash_and_nullifier); - - let pending_partial_notes = DBArray::at( + private_logs::fetch_and_process_private_tagged_logs( contract_address, - DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_SLOT, - ); - - for i in 0..pending_partial_notes.len() { - let pending_partial_note: DeliveredPendingPartialNote = pending_partial_notes.get(i); - - let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); - if maybe_log.is_some() { - let log = maybe_log.unwrap(); - - let complete_serialized_note_content = array::append( - pending_partial_note.serialized_private_note_content, - log.log_content, - ); - - let discovered_notes = attempt_note_nonce_discovery( - log.unique_note_hashes_in_tx, - log.first_nullifier_in_tx, - compute_note_hash_and_nullifier, - contract_address, - pending_partial_note.storage_slot, - pending_partial_note.note_type_id, - complete_serialized_note_content, - ); - - for_each_in_bounded_vec( - discovered_notes, - |discovered_note: DiscoveredNoteInfo, _| { - // TODO:(#10728): handle notes that fail delivery - assert( - deliver_note( - contract_address, - pending_partial_note.storage_slot, - discovered_note.nonce, - complete_serialized_note_content, - discovered_note.note_hash, - discovered_note.inner_nullifier, - log.tx_hash, - pending_partial_note.recipient, - ), - "Failed to deliver note", - ); - }, - ); - - //pending_partial_notes.pop() - } - // todo: handle que nunca aparezca el log? podria no ser nunca - } -} - -unconstrained fn fetch_and_process_private_logs( - _contract_address: AztecAddress, - _compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, -) { - // We will eventually fetch tagged logs, decrypt and process them here, but for now we simply call the `syncNotes` - // oracle, which performs tag synchronization, log download, decryption, and then calls the `process_log` function - // which the decrypted payload, at which point `do_process_log` gets executed and we continue the work ourselves. - sync_notes(); -} - -/// Processes a log given its plaintext by trying to find notes encoded in it. This process involves the discovery of -/// the nonce of any such notes, which requires knowledge of the transaction hash in which the notes would've been -/// created, along with the list of unique note hashes in said transaction. -/// -/// Additionally, this requires a `compute_note_hash_and_nullifier` lambda that is able to compute these values for any -/// note in the contract given their contents. A typical implementation of such a function would look like this: -/// -/// ``` -/// |serialized_note_content, note_header, note_type_id| { -/// let hashes = if note_type_id == MyNoteType::get_note_type_id() { -/// assert(serialized_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); -/// dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( -/// MyNoteType::deserialize_content, -/// note_header, -/// true, -/// serialized_note_content.storage(), -/// ) -/// } else { -/// panic(f"Unknown note type id {note_type_id}") -/// }; -/// -/// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { -/// note_hash: hashes[0], -/// unique_note_hash: hashes[1], -/// inner_nullifier: hashes[3], -/// }) -/// } -/// ``` -pub unconstrained fn do_process_log( - context: UnconstrainedContext, - log_plaintext: BoundedVec, - tx_hash: Field, - unique_note_hashes_in_tx: BoundedVec, - first_nullifier_in_tx: Field, - recipient: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, -) { - let (storage_slot, note_type_id, log_type_id, log_payload) = - destructure_log_plaintext(log_plaintext); - - if log_type_id == 0 { - process_private_note_log( - context, - tx_hash, - unique_note_hashes_in_tx, - first_nullifier_in_tx, - recipient, - compute_note_hash_and_nullifier, - storage_slot, - note_type_id, - log_payload, - ); - } else if log_type_id == 1 { - process_partial_note_private_log( - context, - storage_slot, - note_type_id, - log_payload, - recipient, - ); - } else { - panic(f"Unknown log type id {log_type_id}"); - } -} - -unconstrained fn destructure_log_plaintext( - log_plaintext: BoundedVec, -) -> (Field, Field, Field, BoundedVec) { - assert(log_plaintext.len() >= NOTE_PRIVATE_LOG_RESERVED_FIELDS); - - // If NOTE_PRIVATE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for - // `storage_slot` and `note_type_id` must be updated as well. - static_assert( - NOTE_PRIVATE_LOG_RESERVED_FIELDS == 2, - "unepxected value for NOTE_PRIVATE_LOG_RESERVED_FIELDS", - ); - let storage_slot = log_plaintext.get(0); - - // The combined type id is the bit packing of the note type id (which is 7 bits big) and the log type id (which is - // currently a single bit, and right above it). - let combined_type_id = log_plaintext.get(1); - let note_type_id = ((combined_type_id as u64) % 128) as Field; - let log_type_id = combined_type_id / 128; - - let log_payload = array::subbvec(log_plaintext, NOTE_PRIVATE_LOG_RESERVED_FIELDS); - - (storage_slot, note_type_id, log_type_id, log_payload) -} - -unconstrained fn process_private_note_log( - context: UnconstrainedContext, - tx_hash: Field, - unique_note_hashes_in_tx: BoundedVec, - first_nullifier_in_tx: Field, - recipient: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, - storage_slot: Field, - note_type_id: Field, - serialized_note_content: BoundedVec, -) { - let discovered_notes = attempt_note_nonce_discovery( - unique_note_hashes_in_tx, - first_nullifier_in_tx, compute_note_hash_and_nullifier, - context.this_address(), - storage_slot, - note_type_id, - serialized_note_content, ); - for_each_in_bounded_vec( - discovered_notes, - |discovered_note: DiscoveredNoteInfo, _| { - // TODO:(#10728): handle notes that fail delivery - assert( - deliver_note( - context.this_address(), // TODO(#10727): allow other contracts to deliver notes - storage_slot, - discovered_note.nonce, - serialized_note_content, - discovered_note.note_hash, - discovered_note.inner_nullifier, - tx_hash, - recipient, - ), - "Failed to deliver note", - ); - }, - ); -} - -struct DiscoveredNoteInfo { - nonce: Field, - note_hash: Field, - inner_nullifier: Field, -} - -unconstrained fn attempt_note_nonce_discovery( - unique_note_hashes_in_tx: BoundedVec, - first_nullifier_in_tx: Field, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, Field, Field) -> Option, - contract_address: AztecAddress, - storage_slot: Field, - note_type_id: Field, - serialized_note_content: BoundedVec, -) -> BoundedVec { - let mut discovered_notes = BoundedVec::new(); - - // We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash - for_each_in_bounded_vec( - unique_note_hashes_in_tx, - |expected_unique_note_hash, i| { - let candidate_nonce = compute_note_hash_nonce(first_nullifier_in_tx, i); - - // TODO(#11157): handle failed note_hash_and_nullifier computation - let hashes = compute_note_hash_and_nullifier( - serialized_note_content, - storage_slot, - note_type_id, - ) - .unwrap(); - - let siloed_note_hash = compute_siloed_note_hash(contract_address, hashes.note_hash); - let unique_note_hash = compute_unique_note_hash(candidate_nonce, siloed_note_hash); - - if unique_note_hash == expected_unique_note_hash { - // Note that we perform no validataions on the nullifier - we fundamentally cannot, since only the - // application knows how to compute nullifiers, and we simply trust it to have provided the correct one. - // If it hasn't, then PXE may fail to realize that a given note has been nullified already and end up - // producing invalid transactions (with duplicate nullifiers). - discovered_notes.push( - DiscoveredNoteInfo { - nonce: candidate_nonce, - note_hash: hashes.note_hash, - inner_nullifier: hashes.inner_nullifier, - }, - ); - // We don't exit the loop - it is possible (though rare) for the exact same note content to be present - // multiple times in the same transaction with different nonces. This typically doesn't happen due to - // notes containing random values in order to hide their contents. - } - }, + partial_notes::fetch_and_process_public_partial_note_completion_logs( + contract_address, + compute_note_hash_and_nullifier, ); - - discovered_notes -} - -struct DeliveredPendingPartialNote { - note_completion_log_tag: Field, - storage_slot: Field, - note_type_id: Field, - serialized_private_note_content: BoundedVec, - recipient: AztecAddress, -} - -// TODO: use `derive(Serialize)`, but we need for generics in struct fields to be handled properly before that happens. -// Currently blocked by Noir's StructDefinition::fields. -impl Serialize for DeliveredPendingPartialNote { - fn serialize(self) -> [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5] { - [ - self.note_completion_log_tag, - self.storage_slot, - self.note_type_id, - self.serialized_private_note_content.storage()[0], - self.serialized_private_note_content.storage()[1], - self.serialized_private_note_content.storage()[2], - self.serialized_private_note_content.storage()[3], - self.serialized_private_note_content.storage()[4], - self.serialized_private_note_content.storage()[5], - self.serialized_private_note_content.storage()[6], - self.serialized_private_note_content.storage()[7], - self.serialized_private_note_content.storage()[8], - self.serialized_private_note_content.storage()[9], - self.serialized_private_note_content.storage()[10], - self.serialized_private_note_content.storage()[12], - self.serialized_private_note_content.storage()[13], - self.serialized_private_note_content.storage()[14], - self.serialized_private_note_content.storage()[15], - self.serialized_private_note_content.len() as Field, - self.recipient.to_field(), - ] - } -} - -impl Deserialize for DeliveredPendingPartialNote { - fn deserialize(values: [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5]) -> Self { - Self { - note_completion_log_tag: values[0], - storage_slot: values[1], - note_type_id: values[2], - serialized_private_note_content: BoundedVec::from_parts( - array::subarray(values, 3), - values[18] as u32, - ), - recipient: AztecAddress::from_field(values[19]), - } - } -} - -global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_SLOT: Field = 5; - -unconstrained fn process_partial_note_private_log( - context: UnconstrainedContext, - storage_slot: Field, - note_type_id: Field, - log_payload: BoundedVec, - recipient: AztecAddress, -) { - let pending = DeliveredPendingPartialNote { - note_completion_log_tag: log_payload.get(0), - storage_slot, - note_type_id, - serialized_private_note_content: array::subbvec(log_payload, 1), - recipient, - }; - - DBArray::at( - context.this_address(), - DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_SLOT, - ) - .push(pending); -} - -unconstrained fn for_each_in_bounded_vec( - vec: BoundedVec, - f: fn[Env](T, u32) -> (), -) { - for i in 0..vec.len() { - f(vec.get_unchecked(i), i); - } } diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr new file mode 100644 index 000000000000..3e905cb82deb --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr @@ -0,0 +1,78 @@ +use crate::{note::discovery::{MAX_NOTE_SERIALIZED_LEN, NoteHashAndNullifier}, utils::array}; + +use dep::protocol_types::{ + address::AztecAddress, + constants::MAX_NOTE_HASHES_PER_TX, + debug_log::debug_log_format, + hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, + traits::ToField, +}; + +pub(crate) struct DiscoveredNoteInfo { + pub(crate) nonce: Field, + pub(crate) note_hash: Field, + pub(crate) inner_nullifier: Field, +} + +unconstrained fn attempt_note_nonce_discovery( + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + contract_address: AztecAddress, + storage_slot: Field, + note_type_id: Field, + serialized_note_content: BoundedVec, +) -> BoundedVec { + let discovered_notes = &mut BoundedVec::new(); + + debug_log_format( + "Attempting note discovery on {0} potential notes on contract {1} for storage slot {2}", + [unique_note_hashes_in_tx.len() as Field, contract_address.to_field(), storage_slot], + ); + + // We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash + array::for_each_in_bounded_vec( + unique_note_hashes_in_tx, + |expected_unique_note_hash, i| { + let candidate_nonce = compute_note_hash_nonce(first_nullifier_in_tx, i); + + // TODO(#11157): handle failed note_hash_and_nullifier computation + let hashes = compute_note_hash_and_nullifier( + serialized_note_content, + contract_address, + candidate_nonce, + storage_slot, + note_type_id, + ) + .unwrap(); + + let siloed_note_hash = compute_siloed_note_hash(contract_address, hashes.note_hash); + let unique_note_hash = compute_unique_note_hash(candidate_nonce, siloed_note_hash); + + if unique_note_hash == expected_unique_note_hash { + // Note that we perform no validations on the nullifier - we fundamentally cannot, since only the + // application knows how to compute nullifiers, and we simply trust it to have provided the correct one. + // If it hasn't, then PXE may fail to realize that a given note has been nullified already and end up + // producing invalid transactions (with duplicate nullifiers). + discovered_notes.push( + DiscoveredNoteInfo { + nonce: candidate_nonce, + note_hash: hashes.note_hash, + inner_nullifier: hashes.inner_nullifier, + }, + ); + + // We don't exit the loop - it is possible (though rare) for the exact same note content to be present + // multiple times in the same transaction with different nonces. This typically doesn't happen due to + // notes containing random values in order to hide their contents. + } + }, + ); + + debug_log_format( + "Discovered a total of {0} notes", + [discovered_notes.len() as Field], + ); + + *discovered_notes +} diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr new file mode 100644 index 000000000000..9ee0148f0f8c --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr @@ -0,0 +1,129 @@ +use crate::{ + note::discovery::{ + MAX_NOTE_SERIALIZED_LEN, + MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN, + nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, + NoteHashAndNullifier, + }, + oracle::note_discovery::{deliver_note, get_log_by_tag}, + pxe_db::DBArray, + utils::array, +}; + +use dep::protocol_types::{ + address::AztecAddress, + constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, + hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, + traits::{Deserialize, FromField, Serialize, ToField}, +}; + +pub global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT: Field = 5; + +pub(crate) struct DeliveredPendingPartialNote { + pub(crate) note_completion_log_tag: Field, + pub(crate) storage_slot: Field, + pub(crate) note_type_id: Field, + pub(crate) serialized_private_note_content: BoundedVec, + pub(crate) recipient: AztecAddress, +} + +// TODO: use `derive(Serialize)`, but we need for generics in struct fields to be handled properly before that happens. +// Currently blocked by Noir's StructDefinition::fields. +impl Serialize for DeliveredPendingPartialNote { + fn serialize(self) -> [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5] { + [ + self.note_completion_log_tag, + self.storage_slot, + self.note_type_id, + self.serialized_private_note_content.storage()[0], + self.serialized_private_note_content.storage()[1], + self.serialized_private_note_content.storage()[2], + self.serialized_private_note_content.storage()[3], + self.serialized_private_note_content.storage()[4], + self.serialized_private_note_content.storage()[5], + self.serialized_private_note_content.storage()[6], + self.serialized_private_note_content.storage()[7], + self.serialized_private_note_content.storage()[8], + self.serialized_private_note_content.storage()[9], + self.serialized_private_note_content.storage()[10], + self.serialized_private_note_content.storage()[12], + self.serialized_private_note_content.storage()[13], + self.serialized_private_note_content.storage()[14], + self.serialized_private_note_content.storage()[15], + self.serialized_private_note_content.len() as Field, + self.recipient.to_field(), + ] + } +} + +impl Deserialize for DeliveredPendingPartialNote { + fn deserialize(values: [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5]) -> Self { + Self { + note_completion_log_tag: values[0], + storage_slot: values[1], + note_type_id: values[2], + serialized_private_note_content: BoundedVec::from_parts( + array::subarray(values, 3), + values[18] as u32, + ), + recipient: AztecAddress::from_field(values[19]), + } + } +} + +pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( + contract_address: AztecAddress, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, +) { + let pending_partial_notes = DBArray::at( + contract_address, + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT, + ); + + for i in 0..pending_partial_notes.len() { + let pending_partial_note: DeliveredPendingPartialNote = pending_partial_notes.get(i); + + let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); + if maybe_log.is_some() { + let log = maybe_log.unwrap(); + + let complete_serialized_note_content = array::append( + pending_partial_note.serialized_private_note_content, + log.log_content, + ); + + let discovered_notes = attempt_note_nonce_discovery( + log.unique_note_hashes_in_tx, + log.first_nullifier_in_tx, + compute_note_hash_and_nullifier, + contract_address, + pending_partial_note.storage_slot, + pending_partial_note.note_type_id, + complete_serialized_note_content, + ); + + array::for_each_in_bounded_vec( + discovered_notes, + |discovered_note: DiscoveredNoteInfo, _| { + // TODO:(#10728): handle notes that fail delivery + assert( + deliver_note( + contract_address, + pending_partial_note.storage_slot, + discovered_note.nonce, + complete_serialized_note_content, + discovered_note.note_hash, + discovered_note.inner_nullifier, + log.tx_hash, + pending_partial_note.recipient, + ), + "Failed to deliver note", + ); + }, + ); + + //pending_partial_notes.pop() + } + // todo: handle que nunca aparezca el log? podria no ser nunca + } +} diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr new file mode 100644 index 000000000000..71ba2b43f3a9 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr @@ -0,0 +1,200 @@ +use std::static_assert; + +use crate::{ + oracle::note_discovery::{deliver_note, get_log_by_tag, sync_notes}, + pxe_db::DBArray, + utils::array, +}; + +use dep::protocol_types::{ + address::AztecAddress, + constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, + debug_log::{debug_log, debug_log_format}, + hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, + traits::{Deserialize, FromField, Serialize, ToField}, +}; + +use crate::note::discovery::{ + MAX_NOTE_SERIALIZED_LEN, + MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN, + nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, + NOTE_PRIVATE_LOG_RESERVED_FIELDS, + NoteHashAndNullifier, + partial_notes::{ + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT, DeliveredPendingPartialNote, + }, + PUBLIC_LOG_SIZE_IN_FIELDS, +}; + +pub unconstrained fn fetch_and_process_private_tagged_logs( + _contract_address: AztecAddress, + _compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, +) { + // We will eventually fetch tagged logs, decrypt and process them here, but for now we simply call the `syncNotes` + // oracle, which performs tag synchronization, log download, decryption, and then calls the `process_log` function + // which the decrypted payload, at which point `do_process_log` gets executed and we continue the work ourselves. + sync_notes(); +} + +/// 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( + contract_address: AztecAddress, + log_plaintext: BoundedVec, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, +) { + let (storage_slot, note_type_id, log_type_id, log_payload) = + destructure_log_plaintext(log_plaintext); + + if log_type_id == 0 { + debug_log("Processing private note log"); + + process_private_note_log( + contract_address, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, + compute_note_hash_and_nullifier, + storage_slot, + note_type_id, + log_payload, + ); + } else if log_type_id == 1 { + debug_log("Processing partial note private log"); + + process_partial_note_private_log( + contract_address, + storage_slot, + note_type_id, + log_payload, + recipient, + ); + } else { + panic(f"Unknown log type id {log_type_id}"); + } +} + +unconstrained fn destructure_log_plaintext( + log_plaintext: BoundedVec, +) -> (Field, Field, Field, BoundedVec) { + assert(log_plaintext.len() >= NOTE_PRIVATE_LOG_RESERVED_FIELDS); + + // If NOTE_PRIVATE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for + // `storage_slot` and `note_type_id` must be updated as well. + static_assert( + NOTE_PRIVATE_LOG_RESERVED_FIELDS == 2, + "unepxected value for NOTE_PRIVATE_LOG_RESERVED_FIELDS", + ); + let storage_slot = log_plaintext.get(0); + + // The combined type id is the bit packing of the note type id (which is 7 bits big) and the log type id (which is + // currently a single bit, and right above it). + let combined_type_id = log_plaintext.get(1); + let note_type_id = ((combined_type_id as u64) % 128) as Field; + let log_type_id = combined_type_id / 128; + + let log_payload = array::subbvec(log_plaintext, NOTE_PRIVATE_LOG_RESERVED_FIELDS); + + (storage_slot, note_type_id, log_type_id, log_payload) +} + +unconstrained fn process_private_note_log( + contract_address: AztecAddress, + tx_hash: Field, + unique_note_hashes_in_tx: BoundedVec, + first_nullifier_in_tx: Field, + recipient: AztecAddress, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + storage_slot: Field, + note_type_id: Field, + serialized_note_content: BoundedVec, +) { + let discovered_notes = attempt_note_nonce_discovery( + unique_note_hashes_in_tx, + first_nullifier_in_tx, + compute_note_hash_and_nullifier, + contract_address, + storage_slot, + note_type_id, + serialized_note_content, + ); + + array::for_each_in_bounded_vec( + discovered_notes, + |discovered_note: DiscoveredNoteInfo, _| { + // TODO:(#10728): handle notes that fail delivery. This could be due to e.g. a temporary node connectivity + // issue, and we should perhaps not mark the tag index as taken. + assert( + deliver_note( + contract_address, + storage_slot, + discovered_note.nonce, + serialized_note_content, + discovered_note.note_hash, + discovered_note.inner_nullifier, + tx_hash, + recipient, + ), + "Failed to deliver note", + ); + }, + ); +} + +unconstrained fn process_partial_note_private_log( + contract_address: AztecAddress, + storage_slot: Field, + note_type_id: Field, + log_payload: BoundedVec, + recipient: AztecAddress, +) { + let pending = DeliveredPendingPartialNote { + note_completion_log_tag: log_payload.get(0), + storage_slot, + note_type_id, + serialized_private_note_content: array::subbvec(log_payload, 1), + recipient, + }; + + debug_log_format( + "note completion log tag {0} storage_slot {1} note type id {2}", + [pending.note_completion_log_tag, storage_slot, note_type_id], + ); + + DBArray::at( + contract_address, + DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT, + ) + .push(pending); +} diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 55c5870d0ff6..859da2910b37 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -4,9 +4,13 @@ use crate::{ utils::array, }; -use dep::protocol_types::hash::{ - compute_siloed_note_hash, compute_siloed_nullifier as compute_siloed_nullifier_from_preimage, - compute_unique_note_hash, +use dep::protocol_types::{ + address::AztecAddress, + hash::{ + compute_siloed_note_hash, + compute_siloed_nullifier as compute_siloed_nullifier_from_preimage, + compute_unique_note_hash, + }, }; pub fn compute_siloed_nullifier( @@ -118,7 +122,9 @@ where pub unconstrained fn compute_note_hash_and_optionally_a_nullifier( deserialize_content: fn([Field; N]) -> T, - note_header: NoteHeader, + contract_address: AztecAddress, + nonce: Field, + storage_slot: Field, compute_nullifier: bool, serialized_note: [Field; S], ) -> [Field; 4] @@ -126,11 +132,11 @@ where T: NoteInterface + NullifiableNote, { let mut note = deserialize_content(array::subarray(serialized_note, 0)); - note.set_header(note_header); + note.set_header(NoteHeader::new(contract_address, nonce, storage_slot)); let note_hash = note.compute_note_hash(); - let siloed_note_hash = compute_siloed_note_hash(note_header.contract_address, note_hash); - let unique_note_hash = compute_unique_note_hash(note_header.nonce, siloed_note_hash); + let siloed_note_hash = compute_siloed_note_hash(contract_address, note_hash); + let unique_note_hash = compute_unique_note_hash(nonce, siloed_note_hash); let inner_nullifier = if compute_nullifier { note.compute_nullifier_without_context() 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 47ffd3d61480..72c49903835a 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 crate::note::discovery::MAX_NOTE_SERIALIZED_LEN; -use dep::protocol_types::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX}; +use dep::protocol_types::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX, log_with_tx_data::LogWithTxData}; pub global PUBLIC_LOG_SIZE_IN_FIELDS: u32 = 5; // todo replace with constant from miranda's pr. make sure to use data and not raw size (excl addr) and remove tag @@ -66,16 +66,9 @@ unconstrained fn deliver_note_oracle( recipient: AztecAddress, ) -> bool {} -pub struct LogWithTxData { - pub log_content: BoundedVec, - pub tx_hash: Field, - pub unique_note_hashes_in_tx: BoundedVec, - pub first_nullifier_in_tx: Field, -} - pub unconstrained fn get_log_by_tag(tag: Field) -> Option { get_log_by_tag_oracle(tag) } -#[oracle(deliverNote)] +#[oracle(getLogByTag)] unconstrained fn get_log_by_tag_oracle(tag: Field) -> Option {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index 6b43c4cecea8..1611f16407c8 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -173,8 +173,10 @@ pub unconstrained fn get_notes, { + // inject this call in all #[private] fns via macros + // TODO(#10727): allow other contracts to discover notes - discover_new_notes(oracle::execution::get_contract_address()); + // discover_new_notes(oracle::execution::get_contract_address()); let fields = get_notes_oracle_wrapper( storage_slot, 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 6763e7a1a16b..d8628c345d72 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr @@ -7,3 +7,12 @@ pub use append::append; pub use collapse::collapse; pub use subarray::subarray; pub use subbvec::subbvec; + +pub unconstrained fn for_each_in_bounded_vec( + vec: BoundedVec, + f: fn[Env](T, u32) -> (), +) { + for i in 0..vec.len() { + f(vec.get_unchecked(i), i); + } +} diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/lib.nr b/noir-projects/noir-protocol-circuits/crates/types/src/lib.nr index 4a3ccfc32d03..180cee84f73c 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/lib.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/lib.nr @@ -30,6 +30,7 @@ pub mod storage; pub mod validate; pub mod meta; pub mod indexed_tagging_secret; +pub mod log_with_tx_data; pub use abis::kernel_circuit_public_inputs::{ PrivateKernelCircuitPublicInputs, PrivateToRollupKernelCircuitPublicInputs, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr b/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr new file mode 100644 index 000000000000..a7e8fef51637 --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr @@ -0,0 +1,6 @@ +pub struct LogWithTxData { + pub log_content: BoundedVec, + pub tx_hash: Field, + pub unique_note_hashes_in_tx: BoundedVec, + pub first_nullifier_in_tx: Field, +} \ No newline at end of file diff --git a/yarn-project/end-to-end/src/e2e_partial_stuff.test.ts b/yarn-project/end-to-end/src/e2e_partial_stuff.test.ts new file mode 100644 index 000000000000..030222cb4e93 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_partial_stuff.test.ts @@ -0,0 +1,42 @@ +import { type AccountWallet, type Logger } from '@aztec/aztec.js'; +import { type TokenContract } from '@aztec/noir-contracts.js/Token'; + +import { jest } from '@jest/globals'; + +import { deployToken, mintTokensToPrivate } from './fixtures/token_utils.js'; +import { setup } from './fixtures/utils.js'; + +const TIMEOUT = 120_000; + +describe('partial_stuff', () => { + jest.setTimeout(TIMEOUT); + + let teardown: () => Promise; + + let logger: Logger; + + let adminWallet: AccountWallet; + let liquidityProvider: AccountWallet; + + let token0: TokenContract; + + // We need a large token amount so that the swap fee (0.3%) is observable. + const INITIAL_TOKEN_BALANCE = 1_000_000_000n; + + beforeAll(async () => { + ({ + teardown, + wallets: [adminWallet, liquidityProvider], + logger, + } = await setup(2)); + + token0 = await deployToken(adminWallet, 0n, logger); + }); + + afterAll(() => teardown()); + + it('mint to private', async () => { + await mintTokensToPrivate(token0, adminWallet, liquidityProvider.getAddress(), INITIAL_TOKEN_BALANCE); + console.log(await token0.methods.balance_of_private(liquidityProvider.getAddress()).simulate()); + }); +}); diff --git a/yarn-project/simulator/src/acvm/deserialize.ts b/yarn-project/simulator/src/acvm/deserialize.ts index 57ad9fe8d0df..af6794196a8a 100644 --- a/yarn-project/simulator/src/acvm/deserialize.ts +++ b/yarn-project/simulator/src/acvm/deserialize.ts @@ -31,7 +31,7 @@ export function frToBoolean(fr: Fr): boolean { /** * Converts a Noir BoundedVec of Fields into an Fr array. Note that BoundedVecs are structs, and therefore translated as - * two separate ACVMField arrays. + * two separate ACVMField values (an array and a single field). * * @param storage The array with the BoundedVec's storage (i.e. BoundedVec::storage()) * @param length The length of the BoundedVec (i.e. BoundedVec::len()) diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 5351def9eb4d..080aef3008a4 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -1,11 +1,12 @@ import { MerkleTreeId, UnencryptedL2Log } from '@aztec/circuit-types'; +import { MAX_NOTE_HASHES_PER_TX } from '@aztec/circuits.js'; import { FunctionSelector, NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; import { type ACVMField } from '../acvm_types.js'; import { frToBoolean, frToNumber, fromACVMField, fromBoundedVec } from '../deserialize.js'; -import { toACVMField } from '../serialize.js'; +import { toACVMBoundedVec, toACVMField } from '../serialize.js'; import { type TypedOracle } from './typed_oracle.js'; /** @@ -409,6 +410,26 @@ export class Oracle { return toACVMField(true); } + async getLogByTag([tag]: ACVMField[]): Promise<(ACVMField | ACVMField[])[]> { + // 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. + + const log = await this.typedOracle.getLogByTag(fromACVMField(tag)); + + const logContentBVec = toACVMBoundedVec(log.logContent, PUBLIC_LOG_SIZE_IN_FIELDS); + const uniqueNoteHashesInTxBVec = toACVMBoundedVec(log.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX); + + return [ + logContentBVec.storage, + logContentBVec.len, + toACVMField(log.txHash), + uniqueNoteHashesInTxBVec.storage, + uniqueNoteHashesInTxBVec.len, + toACVMField(log.firstNullifierInTx), + ]; + } + async dbStore([contractAddress]: ACVMField[], [slot]: ACVMField[], values: ACVMField[]) { await this.typedOracle.dbStore( AztecAddress.fromField(fromACVMField(contractAddress)), diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index e9fbf9d5dc70..46b9b69eaa0a 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -250,6 +250,10 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('deliverNote'); } + getLogByTag(_tag: Fr): Promise { + throw new OracleMethodNotAvailableError('getLogByTag'); + } + dbStore(_contractAddress: AztecAddress, _key: Fr, _values: Fr[]): Promise { throw new OracleMethodNotAvailableError('dbStore'); } diff --git a/yarn-project/simulator/src/acvm/serialize.ts b/yarn-project/simulator/src/acvm/serialize.ts index 0ae28ed6ce5c..557fe5346b1c 100644 --- a/yarn-project/simulator/src/acvm/serialize.ts +++ b/yarn-project/simulator/src/acvm/serialize.ts @@ -51,3 +51,25 @@ export function toACVMWitness(witnessStartIndex: number, fields: Parameters()); } + +/** + * Converts a Ts Fr array into a Noir BoundedVec of Fields. Note that BoundedVecs are structs, and therefore translated + * as two separate ACVMField arrays. + * + * @param values The array with the field elements + * @param maxLength The maximum number of elements in the Noir BoundedVec. `values` must have a length smaller or equal + * to this. + * @returns The elements of the Noir BoundedVec. + */ +export function toACVMBoundedVec(values: Fr[], maxLength: number): { storage: ACVMField[]; len: ACVMField } { + if (values.length > maxLength) { + throw new Error( + `Cannot convert an array of length ${values.length} into a BoundedVec of maximum length ${maxLength}`, + ); + } + + return { + storage: values.map(toACVMField).concat(Array(maxLength - values.length).fill(toACVMField(0))), + len: toACVMField(values.length), + }; +} diff --git a/yarn-project/simulator/src/client/client_execution_context.ts b/yarn-project/simulator/src/client/client_execution_context.ts index 342fe8fdc651..69eb1970a8cc 100644 --- a/yarn-project/simulator/src/client/client_execution_context.ts +++ b/yarn-project/simulator/src/client/client_execution_context.ts @@ -278,6 +278,13 @@ export class ClientExecutionContext extends ViewDataOracle { noteHash: Fr, counter: number, ) { + this.log.debug(`Notified of new note with inner hash ${noteHash}`, { + contractAddress: this.callContext.contractAddress, + storageSlot, + noteTypeId, + counter, + }); + const note = new Note(noteItems); this.noteCache.addNewNote( { diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 594af643bccb..2aa652acd739 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -17,7 +17,7 @@ import { type FunctionArtifact, type FunctionSelector } from '@aztec/foundation/ import { type AztecAddress } from '@aztec/foundation/aztec-address'; import { type Fr } from '@aztec/foundation/fields'; -import { type NoteData } from '../acvm/index.js'; +import { type LogWithTxData, type NoteData } from '../acvm/index.js'; import { type CommitmentsDB } from '../public/db_interfaces.js'; /** @@ -257,6 +257,8 @@ export interface DBOracle extends CommitmentsDB { recipient: AztecAddress, ): Promise; + getLogByTag(tag: Fr): Promise; + /** * Removes all of a contract's notes that have been nullified from the note database. */ From 70c75c18f7254bd2cf9057deda9f1b107864a59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 29 Jan 2025 20:57:28 +0000 Subject: [PATCH 27/39] almost there --- .../default_aes128/note.nr | 2 +- .../aztec-nr/aztec/src/note/discovery/mod.nr | 22 +--- .../src/note/discovery/nonce_discovery.nr | 10 +- .../aztec/src/note/discovery/partial_notes.nr | 121 +++++++++++------- .../aztec/src/note/discovery/private_logs.nr | 10 +- .../aztec/src/oracle/note_discovery.nr | 12 +- noir-projects/aztec-nr/uint-note/src/lib.nr | 1 + .../uint-note/src/partial_uint_note.nr | 92 +++++++++++++ .../aztec-nr/uint-note/src/uint_note.nr | 2 +- .../contracts/token_contract/src/main.nr | 61 +++++---- .../crates/types/src/log_with_tx_data.nr | 6 +- .../archiver/kv_archiver_store/log_store.ts | 48 +++---- .../memory_archiver_store.ts | 47 +++---- .../src/structs/log_with_tx_data.ts | 8 +- .../pxe/src/simulator_oracle/index.ts | 6 +- .../simulator/src/acvm/oracle/oracle.ts | 8 +- yarn-project/simulator/src/acvm/serialize.ts | 22 ---- .../simulator/src/client/db_oracle.ts | 4 +- 18 files changed, 286 insertions(+), 196 deletions(-) create mode 100644 noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr index eb368074aed1..1bfd0fbc60fe 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr @@ -247,7 +247,7 @@ where plaintext_bytes } -fn compute_log( +pub fn compute_log( context: PrivateContext, note: Note, recipient: AztecAddress, 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 ac7a3ecdd06d..3c2ba293c069 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -1,24 +1,12 @@ -use std::static_assert; - -use crate::{ - context::unconstrained_context::UnconstrainedContext, - oracle::note_discovery::{deliver_note, get_log_by_tag, sync_notes}, - pxe_db::DBArray, - utils::array, -}; - use dep::protocol_types::{ - address::AztecAddress, - constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, - hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, - traits::{Deserialize, FromField, Serialize, ToField}, + address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, debug_log::debug_log, }; pub global PUBLIC_LOG_SIZE_IN_FIELDS: u32 = 14; // todo: move to constants -mod private_logs; -mod partial_notes; -mod nonce_discovery; +pub mod private_logs; +pub mod partial_notes; +pub mod nonce_discovery; // We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and // one for the note type id. @@ -40,6 +28,8 @@ pub unconstrained fn discover_new_notes( contract_address: AztecAddress, compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, ) { + debug_log("Performing note discovery"); + private_logs::fetch_and_process_private_tagged_logs( contract_address, compute_note_hash_and_nullifier, diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr index 3e905cb82deb..19d067414df7 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr @@ -8,13 +8,13 @@ use dep::protocol_types::{ traits::ToField, }; -pub(crate) struct DiscoveredNoteInfo { - pub(crate) nonce: Field, - pub(crate) note_hash: Field, - pub(crate) inner_nullifier: Field, +pub struct DiscoveredNoteInfo { + pub nonce: Field, + pub note_hash: Field, + pub inner_nullifier: Field, } -unconstrained fn attempt_note_nonce_discovery( +pub unconstrained fn attempt_note_nonce_discovery( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr index 9ee0148f0f8c..706c232f4b4d 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr @@ -12,8 +12,7 @@ use crate::{ use dep::protocol_types::{ address::AztecAddress, - constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, - hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, + debug_log::debug_log_format, traits::{Deserialize, FromField, Serialize, ToField}, }; @@ -29,8 +28,8 @@ pub(crate) struct DeliveredPendingPartialNote { // TODO: use `derive(Serialize)`, but we need for generics in struct fields to be handled properly before that happens. // Currently blocked by Noir's StructDefinition::fields. -impl Serialize for DeliveredPendingPartialNote { - fn serialize(self) -> [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5] { +impl Serialize for DeliveredPendingPartialNote { + fn serialize(self) -> [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 4] { [ self.note_completion_log_tag, self.storage_slot, @@ -49,24 +48,23 @@ impl Serialize for DeliveredPending self.serialized_private_note_content.storage()[12], self.serialized_private_note_content.storage()[13], self.serialized_private_note_content.storage()[14], - self.serialized_private_note_content.storage()[15], self.serialized_private_note_content.len() as Field, self.recipient.to_field(), ] } } -impl Deserialize for DeliveredPendingPartialNote { - fn deserialize(values: [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 5]) -> Self { +impl Deserialize for DeliveredPendingPartialNote { + fn deserialize(values: [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 4]) -> Self { Self { note_completion_log_tag: values[0], storage_slot: values[1], note_type_id: values[2], serialized_private_note_content: BoundedVec::from_parts( array::subarray(values, 3), - values[18] as u32, + values[17] as u32, ), - recipient: AztecAddress::from_field(values[19]), + recipient: AztecAddress::from_field(values[18]), } } } @@ -80,50 +78,75 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT, ); - for i in 0..pending_partial_notes.len() { - let pending_partial_note: DeliveredPendingPartialNote = pending_partial_notes.get(i); + debug_log_format( + "{} pending partial notes", + [pending_partial_notes.len() as Field], + ); - let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); - if maybe_log.is_some() { - let log = maybe_log.unwrap(); + let mut i = &mut 0; + whyle( + || *i < pending_partial_notes.len(), + || { + let pending_partial_note: DeliveredPendingPartialNote = pending_partial_notes.get(*i); - let complete_serialized_note_content = array::append( - pending_partial_note.serialized_private_note_content, - log.log_content, - ); + let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); + if maybe_log.is_none() { + debug_log_format("Found no logs for partial note #{}", [(*i) as Field]); + *i += 1 as u32; + } else { + debug_log_format("Log found for partial note #{}!", [(*i) as Field]); + let log = maybe_log.unwrap(); - let discovered_notes = attempt_note_nonce_discovery( - log.unique_note_hashes_in_tx, - log.first_nullifier_in_tx, - compute_note_hash_and_nullifier, - contract_address, - pending_partial_note.storage_slot, - pending_partial_note.note_type_id, - complete_serialized_note_content, - ); + let complete_serialized_note_content = array::append( + pending_partial_note.serialized_private_note_content, + log.log_content, + ); - array::for_each_in_bounded_vec( - discovered_notes, - |discovered_note: DiscoveredNoteInfo, _| { - // TODO:(#10728): handle notes that fail delivery - assert( - deliver_note( - contract_address, - pending_partial_note.storage_slot, - discovered_note.nonce, - complete_serialized_note_content, - discovered_note.note_hash, - discovered_note.inner_nullifier, - log.tx_hash, - pending_partial_note.recipient, - ), - "Failed to deliver note", - ); - }, - ); + let discovered_notes = attempt_note_nonce_discovery( + log.unique_note_hashes_in_tx, + log.first_nullifier_in_tx, + compute_note_hash_and_nullifier, + contract_address, + pending_partial_note.storage_slot, + pending_partial_note.note_type_id, + complete_serialized_note_content, + ); - //pending_partial_notes.pop() - } - // todo: handle que nunca aparezca el log? podria no ser nunca + debug_log_format( + "Discovered {0} notes for partial note {1}", + [discovered_notes.len() as Field, (*i) as Field], + ); + + array::for_each_in_bounded_vec( + discovered_notes, + |discovered_note: DiscoveredNoteInfo, _| { + // TODO:(#10728): handle notes that fail delivery + assert( + deliver_note( + contract_address, + pending_partial_note.storage_slot, + discovered_note.nonce, + complete_serialized_note_content, + discovered_note.note_hash, + discovered_note.inner_nullifier, + log.tx_hash, + pending_partial_note.recipient, + ), + "Failed to deliver note", + ); + }, + ); + + pending_partial_notes.remove(*i); + } + // todo: handle the log never showing up? + }, + ); +} + +fn whyle(condition: fn[Env]() -> bool, body: fn[Env2]() -> ()) { + if condition() { + body(); + whyle(condition, body); } } diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr index 71ba2b43f3a9..d54e989680d1 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr @@ -1,29 +1,21 @@ use std::static_assert; -use crate::{ - oracle::note_discovery::{deliver_note, get_log_by_tag, sync_notes}, - pxe_db::DBArray, - utils::array, -}; +use crate::{oracle::note_discovery::{deliver_note, sync_notes}, pxe_db::DBArray, utils::array}; use dep::protocol_types::{ address::AztecAddress, constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, debug_log::{debug_log, debug_log_format}, - hash::{compute_note_hash_nonce, compute_siloed_note_hash, compute_unique_note_hash}, - traits::{Deserialize, FromField, Serialize, ToField}, }; use crate::note::discovery::{ MAX_NOTE_SERIALIZED_LEN, - MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN, nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, NOTE_PRIVATE_LOG_RESERVED_FIELDS, NoteHashAndNullifier, partial_notes::{ DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT, DeliveredPendingPartialNote, }, - PUBLIC_LOG_SIZE_IN_FIELDS, }; pub unconstrained fn fetch_and_process_private_tagged_logs( 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 e5e6344b5e1e..da286c19c073 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr @@ -1,10 +1,11 @@ use crate::note::discovery::MAX_NOTE_SERIALIZED_LEN; -use dep::protocol_types::{address::AztecAddress, constants::{MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS}, log_with_tx_data::LogWithTxData}; +use dep::protocol_types::{address::AztecAddress, log_with_tx_data::LogWithTxData}; /// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them available /// for later querying via the `get_notes` oracle. pub fn sync_notes() { - // This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to call. + /// Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to + /// call. unsafe { sync_notes_oracle_wrapper(); } @@ -52,13 +53,6 @@ pub unconstrained fn deliver_note( ) } -pub struct LogWithTxData { - pub log_content: BoundedVec, - pub tx_hash: Field, - pub unique_note_hashes_in_tx: BoundedVec, - pub first_nullifier_in_tx: Field, -} - // todo: return a bounded vec of these pub unconstrained fn get_log_by_tag(tag: Field) -> Option { get_log_by_tag_oracle(tag) diff --git a/noir-projects/aztec-nr/uint-note/src/lib.nr b/noir-projects/aztec-nr/uint-note/src/lib.nr index 2225eccc2a25..dd64fd22470b 100644 --- a/noir-projects/aztec-nr/uint-note/src/lib.nr +++ b/noir-projects/aztec-nr/uint-note/src/lib.nr @@ -1 +1,2 @@ mod uint_note; +mod partial_uint_note; diff --git a/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr b/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr new file mode 100644 index 000000000000..6123e286c9a1 --- /dev/null +++ b/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr @@ -0,0 +1,92 @@ +use dep::aztec::{ + generators::{G_slot, Ga1, Ga2, Ga3}, + note::note_interface::NoteInterface, + oracle::random::random, + prelude::NoteHeader, + protocol_types::{address::AztecAddress, point::Point, traits::{Empty, Packable, ToField}}, +}; +use std::static_assert; + +use crate::uint_note::UintNote; + +pub struct PartialUintNoteHidingPoint { + pub value: Point, +} + +impl PartialUintNoteHidingPoint { + fn new(value: Point) -> Self { + Self { value } + } + + fn complete(self, amount: U128) -> Field { + let note_point = self.value + + std::embedded_curve_ops::multi_scalar_mul( + [Ga3], + [std::hash::from_field_unsafe(amount.pack()[0])], + ); + + note_point.x + } +} + +pub struct PartialUintNote { + pub owner: AztecAddress, + pub randomness: Field, + pub hiding_point: PartialUintNoteHidingPoint, + pub header: NoteHeader, +} + +impl PartialUintNote { + pub fn new(owner: AztecAddress, storage_slot: Field) -> Self { + /// Safety: We use the randomness to preserve the privacy of the note recipient by preventing brute-forcing, + /// so a malicious sender could use non-random values to make the note less private. But they already know + /// the full note pre-image anyway, and so the recipient already trusts them to not disclose this + /// information. We can therefore assume that the sender will cooperate in the random value generation. + let randomness = unsafe { random() }; + let mut header = NoteHeader::empty(); + header.storage_slot = storage_slot; + + let hiding_point = PartialUintNoteHidingPoint::new( + std::embedded_curve_ops::multi_scalar_mul( + [Ga1, Ga2, G_slot], + [ + std::hash::from_field_unsafe(owner.to_field()), + std::hash::from_field_unsafe(randomness), + std::hash::from_field_unsafe(storage_slot), + ], + ), + ); + + Self { owner, randomness, hiding_point, header } + } +} + +global PARTIAL_UINT_NOTE_SER_LENGTH: u32 = 3; + +impl NoteInterface for PartialUintNote { + fn serialize_content(self) -> [Field; PARTIAL_UINT_NOTE_SER_LENGTH] { + [self.hiding_point.value.x, self.owner.to_field(), self.randomness] + } + + fn deserialize_content(_fields: [Field; PARTIAL_UINT_NOTE_SER_LENGTH]) -> Self { + static_assert(false, "unsupported"); + panic(f"unsupported") + } + + fn get_header(self) -> NoteHeader { + self.header + } + + fn set_header(&mut self, header: NoteHeader) { + self.header = header; + } + + fn get_note_type_id() -> Field { + UintNote::get_note_type_id() + 128 + } + + fn compute_note_hash(self) -> Field { + static_assert(false, "unsupported"); + panic(f"unsupported") + } +} diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 368f57bf30db..9fc4a6a9b1f9 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -14,10 +14,10 @@ use dep::aztec::{ #[partial_note(quote {value})] pub struct UintNote { // The amount of tokens in the note - value: U128, owner: AztecAddress, // Randomness of the note to hide its contents randomness: Field, + value: U128, } // docs:end:UintNote 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 4fde31bc841c..40b6d9e0c09b 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -22,7 +22,7 @@ contract Token { context::{PrivateCallInterface, PrivateContext}, encrypted_logs::log_assembly_strategies::default_aes128::{ event::encode_and_encrypt_event_unconstrained, - note::{encode_and_encrypt_note, encode_and_encrypt_note_unconstrained}, + note::{compute_log, encode_and_encrypt_note, encode_and_encrypt_note_unconstrained}, }, macros::{ events::event, @@ -33,10 +33,13 @@ contract Token { prelude::{ AztecAddress, FunctionSelector, Map, PublicContext, PublicImmutable, PublicMutable, }, - protocol_types::{point::Point, traits::Serialize}, + protocol_types::{debug_log::debug_log_format, point::Point, traits::Serialize}, }; - use dep::uint_note::uint_note::UintNote; + use dep::uint_note::{ + partial_uint_note::{PartialUintNote, PartialUintNoteHidingPoint}, + uint_note::UintNote, + }; // docs:start:import_authwit use dep::authwit::auth::{ @@ -448,15 +451,22 @@ contract Token { // TODO(#7775): Manually fetching the randomness here is not great. If we decide to include randomness in all // notes we could just inject it in macros. - /// Safety: We use the randomness to preserve the privacy of the note recipient by preventing brute-forcing, - /// so a malicious sender could use non-random values to make the note less private. But they already know - /// the full note pre-image anyway, and so the recipient already trusts them to not disclose this - /// information. We can therefore assume that the sender will cooperate in the random value generation. - let note_randomness = unsafe { random() }; - let note_setup_payload = UintNote::setup_payload().new(to, note_randomness, to_note_slot); + // Safety: We use the randomness to preserve the privacy of the note recipient by preventing brute-forcing, + // so a malicious sender could use non-random values to make the note less private. But they already know + // the full note pre-image anyway, and so the recipient already trusts them to not disclose this + // information. We can therefore assume that the sender will cooperate in the random value generation. + + let partial = PartialUintNote::new(to, to_note_slot); + debug_log_format( + "Producing partial note: tag {0} storage slot {1} note type id {2}", + [partial.hiding_point.value.x, to_note_slot, UintNote::get_note_type_id() as Field], + ); + let encrypted_log = compute_log(*context, partial, to, from); + context.emit_private_log(encrypted_log); - // We get the keys and encrypt the log of the note - let setup_log = note_setup_payload.encrypt_log(context, to, from); + Token::at(context.this_address()) + ._store_private_balance_increase_hiding_point(partial.hiding_point.value) + .enqueue(context); // Using the x-coordinate as a hiding point slot is safe against someone else interfering with it because // we have a guarantee that the public functions of the transaction are executed right after the private ones @@ -469,20 +479,12 @@ contract Token { // We can also be sure that the `hiding_point_slot` will not overwrite any other value in the storage because // in our state variables we derive slots using a different hash function from multi scalar multiplication // (MSM). - let hiding_point_slot = note_setup_payload.hiding_point.x; // We don't need to perform a check that the value overwritten by `_store_point_in_transient_storage_unsafe` // is zero because the slot is the x-coordinate of the hiding point and hence we could only overwrite // the value in the slot with the same value. This makes usage of the `unsafe` method safe. - Token::at(context.this_address()) - ._store_payload_in_transient_storage_unsafe( - hiding_point_slot, - note_setup_payload.hiding_point, - setup_log, - ) - .enqueue(context); - hiding_point_slot + partial.hiding_point.value.x } // docs:start:finalize_transfer_to_private @@ -595,12 +597,15 @@ contract Token { let supply = storage.total_supply.read().add(amount); storage.total_supply.write(supply); - // Then we finalize the partial note with the `amount` - let finalization_payload = - UintNote::finalization_payload().new(context, hiding_point_slot, amount); + let hiding_point = PartialUintNoteHidingPoint::new(context.storage_read(hiding_point_slot)); + assert( + !aztec::protocol_types::traits::is_empty(hiding_point.value), + "transfer not prepared", + ); + let note_hash = hiding_point.complete(amount); - // At last we emit the note hash and the final log - finalization_payload.emit(); + context.push_note_hash(note_hash); + context.emit_public_log([hiding_point.value.x, amount.pack()[0]]); } // docs:start:setup_refund @@ -653,6 +658,12 @@ contract Token { } // docs:end:setup_refund + #[public] + #[internal] + fn _store_private_balance_increase_hiding_point(hiding_point: Point) { + context.storage_write(hiding_point.x, hiding_point); + } + // TODO(#9375): Having to define the note log length here is very unfortunate as it's basically impossible for // users to derive manually. This will however go away once we have a real transient storage since we will not need // the public call and instead we would do something like `context.transient_storage_write(slot, payload)` and that diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr b/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr index a7e8fef51637..d225b9a4f8b8 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr @@ -1,6 +1,8 @@ +use crate::constants::{MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS}; + pub struct LogWithTxData { - pub log_content: BoundedVec, + pub log_content: BoundedVec, pub tx_hash: Field, pub unique_note_hashes_in_tx: BoundedVec, pub first_nullifier_in_tx: Field, -} \ No newline at end of file +} diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts index c1821c09555d..6a67b45f0b66 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts @@ -79,29 +79,31 @@ export class LogStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.publicLogs.forEach(log => { - // Check that each log stores 2 lengths in its first field. If not, it's not a tagged log: - const firstFieldBuf = log.log[0].toBuffer(); - // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). - // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. - if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { - // See parseLogFromPublic - the first field of a tagged log is 5 bytes structured: - // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] - this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); - return; - } - // Check that the length values line up with the log contents - const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); - const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); - // Add 1 for the first field holding lengths - const totalLogLength = 1 + publicValuesLength + privateValuesLength; - // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length - if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { - this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); - return; - } - - // The first elt stores lengths as above => tag is in fields[1] - const tag = log.log[1]; + // // Check that each log stores 2 lengths in its first field. If not, it's not a tagged log: + // const firstFieldBuf = log.log[0].toBuffer(); + // // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). + // // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. + // if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { + // // See parseLogFromPublic - the first field of a tagged log is 5 bytes structured: + // // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] + // this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); + // return; + // } + // // Check that the length values line up with the log contents + // const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); + // const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); + // // Add 1 for the first field holding lengths + // const totalLogLength = 1 + publicValuesLength + privateValuesLength; + // // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length + // if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { + // this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); + // return; + // } + + // // The first elt stores lengths as above => tag is in fields[1] + // const tag = log.log[1]; + + const tag = log.log[0]; this.#log.debug(`Found tagged public log with tag ${tag.toString()} in block ${block.number}`); const currentLogs = taggedLogs.get(tag.toString()) ?? []; diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 55b426cce3fa..98886e1c9ceb 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -256,29 +256,32 @@ export class MemoryArchiverStore implements ArchiverDataStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.publicLogs.forEach(log => { - // Check that each log stores 3 lengths in its first field. If not, it's not a tagged log: - // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). - // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. - const firstFieldBuf = log.log[0].toBuffer(); - if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { - // See parseLogFromPublic - the first field of a tagged log is 8 bytes structured: - // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] - this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); - return; - } - // Check that the length values line up with the log contents - const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); - const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); - // Add 1 for the first field holding lengths - const totalLogLength = 1 + publicValuesLength + privateValuesLength; - // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length - if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { - this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); - return; - } + // // Check that each log stores 3 lengths in its first field. If not, it's not a tagged log: + // // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). + // // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. + // const firstFieldBuf = log.log[0].toBuffer(); + // if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { + // // See parseLogFromPublic - the first field of a tagged log is 8 bytes structured: + // // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] + // this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); + // return; + // } + // // Check that the length values line up with the log contents + // const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); + // const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); + // // Add 1 for the first field holding lengths + // const totalLogLength = 1 + publicValuesLength + privateValuesLength; + // // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length + // if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { + // this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); + // return; + // } + + // // The first elt stores lengths => tag is in fields[1] + // const tag = log.log[1]; + + const tag = log.log[0]; - // The first elt stores lengths => tag is in fields[1] - const tag = log.log[1]; this.#log.verbose(`Storing public tagged log with tag ${tag.toString()} in block ${block.number}`); const currentLogs = this.taggedLogs.get(tag.toString()) || []; this.taggedLogs.set(tag.toString(), [ diff --git a/yarn-project/circuits.js/src/structs/log_with_tx_data.ts b/yarn-project/circuits.js/src/structs/log_with_tx_data.ts index 94a86b95709a..9e5ebb1b8db9 100644 --- a/yarn-project/circuits.js/src/structs/log_with_tx_data.ts +++ b/yarn-project/circuits.js/src/structs/log_with_tx_data.ts @@ -12,9 +12,9 @@ export class LogWithTxData { toNoirSerialization(): (Fr | Fr[])[] { return [ - toBoundedVecStorage(this.logContent, PUBLIC_LOG_DATA_SIZE_IN_FIELDS), + ...toBoundedVecSerialization(this.logContent, PUBLIC_LOG_DATA_SIZE_IN_FIELDS), this.txHash, - toBoundedVecStorage(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), + ...toBoundedVecSerialization(this.uniqueNoteHashesInTx, MAX_NOTE_HASHES_PER_TX), this.firstNullifierInTx, ]; } @@ -24,12 +24,12 @@ export class LogWithTxData { } } -function toBoundedVecStorage(array: Fr[], maxLength: number) { +function toBoundedVecSerialization(array: Fr[], maxLength: number) { if (array.length > maxLength) { throw new Error( `An array of length ${array.length} cannot be converted to a BoundedVec of max length ${maxLength}`, ); } - return array.concat(Array(maxLength - array.length).fill(new Fr(0))); + return [array.concat(Array(maxLength - array.length).fill(new Fr(0))), new Fr(array.length)]; } diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index bf8fa60d1189..698102910cda 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -713,10 +713,14 @@ export class SimulatorOracle implements DBOracle { const logs = await this.aztecNode.getLogsByTags([tag]); const logsForTag = logs[0]; + this.log.debug(`Got ${logsForTag.length} logs for tag ${tag}`); + if (logsForTag.length == 0) { return null; } else if (logsForTag.length > 1) { - throw new Error(`Got ${logsForTag.length} logs for tag ${tag}. getLogByTag currently only supports a single log`); + throw new Error( + `Got ${logsForTag.length} logs for tag ${tag}. getLogByTag currently only supports a single log per tag`, + ); } const log = logsForTag[0]; diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 010de1f4b7b0..f9698f9de58d 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -1,16 +1,14 @@ import { MerkleTreeId, UnencryptedL2Log } from '@aztec/circuit-types'; -<<<<<<< HEAD -import { MAX_NOTE_HASHES_PER_TX } from '@aztec/circuits.js'; -======= import { LogWithTxData } from '@aztec/circuits.js'; ->>>>>>> nv/get-logs-oracle import { FunctionSelector, NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; +import { timeStamp } from 'console'; + import { type ACVMField } from '../acvm_types.js'; import { frToBoolean, frToNumber, fromACVMField, fromBoundedVec } from '../deserialize.js'; -import { toACVMBoundedVec, toACVMField } from '../serialize.js'; +import { toACVMField } from '../serialize.js'; import { type TypedOracle } from './typed_oracle.js'; /** diff --git a/yarn-project/simulator/src/acvm/serialize.ts b/yarn-project/simulator/src/acvm/serialize.ts index 557fe5346b1c..0ae28ed6ce5c 100644 --- a/yarn-project/simulator/src/acvm/serialize.ts +++ b/yarn-project/simulator/src/acvm/serialize.ts @@ -51,25 +51,3 @@ export function toACVMWitness(witnessStartIndex: number, fields: Parameters()); } - -/** - * Converts a Ts Fr array into a Noir BoundedVec of Fields. Note that BoundedVecs are structs, and therefore translated - * as two separate ACVMField arrays. - * - * @param values The array with the field elements - * @param maxLength The maximum number of elements in the Noir BoundedVec. `values` must have a length smaller or equal - * to this. - * @returns The elements of the Noir BoundedVec. - */ -export function toACVMBoundedVec(values: Fr[], maxLength: number): { storage: ACVMField[]; len: ACVMField } { - if (values.length > maxLength) { - throw new Error( - `Cannot convert an array of length ${values.length} into a BoundedVec of maximum length ${maxLength}`, - ); - } - - return { - storage: values.map(toACVMField).concat(Array(maxLength - values.length).fill(toACVMField(0))), - len: toACVMField(values.length), - }; -} diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 7d707b9c5fe4..4e1ade7dcfb4 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -12,13 +12,13 @@ import { type ContractInstance, type IndexedTaggingSecret, type KeyValidationRequest, - LogWithTxData, + type LogWithTxData, } from '@aztec/circuits.js'; import { type FunctionArtifact, type FunctionSelector } from '@aztec/foundation/abi'; import { type AztecAddress } from '@aztec/foundation/aztec-address'; import { type Fr } from '@aztec/foundation/fields'; -import { type LogWithTxData, type NoteData } from '../acvm/index.js'; +import { type NoteData } from '../acvm/index.js'; import { type CommitmentsDB } from '../public/db_interfaces.js'; /** From 061aecd22c412e1a49f1751c96277e67e85ac48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 30 Jan 2025 13:24:41 +0000 Subject: [PATCH 28/39] IT LIVES! --- .../aztec-nr/aztec/src/macros/notes/mod.nr | 2 ++ .../aztec/src/note/discovery/partial_notes.nr | 1 + noir-projects/aztec-nr/aztec/src/note/utils.nr | 6 ++++++ .../aztec-nr/uint-note/src/partial_uint_note.nr | 10 +++++----- .../contracts/token_contract/src/main.nr | 6 +----- yarn-project/pxe/src/simulator_oracle/index.ts | 14 ++++++++------ 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr index 2f480ebf68c1..4ff84a468a37 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr @@ -899,6 +899,8 @@ pub comptime fn partial_note(s: StructDefinition, nullable_fields: [Quoted]) -> indexed_nullable_fields, ); + println(note_interface_impl); + quote { $note_properties $setup_payload_impl diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr index 706c232f4b4d..c38bc827eaee 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr @@ -137,6 +137,7 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( }, ); + // we assume that there is only a single log per tag, so if the log didnt work then we cry pending_partial_notes.remove(*i); } // todo: handle the log never showing up? diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 44ec8ba7a281..845dad3b94f0 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -6,6 +6,7 @@ use crate::{ use dep::protocol_types::{ address::AztecAddress, + debug_log::debug_log_format, hash::{ compute_siloed_note_hash, compute_siloed_nullifier as compute_siloed_nullifier_from_preimage, @@ -134,6 +135,11 @@ where let mut note = unpack_content(array::subarray(packed_note_content, 0)); note.set_header(NoteHeader::new(contract_address, nonce, storage_slot)); + debug_log_format( + "addr {0} nonce {1} storage_slot {2}", + [contract_address.to_field(), nonce, storage_slot], + ); + let note_hash = note.compute_note_hash(); let siloed_note_hash = compute_siloed_note_hash(contract_address, note_hash); let unique_note_hash = compute_unique_note_hash(nonce, siloed_note_hash); diff --git a/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr b/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr index 1a7123efd2f6..5c1abc807f46 100644 --- a/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr @@ -1,5 +1,5 @@ use dep::aztec::{ - generators::{G_slot, Ga1, Ga2, Ga3}, + generators::{G_slot, Ga1, Ga2, Ga3, Ga4}, note::note_interface::NoteInterface, oracle::random::random, prelude::NoteHeader, @@ -21,7 +21,7 @@ impl PartialUintNoteHidingPoint { fn complete(self, amount: U128) -> Field { let note_point = self.value + std::embedded_curve_ops::multi_scalar_mul( - [Ga3], + [G_slot], [std::hash::from_field_unsafe(amount.pack()[0])], ); @@ -48,10 +48,10 @@ impl PartialUintNote { let hiding_point = PartialUintNoteHidingPoint::new( std::embedded_curve_ops::multi_scalar_mul( - [Ga1, Ga2, G_slot], + [Ga2, Ga3, G_slot], [ - std::hash::from_field_unsafe(owner.to_field()), - std::hash::from_field_unsafe(randomness), + std::hash::from_field_unsafe(owner.pack()[0]), + std::hash::from_field_unsafe(randomness.pack()[0]), std::hash::from_field_unsafe(storage_slot), ], ), 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 40b6d9e0c09b..530031cfcbe2 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -33,7 +33,7 @@ contract Token { prelude::{ AztecAddress, FunctionSelector, Map, PublicContext, PublicImmutable, PublicMutable, }, - protocol_types::{debug_log::debug_log_format, point::Point, traits::Serialize}, + protocol_types::{point::Point, traits::Serialize}, }; use dep::uint_note::{ @@ -457,10 +457,6 @@ contract Token { // information. We can therefore assume that the sender will cooperate in the random value generation. let partial = PartialUintNote::new(to, to_note_slot); - debug_log_format( - "Producing partial note: tag {0} storage slot {1} note type id {2}", - [partial.hiding_point.value.x, to_note_slot, UintNote::get_note_type_id() as Field], - ); let encrypted_log = compute_log(*context, partial, to, from); context.emit_private_log(encrypted_log); diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 8a86c7e33b5e..08d127ca3d0e 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -732,12 +732,14 @@ export class SimulatorOracle implements DBOracle { } const reader = BufferReader.asReader(log.logData); - return new LogWithTxData( - reader.readArray(PUBLIC_LOG_DATA_SIZE_IN_FIELDS, Fr), - log.txHash.hash, - txEffect.data.noteHashes, - txEffect.data.nullifiers[0], - ); + const logArray = reader.readArray(PUBLIC_LOG_DATA_SIZE_IN_FIELDS, Fr); + + this.log.debug(`Log content ${logArray}, length: ${logArray.length}`); + + const modifiedLog = logArray.slice(2, 3); + this.log.debug(`Modified ${modifiedLog}`); + + return new LogWithTxData(modifiedLog, log.txHash.hash, txEffect.data.noteHashes, txEffect.data.nullifiers[0]); } public async removeNullifiedNotes(contractAddress: AztecAddress) { From e625ad9e1973064c43f9596c295fd3042e5eeb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 30 Jan 2025 19:55:18 +0000 Subject: [PATCH 29/39] Doc improvements --- .../aztec-nr/aztec/src/macros/mod.nr | 4 +- .../aztec-nr/aztec/src/note/discovery/mod.nr | 55 ++++++-- .../src/note/discovery/nonce_discovery.nr | 24 +++- .../aztec/src/note/discovery/partial_notes.nr | 120 +++++++++--------- .../aztec/src/note/discovery/private_logs.nr | 97 +++++++------- .../aztec/src/oracle/note_discovery.nr | 13 +- .../pxe/src/simulator_oracle/index.ts | 16 ++- 7 files changed, 193 insertions(+), 136 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 5c74d389d5a5..3600bafb7a69 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -167,7 +167,7 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ storage_slot: Field, note_type_id: Field, compute_nullifier: bool, - packed_note_content: BoundedVec, + packed_note_content: BoundedVec, ) -> pub [Field; 4] { $body } @@ -211,7 +211,7 @@ comptime fn generate_process_log() -> Quoted { // A typical implementation of the lambda looks something like this: // ``` - // |packed_note_content: BoundedVec, note_header: NoteHeader, note_type_id: Field| { + // |packed_note_content: BoundedVec, note_header: NoteHeader, note_type_id: Field| { // let hashes = if note_type_id == MyNoteType::get_note_type_id() { // assert(packed_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); // dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( 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 3c2ba293c069..a4cdacecb4cd 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -2,31 +2,64 @@ use dep::protocol_types::{ address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, debug_log::debug_log, }; -pub global PUBLIC_LOG_SIZE_IN_FIELDS: u32 = 14; // todo: move to constants - pub mod private_logs; pub mod partial_notes; pub mod nonce_discovery; -// We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and -// one for the note type id. +/// We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and +/// one for the combined log and note type ID. global NOTE_PRIVATE_LOG_RESERVED_FIELDS: u32 = 2; -pub global MAX_NOTE_SERIALIZED_LEN: u32 = - PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; - -pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; -pub global MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN: u32 = - MAX_NOTE_SERIALIZED_LEN - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN; +/// The maximum length of the packed representation of a note's contents. This is limited by private log size and extra +/// fields in the log (e.g. the combined log and note type ID). +// TODO (#11634): we're assuming here that the entire log is plaintext, which is not true due to headers, encryption +// padding, etc. Notes can't actually be this large. +pub global MAX_NOTE_PACKED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; pub struct NoteHashAndNullifier { pub note_hash: Field, // The result of NoteInterface::compute_note_hash pub inner_nullifier: Field, // The result of NullifiableNote::compute_nullifier_without_context } +/// A function which takes a note's packed content, address of the emitting contract, nonce, storage slot and note type +/// ID and attempts to compute its note hash (not siloed by nonce nor address) and inner nullifier (not siloed by +/// address). +/// +/// This function must be user-provided as its implementation requires knowledge of how note type IDs are allocated in a +/// contract. A typical implementation would look like this: +/// +/// ``` +/// |packed_note_content, contract_address, nonce, storage_slot, note_type_id| { +/// if note_type_id == MyNoteType::get_note_type_id() { +/// assert(packed_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); +/// let hashes = dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( +/// MyNoteType::unpack_content, +/// note_header, +/// true, +/// packed_note_content.storage(), +/// ) +/// +/// Option::some(dep::aztec::oracle::management::NoteHashesAndNullifier { +/// note_hash: hashes[0], +/// inner_nullifier: hashes[3], +/// }) +/// } else if note_type_id == MyOtherNoteType::get_note_type_id() { +/// ... // Similar to above but calling MyOtherNoteType::unpack_content +/// } else { +/// Option::none() // Uknown note type ID +/// }; +/// } +/// ``` +type ComputeNoteHashAndNullifier = fn[Env](/* packed_note_content */BoundedVec, /* contract_address */ AztecAddress, /* nonce */ Field, /* storage_slot */ Field, /* note_type_id */ Field) -> Option; + +/// Performs the note discovery process, in which private and public logs are downloaded and inspected to find private +/// notes, partial notes, and their completion. This is the mechanism via which PXE learns of new notes. +/// +/// Receives the address of the contract on which discovery is performed (i.e. the contract that emitted the notes) +/// along with its `compute_note_hash_and_nullifier` function. pub unconstrained fn discover_new_notes( contract_address: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, ) { debug_log("Performing note discovery"); diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr index 19d067414df7..2ba318768604 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr @@ -1,4 +1,4 @@ -use crate::{note::discovery::{MAX_NOTE_SERIALIZED_LEN, NoteHashAndNullifier}, utils::array}; +use crate::{note::discovery::{MAX_NOTE_PACKED_LEN, NoteHashAndNullifier}, utils::array}; use dep::protocol_types::{ address::AztecAddress, @@ -8,20 +8,29 @@ use dep::protocol_types::{ traits::ToField, }; +/// A struct with the discovered information of a complete note, required for delivery to PXE. Note that this is *not* +/// the complete note information, since it does not include content, storage slot, etc. pub struct DiscoveredNoteInfo { pub nonce: Field, pub note_hash: Field, pub inner_nullifier: Field, } +/// Searches for note nonces that will result in a note that was emitted in a transaction. While rare, it is possible +/// for multiple notes to have the exact same packed content and storage slot but different nonces, resulting in +/// different unique note hashes. Because of this this function returns a *vector* of discovered notes, though in most +/// cases it will contain a single element. +/// +/// Due to how nonces are computed, this function requires knowledge of the transaction in which the note was created, +/// more specifically the list of all unique note hashes in it plus the value of its first nullifier. pub unconstrained fn attempt_note_nonce_discovery( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, contract_address: AztecAddress, storage_slot: Field, note_type_id: Field, - serialized_note_content: BoundedVec, + packed_note_content: BoundedVec, ) -> BoundedVec { let discovered_notes = &mut BoundedVec::new(); @@ -30,15 +39,20 @@ pub unconstrained fn attempt_note_nonce_discovery( [unique_note_hashes_in_tx.len() as Field, contract_address.to_field(), storage_slot], ); - // We need to find the note's nonce, which is the one that results in one of the unique note hashes from tx_hash + // We need to find nonces (typically just one) that result in a note hash that, once siloed into a unique note hash, + // is one of the note hashes created by the transaction. array::for_each_in_bounded_vec( unique_note_hashes_in_tx, |expected_unique_note_hash, i| { + // Nonces are computed by hashing the first nullifier in the transaction with the index of the note in the + // new note hashes array. We therefore know for each note in every transaction what its nonce is. let candidate_nonce = compute_note_hash_nonce(first_nullifier_in_tx, i); + // Given nonce, note content and metadata, we can compute the note hash and silo it to check if it matches + // the note hash at the array index we're currently processing. // TODO(#11157): handle failed note_hash_and_nullifier computation let hashes = compute_note_hash_and_nullifier( - serialized_note_content, + packed_note_content, contract_address, candidate_nonce, storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr index c38bc827eaee..e1cfbe876d47 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr @@ -1,9 +1,8 @@ use crate::{ note::discovery::{ - MAX_NOTE_SERIALIZED_LEN, - MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN, + ComputeNoteHashAndNullifier, nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, - NoteHashAndNullifier, + private_logs::MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN, }, oracle::note_discovery::{deliver_note, get_log_by_tag}, pxe_db::DBArray, @@ -12,66 +11,42 @@ use crate::{ use dep::protocol_types::{ address::AztecAddress, + constants::PUBLIC_LOG_DATA_SIZE_IN_FIELDS, debug_log::debug_log_format, - traits::{Deserialize, FromField, Serialize, ToField}, + traits::{Deserialize, Serialize, ToField}, }; -pub global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT: Field = 5; +/// The slot in the PXE DB where we store a `DBArray` of `DeliveredPendingPartialNote`. +// TODO(#11630): come up with some sort of slot allocation scheme. +pub global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT: Field = 77; +/// Public logs contain an extra field at the beginning with the address of the contract that emitted them, and partial +/// notes emit their completion tag in the log, resulting in the first two fields in the public log not being part of +/// the packed public content. +// TODO(#10273): improve how contract log siloing is handled +pub global NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG: u32 = 2; + +/// The maximum length of the packed representation of public fields in a partial note. This is limited by public log +/// size and extra fields in the log (e.g. the tag). +pub global MAX_PUBLIC_PARTIAL_NOTE_PACKED_CONTENT_LENGTH: u32 = + PUBLIC_LOG_DATA_SIZE_IN_FIELDS - NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG; + +/// A partial note that was delivered but is still pending completion. Contains the information necessary to find the +/// log that will complete it and lead to a note being discovered and delivered. +#[derive(Serialize, Deserialize)] pub(crate) struct DeliveredPendingPartialNote { pub(crate) note_completion_log_tag: Field, pub(crate) storage_slot: Field, pub(crate) note_type_id: Field, - pub(crate) serialized_private_note_content: BoundedVec, + pub(crate) packed_private_note_content: BoundedVec, pub(crate) recipient: AztecAddress, } -// TODO: use `derive(Serialize)`, but we need for generics in struct fields to be handled properly before that happens. -// Currently blocked by Noir's StructDefinition::fields. -impl Serialize for DeliveredPendingPartialNote { - fn serialize(self) -> [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 4] { - [ - self.note_completion_log_tag, - self.storage_slot, - self.note_type_id, - self.serialized_private_note_content.storage()[0], - self.serialized_private_note_content.storage()[1], - self.serialized_private_note_content.storage()[2], - self.serialized_private_note_content.storage()[3], - self.serialized_private_note_content.storage()[4], - self.serialized_private_note_content.storage()[5], - self.serialized_private_note_content.storage()[6], - self.serialized_private_note_content.storage()[7], - self.serialized_private_note_content.storage()[8], - self.serialized_private_note_content.storage()[9], - self.serialized_private_note_content.storage()[10], - self.serialized_private_note_content.storage()[12], - self.serialized_private_note_content.storage()[13], - self.serialized_private_note_content.storage()[14], - self.serialized_private_note_content.len() as Field, - self.recipient.to_field(), - ] - } -} - -impl Deserialize for DeliveredPendingPartialNote { - fn deserialize(values: [Field; MAX_PARTIAL_NOTE_PRIVATE_SERIALIZED_LEN + 4]) -> Self { - Self { - note_completion_log_tag: values[0], - storage_slot: values[1], - note_type_id: values[2], - serialized_private_note_content: BoundedVec::from_parts( - array::subarray(values, 3), - values[17] as u32, - ), - recipient: AztecAddress::from_field(values[18]), - } - } -} - +/// Searches for public logs that would result in the completion of pending partial notes, ultimately resulting in the +/// notes being delivered to PXE if completed. pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( contract_address: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, ) { let pending_partial_notes = DBArray::at( contract_address, @@ -91,15 +66,35 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); if maybe_log.is_none() { - debug_log_format("Found no logs for partial note #{}", [(*i) as Field]); + debug_log_format( + "Found nocompletion logs for partial note #{}", + [(*i) as Field], + ); *i += 1 as u32; + // Note that we're not removing the pending partial note from the PXE DB, so we will continue searching + // for this tagged log when performing note discovery in the future until we either find it or the entry + // is somehow removed from the PXE DB. } else { - debug_log_format("Log found for partial note #{}!", [(*i) as Field]); + debug_log_format("Completion log found for partial note #{}", [(*i) as Field]); let log = maybe_log.unwrap(); - let complete_serialized_note_content = array::append( - pending_partial_note.serialized_private_note_content, - log.log_content, + // Public logs have an extra field at the beginning with the contract address, which we use to verify + // that we're getting the logs from the expected contract. + // TODO(#10273): improve how contract log siloing is handled + assert_eq( + log.log_content.get(0), + contract_address.to_field(), + "Got a public log emitted by a different contract", + ); + + // Public fields are assumed to all be placed at the end of the packed representation, so we combine the + // private and public packed fields (i.e. the contents of the log sans the extra fields) to get the + // complete packed content. + let packed_public_note_content: BoundedVec<_, MAX_PUBLIC_PARTIAL_NOTE_PACKED_CONTENT_LENGTH> = + array::subbvec(log.log_content, NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG); + let complete_packed_note_content = array::append( + pending_partial_note.packed_private_note_content, + packed_public_note_content, ); let discovered_notes = attempt_note_nonce_discovery( @@ -109,7 +104,7 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( contract_address, pending_partial_note.storage_slot, pending_partial_note.note_type_id, - complete_serialized_note_content, + complete_packed_note_content, ); debug_log_format( @@ -120,13 +115,14 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( array::for_each_in_bounded_vec( discovered_notes, |discovered_note: DiscoveredNoteInfo, _| { - // TODO:(#10728): handle notes that fail delivery + // TODO:(#10728): decide handle notes that fail delivery. This could be due to e.g. a temporary + // node connectivity issue - is simply throwing good enough here? assert( deliver_note( contract_address, pending_partial_note.storage_slot, discovered_note.nonce, - complete_serialized_note_content, + complete_packed_note_content, discovered_note.note_hash, discovered_note.inner_nullifier, log.tx_hash, @@ -137,14 +133,18 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( }, ); - // we assume that there is only a single log per tag, so if the log didnt work then we cry + // Because we there is only a single log for a given tag, once we've processed the tagged log then we + // simply delete the pending work entry, regardless of whether it was actually completed or not. + // TODO(#11627): only remove the pending entry if we actually process a log that results in the note + // being completed. pending_partial_notes.remove(*i); } - // todo: handle the log never showing up? }, ); } +/// Custom version of a while loop, calls `body` repeatedly until `condition` returns false. To be removed once Noir +/// supports looping in unconstrained code. fn whyle(condition: fn[Env]() -> bool, body: fn[Env2]() -> ()) { if condition() { body(); diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr index d54e989680d1..91922c4b35ea 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr @@ -9,53 +9,39 @@ use dep::protocol_types::{ }; use crate::note::discovery::{ - MAX_NOTE_SERIALIZED_LEN, + ComputeNoteHashAndNullifier, + MAX_NOTE_PACKED_LEN, nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, NOTE_PRIVATE_LOG_RESERVED_FIELDS, - NoteHashAndNullifier, partial_notes::{ DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT, DeliveredPendingPartialNote, }, }; +pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; +/// Partial notes have a maximum packed length of their private fields bound by extra content in their private log (i.e. +/// the note completion log tag). +pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 = + MAX_NOTE_PACKED_LEN - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN; + +/// Searches for private logs that signal new private notes that are then delivered to PXE, or new partial notes that +/// are stored in the PXE DB to so that `fetch_and_process_public_partial_note_completion_logs` can later search for +/// public logs that will complete them. pub unconstrained fn fetch_and_process_private_tagged_logs( _contract_address: AztecAddress, - _compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + _compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, ) { // We will eventually fetch tagged logs, decrypt and process them here, but for now we simply call the `syncNotes` - // oracle, which performs tag synchronization, log download, decryption, and then calls the `process_log` function - // which the decrypted payload, at which point `do_process_log` gets executed and we continue the work ourselves. + // oracle. This has PXE perform tag synchronization, log download, decryption, and finally calls to the the + // `process_log` contract function with the decrypted payload, which will in turn call `do_process_log` with a + // decrypted log, letting us continue the work outside of PXE. sync_notes(); } -/// Processes a log 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], -/// }) -/// } -/// ``` +/// Processes a log's plaintext, searching for private notes or partial notes. Private notes result in nonce discovery +/// being performed prior to delivery, which requires knowledge of the transaction hash in which the notes would've been +/// created (typically the same transaction in which the log was emitted), along with the list of unique note hashes in +/// said transaction and the `compute_note_hash_and_nullifier` function. pub unconstrained fn do_process_log( contract_address: AztecAddress, log_plaintext: BoundedVec, @@ -63,8 +49,12 @@ pub unconstrained fn do_process_log( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, ) { + // The first thing to do is to determine what type of private log we're processing. We currently just have two log + // types: 0 for private notes and 1 for partial notes. This will likely be expanded and improved upon in the future + // to also handle events, etc. + let (storage_slot, note_type_id, log_type_id, log_payload) = destructure_log_plaintext(log_plaintext); @@ -99,19 +89,23 @@ pub unconstrained fn do_process_log( unconstrained fn destructure_log_plaintext( log_plaintext: BoundedVec, -) -> (Field, Field, Field, BoundedVec) { +) -> (Field, Field, Field, BoundedVec) { assert(log_plaintext.len() >= NOTE_PRIVATE_LOG_RESERVED_FIELDS); // If NOTE_PRIVATE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for // `storage_slot` and `note_type_id` must be updated as well. static_assert( NOTE_PRIVATE_LOG_RESERVED_FIELDS == 2, - "unepxected value for NOTE_PRIVATE_LOG_RESERVED_FIELDS", + "unexpected value for NOTE_PRIVATE_LOG_RESERVED_FIELDS", ); let storage_slot = log_plaintext.get(0); - // The combined type id is the bit packing of the note type id (which is 7 bits big) and the log type id (which is - // currently a single bit, and right above it). + // We currently identify log types by packing the log type ID and note type ID into a single field, called the + // combined type ID. We can do this because the note type ID is only 7 bits long, and so use an 8th bit to + // distinguish private note logs and partial note logs. + // This abuses the fact that the encoding of both of these logs is extremely similar, and will need improving and + // more formalization once we introduce other disimilar log types, such as events. Ideally we'd be able to leverage + // enums and tagged unions to achieve this goal. let combined_type_id = log_plaintext.get(1); let note_type_id = ((combined_type_id as u64) % 128) as Field; let log_type_id = combined_type_id / 128; @@ -127,10 +121,10 @@ unconstrained fn process_private_note_log( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, - compute_note_hash_and_nullifier: fn[Env](BoundedVec, AztecAddress, Field, Field, Field) -> Option, + compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, storage_slot: Field, note_type_id: Field, - serialized_note_content: BoundedVec, + serialized_note_content: BoundedVec, ) { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, @@ -142,11 +136,16 @@ unconstrained fn process_private_note_log( serialized_note_content, ); + debug_log_format( + "Discovered {0} notes from a private log", + [discovered_notes.len() as Field], + ); + array::for_each_in_bounded_vec( discovered_notes, |discovered_note: DiscoveredNoteInfo, _| { // TODO:(#10728): handle notes that fail delivery. This could be due to e.g. a temporary node connectivity - // issue, and we should perhaps not mark the tag index as taken. + // issue, and we should perhaps not have marked the tag index as taken. assert( deliver_note( contract_address, @@ -168,22 +167,24 @@ unconstrained fn process_partial_note_private_log( contract_address: AztecAddress, storage_slot: Field, note_type_id: Field, - log_payload: BoundedVec, + log_payload: BoundedVec, recipient: AztecAddress, ) { + // We store the information of the partial note we found so that we can later search for the public log that will + // complete it. The tag is the first value in the payload, with the packed note content taking up the rest of it. + static_assert( + PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN == 1, + "unexpected value for PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN", + ); + let pending = DeliveredPendingPartialNote { note_completion_log_tag: log_payload.get(0), storage_slot, note_type_id, - serialized_private_note_content: array::subbvec(log_payload, 1), + packed_private_note_content: array::subbvec(log_payload, 1), recipient, }; - debug_log_format( - "note completion log tag {0} storage_slot {1} note type id {2}", - [pending.note_completion_log_tag, storage_slot, note_type_id], - ); - DBArray::at( contract_address, DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_DB_SLOT, 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 da286c19c073..77929943ffd5 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr @@ -1,4 +1,4 @@ -use crate::note::discovery::MAX_NOTE_SERIALIZED_LEN; +use crate::note::discovery::MAX_NOTE_PACKED_LEN; use dep::protocol_types::{address::AztecAddress, log_with_tx_data::LogWithTxData}; /// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them available @@ -35,7 +35,7 @@ pub unconstrained fn deliver_note( contract_address: AztecAddress, storage_slot: Field, nonce: Field, - content: BoundedVec, + content: BoundedVec, note_hash: Field, nullifier: Field, tx_hash: Field, @@ -53,7 +53,12 @@ pub unconstrained fn deliver_note( ) } -// todo: return a bounded vec of these +/// Fetches a log from the node that has the corresponding `tag`. The log can be either a public or a private log, and +/// the tag is the first field in the log's content. Returns `Option::none` if no such log exists. Throws if more than +/// one log with that tag exists. +/// Public logs have an extra field included at the beginning with the address of the contract that emtitted them. +// TODO(#11627): handle multiple logs with the same tag. +// TODO(#10273): improve contract siloing of logs, don't introduce an extra field. pub unconstrained fn get_log_by_tag(tag: Field) -> Option { get_log_by_tag_oracle(tag) } @@ -63,7 +68,7 @@ unconstrained fn deliver_note_oracle( contract_address: AztecAddress, storage_slot: Field, nonce: Field, - content: BoundedVec, + content: BoundedVec, note_hash: Field, nullifier: Field, tx_hash: Field, diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 08d127ca3d0e..381d99c76695 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -726,6 +726,9 @@ export class SimulatorOracle implements DBOracle { const log = logsForTag[0]; + // getLogsByTag doesn't have all of the information that we need (notable note hashes and the first nullifier), so + // we need to make a second call to the node for `getTxEffect`. + // TODO(#9789): bundle this information in the `getLogsByTag` call. const txEffect = await this.aztecNode.getTxEffect(log.txHash); if (txEffect == undefined) { throw new Error(`Unexpected: failed to retrieve tx effects for tx ${log.txHash} which is known to exist`); @@ -734,16 +737,17 @@ export class SimulatorOracle implements DBOracle { const reader = BufferReader.asReader(log.logData); const logArray = reader.readArray(PUBLIC_LOG_DATA_SIZE_IN_FIELDS, Fr); - this.log.debug(`Log content ${logArray}, length: ${logArray.length}`); + // Public logs always take up all available fields by padding with zeroes, and the length of the originally emitted + // log is lost. Until this is improved, we simply remove all of the zero elements (which are expected to be at the + // end). + // TODO(#11636): use the actual log length. + const trimmedLog = logArray.filter(x => !x.isZero()); - const modifiedLog = logArray.slice(2, 3); - this.log.debug(`Modified ${modifiedLog}`); - - return new LogWithTxData(modifiedLog, log.txHash.hash, txEffect.data.noteHashes, txEffect.data.nullifiers[0]); + return new LogWithTxData(trimmedLog, log.txHash.hash, txEffect.data.noteHashes, txEffect.data.nullifiers[0]); } public async removeNullifiedNotes(contractAddress: AztecAddress) { - this.log.verbose('Removing nullified notes', { contract: contractAddress }); + this.log.verbose('Searching for nullifiers of known notes', { contract: contractAddress }); for (const recipient of await this.keyStore.getAccounts()) { const currentNotesForRecipient = await this.db.getNotes({ contractAddress, owner: recipient }); From 49488f3d904647cbdf47e6e28ba405fadd9081c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 30 Jan 2025 21:03:21 +0000 Subject: [PATCH 30/39] Doc improvements --- .../aztec/src/encrypted_logs/partial.nr | 34 ---------- .../aztec/src/macros/functions/mod.nr | 51 ++++++++++---- .../aztec-nr/aztec/src/macros/mod.nr | 68 ++++++++----------- .../aztec-nr/aztec/src/macros/notes/mod.nr | 2 - .../aztec-nr/aztec/src/note/discovery/mod.nr | 6 +- .../aztec-nr/aztec/src/note/utils.nr | 8 +-- .../aztec/src/oracle/note_discovery.nr | 10 +-- .../aztec-nr/aztec/src/oracle/notes.nr | 19 ++---- .../aztec-nr/aztec/src/utils/array/append.nr | 37 ++++++++++ .../aztec-nr/aztec/src/utils/array/mod.nr | 1 + .../uint-note/src/partial_uint_note.nr | 42 ++++++------ .../aztec-nr/uint-note/src/uint_note.nr | 2 +- .../crates/types/src/log_with_tx_data.nr | 6 ++ .../archiver/kv_archiver_store/log_store.ts | 26 +------ .../memory_archiver_store.ts | 26 +------ .../src/structs/log_with_tx_data.ts | 1 + ...tuff.test.ts => e2e_partial_notes.test.ts} | 3 +- .../pxe/src/simulator_oracle/index.ts | 1 + .../simulator/src/acvm/oracle/oracle.ts | 8 +-- yarn-project/simulator/src/acvm/serialize.ts | 7 ++ .../simulator/src/client/db_oracle.ts | 6 ++ 21 files changed, 165 insertions(+), 199 deletions(-) delete mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr rename yarn-project/end-to-end/src/{e2e_partial_stuff.test.ts => e2e_partial_notes.test.ts} (90%) diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr deleted file mode 100644 index 56b48fd81365..000000000000 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/partial.nr +++ /dev/null @@ -1,34 +0,0 @@ -use crate::context::private_context::PrivateContext; -use protocol_types::address::aztec_address::AztecAddress; - -pub fn compute_and_emit_partial_note_private_log( - context: &mut PrivateContext, - recipient: AztecAddress, - sender: AztecAddress, - log_plaintext: [u8; PLAINTEXT_LEN], -) { - // let magic_tag: Field = 42424242; - // let magic_tag_encoded: [u8; 32] = magic_tag.to_be_bytes(); - - let mut mutated_plaintext: [u8; PLAINTEXT_LEN + 32] = std::mem::zeroed(); - - for i in 0..log_plaintext.len() { - mutated_plaintext[i] = log_plaintext[i]; - } - // for i in 0..32 { - // mutated_plaintext[i] = log_plaintext[i]; - // mutated_plaintext[i + 32] = log_plaintext[i + 32]; - // } - mutated_plaintext[32 + 31] = log_plaintext[32 + 31] + 128; - - /// Safety: lol - let contract_address = unsafe { crate::oracle::execution::get_contract_address() }; - - let payload = super::payload::compute_private_log_payload( - contract_address, - recipient, - sender, - mutated_plaintext, - ); - context.emit_private_log(payload); -} diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index 9ca62ad32edc..6e84fa2c7b1b 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -1,9 +1,12 @@ pub mod interfaces; pub mod initialization_utils; -use super::utils::{ - add_to_hasher, fn_has_noinitcheck, get_fn_visibility, is_fn_initializer, is_fn_internal, - is_fn_private, is_fn_view, modify_fn_body, module_has_initializer, module_has_storage, +use super::{ + notes::NOTES, + utils::{ + add_to_hasher, fn_has_noinitcheck, get_fn_visibility, is_fn_initializer, is_fn_internal, + is_fn_private, is_fn_view, modify_fn_body, module_has_initializer, module_has_storage, + }, }; use protocol_types::meta::generate_serialize_to_fields; use std::meta::type_of; @@ -81,18 +84,28 @@ comptime fn create_init_check(f: FunctionDefinition) -> Quoted { .quoted_contents() } +/// Injects a call to `aztec::note::discovery::discover_new_notes`, causing for new notes to be added to PXE and made +/// available for the current execution. comptime fn create_note_discovery_call() -> Quoted { quote { - unsafe { dep::aztec::note::discovery::discover_new_notes(context.this_address(), |serialized_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { - let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, serialized_note_content); - - Option::some( - aztec::note::discovery::NoteHashAndNullifier { - note_hash: hashes[0], - inner_nullifier: hashes[3], + /// Safety: note discovery returns nothing and is performed solely for its side-effects. It is therefore always + /// safe to call. + unsafe { + dep::aztec::note::discovery::discover_new_notes( + context.this_address(), + |serialized_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { + // _compute_note_hash_and_optionally_a_nullifier is a contract library method injected by `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` + let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, serialized_note_content); + + Option::some( + aztec::note::discovery::NoteHashAndNullifier { + note_hash: hashes[0], + inner_nullifier: hashes[3], + }, + ) }, ) - }) }; + }; } } @@ -175,7 +188,13 @@ pub comptime fn private(f: FunctionDefinition) -> Quoted { quote {} }; - let note_discovery_call = create_note_discovery_call(); + // All private functions perform note discovery, since they may need to access notes. This is slightly inefficient + // and could be improved by only doing it once we actually attempt to read any. + let note_discovery_call = if NOTES.len() > 0 { + create_note_discovery_call() + } else { + quote {} + }; // Finally, we need to change the return type to be `PrivateCircuitPublicInputs`, which is what the Private Kernel // circuit expects. @@ -355,7 +374,13 @@ pub comptime fn transform_unconstrained(f: FunctionDefinition) { quote {} }; - let note_discovery_call = create_note_discovery_call(); + // All unconstrained functions perform note discovery, since they may need to access notes. This is slightly + // inefficient and could be improved by only doing it once we actually attempt to read any. + let note_discovery_call = if NOTES.len() > 0 { + create_note_discovery_call() + } else { + quote {} + }; let to_prepend = quote { $context_creation diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 3600bafb7a69..03664105a564 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -114,12 +114,23 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } +/// Generates a contract library method called `_compute_note_hash_and_optionally_a_nullifier` which is used for note +/// discovery (to create the `aztec::note::discovery::ComputeNoteHashAndNullifier` function) and to implement the +/// `compute_note_hash_and_nullifier` unconstrained contract function. comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier() -> Quoted { let notes = NOTES.entries(); - let body = if notes.len() == 0 { + // We'll produce the entire body of the function in one go and then insert it into the function. + let body: Quoted = if notes.len() == 0 { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. quote { panic(f"This contract does not use private notes") } } else { + // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the + // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we + // know it we call `aztec::note::utils::compute_note_hash_and_optionally_a_nullifier` (which is the one that + // actually does the work) with the correct `unpack_content()` function. + let mut if_note_type_id_match_statements_list = &[]; for i in 0..notes.len() { let (typ, (_, serialized_note_length, _, _)) = notes[i]; @@ -133,7 +144,7 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( quote { $if_or_else_if note_type_id == $typ::get_note_type_id() { - // As an extra safety check we make sure that the packed_note_content bounded vec has the + // As an extra safety check we make sure that the `packed_note_content` BoundedVec 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; @@ -175,9 +186,14 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ } comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { - // For historical reasons we keep this function taking an array of a length of up to the longest note, receiving a - // boolean and returning four fields. The contract library method `_compute_note_hash...` is affected by this. - // Ideally we'd eventually remove these things. + // For historical reasons we keep this function taking an array of a length of up to the longest note (instead of a + // BoundedVec), receiving a boolean (even though we could compute the nullifier always) and returning four fields + // (even though the caller should likely perform note hash siloing on their own and not trust this). The contract + // library method `_compute_note_hash...` is affected by this. + // TODO(#11638): In the future we might remove these things as we rely less and less on this function, and then + // change the `_compute_note_hash...` contract library method to be of type + // `aztec::note::discovery::ComputeNoteHashAndNullifier`, simplifying other macros by removing the need to create + // intermediate lambdas that adapt their interfaces. let max_note_packed_length = NOTES.entries().fold( 0, @@ -201,44 +217,18 @@ 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. + // This mandatory function processes a log emitted by the contract. This is currently used to process private logs + // and perform note discovery of either private notes or partial notes. // The bulk of the work of this function is done by aztec::note::discovery::do_process_log, so all we need to do - // is call that function. However, one of its parameters is a lambda function that computes note hash and nullifier - // given note contents and metadata (e.g. note type id), since this behavior is contract-specific (as it - // depends on the note types implemented by each contract). - // The job of this macro is therefore to implement this lambda function and then call `do_process_log` with it. - - // A typical implementation of the lambda looks something like this: - // ``` - // |packed_note_content: BoundedVec, note_header: NoteHeader, note_type_id: Field| { - // let hashes = if note_type_id == MyNoteType::get_note_type_id() { - // assert(packed_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); - // dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier( - // MyNoteType::unpack_content, - // note_header, - // true, - // packed_note_content.storage(), - // ) - // } else { - // panic(f"Unknown note type id {note_type_id}") - // }; - // - // Option::some(dep::aztec::note::discovery::NoteHashesAndNullifier { - // note_hash: hashes[0], - // unique_note_hash: hashes[1], - // inner_nullifier: hashes[3], - // }) - // } - // ``` - // - // We create this implementation by iterating over the different note types, creating an `if` or `else if` clause - // for each of them and calling `compute_note_hash_and_optionally_a_nullifier` with the note's deserialization - // function, and finally produce the required `NoteHashesAndNullifier` object. + // is call that function. We use the contract library method injected by + // `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` in order to create the required + // `aztec::note::discovery::ComputeNoteHashAndNullifier` function. + // We'll produce the entire body of the function in one go and then insert it into the function. let notes = NOTES.entries(); - let body = if notes.len() == 0 { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. quote { panic(f"This contract does not use private notes") } } else { quote { diff --git a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr index 4ff84a468a37..2f480ebf68c1 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr @@ -899,8 +899,6 @@ pub comptime fn partial_note(s: StructDefinition, nullable_fields: [Quoted]) -> indexed_nullable_fields, ); - println(note_interface_impl); - quote { $note_properties $setup_payload_impl 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 a4cdacecb4cd..b3cb0f160158 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -17,8 +17,10 @@ global NOTE_PRIVATE_LOG_RESERVED_FIELDS: u32 = 2; pub global MAX_NOTE_PACKED_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; pub struct NoteHashAndNullifier { - pub note_hash: Field, // The result of NoteInterface::compute_note_hash - pub inner_nullifier: Field, // The result of NullifiableNote::compute_nullifier_without_context + /// The result of NoteInterface::compute_note_hash + pub note_hash: Field, + /// The result of NullifiableNote::compute_nullifier_without_context + pub inner_nullifier: Field, } /// A function which takes a note's packed content, address of the emitting contract, nonce, storage slot and note type diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 845dad3b94f0..248a3fc3e115 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -6,7 +6,6 @@ use crate::{ use dep::protocol_types::{ address::AztecAddress, - debug_log::debug_log_format, hash::{ compute_siloed_note_hash, compute_siloed_nullifier as compute_siloed_nullifier_from_preimage, @@ -121,6 +120,8 @@ where compute_note_hash_for_nullify_internal(note, note_hash_for_read_request) } +// TODO (#11638): simplify or remove this function by inlining it in the `_compute_note_hash_and_nullifier` contract +// library method that is autogenerated by macros. pub unconstrained fn compute_note_hash_and_optionally_a_nullifier( unpack_content: fn([Field; N]) -> T, contract_address: AztecAddress, @@ -135,11 +136,6 @@ where let mut note = unpack_content(array::subarray(packed_note_content, 0)); note.set_header(NoteHeader::new(contract_address, nonce, storage_slot)); - debug_log_format( - "addr {0} nonce {1} storage_slot {2}", - [contract_address.to_field(), nonce, storage_slot], - ); - let note_hash = note.compute_note_hash(); let siloed_note_hash = compute_siloed_note_hash(contract_address, note_hash); let unique_note_hash = compute_unique_note_hash(nonce, siloed_note_hash); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr index 77929943ffd5..7e0d74df7151 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr @@ -3,15 +3,7 @@ use dep::protocol_types::{address::AztecAddress, log_with_tx_data::LogWithTxData /// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them available /// for later querying via the `get_notes` oracle. -pub fn sync_notes() { - /// Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to - /// call. - unsafe { - sync_notes_oracle_wrapper(); - } -} - -unconstrained fn sync_notes_oracle_wrapper() { +pub unconstrained fn sync_notes() { sync_notes_oracle(); } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index f478fc4357af..ef2b1cbcc998 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -1,12 +1,9 @@ -use crate::{ - note::{discovery::discover_new_notes, note_header::NoteHeader, note_interface::NoteInterface}, - oracle, - utils::array, -}; +use crate::{note::{note_header::NoteHeader, note_interface::NoteInterface}, utils::array}; use dep::protocol_types::{ address::AztecAddress, indexed_tagging_secret::{INDEXED_TAGGING_SECRET_LENGTH, IndexedTaggingSecret}, + traits::{Deserialize, FromField}, }; /// Notifies the simulator that a note has been created, so that it can be returned in future read requests in the same @@ -35,14 +32,15 @@ pub fn notify_created_note( /// the same transaction. This note should only be removed to the non-volatile database if its nullifier is found in an /// actual block. pub fn notify_nullified_note(nullifier: Field, note_hash: Field, counter: u32) { - /// Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe - /// to call. + /// Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to + /// call. unsafe { notify_nullified_note_oracle_wrapper(nullifier, note_hash, counter) }; } /// Notifies the simulator that a non-note nullifier has been created, so that it can be used for note nonces. pub fn notify_created_nullifier(nullifier: Field) { - // This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to call. + /// Safety: This oracle call returns nothing: we only call it for its side effects. It is therefore always safe to + /// call. unsafe { notify_created_nullifier_oracle_wrapper(nullifier) }; } @@ -173,11 +171,6 @@ pub unconstrained fn get_notes, { - // inject this call in all #[private] fns via macros - - // TODO(#10727): allow other contracts to discover notes - // discover_new_notes(oracle::execution::get_contract_address()); - let fields = get_notes_oracle_wrapper( storage_slot, num_selects, diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/append.nr b/noir-projects/aztec-nr/aztec/src/utils/array/append.nr index 505f84e039f7..dbc370346c5d 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/append.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/append.nr @@ -1,3 +1,6 @@ +/// Appends two `BoundedVec`s together, returning one that contains all of the elements of the first one followed by all +/// of the elements of the second one. The resulting `BoundedVec` can have any arbitrary maximum length, but it must be +/// large enough to fit all of the elements of both the first and second vectors. pub fn append( a: BoundedVec, b: BoundedVec, @@ -9,3 +12,37 @@ pub fn append( dst } + +mod test { + use super::append; + + #[test] + unconstrained fn append_empty_vecs() { + let a: BoundedVec<_, 3> = BoundedVec::new(); + let b: BoundedVec<_, 14> = BoundedVec::new(); + + let result: BoundedVec = append(a, b); + + assert_eq(result.len(), 0); + assert_eq(result.storage(), std::mem::zeroed()); + } + + #[test] + unconstrained fn append_non_empty_vecs() { + let a: BoundedVec<_, 3> = BoundedVec::from_array([1, 2, 3]); + let b: BoundedVec<_, 14> = BoundedVec::from_array([4, 5, 6]); + + let result: BoundedVec = append(a, b); + + assert_eq(result.len(), 6); + assert_eq(result.storage(), [1, 2, 3, 4, 5, 6, std::mem::zeroed(), std::mem::zeroed()]); + } + + #[test(should_fail_with = "out of bounds")] + unconstrained fn append_non_empty_vecs_insufficient_max_len() { + let a: BoundedVec<_, 3> = BoundedVec::from_array([1, 2, 3]); + let b: BoundedVec<_, 14> = BoundedVec::from_array([4, 5, 6]); + + let _: BoundedVec = append(a, b); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr index d8628c345d72..291dc9241663 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr @@ -8,6 +8,7 @@ pub use collapse::collapse; pub use subarray::subarray; pub use subbvec::subbvec; +// This will eventually be replaced by `BoundedVec::for_each`, once that's implemented. pub unconstrained fn for_each_in_bounded_vec( vec: BoundedVec, f: fn[Env](T, u32) -> (), diff --git a/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr b/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr index 5c1abc807f46..6d3669220039 100644 --- a/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr @@ -1,5 +1,5 @@ use dep::aztec::{ - generators::{G_slot, Ga1, Ga2, Ga3, Ga4}, + generators::{G_slot, Ga2, Ga3}, note::note_interface::NoteInterface, oracle::random::random, prelude::NoteHeader, @@ -9,26 +9,6 @@ use std::static_assert; use crate::uint_note::UintNote; -pub struct PartialUintNoteHidingPoint { - pub value: Point, -} - -impl PartialUintNoteHidingPoint { - fn new(value: Point) -> Self { - Self { value } - } - - fn complete(self, amount: U128) -> Field { - let note_point = self.value - + std::embedded_curve_ops::multi_scalar_mul( - [G_slot], - [std::hash::from_field_unsafe(amount.pack()[0])], - ); - - note_point.x - } -} - pub struct PartialUintNote { pub owner: AztecAddress, pub randomness: Field, @@ -90,3 +70,23 @@ impl NoteInterface for PartialUintNote { panic(f"unsupported") } } + +pub struct PartialUintNoteHidingPoint { + pub value: Point, +} + +impl PartialUintNoteHidingPoint { + fn new(value: Point) -> Self { + Self { value } + } + + fn complete(self, amount: U128) -> Field { + let note_point = self.value + + std::embedded_curve_ops::multi_scalar_mul( + [G_slot], + [std::hash::from_field_unsafe(amount.pack()[0])], + ); + + note_point.x + } +} diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 143b69d68efd..8d53a1a229f9 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -16,10 +16,10 @@ use dep::aztec::{ #[partial_note(quote {value})] #[derive(Serialize)] pub struct UintNote { - // The amount of tokens in the note owner: AztecAddress, // Randomness of the note to hide its contents randomness: Field, + // The amount of tokens in the note value: U128, } // docs:end:UintNote diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr b/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr index d225b9a4f8b8..9ced1852d103 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/log_with_tx_data.nr @@ -1,8 +1,14 @@ use crate::constants::{MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS}; +/// The contents of a public log, plus contextual information about the transaction in which the log was emitted. This +/// is the data required in order to discover notes that are being delivered in a log. +// TODO(#11639): this could also be used to fetch private logs, but the `BoundedVec` maximum length is that of a public +// log. pub struct LogWithTxData { pub log_content: BoundedVec, pub tx_hash: Field, + /// The array of new note hashes created by `tx_hash` pub unique_note_hashes_in_tx: BoundedVec, + /// The first nullifier created by `tx_hash` pub first_nullifier_in_tx: Field, } diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts index 6a67b45f0b66..38550b0f06a2 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts @@ -79,33 +79,9 @@ export class LogStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.publicLogs.forEach(log => { - // // Check that each log stores 2 lengths in its first field. If not, it's not a tagged log: - // const firstFieldBuf = log.log[0].toBuffer(); - // // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). - // // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. - // if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { - // // See parseLogFromPublic - the first field of a tagged log is 5 bytes structured: - // // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] - // this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); - // return; - // } - // // Check that the length values line up with the log contents - // const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); - // const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); - // // Add 1 for the first field holding lengths - // const totalLogLength = 1 + publicValuesLength + privateValuesLength; - // // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length - // if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { - // this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); - // return; - // } - - // // The first elt stores lengths as above => tag is in fields[1] - // const tag = log.log[1]; - const tag = log.log[0]; - this.#log.debug(`Found tagged public log with tag ${tag.toString()} in block ${block.number}`); + const currentLogs = taggedLogs.get(tag.toString()) ?? []; currentLogs.push( new TxScopedL2Log( diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 09dbd154c4d4..84562d1c50d9 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -260,33 +260,9 @@ export class MemoryArchiverStore implements ArchiverDataStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.publicLogs.forEach(log => { - // // Check that each log stores 3 lengths in its first field. If not, it's not a tagged log: - // // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). - // // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. - // const firstFieldBuf = log.log[0].toBuffer(); - // if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { - // // See parseLogFromPublic - the first field of a tagged log is 8 bytes structured: - // // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] - // this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); - // return; - // } - // // Check that the length values line up with the log contents - // const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); - // const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); - // // Add 1 for the first field holding lengths - // const totalLogLength = 1 + publicValuesLength + privateValuesLength; - // // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length - // if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { - // this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); - // return; - // } - - // // The first elt stores lengths => tag is in fields[1] - // const tag = log.log[1]; - const tag = log.log[0]; - this.#log.verbose(`Storing public tagged log with tag ${tag.toString()} in block ${block.number}`); + const currentLogs = this.taggedLogs.get(tag.toString()) || []; this.taggedLogs.set(tag.toString(), [ ...currentLogs, diff --git a/yarn-project/circuits.js/src/structs/log_with_tx_data.ts b/yarn-project/circuits.js/src/structs/log_with_tx_data.ts index 9e5ebb1b8db9..d4e1d80b1a21 100644 --- a/yarn-project/circuits.js/src/structs/log_with_tx_data.ts +++ b/yarn-project/circuits.js/src/structs/log_with_tx_data.ts @@ -2,6 +2,7 @@ import { Fr } from '@aztec/foundation/fields'; import { MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS } from '../constants.gen.js'; +// TypeScript representation of the Noir protocol_types::log_with_tx_data::LogWithTxData struct. export class LogWithTxData { constructor( public logContent: Fr[], diff --git a/yarn-project/end-to-end/src/e2e_partial_stuff.test.ts b/yarn-project/end-to-end/src/e2e_partial_notes.test.ts similarity index 90% rename from yarn-project/end-to-end/src/e2e_partial_stuff.test.ts rename to yarn-project/end-to-end/src/e2e_partial_notes.test.ts index 030222cb4e93..ec94b7982c34 100644 --- a/yarn-project/end-to-end/src/e2e_partial_stuff.test.ts +++ b/yarn-project/end-to-end/src/e2e_partial_notes.test.ts @@ -8,7 +8,7 @@ import { setup } from './fixtures/utils.js'; const TIMEOUT = 120_000; -describe('partial_stuff', () => { +describe('partial notes', () => { jest.setTimeout(TIMEOUT); let teardown: () => Promise; @@ -20,7 +20,6 @@ describe('partial_stuff', () => { let token0: TokenContract; - // We need a large token amount so that the swap fee (0.3%) is observable. const INITIAL_TOKEN_BALANCE = 1_000_000_000n; beforeAll(async () => { diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 381d99c76695..25007bd0d1fd 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -719,6 +719,7 @@ export class SimulatorOracle implements DBOracle { if (logsForTag.length == 0) { return null; } else if (logsForTag.length > 1) { + // TODO(#11627): handle this case throw new Error( `Got ${logsForTag.length} logs for tag ${tag}. getLogByTag currently only supports a single log per tag`, ); diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index f9698f9de58d..77f7ab278452 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -4,11 +4,9 @@ import { FunctionSelector, NoteSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; -import { timeStamp } from 'console'; - import { type ACVMField } from '../acvm_types.js'; import { frToBoolean, frToNumber, fromACVMField, fromBoundedVec } from '../deserialize.js'; -import { toACVMField } from '../serialize.js'; +import { toACVMField, toACVMFieldSingleOrArray } from '../serialize.js'; import { type TypedOracle } from './typed_oracle.js'; /** @@ -464,7 +462,3 @@ export class Oracle { ); } } - -function toACVMFieldSingleOrArray(value: Fr | Fr[]) { - return Array.isArray(value) ? value.map(toACVMField) : toACVMField(value); -} diff --git a/yarn-project/simulator/src/acvm/serialize.ts b/yarn-project/simulator/src/acvm/serialize.ts index 0ae28ed6ce5c..d76f0d6438c8 100644 --- a/yarn-project/simulator/src/acvm/serialize.ts +++ b/yarn-project/simulator/src/acvm/serialize.ts @@ -39,6 +39,13 @@ export function toACVMField( return `0x${adaptBufferSize(buffer).toString('hex')}`; } +/** + * Converts a single value or an array of single values into the equivalent ACVM field representation. + */ +export function toACVMFieldSingleOrArray(value: Fr | Fr[]) { + return Array.isArray(value) ? value.map(toACVMField) : toACVMField(value); +} + /** * Inserts a list of ACVM fields to a witness. * @param witnessStartIndex - The index where to start inserting the fields. diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index 4e1ade7dcfb4..c1a1e2d7baf3 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -258,6 +258,12 @@ export interface DBOracle extends CommitmentsDB { recipient: AztecAddress, ): Promise; + /** + * Searches for a log with the corresponding `tag` and returns it along with contextual transaction information. + * Returns null if no such log exists, and throws if more than one exists. + * + * @param tag - The log tag to search for. + */ getLogByTag(tag: Fr): Promise; /** From 4d3ada9d1dcf32634720cd6c99ea48a0bf602fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 5 Feb 2025 15:31:03 +0000 Subject: [PATCH 31/39] Adddress typos --- .../aztec-nr/aztec/src/macros/functions/mod.nr | 4 ++-- noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr | 2 +- .../aztec-nr/aztec/src/note/discovery/partial_notes.nr | 8 ++++---- .../aztec-nr/aztec/src/note/discovery/private_logs.nr | 10 +++++----- yarn-project/pxe/src/simulator_oracle/index.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index 6e84fa2c7b1b..fe1deafdc564 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -93,9 +93,9 @@ comptime fn create_note_discovery_call() -> Quoted { unsafe { dep::aztec::note::discovery::discover_new_notes( context.this_address(), - |serialized_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { + |packed_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { // _compute_note_hash_and_optionally_a_nullifier is a contract library method injected by `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` - let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, serialized_note_content); + let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, packed_note_content); Option::some( aztec::note::discovery::NoteHashAndNullifier { 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 b3cb0f160158..0988d95ead0e 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr @@ -48,7 +48,7 @@ pub struct NoteHashAndNullifier { /// } else if note_type_id == MyOtherNoteType::get_note_type_id() { /// ... // Similar to above but calling MyOtherNoteType::unpack_content /// } else { -/// Option::none() // Uknown note type ID +/// Option::none() // Unknown note type ID /// }; /// } /// ``` diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr index e1cfbe876d47..17e1601107bb 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr @@ -67,7 +67,7 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( let maybe_log = get_log_by_tag(pending_partial_note.note_completion_log_tag); if maybe_log.is_none() { debug_log_format( - "Found nocompletion logs for partial note #{}", + "Found no completion logs for partial note #{}", [(*i) as Field], ); *i += 1 as u32; @@ -115,8 +115,8 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( array::for_each_in_bounded_vec( discovered_notes, |discovered_note: DiscoveredNoteInfo, _| { - // TODO:(#10728): decide handle notes that fail delivery. This could be due to e.g. a temporary - // node connectivity issue - is simply throwing good enough here? + // TODO:(#10728): decide how to handle notes that fail delivery. This could be due to e.g. a + // temporary node connectivity issue - is simply throwing good enough here? assert( deliver_note( contract_address, @@ -133,7 +133,7 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( }, ); - // Because we there is only a single log for a given tag, once we've processed the tagged log then we + // Because there is only a single log for a given tag, once we've processed the tagged log then we // simply delete the pending work entry, regardless of whether it was actually completed or not. // TODO(#11627): only remove the pending entry if we actually process a log that results in the note // being completed. diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr index 91922c4b35ea..c6aeb03fe19b 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr @@ -25,8 +25,8 @@ pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 = MAX_NOTE_PACKED_LEN - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN; /// Searches for private logs that signal new private notes that are then delivered to PXE, or new partial notes that -/// are stored in the PXE DB to so that `fetch_and_process_public_partial_note_completion_logs` can later search for -/// public logs that will complete them. +/// are stored in the PXE DB so that `fetch_and_process_public_partial_note_completion_logs` can later search for public +/// logs that will complete them. pub unconstrained fn fetch_and_process_private_tagged_logs( _contract_address: AztecAddress, _compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, @@ -124,7 +124,7 @@ unconstrained fn process_private_note_log( compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, storage_slot: Field, note_type_id: Field, - serialized_note_content: BoundedVec, + packed_note_content: BoundedVec, ) { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, @@ -133,7 +133,7 @@ unconstrained fn process_private_note_log( contract_address, storage_slot, note_type_id, - serialized_note_content, + packed_note_content, ); debug_log_format( @@ -151,7 +151,7 @@ unconstrained fn process_private_note_log( contract_address, storage_slot, discovered_note.nonce, - serialized_note_content, + packed_note_content, discovered_note.note_hash, discovered_note.inner_nullifier, tx_hash, diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 25007bd0d1fd..2dc6c4d2bfa4 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -727,7 +727,7 @@ export class SimulatorOracle implements DBOracle { const log = logsForTag[0]; - // getLogsByTag doesn't have all of the information that we need (notable note hashes and the first nullifier), so + // getLogsByTag doesn't have all of the information that we need (notably note hashes and the first nullifier), so // we need to make a second call to the node for `getTxEffect`. // TODO(#9789): bundle this information in the `getLogsByTag` call. const txEffect = await this.aztecNode.getTxEffect(log.txHash); From 84ed736099faa2aea8aaee56dd22520c50211c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 5 Feb 2025 16:52:53 +0000 Subject: [PATCH 32/39] Address review comments --- .../src/note/discovery/nonce_discovery.nr | 12 ++- noir-projects/aztec-nr/uint-note/src/lib.nr | 1 - .../uint-note/src/partial_uint_note.nr | 92 ------------------- 3 files changed, 7 insertions(+), 98 deletions(-) delete mode 100644 noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr index 2ba318768604..531fda3eb878 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr @@ -58,16 +58,18 @@ pub unconstrained fn attempt_note_nonce_discovery( storage_slot, note_type_id, ) - .unwrap(); + .expect(f"Failed to compute a note hash for note type {note_type_id}"); let siloed_note_hash = compute_siloed_note_hash(contract_address, hashes.note_hash); let unique_note_hash = compute_unique_note_hash(candidate_nonce, siloed_note_hash); if unique_note_hash == expected_unique_note_hash { - // Note that we perform no validations on the nullifier - we fundamentally cannot, since only the - // application knows how to compute nullifiers, and we simply trust it to have provided the correct one. - // If it hasn't, then PXE may fail to realize that a given note has been nullified already and end up - // producing invalid transactions (with duplicate nullifiers). + // Note that while we did check that the note hash is the preimage of the expected unique note hash, we + // perform no validations on the nullifier - we fundamentally cannot, since only the application knows + // how to compute nullifiers. We simply trust it to have provided the correct one: if it hasn't, then + // PXE may fail to realize that a given note has been nullified already, and calls to the application + // could result in invalid transactions (with duplicate nullifiers). This is not a concern because an + // application already has more direct means of making a call to it fail the transaction. discovered_notes.push( DiscoveredNoteInfo { nonce: candidate_nonce, diff --git a/noir-projects/aztec-nr/uint-note/src/lib.nr b/noir-projects/aztec-nr/uint-note/src/lib.nr index dd64fd22470b..2225eccc2a25 100644 --- a/noir-projects/aztec-nr/uint-note/src/lib.nr +++ b/noir-projects/aztec-nr/uint-note/src/lib.nr @@ -1,2 +1 @@ mod uint_note; -mod partial_uint_note; diff --git a/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr b/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr deleted file mode 100644 index 6d3669220039..000000000000 --- a/noir-projects/aztec-nr/uint-note/src/partial_uint_note.nr +++ /dev/null @@ -1,92 +0,0 @@ -use dep::aztec::{ - generators::{G_slot, Ga2, Ga3}, - note::note_interface::NoteInterface, - oracle::random::random, - prelude::NoteHeader, - protocol_types::{address::AztecAddress, point::Point, traits::{Empty, Packable, ToField}}, -}; -use std::static_assert; - -use crate::uint_note::UintNote; - -pub struct PartialUintNote { - pub owner: AztecAddress, - pub randomness: Field, - pub hiding_point: PartialUintNoteHidingPoint, - pub header: NoteHeader, -} - -impl PartialUintNote { - pub fn new(owner: AztecAddress, storage_slot: Field) -> Self { - /// Safety: We use the randomness to preserve the privacy of the note recipient by preventing brute-forcing, - /// so a malicious sender could use non-random values to make the note less private. But they already know - /// the full note pre-image anyway, and so the recipient already trusts them to not disclose this - /// information. We can therefore assume that the sender will cooperate in the random value generation. - let randomness = unsafe { random() }; - let mut header = NoteHeader::empty(); - header.storage_slot = storage_slot; - - let hiding_point = PartialUintNoteHidingPoint::new( - std::embedded_curve_ops::multi_scalar_mul( - [Ga2, Ga3, G_slot], - [ - std::hash::from_field_unsafe(owner.pack()[0]), - std::hash::from_field_unsafe(randomness.pack()[0]), - std::hash::from_field_unsafe(storage_slot), - ], - ), - ); - - Self { owner, randomness, hiding_point, header } - } -} - -global PARTIAL_UINT_NOTE_SER_LENGTH: u32 = 3; - -impl NoteInterface for PartialUintNote { - fn pack_content(self) -> [Field; PARTIAL_UINT_NOTE_SER_LENGTH] { - [self.hiding_point.value.x, self.owner.to_field(), self.randomness] - } - - fn unpack_content(_fields: [Field; PARTIAL_UINT_NOTE_SER_LENGTH]) -> Self { - static_assert(false, "unsupported"); - panic(f"unsupported") - } - - fn get_header(self) -> NoteHeader { - self.header - } - - fn set_header(&mut self, header: NoteHeader) { - self.header = header; - } - - fn get_note_type_id() -> Field { - UintNote::get_note_type_id() + 128 - } - - fn compute_note_hash(self) -> Field { - static_assert(false, "unsupported"); - panic(f"unsupported") - } -} - -pub struct PartialUintNoteHidingPoint { - pub value: Point, -} - -impl PartialUintNoteHidingPoint { - fn new(value: Point) -> Self { - Self { value } - } - - fn complete(self, amount: U128) -> Field { - let note_point = self.value - + std::embedded_curve_ops::multi_scalar_mul( - [G_slot], - [std::hash::from_field_unsafe(amount.pack()[0])], - ); - - note_point.x - } -} From 713922318808c943215527ca1dd0c40b17f731de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 5 Feb 2025 17:05:15 +0000 Subject: [PATCH 33/39] Rename to aztec::discovery --- .../aztec/src/{note => }/discovery/mod.nr | 0 .../src/{note => }/discovery/nonce_discovery.nr | 2 +- .../src/{note => }/discovery/partial_notes.nr | 2 +- .../src/{note => }/discovery/private_logs.nr | 2 +- .../default_aes128/note.nr | 2 +- noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../aztec-nr/aztec/src/macros/functions/mod.nr | 6 +++--- noir-projects/aztec-nr/aztec/src/macros/mod.nr | 16 ++++++++-------- noir-projects/aztec-nr/aztec/src/note/mod.nr | 1 - .../aztec-nr/aztec/src/oracle/note_discovery.nr | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) rename noir-projects/aztec-nr/aztec/src/{note => }/discovery/mod.nr (100%) rename noir-projects/aztec-nr/aztec/src/{note => }/discovery/nonce_discovery.nr (98%) rename noir-projects/aztec-nr/aztec/src/{note => }/discovery/partial_notes.nr (99%) rename noir-projects/aztec-nr/aztec/src/{note => }/discovery/private_logs.nr (99%) diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr similarity index 100% rename from noir-projects/aztec-nr/aztec/src/note/discovery/mod.nr rename to noir-projects/aztec-nr/aztec/src/discovery/mod.nr diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr similarity index 98% rename from noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr rename to noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr index 531fda3eb878..a96985796bc9 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr @@ -1,4 +1,4 @@ -use crate::{note::discovery::{MAX_NOTE_PACKED_LEN, NoteHashAndNullifier}, utils::array}; +use crate::{discovery::{MAX_NOTE_PACKED_LEN, NoteHashAndNullifier}, utils::array}; use dep::protocol_types::{ address::AztecAddress, diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr similarity index 99% rename from noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr rename to noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr index 17e1601107bb..f8263be8d1f3 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr @@ -1,5 +1,5 @@ use crate::{ - note::discovery::{ + discovery::{ ComputeNoteHashAndNullifier, nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, private_logs::MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN, diff --git a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr similarity index 99% rename from noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr rename to noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr index c6aeb03fe19b..4c021283899e 100644 --- a/noir-projects/aztec-nr/aztec/src/note/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -8,7 +8,7 @@ use dep::protocol_types::{ debug_log::{debug_log, debug_log_format}, }; -use crate::note::discovery::{ +use crate::discovery::{ ComputeNoteHashAndNullifier, MAX_NOTE_PACKED_LEN, nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr index a3812f1a56f1..e80f82f07f71 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr @@ -247,7 +247,7 @@ where plaintext_bytes } -pub fn compute_log( +fn compute_log( context: PrivateContext, note: Note, recipient: AztecAddress, diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 95584ea91088..20eeb7bb07d8 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -1,5 +1,6 @@ mod context; mod deploy; +mod discovery; mod generators; mod hash; mod history; diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index fe1deafdc564..1ef481fc5a86 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -84,21 +84,21 @@ comptime fn create_init_check(f: FunctionDefinition) -> Quoted { .quoted_contents() } -/// Injects a call to `aztec::note::discovery::discover_new_notes`, causing for new notes to be added to PXE and made +/// Injects a call to `aztec::discovery::discover_new_notes`, causing for new notes to be added to PXE and made /// available for the current execution. comptime fn create_note_discovery_call() -> Quoted { quote { /// Safety: note discovery returns nothing and is performed solely for its side-effects. It is therefore always /// safe to call. unsafe { - dep::aztec::note::discovery::discover_new_notes( + dep::aztec::discovery::discover_new_notes( context.this_address(), |packed_note_content: BoundedVec, contract_address: aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field| { // _compute_note_hash_and_optionally_a_nullifier is a contract library method injected by `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, packed_note_content); Option::some( - aztec::note::discovery::NoteHashAndNullifier { + aztec::discovery::NoteHashAndNullifier { note_hash: hashes[0], inner_nullifier: hashes[3], }, diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 03664105a564..b6a3d111b65b 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -115,7 +115,7 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } /// Generates a contract library method called `_compute_note_hash_and_optionally_a_nullifier` which is used for note -/// discovery (to create the `aztec::note::discovery::ComputeNoteHashAndNullifier` function) and to implement the +/// discovery (to create the `aztec::discovery::ComputeNoteHashAndNullifier` function) and to implement the /// `compute_note_hash_and_nullifier` unconstrained contract function. comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier() -> Quoted { let notes = NOTES.entries(); @@ -178,7 +178,7 @@ comptime fn generate_contract_library_method_compute_note_hash_and_optionally_a_ storage_slot: Field, note_type_id: Field, compute_nullifier: bool, - packed_note_content: BoundedVec, + packed_note_content: BoundedVec, ) -> pub [Field; 4] { $body } @@ -192,7 +192,7 @@ comptime fn generate_compute_note_hash_and_optionally_a_nullifier() -> Quoted { // library method `_compute_note_hash...` is affected by this. // TODO(#11638): In the future we might remove these things as we rely less and less on this function, and then // change the `_compute_note_hash...` contract library method to be of type - // `aztec::note::discovery::ComputeNoteHashAndNullifier`, simplifying other macros by removing the need to create + // `aztec::discovery::ComputeNoteHashAndNullifier`, simplifying other macros by removing the need to create // intermediate lambdas that adapt their interfaces. let max_note_packed_length = NOTES.entries().fold( @@ -219,10 +219,10 @@ 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 process private logs // and perform note discovery of either private notes or partial notes. - // The bulk of the work of this function is done by aztec::note::discovery::do_process_log, so all we need to do - // is call that function. We use the contract library method injected by + // The bulk of the work of this function is done by aztec::discovery::do_process_log, so all we need to do is call + // that function. We use the contract library method injected by // `generate_contract_library_method_compute_note_hash_and_optionally_a_nullifier` in order to create the required - // `aztec::note::discovery::ComputeNoteHashAndNullifier` function. + // `aztec::discovery::ComputeNoteHashAndNullifier` function. // We'll produce the entire body of the function in one go and then insert it into the function. let notes = NOTES.entries(); @@ -240,7 +240,7 @@ comptime fn generate_process_log() -> Quoted { // TODO(#10727): allow other contracts to process logs and deliver notes let contract_address = context.this_address(); - aztec::note::discovery::private_logs::do_process_log( + aztec::discovery::private_logs::do_process_log( contract_address, log_plaintext, tx_hash, @@ -251,7 +251,7 @@ comptime fn generate_process_log() -> Quoted { let hashes = _compute_note_hash_and_optionally_a_nullifier(contract_address, nonce, storage_slot, note_type_id, true, packed_note_content); Option::some( - aztec::note::discovery::NoteHashAndNullifier { + aztec::discovery::NoteHashAndNullifier { note_hash: hashes[0], inner_nullifier: hashes[3], }, diff --git a/noir-projects/aztec-nr/aztec/src/note/mod.nr b/noir-projects/aztec-nr/aztec/src/note/mod.nr index 593a00b03adc..6ada1a1fabfd 100644 --- a/noir-projects/aztec-nr/aztec/src/note/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/mod.nr @@ -1,5 +1,4 @@ pub mod constants; -pub mod discovery; pub mod lifecycle; pub mod note_getter; pub mod note_getter_options; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr index 7e0d74df7151..3c3dfbe432bf 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/note_discovery.nr @@ -1,4 +1,4 @@ -use crate::note::discovery::MAX_NOTE_PACKED_LEN; +use crate::discovery::MAX_NOTE_PACKED_LEN; use dep::protocol_types::{address::AztecAddress, log_with_tx_data::LogWithTxData}; /// Finds new notes that may have been sent to all registered accounts in PXE in the current contract and makes them available From 0172f397642efe67286bfac77d231509342d1c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 5 Feb 2025 17:15:17 +0000 Subject: [PATCH 34/39] Remove archiver and token changes --- .../aztec-nr/uint-note/src/uint_note.nr | 4 +- .../contracts/token_contract/src/main.nr | 42 +++++++++---------- .../archiver/kv_archiver_store/log_store.ts | 25 ++++++++++- .../memory_archiver_store.ts | 25 ++++++++++- .../end-to-end/src/e2e_partial_notes.test.ts | 41 ------------------ 5 files changed, 71 insertions(+), 66 deletions(-) delete mode 100644 yarn-project/end-to-end/src/e2e_partial_notes.test.ts diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 8d53a1a229f9..f8ada18c0adf 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -16,11 +16,11 @@ use dep::aztec::{ #[partial_note(quote {value})] #[derive(Serialize)] pub struct UintNote { + // The amount of tokens in the note + value: U128, owner: AztecAddress, // Randomness of the note to hide its contents randomness: Field, - // The amount of tokens in the note - value: U128, } // docs:end:UintNote 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 530031cfcbe2..e32922a1b88a 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -22,7 +22,7 @@ contract Token { context::{PrivateCallInterface, PrivateContext}, encrypted_logs::log_assembly_strategies::default_aes128::{ event::encode_and_encrypt_event_unconstrained, - note::{compute_log, encode_and_encrypt_note, encode_and_encrypt_note_unconstrained}, + note::{encode_and_encrypt_note, encode_and_encrypt_note_unconstrained}, }, macros::{ events::event, @@ -36,10 +36,7 @@ contract Token { protocol_types::{point::Point, traits::Serialize}, }; - use dep::uint_note::{ - partial_uint_note::{PartialUintNote, PartialUintNoteHidingPoint}, - uint_note::UintNote, - }; + use dep::uint_note::uint_note::UintNote; // docs:start:import_authwit use dep::authwit::auth::{ @@ -455,14 +452,11 @@ contract Token { // so a malicious sender could use non-random values to make the note less private. But they already know // the full note pre-image anyway, and so the recipient already trusts them to not disclose this // information. We can therefore assume that the sender will cooperate in the random value generation. + let note_randomness = unsafe { random() }; + let note_setup_payload = UintNote::setup_payload().new(to, note_randomness, to_note_slot); - let partial = PartialUintNote::new(to, to_note_slot); - let encrypted_log = compute_log(*context, partial, to, from); - context.emit_private_log(encrypted_log); - - Token::at(context.this_address()) - ._store_private_balance_increase_hiding_point(partial.hiding_point.value) - .enqueue(context); + // We get the keys and encrypt the log of the note + let setup_log = note_setup_payload.encrypt_log(context, to, from); // Using the x-coordinate as a hiding point slot is safe against someone else interfering with it because // we have a guarantee that the public functions of the transaction are executed right after the private ones @@ -476,11 +470,20 @@ contract Token { // in our state variables we derive slots using a different hash function from multi scalar multiplication // (MSM). + let hiding_point_slot = note_setup_payload.hiding_point.x; + // We don't need to perform a check that the value overwritten by `_store_point_in_transient_storage_unsafe` // is zero because the slot is the x-coordinate of the hiding point and hence we could only overwrite // the value in the slot with the same value. This makes usage of the `unsafe` method safe. + Token::at(context.this_address()) + ._store_payload_in_transient_storage_unsafe( + hiding_point_slot, + note_setup_payload.hiding_point, + setup_log, + ) + .enqueue(context); - partial.hiding_point.value.x + hiding_point_slot } // docs:start:finalize_transfer_to_private @@ -593,15 +596,12 @@ contract Token { let supply = storage.total_supply.read().add(amount); storage.total_supply.write(supply); - let hiding_point = PartialUintNoteHidingPoint::new(context.storage_read(hiding_point_slot)); - assert( - !aztec::protocol_types::traits::is_empty(hiding_point.value), - "transfer not prepared", - ); - let note_hash = hiding_point.complete(amount); + // Then we finalize the partial note with the `amount` + let finalization_payload = + UintNote::finalization_payload().new(context, hiding_point_slot, amount); - context.push_note_hash(note_hash); - context.emit_public_log([hiding_point.value.x, amount.pack()[0]]); + // At last we emit the note hash and the final log + finalization_payload.emit(); } // docs:start:setup_refund diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts index 38550b0f06a2..9ccaa60d8e25 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts @@ -53,7 +53,30 @@ export class LogStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.privateLogs.forEach(log => { - const tag = log.fields[0]; + // Check that each log stores 2 lengths in its first field. If not, it's not a tagged log: + const firstFieldBuf = log.log[0].toBuffer(); + // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). + // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. + if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { + // See parseLogFromPublic - the first field of a tagged log is 5 bytes structured: + // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] + this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); + return; + } + // Check that the length values line up with the log contents + const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); + const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); + // Add 1 for the first field holding lengths + const totalLogLength = 1 + publicValuesLength + privateValuesLength; + // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length + if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { + this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); + return; + } + + // The first elt stores lengths as above => tag is in fields[1] + const tag = log.log[1]; + const currentLogs = taggedLogs.get(tag.toString()) ?? []; currentLogs.push( new TxScopedL2Log( diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 84562d1c50d9..8d43d858fe51 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -240,7 +240,30 @@ export class MemoryArchiverStore implements ArchiverDataStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.privateLogs.forEach(log => { - const tag = log.fields[0]; + // Check that each log stores 3 lengths in its first field. If not, it's not a tagged log: + // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). + // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. + const firstFieldBuf = log.log[0].toBuffer(); + if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { + // See parseLogFromPublic - the first field of a tagged log is 8 bytes structured: + // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] + this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); + return; + } + // Check that the length values line up with the log contents + const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); + const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); + // Add 1 for the first field holding lengths + const totalLogLength = 1 + publicValuesLength + privateValuesLength; + // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length + if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { + this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); + return; + } + + // The first elt stores lengths => tag is in fields[1] + const tag = log.log[1]; + const currentLogs = this.taggedLogs.get(tag.toString()) || []; this.taggedLogs.set(tag.toString(), [ ...currentLogs, diff --git a/yarn-project/end-to-end/src/e2e_partial_notes.test.ts b/yarn-project/end-to-end/src/e2e_partial_notes.test.ts deleted file mode 100644 index ec94b7982c34..000000000000 --- a/yarn-project/end-to-end/src/e2e_partial_notes.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { type AccountWallet, type Logger } from '@aztec/aztec.js'; -import { type TokenContract } from '@aztec/noir-contracts.js/Token'; - -import { jest } from '@jest/globals'; - -import { deployToken, mintTokensToPrivate } from './fixtures/token_utils.js'; -import { setup } from './fixtures/utils.js'; - -const TIMEOUT = 120_000; - -describe('partial notes', () => { - jest.setTimeout(TIMEOUT); - - let teardown: () => Promise; - - let logger: Logger; - - let adminWallet: AccountWallet; - let liquidityProvider: AccountWallet; - - let token0: TokenContract; - - const INITIAL_TOKEN_BALANCE = 1_000_000_000n; - - beforeAll(async () => { - ({ - teardown, - wallets: [adminWallet, liquidityProvider], - logger, - } = await setup(2)); - - token0 = await deployToken(adminWallet, 0n, logger); - }); - - afterAll(() => teardown()); - - it('mint to private', async () => { - await mintTokensToPrivate(token0, adminWallet, liquidityProvider.getAddress(), INITIAL_TOKEN_BALANCE); - console.log(await token0.methods.balance_of_private(liquidityProvider.getAddress()).simulate()); - }); -}); From 7089282ca2985b85cbf577bee7ce305e4091a058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 5 Feb 2025 17:20:09 +0000 Subject: [PATCH 35/39] Fix archiver --- .../archiver/kv_archiver_store/log_store.ts | 50 +++++++++---------- .../memory_archiver_store.ts | 40 +++++++-------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts index 9ccaa60d8e25..e1c5d0622e16 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts @@ -53,30 +53,7 @@ export class LogStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.privateLogs.forEach(log => { - // Check that each log stores 2 lengths in its first field. If not, it's not a tagged log: - const firstFieldBuf = log.log[0].toBuffer(); - // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). - // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. - if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { - // See parseLogFromPublic - the first field of a tagged log is 5 bytes structured: - // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] - this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); - return; - } - // Check that the length values line up with the log contents - const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); - const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); - // Add 1 for the first field holding lengths - const totalLogLength = 1 + publicValuesLength + privateValuesLength; - // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length - if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { - this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); - return; - } - - // The first elt stores lengths as above => tag is in fields[1] - const tag = log.log[1]; - + const tag = log.fields[0]; const currentLogs = taggedLogs.get(tag.toString()) ?? []; currentLogs.push( new TxScopedL2Log( @@ -102,7 +79,30 @@ export class LogStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.publicLogs.forEach(log => { - const tag = log.log[0]; + // Check that each log stores 2 lengths in its first field. If not, it's not a tagged log: + const firstFieldBuf = log.log[0].toBuffer(); + // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). + // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. + if (!firstFieldBuf.subarray(0, 27).equals(Buffer.alloc(27)) || firstFieldBuf[29] !== 0) { + // See parseLogFromPublic - the first field of a tagged log is 5 bytes structured: + // [ publicLen[0], publicLen[1], 0, privateLen[0], privateLen[1]] + this.#log.warn(`Skipping public log with invalid first field: ${log.log[0]}`); + return; + } + // Check that the length values line up with the log contents + const publicValuesLength = firstFieldBuf.subarray(-5).readUint16BE(); + const privateValuesLength = firstFieldBuf.subarray(-5).readUint16BE(3); + // Add 1 for the first field holding lengths + const totalLogLength = 1 + publicValuesLength + privateValuesLength; + // Note that zeroes can be valid log values, so we can only assert that we do not go over the given length + if (totalLogLength > PUBLIC_LOG_DATA_SIZE_IN_FIELDS || log.log.slice(totalLogLength).find(f => !f.isZero())) { + this.#log.warn(`Skipping invalid tagged public log with first field: ${log.log[0]}`); + return; + } + + // The first elt stores lengths as above => tag is in fields[1] + const tag = log.log[1]; + this.#log.debug(`Found tagged public log with tag ${tag.toString()} in block ${block.number}`); const currentLogs = taggedLogs.get(tag.toString()) ?? []; diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 8d43d858fe51..ff53ef6e2444 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -240,6 +240,26 @@ export class MemoryArchiverStore implements ArchiverDataStore { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.privateLogs.forEach(log => { + const tag = log.fields[0]; + const currentLogs = this.taggedLogs.get(tag.toString()) || []; + this.taggedLogs.set(tag.toString(), [ + ...currentLogs, + new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, /* isFromPublic */ false, log.toBuffer()), + ]); + const currentTagsInBlock = this.logTagsPerBlock.get(block.number) || []; + this.logTagsPerBlock.set(block.number, [...currentTagsInBlock, tag]); + }); + }); + } + + #storeTaggedLogsFromPublic(block: L2Block): void { + const dataStartIndexForBlock = + block.header.state.partial.noteHashTree.nextAvailableLeafIndex - + block.body.txEffects.length * MAX_NOTE_HASHES_PER_TX; + block.body.txEffects.forEach((txEffect, txIndex) => { + const txHash = txEffect.txHash; + const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; + txEffect.publicLogs.forEach(log => { // Check that each log stores 3 lengths in its first field. If not, it's not a tagged log: // See macros/note/mod/ and see how finalization_log[0] is constructed, to understand this monstrosity. (It wasn't me). // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here. @@ -264,26 +284,6 @@ export class MemoryArchiverStore implements ArchiverDataStore { // The first elt stores lengths => tag is in fields[1] const tag = log.log[1]; - const currentLogs = this.taggedLogs.get(tag.toString()) || []; - this.taggedLogs.set(tag.toString(), [ - ...currentLogs, - new TxScopedL2Log(txHash, dataStartIndexForTx, block.number, /* isFromPublic */ false, log.toBuffer()), - ]); - const currentTagsInBlock = this.logTagsPerBlock.get(block.number) || []; - this.logTagsPerBlock.set(block.number, [...currentTagsInBlock, tag]); - }); - }); - } - - #storeTaggedLogsFromPublic(block: L2Block): void { - const dataStartIndexForBlock = - block.header.state.partial.noteHashTree.nextAvailableLeafIndex - - block.body.txEffects.length * MAX_NOTE_HASHES_PER_TX; - block.body.txEffects.forEach((txEffect, txIndex) => { - const txHash = txEffect.txHash; - const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; - txEffect.publicLogs.forEach(log => { - const tag = log.log[0]; this.#log.verbose(`Storing public tagged log with tag ${tag.toString()} in block ${block.number}`); const currentLogs = this.taggedLogs.get(tag.toString()) || []; From 774e9abc221e422bed67469d55a56c7999a6577a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 5 Feb 2025 17:30:37 +0000 Subject: [PATCH 36/39] remove the final differences --- .../noir-contracts/contracts/token_contract/src/main.nr | 9 ++++----- .../archiver/src/archiver/kv_archiver_store/log_store.ts | 1 - .../memory_archiver_store/memory_archiver_store.ts | 2 -- 3 files changed, 4 insertions(+), 8 deletions(-) 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 f2a2d7cc7405..2280e439c671 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -448,10 +448,10 @@ pub contract Token { // TODO(#7775): Manually fetching the randomness here is not great. If we decide to include randomness in all // notes we could just inject it in macros. - // Safety: We use the randomness to preserve the privacy of the note recipient by preventing brute-forcing, - // so a malicious sender could use non-random values to make the note less private. But they already know - // the full note pre-image anyway, and so the recipient already trusts them to not disclose this - // information. We can therefore assume that the sender will cooperate in the random value generation. + /// Safety: We use the randomness to preserve the privacy of the note recipient by preventing brute-forcing, + /// so a malicious sender could use non-random values to make the note less private. But they already know + /// the full note pre-image anyway, and so the recipient already trusts them to not disclose this + /// information. We can therefore assume that the sender will cooperate in the random value generation. let note_randomness = unsafe { random() }; let note_setup_payload = UintNote::setup_payload().new(to, note_randomness, to_note_slot); @@ -469,7 +469,6 @@ pub contract Token { // We can also be sure that the `hiding_point_slot` will not overwrite any other value in the storage because // in our state variables we derive slots using a different hash function from multi scalar multiplication // (MSM). - let hiding_point_slot = note_setup_payload.hiding_point.x; // We don't need to perform a check that the value overwritten by `_store_point_in_transient_storage_unsafe` diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts index 0415e66e27d1..2c9d6d8a04a7 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/log_store.ts @@ -104,7 +104,6 @@ export class LogStore { const tag = log.log[1]; this.#log.debug(`Found tagged public log with tag ${tag.toString()} in block ${block.number}`); - const currentLogs = taggedLogs.get(tag.toString()) ?? []; currentLogs.push( new TxScopedL2Log( diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 84ee2215838a..2ddcbc77a004 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -283,9 +283,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { // The first elt stores lengths => tag is in fields[1] const tag = log.log[1]; - this.#log.verbose(`Storing public tagged log with tag ${tag.toString()} in block ${block.number}`); - const currentLogs = this.taggedLogs.get(tag.toString()) || []; this.taggedLogs.set(tag.toString(), [ ...currentLogs, From 26f77a52612da7b63de7bf8e2c5fa02fe1b0c503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 6 Feb 2025 12:28:31 +0000 Subject: [PATCH 37/39] Fix log type destructuring --- noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr index 4c021283899e..31f2b32df231 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -108,7 +108,7 @@ unconstrained fn destructure_log_plaintext( // enums and tagged unions to achieve this goal. let combined_type_id = log_plaintext.get(1); let note_type_id = ((combined_type_id as u64) % 128) as Field; - let log_type_id = combined_type_id / 128; + let log_type_id = ((combined_type_id as u64) / 128) as Field; let log_payload = array::subbvec(log_plaintext, NOTE_PRIVATE_LOG_RESERVED_FIELDS); From 3e21afff1faa742c2322fcda16b85283b716a874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 6 Feb 2025 14:50:10 +0000 Subject: [PATCH 38/39] Fix TXE tests by forcing syncs when appropriate --- .../contracts/counter_contract/src/main.nr | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr index acc1d24a8804..69ceb16ff6c8 100644 --- a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr @@ -140,9 +140,11 @@ pub contract Counter { unconstrained fn inclusion_proofs() { let initial_value = 5; let (env, contract_address, owner) = test::setup(initial_value); + env.impersonate(contract_address); + env.advance_block_by(1); + sync_notes(); - env.impersonate(contract_address); let counter_slot = Counter::storage_layout().counters.slot; let owner_slot = derive_storage_slot_in_map(counter_slot, owner); let mut options = NoteViewerOptions::new(); @@ -159,6 +161,7 @@ pub contract Counter { destroy_note(&mut env.private(), old_note); env.advance_block_by(1); + sync_notes(); env.private().get_block_header().prove_note_is_nullified(old_note, &mut env.private()); env.private().get_block_header().prove_note_inclusion(old_note); @@ -187,6 +190,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); env.impersonate(contract_address); let counter_slot = Counter::storage_layout().counters.slot; let owner_slot = derive_storage_slot_in_map(counter_slot, owner); @@ -207,6 +211,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); let notes: BoundedVec = view_notes(owner_slot, options); assert(get_counter(owner) == 7); assert(notes.len() == 3); @@ -220,6 +225,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); let notes: BoundedVec = view_notes(owner_slot, options); assert(notes.len() == 4); assert(get_counter(owner) == 7); @@ -232,6 +238,7 @@ pub contract Counter { // Checking that the note was discovered from private logs env.advance_block_by(1); + sync_notes(); let notes: BoundedVec = view_notes(owner_slot, options); assert(get_counter(owner) == 6); assert(notes.len() == 4); From 14ebf3d7c8dd0062255fc5d744633591ec32e4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 6 Feb 2025 17:39:47 +0000 Subject: [PATCH 39/39] Fix simulator tests --- yarn-project/simulator/src/client/private_execution.test.ts | 4 ++++ .../simulator/src/client/unconstrained_execution.test.ts | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index c8797415dd87..36af33005e6d 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -7,6 +7,7 @@ import { Note, PublicExecutionRequest, TxExecutionRequest, + type TxScopedL2Log, } from '@aztec/circuit-types'; import { AppendOnlyTreeSnapshot, @@ -247,6 +248,9 @@ describe('Private Execution test suite', () => { }, ); + oracle.syncTaggedLogs.mockImplementation((_, __, ___) => Promise.resolve(new Map())); + oracle.dbLoad.mockImplementation((_, __) => Promise.resolve(null)); + node = mock(); node.getPublicStorageAt.mockImplementation( (_address: AztecAddress, _storageSlot: Fr, _blockNumber: L2BlockNumber) => { diff --git a/yarn-project/simulator/src/client/unconstrained_execution.test.ts b/yarn-project/simulator/src/client/unconstrained_execution.test.ts index 11163cef2758..e1779622389f 100644 --- a/yarn-project/simulator/src/client/unconstrained_execution.test.ts +++ b/yarn-project/simulator/src/client/unconstrained_execution.test.ts @@ -1,4 +1,4 @@ -import { type AztecNode, type FunctionCall, Note } from '@aztec/circuit-types'; +import { type AztecNode, type FunctionCall, Note, type TxScopedL2Log } from '@aztec/circuit-types'; import { BlockHeader, CompleteAddress } from '@aztec/circuits.js'; import { FunctionSelector, FunctionType, encodeArguments } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; @@ -72,6 +72,9 @@ describe('Unconstrained Execution test suite', () => { })), ); + oracle.syncTaggedLogs.mockImplementation((_, __, ___) => Promise.resolve(new Map())); + oracle.dbLoad.mockImplementation((_, __) => Promise.resolve(null)); + const execRequest: FunctionCall = { name: artifact.name, to: contractAddress,