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 04e34bdec90c..76ada5a526fc 100644 --- a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr @@ -1,11 +1,12 @@ // docs:start:setup use dep::aztec::macros::aztec; +use test::utils; #[aztec] contract Counter { // docs:end:setup // docs:start:imports - use aztec::macros::{functions::{initializer, private}, storage::storage}; + use aztec::macros::{functions::{initializer, private, public}, storage::storage}; use aztec::prelude::{AztecAddress, Map}; use easy_private_state::EasyPrivateUint; use value_note::{balance_utils, value_note::ValueNote}; @@ -41,6 +42,45 @@ contract Counter { counters.at(owner).add(1, owner, outgoing_viewer, outgoing_viewer); } // docs:end:increment + + #[private] + fn increment_twice(owner: AztecAddress, outgoing_viewer: AztecAddress) { + unsafe { + dep::aztec::oracle::debug_log::debug_log_format( + "Incrementing counter twice for owner {0}", + [owner.to_field()], + ); + } + let counters = storage.counters; + counters.at(owner).add(1, owner, outgoing_viewer, outgoing_viewer); + counters.at(owner).add(1, owner, outgoing_viewer, outgoing_viewer); + } + + #[private] + fn increment_and_decrement(owner: AztecAddress, outgoing_viewer: AztecAddress) { + unsafe { + dep::aztec::oracle::debug_log::debug_log_format( + "Incrementing and decrementing counter for owner {0}", + [owner.to_field()], + ); + } + let counters = storage.counters; + counters.at(owner).add(1, owner, outgoing_viewer, outgoing_viewer); + counters.at(owner).sub(1, owner, outgoing_viewer, outgoing_viewer); + } + + #[private] + fn decrement(owner: AztecAddress, outgoing_viewer: AztecAddress) { + unsafe { + dep::aztec::oracle::debug_log::debug_log_format( + "Decrementing counter for owner {0}", + [owner.to_field()], + ); + } + let counters = storage.counters; + counters.at(owner).sub(1, owner, outgoing_viewer, outgoing_viewer); + } + // docs:start:get_counter unconstrained fn get_counter(owner: AztecAddress) -> pub Field { let counters = storage.counters; @@ -49,10 +89,14 @@ contract Counter { // docs:end:get_counter // docs:start:test_imports + use dep::aztec::note::lifecycle::destroy_note; use dep::aztec::note::note_getter::{MAX_NOTES_PER_PAGE, view_notes}; use dep::aztec::note::note_viewer_options::NoteViewerOptions; + + use dep::aztec::protocol_types::debug_log::debug_log_format; use dep::aztec::protocol_types::storage::map::derive_storage_slot_in_map; use dep::aztec::test::helpers::{cheatcodes, test_environment::TestEnvironment}; + // docs:end:test_imports // docs:start:txe_test_increment #[test] @@ -92,4 +136,107 @@ contract Counter { ); } // docs:end:txe_test_increment + + #[test] + unconstrained fn inclusion_proofs() { + let (env, contract_address, owner, outgoing_viewer) = utils::setup(5); + env.advance_block_by(1); + + 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(); + let notes: BoundedVec = view_notes(owner_slot, options); + let initial_note_value = notes.get(0).value; + assert( + initial_note_value == initial_value, + f"Expected {initial_value} but got {initial_note_value}", + ); + + let old_note = notes.get(0); + + env.private().get_header().prove_note_validity(old_note, &mut env.private()); + + destroy_note(&mut env.private(), old_note); + env.advance_block_by(1); + + env.private().get_header().prove_note_is_nullified(old_note, &mut env.private()); + env.private().get_header().prove_note_inclusion(old_note); + + let notes: BoundedVec = view_notes(owner_slot, options); + + assert(notes.len() == 0); + } + + #[test] + unconstrained fn extended_incrementing_and_decrementing() { + let (env, contract_address, owner, outgoing_viewer) = utils::setup(5); + + // Checking from the note cache + 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(); + let notes: BoundedVec = view_notes(owner_slot, options); + let initial_note_value = notes.get(0).value; + assert( + initial_note_value == initial_value, + f"Expected {initial_value} but got {initial_note_value}", + ); + + // Checking that the note was discovered from private logs + env.advance_block_by(1); + 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(); + let notes: BoundedVec = view_notes(owner_slot, options); + let initial_note_value = notes.get(0).value; + assert( + initial_note_value == initial_value, + f"Expected {initial_value} but got {initial_note_value}", + ); + + Counter::at(contract_address).increment_two(owner, outgoing_viewer).call(&mut env.private()); + + // Checking from the note cache + let notes: BoundedVec = view_notes(owner_slot, options); + assert(notes.len() == 3); + assert(get_counter(owner) == 7); + + // Checking that the note was discovered from private logs + env.advance_block_by(1); + let notes: BoundedVec = view_notes(owner_slot, options); + assert(get_counter(owner) == 7); + assert(notes.len() == 3); + + // Checking from the note cache + Counter::at(contract_address).increment_and_decrement(owner, outgoing_viewer).call( + &mut env.private(), + ); + let notes: BoundedVec = view_notes(owner_slot, options); + assert(get_counter(owner) == 7); + // We have a change note of 0 + assert(notes.len() == 4); + + // Checking that the note was discovered from private logs + env.advance_block_by(1); + let notes: BoundedVec = view_notes(owner_slot, options); + assert(notes.len() == 4); + assert(get_counter(owner) == 7); + + // Checking from the note cache, note that we MUST advance the block to get a correct and updated value. + Counter::at(contract_address).decrement(owner, outgoing_viewer).call( + &mut env.private(), + ); + let notes: BoundedVec = view_notes(owner_slot, options); + assert(get_counter(owner) == 11); + assert(notes.len() == 5); + + // We advance the block here to have the nullification of the prior note be applied. Should we try nullifiying notes in our DB on notifyNullifiedNote ? + env.advance_block_by(1); + let notes: BoundedVec = view_notes(owner_slot, options); + assert(get_counter(owner) == 6); + assert(notes.len() == 4); + } } diff --git a/noir-projects/noir-contracts/contracts/counter_contract/src/test.nr b/noir-projects/noir-contracts/contracts/counter_contract/src/test.nr new file mode 100644 index 000000000000..a61f7fc03c32 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/counter_contract/src/test.nr @@ -0,0 +1,15 @@ +use dep::aztec::{prelude::AztecAddress, test::helpers::test_environment::TestEnvironment}; + +pub unconstrained fn setup(initial_value: Field) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress) { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + let owner = env.create_account(); + let outgoing_viewer = env.create_account(); + env.impersonate(owner); + // Deploy contract and initialize + let initializer = + Counter::interface().initialize(initial_value as u64, owner, outgoing_viewer); + let counter_contract = env.deploy_self("Counter").with_private_initializer(initializer); + let contract_address = counter_contract.to_address(); + (env, contract_address, owner, outgoing_viewer) +} diff --git a/yarn-project/pxe/src/index.ts b/yarn-project/pxe/src/index.ts index d6e46f5ad840..c962ab18f48d 100644 --- a/yarn-project/pxe/src/index.ts +++ b/yarn-project/pxe/src/index.ts @@ -12,3 +12,4 @@ export * from '@aztec/key-store'; export * from './database/index.js'; export { ContractDataOracle } from './contract_data_oracle/index.js'; export { PrivateFunctionsTree } from './contract_data_oracle/private_functions_tree.js'; +export { SimulatorOracle } from './simulator_oracle/index.js'; diff --git a/yarn-project/simulator/src/client/execution_note_cache.ts b/yarn-project/simulator/src/client/execution_note_cache.ts index f8e1ec0a670e..08c2c8d33dd4 100644 --- a/yarn-project/simulator/src/client/execution_note_cache.ts +++ b/yarn-project/simulator/src/client/execution_note_cache.ts @@ -1,6 +1,6 @@ import { computeNoteHashNonce, computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/circuits.js/hash'; -import { type AztecAddress } from '@aztec/foundation/aztec-address'; -import { type Fr } from '@aztec/foundation/fields'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr } from '@aztec/foundation/fields'; import { type NoteData } from '../acvm/index.js'; @@ -146,4 +146,14 @@ export class ExecutionNoteCache { notes.push(note); this.noteMap.set(note.note.contractAddress.toBigInt(), notes); } + + getAllNotes(): PendingNote[] { + return this.notes; + } + + getAllNullifiers(): Fr[] { + return [...this.nullifierMap.values()].flatMap(nullifierArray => + [...nullifierArray.values()].map(val => new Fr(val)), + ); + } } diff --git a/yarn-project/txe/src/node/txe_node.ts b/yarn-project/txe/src/node/txe_node.ts new file mode 100644 index 000000000000..f89231b6e558 --- /dev/null +++ b/yarn-project/txe/src/node/txe_node.ts @@ -0,0 +1,656 @@ +import { type ContractArtifact, createDebugLogger } from '@aztec/aztec.js'; +import { + type AztecNode, + type EpochProofQuote, + type GetUnencryptedLogsResponse, + type InBlock, + type L2Block, + type L2BlockNumber, + type L2Tips, + type LogFilter, + type MerkleTreeId, + type NullifierMembershipWitness, + type ProverConfig, + type PublicDataWitness, + type PublicSimulationOutput, + type SequencerConfig, + type SiblingPath, + type Tx, + type TxEffect, + TxHash, + type TxReceipt, + TxScopedL2Log, + type UnencryptedL2Log, +} from '@aztec/circuit-types'; +import { + type ARCHIVE_HEIGHT, + type AztecAddress, + type ContractClassPublic, + type ContractInstanceWithAddress, + type GasFees, + type Header, + type L1_TO_L2_MSG_TREE_HEIGHT, + type NOTE_HASH_TREE_HEIGHT, + type NULLIFIER_TREE_HEIGHT, + type PUBLIC_DATA_TREE_HEIGHT, + type PrivateLog, + type ProtocolContractAddresses, +} from '@aztec/circuits.js'; +import { type L1ContractAddresses } from '@aztec/ethereum'; +import { Fr } from '@aztec/foundation/fields'; + +export class TXENode implements AztecNode { + #logsByTags = new Map(); + #txEffectsByTxHash = new Map | undefined>(); + #blockNumberToNullifiers = new Map(); + #noteIndex = 0; + + #blockNumber: number; + #logger = createDebugLogger('aztec:txe_node'); + + constructor(blockNumber: number) { + this.#blockNumber = blockNumber; + } + + /** + * Fetches the current block number. + * @returns The block number. + */ + getBlockNumber(): Promise { + return Promise.resolve(this.#blockNumber); + } + + /** + * Sets the current block number of the node. + * @param - The block number to set. + */ + setBlockNumber(blockNumber: number) { + this.#blockNumber = blockNumber; + } + + /** + * Get a tx effect. + * @param txHash - The hash of a transaction which resulted in the returned tx effect. + * @returns The requested tx effect. + */ + getTxEffect(txHash: TxHash): Promise | undefined> { + const txEffect = this.#txEffectsByTxHash.get(new Fr(txHash.toBuffer()).toString()); + + return Promise.resolve(txEffect); + } + + /** + * Sets a tx effect 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(), + l2BlockNumber: blockNumber, + data: effect, + }); + } + + /** + * Returns the indexes of the given nullifiers in the nullifier tree, + * scoped to the block they were included in. + * @param blockNumber - The block number at which to get the data. + * @param nullifiers - The nullifiers to search for. + * @returns The block scoped indexes of the given nullifiers in the nullifier tree, or undefined if not found. + */ + async findNullifiersIndexesWithBlock( + blockNumber: L2BlockNumber, + nullifiers: Fr[], + ): Promise<(InBlock | undefined)[]> { + const parsedBlockNumber = blockNumber === 'latest' ? await this.getBlockNumber() : blockNumber; + + const nullifiersInBlock: Fr[] = []; + for (const [key, val] of this.#blockNumberToNullifiers.entries()) { + if (key < parsedBlockNumber) { + nullifiersInBlock.push(...val); + } + } + + return nullifiers.map(nullifier => { + const possibleNullifierIndex = nullifiersInBlock.findIndex(nullifierInBlock => + nullifierInBlock.equals(nullifier), + ); + return possibleNullifierIndex === -1 + ? undefined + : { + l2BlockNumber: parsedBlockNumber, + l2BlockHash: new Fr(parsedBlockNumber).toString(), + data: BigInt(possibleNullifierIndex), + }; + }); + } + + /** + * Returns the indexes of the given nullifiers in the nullifier tree, + * scoped to the block they were included in. + * @param blockNumber - The block number at which to get the data. + * @param nullifiers - The nullifiers to search for. + * @returns The block scoped indexes of the given nullifiers in the nullifier tree, or undefined if not found. + */ + setNullifiersIndexesWithBlock(blockNumber: number, nullifiers: Fr[]) { + this.#blockNumberToNullifiers.set(blockNumber, nullifiers); + } + + /** + * Adds note logs to the txe node, given a block + * @param blockNumber - The block number at which to add the note logs. + * @param privateLogs - The privateLogs that contain the note logs to be added. + */ + addNoteLogsByTags(blockNumber: number, privateLogs: PrivateLog[]) { + privateLogs.forEach(log => { + const tag = log.fields[0]; + const currentLogs = this.#logsByTags.get(tag.toString()) ?? []; + const scopedLog = new TxScopedL2Log( + new TxHash(new Fr(blockNumber).toBuffer()), + this.#noteIndex, + blockNumber, + false, + log.toBuffer(), + ); + currentLogs.push(scopedLog); + this.#logsByTags.set(tag.toString(), currentLogs); + }); + + // TODO: DISTINGUISH BETWEEN EVENT LOGS AND NOTE LOGS ? + this.#noteIndex += privateLogs.length; + } + + /** + * Adds public logs to the txe node, given a block + * @param blockNumber - The block number at which to add the public logs. + * @param privateLogs - The unencrypted logs to be added. + */ + addPublicLogsByTags(blockNumber: number, unencryptedLogs: UnencryptedL2Log[]) { + unencryptedLogs.forEach(log => { + if (log.data.length < 32 * 33) { + // TODO remove when #9835 and #9836 are fixed + this.#logger.warn(`Skipping unencrypted log with insufficient data length: ${log.data.length}`); + return; + } + try { + // TODO remove when #9835 and #9836 are fixed. The partial note logs are emitted as bytes, but encoded as Fields. + // This means that for every 32 bytes of payload, we only have 1 byte of data. + // Also, the tag is not stored in the first 32 bytes of the log, (that's the length of public fields now) but in the next 32. + const correctedBuffer = Buffer.alloc(32); + const initialOffset = 32; + for (let i = 0; i < 32; i++) { + const byte = Fr.fromBuffer(log.data.subarray(i * 32 + initialOffset, i * 32 + 32 + initialOffset)).toNumber(); + correctedBuffer.writeUInt8(byte, i); + } + const tag = new Fr(correctedBuffer); + + this.#logger.verbose( + `Found tagged unencrypted log with tag ${tag.toString()} in block ${this.getBlockNumber()}`, + ); + + const currentLogs = this.#logsByTags.get(tag.toString()) ?? []; + const scopedLog = new TxScopedL2Log( + new TxHash(new Fr(blockNumber).toBuffer()), + this.#noteIndex, + blockNumber, + true, + log.toBuffer(), + ); + + currentLogs.push(scopedLog); + this.#logsByTags.set(tag.toString(), currentLogs); + } catch (err) { + this.#logger.warn(`Failed to add tagged log to store: ${err}`); + } + }); + } + /** + * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag). + * @param tags - The tags to filter the logs by. + * @returns For each received tag, an array of matching logs and metadata (e.g. tx hash) is returned. An empty + array implies no logs match that tag. + */ + getLogsByTags(tags: Fr[]): Promise { + const noteLogs = tags.map(tag => this.#logsByTags.get(tag.toString()) ?? []); + + return Promise.resolve(noteLogs); + } + + /** + * Returns the tips of the L2 chain. + */ + getL2Tips(): Promise { + throw new Error('TXE Node method getL2Tips not implemented'); + } + + /** + * Find the indexes of the given leaves in the given tree. + * @param blockNumber - The block number at which to get the data or 'latest' for latest data + * @param treeId - The tree to search in. + * @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[], + ): Promise<(bigint | undefined)[]> { + throw new Error('TXE Node method findLeavesIndexes not implemented'); + } + + /** + * Returns a sibling path for the given index in the nullifier tree. + * @param blockNumber - The block number at which to get the data. + * @param leafIndex - The index of the leaf for which the sibling path is required. + * @returns The sibling path for the leaf index. + */ + getNullifierSiblingPath( + _blockNumber: L2BlockNumber, + _leafIndex: bigint, + ): Promise> { + throw new Error('TXE Node method getNullifierSiblingPath not implemented'); + } + + /** + * Returns a sibling path for the given index in the note hash tree. + * @param blockNumber - The block number at which to get the data. + * @param leafIndex - The index of the leaf for which the sibling path is required. + * @returns The sibling path for the leaf index. + */ + getNoteHashSiblingPath( + _blockNumber: L2BlockNumber, + _leafIndex: bigint, + ): Promise> { + throw new Error('TXE Node method getNoteHashSiblingPath not implemented'); + } + + /** + * Returns the index and a sibling path for a leaf in the committed l1 to l2 data tree. + * @param blockNumber - The block number at which to get the data. + * @param l1ToL2Message - The l1ToL2Message to get the index / sibling path for. + * @returns A tuple of the index and the sibling path of the L1ToL2Message (undefined if not found). + */ + getL1ToL2MessageMembershipWitness( + _blockNumber: L2BlockNumber, + _l1ToL2Message: Fr, + ): Promise<[bigint, SiblingPath] | undefined> { + throw new Error('TXE Node method getL1ToL2MessageMembershipWitness not implemented'); + } + + /** + * Returns whether an L1 to L2 message is synced by archiver and if it's ready to be included in a block. + * @param l1ToL2Message - The L1 to L2 message to check. + * @returns Whether the message is synced and ready to be included in a block. + */ + isL1ToL2MessageSynced(_l1ToL2Message: Fr): Promise { + throw new Error('TXE Node method isL1ToL2MessageSynced not implemented'); + } + + /** + * Returns a membership witness of an l2ToL1Message in an ephemeral l2 to l1 message tree. + * @dev Membership witness is a consists of the index and the sibling path of the l2ToL1Message. + * @remarks This tree is considered ephemeral because it is created on-demand by: taking all the l2ToL1 messages + * in a single block, and then using them to make a variable depth append-only tree with these messages as leaves. + * The tree is discarded immediately after calculating what we need from it. + * @param blockNumber - The block number at which to get the data. + * @param l2ToL1Message - The l2ToL1Message to get the membership witness for. + * @returns A tuple of the index and the sibling path of the L2ToL1Message. + */ + getL2ToL1MessageMembershipWitness( + _blockNumber: L2BlockNumber, + _l2ToL1Message: Fr, + ): Promise<[bigint, SiblingPath]> { + throw new Error('TXE Node method getL2ToL1MessageMembershipWitness not implemented'); + } + + /** + * Returns a sibling path for a leaf in the committed historic blocks tree. + * @param blockNumber - The block number at which to get the data. + * @param leafIndex - Index of the leaf in the tree. + * @returns The sibling path. + */ + getArchiveSiblingPath(_blockNumber: L2BlockNumber, _leafIndex: bigint): Promise> { + throw new Error('TXE Node method getArchiveSiblingPath not implemented'); + } + + /** + * Returns a sibling path for a leaf in the committed public data tree. + * @param blockNumber - The block number at which to get the data. + * @param leafIndex - Index of the leaf in the tree. + * @returns The sibling path. + */ + getPublicDataSiblingPath( + _blockNumber: L2BlockNumber, + _leafIndex: bigint, + ): Promise> { + throw new Error('TXE Node method getPublicDataSiblingPath not implemented'); + } + + /** + * Returns a nullifier membership witness for a given nullifier at a given block. + * @param blockNumber - The block number at which to get the data. + * @param nullifier - Nullifier we try to find witness for. + * @returns The nullifier membership witness (if found). + */ + getNullifierMembershipWitness( + _blockNumber: L2BlockNumber, + _nullifier: Fr, + ): Promise { + throw new Error('TXE Node method getNullifierMembershipWitness not implemented'); + } + + /** + * Returns a low nullifier membership witness for a given nullifier at a given block. + * @param blockNumber - The block number at which to get the data. + * @param nullifier - Nullifier we try to find the low nullifier witness for. + * @returns The low nullifier membership witness (if found). + * @remarks Low nullifier witness can be used to perform a nullifier non-inclusion proof by leveraging the "linked + * list structure" of leaves and proving that a lower nullifier is pointing to a bigger next value than the nullifier + * we are trying to prove non-inclusion for. + */ + getLowNullifierMembershipWitness( + _blockNumber: L2BlockNumber, + _nullifier: Fr, + ): Promise { + throw new Error('TXE Node method getLowNullifierMembershipWitness not implemented'); + } + + /** + * Returns a public data tree witness for a given leaf slot at a given block. + * @param blockNumber - The block number at which to get the data. + * @param leafSlot - The leaf slot we try to find the witness for. + * @returns The public data witness (if found). + * @remarks The witness can be used to compute the current value of the public data tree leaf. If the low leaf preimage corresponds to an + * "in range" slot, means that the slot doesn't exist and the value is 0. If the low leaf preimage corresponds to the exact slot, the current value + * is contained in the leaf preimage. + */ + getPublicDataTreeWitness(_blockNumber: L2BlockNumber, _leafSlot: Fr): Promise { + throw new Error('TXE Node method getPublicDataTreeWitness not implemented'); + } + + /** + * Get a block specified by its number. + * @param number - The block number being requested. + * @returns The requested block. + */ + getBlock(_number: number): Promise { + throw new Error('TXE Node method getBlock not implemented'); + } + + /** + * Fetches the latest proven block number. + * @returns The block number. + */ + getProvenBlockNumber(): Promise { + throw new Error('TXE Node method getProvenBlockNumber not implemented'); + } + + /** + * Method to determine if the node is ready to accept transactions. + * @returns - Flag indicating the readiness for tx submission. + */ + isReady(): Promise { + throw new Error('TXE Node method isReady not implemented'); + } + + /** + * Method to request blocks. Will attempt to return all requested blocks but will return only those available. + * @param from - The start of the range of blocks to return. + * @param limit - The maximum number of blocks to return. + * @returns The blocks requested. + */ + getBlocks(_from: number, _limit: number): Promise { + throw new Error('TXE Node method getBlocks not implemented'); + } + + /** + * Method to fetch the version of the package. + * @returns The node package version + */ + getNodeVersion(): Promise { + throw new Error('TXE Node method getNodeVersion not implemented'); + } + + /** + * Method to fetch the version of the rollup the node is connected to. + * @returns The rollup version. + */ + getVersion(): Promise { + throw new Error('TXE Node method getVersion not implemented'); + } + + /** + * Method to fetch the chain id of the base-layer for the rollup. + * @returns The chain id. + */ + getChainId(): Promise { + throw new Error('TXE Node method getChainId not implemented'); + } + + /** + * Method to fetch the currently deployed l1 contract addresses. + * @returns The deployed contract addresses. + */ + getL1ContractAddresses(): Promise { + throw new Error('TXE Node method getL1ContractAddresses not implemented'); + } + + /** + * Method to fetch the protocol contract addresses. + */ + getProtocolContractAddresses(): Promise { + throw new Error('TXE Node method getProtocolContractAddresses not implemented'); + } + + /** + * Method to add a contract artifact to the database. + * @param aztecAddress + * @param artifact + */ + addContractArtifact(_address: AztecAddress, _artifact: ContractArtifact): Promise { + throw new Error('TXE Node method addContractArtifact not implemented'); + } + + /** + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + */ + getUnencryptedLogs(_filter: LogFilter): Promise { + throw new Error('TXE Node method getUnencryptedLogs not implemented'); + } + + /** + * Gets contract class logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + */ + getContractClassLogs(_filter: LogFilter): Promise { + throw new Error('TXE Node method getContractClassLogs not implemented'); + } + + /** + * Method to submit a transaction to the p2p pool. + * @param tx - The transaction to be submitted. + * @returns Nothing. + */ + sendTx(_tx: Tx): Promise { + throw new Error('TXE Node method sendTx not implemented'); + } + + /** + * Fetches a transaction receipt for a given transaction hash. Returns a mined receipt if it was added + * to the chain, a pending receipt if it's still in the mempool of the connected Aztec node, or a dropped + * receipt if not found in the connected Aztec node. + * + * @param txHash - The transaction hash. + * @returns A receipt of the transaction. + */ + getTxReceipt(_txHash: TxHash): Promise { + throw new Error('TXE Node method getTxReceipt not implemented'); + } + + /** + * Method to retrieve pending txs. + * @returns The pending txs. + */ + getPendingTxs(): Promise { + throw new Error('TXE Node method getPendingTxs not implemented'); + } + + /** + * Retrieves the number of pending txs + * @returns The number of pending txs. + */ + getPendingTxCount(): Promise { + throw new Error('TXE Node method getPendingTxCount not implemented'); + } + + /** + * Method to retrieve a single pending tx. + * @param txHash - The transaction hash to return. + * @returns The pending tx if it exists. + */ + getTxByHash(_txHash: TxHash): Promise { + throw new Error('TXE Node method getTxByHash not implemented'); + } + + /** + * Gets the storage value at the given contract storage slot. + * + * @remarks The storage slot here refers to the slot as it is defined in Noir not the index in the merkle tree. + * Aztec's version of `eth_getStorageAt`. + * + * @param contract - Address of the contract to query. + * @param slot - Slot to query. + * @param blockNumber - The block number at which to get the data or 'latest'. + * @returns Storage value at the given contract slot. + */ + getPublicStorageAt(_contract: AztecAddress, _slot: Fr, _blockNumber: L2BlockNumber): Promise { + throw new Error('TXE Node method getPublicStorageAt not implemented'); + } + + /** + * Returns the currently committed block header. + * @returns The current committed block header. + */ + getBlockHeader(_blockNumber?: L2BlockNumber): Promise
{ + throw new Error('TXE Node method getBlockHeader not implemented'); + } + + /** + * Simulates the public part of a transaction with the current state. + * This currently just checks that the transaction execution succeeds. + * @param tx - The transaction to simulate. + **/ + simulatePublicCalls(_tx: Tx): Promise { + throw new Error('TXE Node method simulatePublicCalls not implemented'); + } + + /** + * Returns true if the transaction is valid for inclusion at the current state. Valid transactions can be + * made invalid by *other* transactions if e.g. they emit the same nullifiers, or come become invalid + * due to e.g. the max_block_number property. + * @param tx - The transaction to validate for correctness. + * @param isSimulation - True if the transaction is a simulated one without generated proofs. (Optional) + */ + isValidTx(_tx: Tx, _isSimulation?: boolean): Promise { + throw new Error('TXE Node method isValidTx not implemented'); + } + + /** + * Updates the configuration of this node. + * @param config - Updated configuration to be merged with the current one. + */ + setConfig(_config: Partial): Promise { + throw new Error('TXE Node method setConfig not implemented'); + } + + /** + * Returns a registered contract class given its id. + * @param id - Id of the contract class. + */ + getContractClass(_id: Fr): Promise { + throw new Error('TXE Node method getContractClass not implemented'); + } + + /** + * Returns a publicly deployed contract instance given its address. + * @param address - Address of the deployed contract. + */ + getContract(_address: AztecAddress): Promise { + throw new Error('TXE Node method getContract not implemented'); + } + + /** Forces the next block to be built bypassing all time and pending checks. Useful for testing. */ + flushTxs(): Promise { + throw new Error('TXE Node method flushTxs not implemented'); + } + + /** + * Returns the ENR of this node for peer discovery, if available. + */ + getEncodedEnr(): Promise { + throw new Error('TXE Node method getEncodedEnr not implemented'); + } + + /** + * Receives a quote for an epoch proof and stores it in its EpochProofQuotePool + * @param quote - The quote to store + */ + addEpochProofQuote(_quote: EpochProofQuote): Promise { + throw new Error('TXE Node method addEpochProofQuote not implemented'); + } + + /** + * Returns the received quotes for a given epoch + * @param epoch - The epoch for which to get the quotes + */ + getEpochProofQuotes(_epoch: bigint): Promise { + throw new Error('TXE Node method getEpochProofQuotes not implemented'); + } + + /** + * Adds a contract class bypassing the registerer. + * TODO(#10007): Remove this method. + * @param contractClass - The class to register. + */ + addContractClass(_contractClass: ContractClassPublic): Promise { + throw new Error('TXE Node method addContractClass not implemented'); + } + + /** + * Method to fetch the current base fees. + * @returns The current base fees. + */ + getCurrentBaseFees(): Promise { + throw new Error('TXE Node method getCurrentBaseFees not implemented'); + } + + /** + * Retrieves all private logs from up to `limit` blocks, starting from the block number `from`. + * @param from - The block number from which to begin retrieving logs. + * @param limit - The maximum number of blocks to retrieve logs from. + * @returns An array of private logs from the specified range of blocks. + */ + getPrivateLogs(_from: number, _limit: number): Promise { + throw new Error('TXE Node method getPrivateLogs not implemented'); + } + + /** + * Find the block numbers of the given leaf indices in the given tree. + * @param blockNumber - The block number at which to get the data or 'latest' for latest data + * @param treeId - The tree to search in. + * @param leafIndices - The values to search for + * @returns The indexes of the given leaves in the given tree or undefined if not found. + */ + findBlockNumbersForIndexes( + _blockNumber: L2BlockNumber, + _treeId: MerkleTreeId, + _leafIndices: bigint[], + ): Promise<(bigint | undefined)[]> { + throw new Error('TXE Node method findBlockNumbersForIndexes not implemented'); + } +} diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index e9c6d1c01c98..4686b2420665 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -7,6 +7,8 @@ import { PublicDataWitness, PublicExecutionRequest, SimulationError, + TxEffect, + TxHash, type UnencryptedL2Log, } from '@aztec/circuit-types'; import { type CircuitWitnessGenerationStats } from '@aztec/circuit-types/stats'; @@ -28,6 +30,7 @@ import { type PUBLIC_DATA_TREE_HEIGHT, PUBLIC_DISPATCH_SELECTOR, PrivateContextInputs, + type PrivateLog, PublicDataTreeLeaf, type PublicDataTreeLeafPreimage, type PublicDataWrite, @@ -37,7 +40,13 @@ import { getContractClassFromArtifact, } from '@aztec/circuits.js'; import { Schnorr } from '@aztec/circuits.js/barretenberg'; -import { computePublicDataTreeLeafSlot, siloNoteHash, siloNullifier } from '@aztec/circuits.js/hash'; +import { + computeNoteHashNonce, + computePublicDataTreeLeafSlot, + computeUniqueNoteHash, + siloNoteHash, + siloNullifier, +} from '@aztec/circuits.js/hash'; import { type ContractArtifact, type FunctionAbi, @@ -51,10 +60,10 @@ import { Fr } from '@aztec/foundation/fields'; import { type Logger, applyStringFormatting } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { type KeyStore } from '@aztec/key-store'; -import { ContractDataOracle, enrichPublicSimulationError } from '@aztec/pxe'; +import { ContractDataOracle, SimulatorOracle, enrichPublicSimulationError } from '@aztec/pxe'; import { ExecutionError, - type ExecutionNoteCache, + ExecutionNoteCache, type MessageLoadOracleInputs, type NoteData, Oracle, @@ -75,6 +84,7 @@ import { createTxForPublicCall } from '@aztec/simulator/public/fixtures'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; import { MerkleTreeSnapshotOperationsFacade, type MerkleTrees } from '@aztec/world-state'; +import { TXENode } from '../node/txe_node.js'; import { type TXEDatabase } from '../util/txe_database.js'; import { TXEPublicContractDataSource } from '../util/txe_public_contract_data_source.js'; import { TXEWorldStateDB } from '../util/txe_world_state_db.js'; @@ -90,10 +100,20 @@ export class TXE implements TypedOracle { private nestedCallReturndata: Fr[] = []; private contractDataOracle: ContractDataOracle; + private simulatorOracle: SimulatorOracle; private version: Fr = Fr.ONE; private chainId: Fr = Fr.ONE; + private siloedNoteHashesFromPublic: Fr[] = []; + private siloedNullifiersFromPublic: Fr[] = []; + private privateLogs: PrivateLog[] = []; + private publicLogs: UnencryptedL2Log[] = []; + + private committedBlocks = new Set(); + + private node = new TXENode(this.blockNumber); + constructor( private logger: Logger, private trees: MerkleTrees, @@ -106,6 +126,7 @@ export class TXE implements TypedOracle { this.contractAddress = AztecAddress.random(); // 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); } // Utils @@ -156,6 +177,7 @@ export class TXE implements TypedOracle { setBlockNumber(blockNumber: number) { this.blockNumber = blockNumber; + this.node.setBlockNumber(blockNumber); } getTrees() { @@ -236,6 +258,12 @@ export class TXE implements TypedOracle { ); } + async addSiloedNullifiersFromPublic(siloedNullifiers: Fr[]) { + this.siloedNullifiersFromPublic.push(...siloedNullifiers); + + await this.addSiloedNullifiers(siloedNullifiers); + } + async addNullifiers(contractAddress: AztecAddress, nullifiers: Fr[]) { const siloedNullifiers = nullifiers.map(nullifier => siloNullifier(contractAddress, nullifier)); await this.addSiloedNullifiers(siloedNullifiers); @@ -245,11 +273,53 @@ export class TXE implements TypedOracle { const db = await this.trees.getLatest(); await db.appendLeaves(MerkleTreeId.NOTE_HASH_TREE, siloedNoteHashes); } + + async addSiloedNoteHashesFromPublic(siloedNoteHashes: Fr[]) { + this.siloedNoteHashesFromPublic.push(...siloedNoteHashes); + await this.addSiloedNoteHashes(siloedNoteHashes); + } + async addNoteHashes(contractAddress: AztecAddress, noteHashes: Fr[]) { const siloedNoteHashes = noteHashes.map(noteHash => siloNoteHash(contractAddress, noteHash)); + await this.addSiloedNoteHashes(siloedNoteHashes); } + addPrivateLogs(contractAddress: AztecAddress, privateLogs: PrivateLog[]) { + privateLogs.forEach(privateLog => { + privateLog.fields[0] = poseidon2Hash([contractAddress, privateLog.fields[0]]); + }); + + this.privateLogs.push(...privateLogs); + } + + addPublicLogs(logs: UnencryptedL2Log[]) { + logs.forEach(log => { + if (log.data.length < 32 * 33) { + // TODO remove when #9835 and #9836 are fixed + this.logger.warn(`Skipping unencrypted log with insufficient data length: ${log.data.length}`); + return; + } + try { + // TODO remove when #9835 and #9836 are fixed. The partial note logs are emitted as bytes, but encoded as Fields. + // This means that for every 32 bytes of payload, we only have 1 byte of data. + // Also, the tag is not stored in the first 32 bytes of the log, (that's the length of public fields now) but in the next 32. + const correctedBuffer = Buffer.alloc(32); + const initialOffset = 32; + for (let i = 0; i < 32; i++) { + const byte = Fr.fromBuffer(log.data.subarray(i * 32 + initialOffset, i * 32 + 32 + initialOffset)).toNumber(); + correctedBuffer.writeUInt8(byte, i); + } + const tag = new Fr(correctedBuffer); + + this.logger.verbose(`Found tagged unencrypted log with tag ${tag.toString()} in block ${this.blockNumber}`); + this.publicLogs.push(log); + } catch (err) { + this.logger.warn(`Failed to add tagged log to store: ${err}`); + } + }); + } + // TypedOracle getBlockNumber() { @@ -298,11 +368,13 @@ export class TXE implements TypedOracle { async getMembershipWitness(blockNumber: number, treeId: MerkleTreeId, leafValue: Fr): Promise { const db = await this.#getTreesAt(blockNumber); + const index = await db.findLeafIndex(treeId, leafValue.toBuffer()); - if (!index) { + if (index === undefined) { throw new Error(`Leaf value: ${leafValue} not found in ${MerkleTreeId[treeId]} at block ${blockNumber}`); } const siblingPath = await db.getSiblingPath(treeId, index); + return [new Fr(index), ...siblingPath.toFields()]; } @@ -355,11 +427,26 @@ export class TXE implements TypedOracle { } } - getLowNullifierMembershipWitness( - _blockNumber: number, - _nullifier: Fr, + async getLowNullifierMembershipWitness( + blockNumber: number, + nullifier: Fr, ): Promise { - throw new Error('Method not implemented.'); + const committedDb = await this.#getTreesAt(blockNumber); + const findResult = await committedDb.getPreviousValueIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBigInt()); + if (!findResult) { + return undefined; + } + const { index, alreadyPresent } = findResult; + if (alreadyPresent) { + this.logger.warn(`Nullifier ${nullifier.toBigInt()} already exists in the tree`); + } + const preimageData = (await committedDb.getLeafPreimage(MerkleTreeId.NULLIFIER_TREE, index))!; + + const siblingPath = await committedDb.getSiblingPath( + MerkleTreeId.NULLIFIER_TREE, + BigInt(index), + ); + return new NullifierMembershipWitness(BigInt(index), preimageData as NullifierLeafPreimage, siblingPath); } async getHeader(blockNumber: number): Promise
{ @@ -382,7 +469,7 @@ export class TXE implements TypedOracle { throw new Error('Method not implemented.'); } - getNotes( + async getNotes( storageSlot: Fr, numSelects: number, selectByIndexes: number[], @@ -398,10 +485,12 @@ export class TXE implements TypedOracle { offset: number, _status: NoteStatus, ) { - // Nullified pending notes are already removed from the list. + const syncedNotes = await this.simulatorOracle.getNotes(this.contractAddress, storageSlot, _status); const pendingNotes = this.noteCache.getNotes(this.contractAddress, storageSlot); - const notes = pickNotes(pendingNotes, { + const notesToFilter = [...pendingNotes, ...syncedNotes]; + + const notes = pickNotes(notesToFilter, { selects: selectByIndexes.slice(0, numSelects).map((index, i) => ({ selector: { index, offset: selectByOffsets[i], length: selectByLengths[i] }, value: selectValues[i], @@ -421,10 +510,10 @@ export class TXE implements TypedOracle { .join(', ')}`, ); - return Promise.resolve(notes); + return notes; } - notifyCreatedNote(storageSlot: Fr, noteTypeId: NoteSelector, noteItems: Fr[], noteHash: Fr, counter: number) { + notifyCreatedNote(storageSlot: Fr, _noteTypeId: NoteSelector, noteItems: Fr[], noteHash: Fr, counter: number) { const note = new Note(noteItems); this.noteCache.addNewNote( { @@ -506,6 +595,55 @@ export class TXE implements TypedOracle { return publicDataWrites.map(write => write.value); } + async commitState() { + const blockNumber = await this.getBlockNumber(); + if (this.committedBlocks.has(blockNumber)) { + throw new Error('Already committed state'); + } else { + this.committedBlocks.add(blockNumber); + } + + const txEffect = TxEffect.empty(); + + let i = 0; + txEffect.noteHashes = [ + ...this.noteCache + .getAllNotes() + .map(pendingNote => + siloNoteHash( + pendingNote.note.contractAddress, + computeUniqueNoteHash( + computeNoteHashNonce(new Fr(this.blockNumber + 6969), i++), + pendingNote.noteHashForConsumption, + ), + ), + ), + ...this.siloedNoteHashesFromPublic, + ]; + txEffect.nullifiers = [new Fr(blockNumber + 6969), ...this.noteCache.getAllNullifiers()]; + + // Using block number itself, (without adding 6969) gets killed at 1 as it says the slot is already used, + // it seems like we commit a 1 there to the trees before ? To see what I mean, uncomment these lines below + // let index = await (await this.trees.getLatest()).findLeafIndex(MerkleTreeId.NULLIFIER_TREE, Fr.ONE.toBuffer()); + // console.log('INDEX OF ONE', index); + // index = await (await this.trees.getLatest()).findLeafIndex(MerkleTreeId.NULLIFIER_TREE, Fr.random().toBuffer()); + // console.log('INDEX OF RANDOM', index); + + this.node.setTxEffect(blockNumber, new TxHash(new Fr(blockNumber).toBuffer()), txEffect); + this.node.setNullifiersIndexesWithBlock(blockNumber, txEffect.nullifiers); + this.node.addNoteLogsByTags(this.blockNumber, this.privateLogs); + this.node.addPublicLogsByTags(this.blockNumber, this.publicLogs); + + await this.addSiloedNoteHashes(txEffect.noteHashes); + await this.addSiloedNullifiers(txEffect.nullifiers); + + this.privateLogs = []; + this.publicLogs = []; + this.siloedNoteHashesFromPublic = []; + this.siloedNullifiersFromPublic = []; + this.noteCache = new ExecutionNoteCache(new Fr(1)); + } + emitContractClassLog(_log: UnencryptedL2Log, _counter: number): Fr { throw new Error('Method not implemented.'); } @@ -570,14 +708,9 @@ export class TXE implements TypedOracle { const endSideEffectCounter = publicInputs.endSideEffectCounter; this.sideEffectCounter = endSideEffectCounter.toNumber() + 1; - await this.addNullifiers( - targetContractAddress, - publicInputs.nullifiers.filter(nullifier => !nullifier.isEmpty()).map(nullifier => nullifier.value), - ); - - await this.addNoteHashes( + this.addPrivateLogs( targetContractAddress, - publicInputs.noteHashes.filter(noteHash => !noteHash.isEmpty()).map(noteHash => noteHash.value), + publicInputs.privateLogs.filter(privateLog => !privateLog.isEmpty()).map(privateLog => privateLog.log), ); this.setContractAddress(currentContractAddress); @@ -655,6 +788,9 @@ export class TXE implements TypedOracle { const tx = createTxForPublicCall(executionRequest, gasUsedByPrivate, isTeardown); const result = await simulator.simulate(tx); + + this.addPublicLogs(tx.unencryptedLogs.unrollLogs()); + return Promise.resolve(result); } @@ -707,7 +843,7 @@ export class TXE implements TypedOracle { const noteHashes = sideEffects.noteHashes.filter(s => !s.isEmpty()); const nullifiers = sideEffects.nullifiers.filter(s => !s.isEmpty()); await this.addPublicDataWrites(publicDataWrites); - await this.addSiloedNoteHashes(noteHashes); + await this.addSiloedNoteHashesFromPublic(noteHashes); await this.addSiloedNullifiers(nullifiers); this.setContractAddress(currentContractAddress); @@ -765,8 +901,17 @@ export class TXE implements TypedOracle { return siloedSecret; } - syncNotes() { - // TODO: Implement + async syncNotes() { + const taggedLogsByRecipient = await this.simulatorOracle.syncTaggedLogs( + this.contractAddress, + await this.getBlockNumber(), + undefined, + ); + + for (const [recipient, taggedLogs] of taggedLogsByRecipient.entries()) { + await this.simulatorOracle.processTaggedLogs(taggedLogs, AztecAddress.fromString(recipient)); + } + return Promise.resolve(); } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 1fdaee4d6359..34788462f9f5 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -70,6 +70,9 @@ export class TXEService { const nBlocks = fromSingle(blocks).toNumber(); this.logger.debug(`time traveling ${nBlocks} blocks`); const trees = (this.typedOracle as TXE).getTrees(); + + await (this.typedOracle as TXE).commitState(); + for (let i = 0; i < nBlocks; i++) { const blockNumber = await this.typedOracle.getBlockNumber(); const header = Header.empty(); @@ -571,6 +574,16 @@ export class TXEService { return toForeignCallResult([toArray(witness)]); } + async getLowNullifierMembershipWitness(blockNumber: ForeignCallSingle, nullifier: ForeignCallSingle) { + const parsedBlockNumber = fromSingle(blockNumber).toNumber(); + + const witness = await this.typedOracle.getLowNullifierMembershipWitness(parsedBlockNumber, fromSingle(nullifier)); + if (!witness) { + throw new Error(`Low nullifier witness not found for nullifier ${nullifier} at block ${parsedBlockNumber}.`); + } + return toForeignCallResult([toArray(witness.toFields())]); + } + async getAppTaggingSecretAsSender(sender: ForeignCallSingle, recipient: ForeignCallSingle) { const secret = await this.typedOracle.getAppTaggingSecretAsSender( AztecAddress.fromField(fromSingle(sender)), diff --git a/yarn-project/txe/src/util/txe_database.ts b/yarn-project/txe/src/util/txe_database.ts index b154fd8702a2..a756a955a345 100644 --- a/yarn-project/txe/src/util/txe_database.ts +++ b/yarn-project/txe/src/util/txe_database.ts @@ -20,5 +20,6 @@ export class TXEDatabase extends KVPxeDatabase { async setAccount(key: AztecAddress, value: CompleteAddress) { await this.#accounts.set(key.toString(), value.toBuffer()); + await this.addCompleteAddress(value); } }