diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index 51ef7f378e4b..c94c8a3ef7a7 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -253,25 +253,18 @@ unconstrained fn increment_app_tagging_secret_oracle( _recipient: AztecAddress, ) {} -/// Returns the tagging secrets for a given recipient and all the senders in PXE's address book, -// siloed for the current contract address. -/// Includes the last known index used for tagging with this secret. -/// For this to work, PXE must know the ivsk_m of the recipient. -pub unconstrained fn get_app_tagging_secrets_for_senders( - recipient: AztecAddress, -) -> [IndexedTaggingSecret] { - let results = get_app_tagging_secrets_for_senders_oracle(recipient); - let mut indexed_tagging_secrets = &[]; - for i in 0..results.len() { - if i % 3 != 0 { - continue; - } - indexed_tagging_secrets = indexed_tagging_secrets.push_back( - IndexedTaggingSecret::deserialize([results[i], results[i + 1], results[i + 2]]), - ); +/// Finds new notes that may have been sent to `recipient` in the current contract and makes them available +/// for later querying via the `get_notes` oracle. +pub fn sync_notes(recipient: AztecAddress) { + // 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(recipient); } - indexed_tagging_secrets } -#[oracle(getAppTaggingSecretsForSenders)] -unconstrained fn get_app_tagging_secrets_for_senders_oracle(_recipient: AztecAddress) -> [Field] {} +unconstrained fn sync_notes_oracle_wrapper(recipient: AztecAddress) { + sync_notes_oracle(recipient); +} + +#[oracle(syncNotes)] +unconstrained fn sync_notes_oracle(_recipient: AztecAddress) {} diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index a205a5d38810..7fb68148e31a 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -1,5 +1,4 @@ import { - type EncryptedL2NoteLog, type FromLogType, type GetUnencryptedLogsResponse, type InboxLeaf, @@ -15,6 +14,7 @@ import { type TxEffect, type TxHash, type TxReceipt, + type TxScopedEncryptedL2NoteLog, type UnencryptedL2Log, } from '@aztec/circuit-types'; import { @@ -634,7 +634,7 @@ export class Archiver implements ArchiveSource { * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[]): Promise { + getLogsByTags(tags: Fr[]): Promise { return this.store.getLogsByTags(tags); } @@ -934,7 +934,7 @@ class ArchiverStoreHelper ): Promise>[]> { return this.store.getLogs(from, limit, logType); } - getLogsByTags(tags: Fr[]): Promise { + getLogsByTags(tags: Fr[]): Promise { return this.store.getLogsByTags(tags); } getUnencryptedLogs(filter: LogFilter): Promise { diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index 9128b33db443..5038dd4afebb 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -1,5 +1,4 @@ import { - type EncryptedL2NoteLog, type FromLogType, type GetUnencryptedLogsResponse, type InboxLeaf, @@ -10,6 +9,7 @@ import { type TxEffect, type TxHash, type TxReceipt, + type TxScopedEncryptedL2NoteLog, } from '@aztec/circuit-types'; import { type ContractClassPublic, @@ -142,7 +142,7 @@ export interface ArchiverDataStore { * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[]): Promise; + getLogsByTags(tags: Fr[]): Promise; /** * Gets unencrypted logs based on the provided filter. diff --git a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts index a41f01ab6e91..1311c6ec03ee 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -401,8 +401,9 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch logsByTags.forEach((logsByTag, logIndex) => { expect(logsByTag).toHaveLength(1); - const [log] = logsByTag; - expect(log).toEqual( + const [scopedLog] = logsByTag; + expect(scopedLog.txHash).toEqual(blocks[targetBlockIndex].data.body.txEffects[targetTxIndex].txHash); + expect(scopedLog.log).toEqual( blocks[targetBlockIndex].data.body.noteEncryptedLogs.txLogs[targetTxIndex].unrollLogs()[logIndex], ); }); @@ -427,7 +428,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch logsByTags.forEach(logsByTag => { expect(logsByTag).toHaveLength(2); - const [tag0, tag1] = logsByTag.map(log => new Fr(log.data.subarray(0, 32))); + const [tag0, tag1] = logsByTag.map(scopedLog => new Fr(scopedLog.log.data.subarray(0, 32))); expect(tag0).toEqual(tag1); }); }); @@ -450,8 +451,9 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch populatedLogsByTags.forEach((logsByTag, logIndex) => { expect(logsByTag).toHaveLength(1); - const [log] = logsByTag; - expect(log).toEqual( + const [scopedLog] = logsByTag; + expect(scopedLog.txHash).toEqual(blocks[targetBlockIndex].data.body.txEffects[targetTxIndex].txHash); + expect(scopedLog.log).toEqual( blocks[targetBlockIndex].data.body.noteEncryptedLogs.txLogs[targetTxIndex].unrollLogs()[logIndex + 1], ); }); diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts index 0a1949f0a11c..d7ff5f8c1d5e 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts @@ -1,5 +1,4 @@ import { - type EncryptedL2NoteLog, type FromLogType, type GetUnencryptedLogsResponse, type InboxLeaf, @@ -10,6 +9,7 @@ import { type TxEffect, type TxHash, type TxReceipt, + type TxScopedEncryptedL2NoteLog, } from '@aztec/circuit-types'; import { type ContractClassPublic, @@ -245,7 +245,7 @@ export class KVArchiverDataStore implements ArchiverDataStore { * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[]): Promise { + getLogsByTags(tags: Fr[]): Promise { try { return this.#logStore.getLogsByTags(tags); } catch (err) { 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 5c093fb4b1e9..2cdbb52b34c3 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 @@ -1,6 +1,5 @@ import { EncryptedL2BlockL2Logs, - EncryptedL2NoteLog, EncryptedNoteL2BlockL2Logs, ExtendedUnencryptedL2Log, type FromLogType, @@ -10,11 +9,12 @@ import { type LogFilter, LogId, LogType, + TxScopedEncryptedL2NoteLog, UnencryptedL2BlockL2Logs, type UnencryptedL2Log, } from '@aztec/circuit-types'; import { Fr } from '@aztec/circuits.js'; -import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js/constants'; +import { INITIAL_L2_BLOCK_NUM, MAX_NOTE_HASHES_PER_TX } from '@aztec/circuits.js/constants'; import { createDebugLogger } from '@aztec/foundation/log'; import { type AztecKVStore, type AztecMap, type AztecMultiMap } from '@aztec/kv-store'; @@ -52,8 +52,13 @@ export class LogStore { addLogs(blocks: L2Block[]): Promise { return this.db.transaction(() => { blocks.forEach(block => { + const dataStartIndexForBlock = + block.header.state.partial.noteHashTree.nextAvailableLeafIndex - + block.body.numberOfTxsIncludingPadded * MAX_NOTE_HASHES_PER_TX; void this.#noteEncryptedLogsByBlock.set(block.number, block.body.noteEncryptedLogs.toBuffer()); - block.body.noteEncryptedLogs.txLogs.forEach(txLogs => { + block.body.noteEncryptedLogs.txLogs.forEach((txLogs, txIndex) => { + const txHash = block.body.txEffects[txIndex].txHash; + const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; const noteLogs = txLogs.unrollLogs(); noteLogs.forEach(noteLog => { if (noteLog.data.length < 32) { @@ -63,12 +68,15 @@ export class LogStore { try { const tag = new Fr(noteLog.data.subarray(0, 32)); const hexHash = noteLog.hash().toString('hex'); - // Ideally we'd store all of the logs for a matching tag in an AztecMultiMap, but this type doesn't doesn't + // Ideally we'd store all of the logs for a matching tag in an AztecMultiMap, but this type doesn't // handle storing buffers well. The 'ordered-binary' encoding returns an error trying to decode buffers // ('the number <> cannot be converted to a BigInt because it is not an integer'). We therefore store // instead the hashes of the logs. void this.#noteEncryptedLogHashesByTag.set(tag.toString(), hexHash); - void this.#noteEncryptedLogsByHash.set(hexHash, noteLog.toBuffer()); + void this.#noteEncryptedLogsByHash.set( + hexHash, + new TxScopedEncryptedL2NoteLog(txHash, dataStartIndexForTx, noteLog).toBuffer(), + ); void this.#noteEncryptedLogTagsByBlock.set(block.number, tag.toString()); } catch (err) { this.#log.warn(`Failed to add tagged note log to store: ${err}`); @@ -156,7 +164,7 @@ export class LogStore { * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[]): Promise { + getLogsByTags(tags: Fr[]): Promise { return this.db.transaction(() => { return tags.map(tag => { const logHashes = Array.from(this.#noteEncryptedLogHashesByTag.getValues(tag.toString())); @@ -166,7 +174,7 @@ export class LogStore { // addLogs should ensure that we never have undefined logs, but we filter them out regardless to protect // ourselves from database corruption .filter(noteLogBuffer => noteLogBuffer != undefined) - .map(noteLogBuffer => EncryptedL2NoteLog.fromBuffer(noteLogBuffer!)) + .map(noteLogBuffer => TxScopedEncryptedL2NoteLog.fromBuffer(noteLogBuffer!)) ); }); }); 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 4d3e887c0728..14637a80a108 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 @@ -1,6 +1,5 @@ import { type EncryptedL2BlockL2Logs, - type EncryptedL2NoteLog, type EncryptedNoteL2BlockL2Logs, ExtendedUnencryptedL2Log, type FromLogType, @@ -14,6 +13,7 @@ import { type TxEffect, type TxHash, TxReceipt, + TxScopedEncryptedL2NoteLog, type UnencryptedL2BlockL2Logs, } from '@aztec/circuit-types'; import { @@ -24,6 +24,7 @@ import { Fr, type Header, INITIAL_L2_BLOCK_NUM, + MAX_NOTE_HASHES_PER_TX, type UnconstrainedFunctionWithMembershipProof, } from '@aztec/circuits.js'; import { type ContractArtifact } from '@aztec/foundation/abi'; @@ -51,7 +52,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { private noteEncryptedLogsPerBlock: Map = new Map(); - private taggedNoteEncryptedLogs: Map = new Map(); + private taggedNoteEncryptedLogs: Map = new Map(); private noteEncryptedLogTagsPerBlock: Map = new Map(); @@ -213,8 +214,13 @@ export class MemoryArchiverStore implements ArchiverDataStore { */ addLogs(blocks: L2Block[]): Promise { blocks.forEach(block => { + const dataStartIndexForBlock = + block.header.state.partial.noteHashTree.nextAvailableLeafIndex - + block.body.numberOfTxsIncludingPadded * MAX_NOTE_HASHES_PER_TX; this.noteEncryptedLogsPerBlock.set(block.number, block.body.noteEncryptedLogs); - block.body.noteEncryptedLogs.txLogs.forEach(txLogs => { + block.body.noteEncryptedLogs.txLogs.forEach((txLogs, txIndex) => { + const txHash = block.body.txEffects[txIndex].txHash; + const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; const noteLogs = txLogs.unrollLogs(); noteLogs.forEach(noteLog => { if (noteLog.data.length < 32) { @@ -224,7 +230,10 @@ export class MemoryArchiverStore implements ArchiverDataStore { try { const tag = new Fr(noteLog.data.subarray(0, 32)); const currentNoteLogs = this.taggedNoteEncryptedLogs.get(tag.toString()) || []; - this.taggedNoteEncryptedLogs.set(tag.toString(), [...currentNoteLogs, noteLog]); + this.taggedNoteEncryptedLogs.set(tag.toString(), [ + ...currentNoteLogs, + new TxScopedEncryptedL2NoteLog(txHash, dataStartIndexForTx, noteLog), + ]); const currentTagsInBlock = this.noteEncryptedLogTagsPerBlock.get(block.number) || []; this.noteEncryptedLogTagsPerBlock.set(block.number, [...currentTagsInBlock, tag]); } catch (err) { @@ -419,7 +428,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[]): Promise { + getLogsByTags(tags: Fr[]): Promise { const noteLogs = tags.map(tag => this.taggedNoteEncryptedLogs.get(tag.toString()) || []); return Promise.resolve(noteLogs); } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index bc862083ea0d..9483f1d400bb 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -3,7 +3,6 @@ import { BBCircuitVerifier, TestCircuitVerifier } from '@aztec/bb-prover'; import { type AztecNode, type ClientProtocolCircuitVerifier, - type EncryptedL2NoteLog, type EpochProofQuote, type FromLogType, type GetUnencryptedLogsResponse, @@ -27,6 +26,7 @@ import { type TxEffect, type TxHash, TxReceipt, + type TxScopedEncryptedL2NoteLog, TxStatus, type TxValidator, type WorldStateSynchronizer, @@ -313,7 +313,7 @@ export class AztecNodeService implements AztecNode { * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match * that tag. */ - public getLogsByTags(tags: Fr[]): Promise { + public getLogsByTags(tags: Fr[]): Promise { return this.encryptedLogsSource.getLogsByTags(tags); } diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.ts index e12578d7c441..e2d2ac343bc2 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.ts @@ -16,12 +16,12 @@ import type { Fr } from '@aztec/foundation/fields'; import type { L2Block } from '../l2_block.js'; import type { - EncryptedL2NoteLog, FromLogType, GetUnencryptedLogsResponse, L2BlockL2Logs, LogFilter, LogType, + TxScopedEncryptedL2NoteLog, } from '../logs/index.js'; import type { MerkleTreeId } from '../merkle_tree_id.js'; import type { EpochProofQuote } from '../prover_coordination/epoch_proof_quote.js'; @@ -255,10 +255,10 @@ export interface AztecNode extends ProverCoordination { /** * 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 is returned. An empty array implies no logs match - * that tag. + * @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; + getLogsByTags(tags: Fr[]): Promise; /** * Method to submit a transaction to the p2p pool. diff --git a/yarn-project/circuit-types/src/logs/get_logs_response.ts b/yarn-project/circuit-types/src/logs/get_logs_response.ts new file mode 100644 index 000000000000..68b1dbb9068c --- /dev/null +++ b/yarn-project/circuit-types/src/logs/get_logs_response.ts @@ -0,0 +1,51 @@ +import { Fr } from '@aztec/circuits.js'; +import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize'; + +import { EncryptedL2NoteLog, TxHash } from '../index.js'; +import { type ExtendedUnencryptedL2Log } from './extended_unencrypted_l2_log.js'; + +/** + * It provides documentation for the GetUnencryptedLogsResponse type. + */ +export type GetUnencryptedLogsResponse = { + /** + * An array of ExtendedUnencryptedL2Log elements. + */ + logs: ExtendedUnencryptedL2Log[]; + + /** + * Indicates if a limit has been reached. + */ + maxLogsHit: boolean; +}; + +export class TxScopedEncryptedL2NoteLog { + constructor( + /* + * Hash of the tx where the log is included + */ + public txHash: TxHash, + /* + * The next available leaf index for the note hash tree for this transaction. It is stored + * with the log so the noteHashIndex can be reconstructed after decryption. + */ + public dataStartIndexForTx: number, + /* + * The encrypted note log + */ + public log: EncryptedL2NoteLog, + ) {} + + toBuffer() { + return Buffer.concat([this.txHash.toBuffer(), numToUInt32BE(this.dataStartIndexForTx), this.log.toBuffer()]); + } + + static fromBuffer(buffer: Buffer) { + const reader = BufferReader.asReader(buffer); + return new TxScopedEncryptedL2NoteLog( + TxHash.fromField(reader.readObject(Fr)), + reader.readNumber(), + EncryptedL2NoteLog.fromBuffer(reader.readToEnd()), + ); + } +} diff --git a/yarn-project/circuit-types/src/logs/get_unencrypted_logs_response.ts b/yarn-project/circuit-types/src/logs/get_unencrypted_logs_response.ts deleted file mode 100644 index b8c18fa278d8..000000000000 --- a/yarn-project/circuit-types/src/logs/get_unencrypted_logs_response.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type ExtendedUnencryptedL2Log } from './extended_unencrypted_l2_log.js'; - -/** - * It provides documentation for the GetUnencryptedLogsResponse type. - */ -export type GetUnencryptedLogsResponse = { - /** - * An array of ExtendedUnencryptedL2Log elements. - */ - logs: ExtendedUnencryptedL2Log[]; - - /** - * Indicates if a limit has been reached. - */ - maxLogsHit: boolean; -}; diff --git a/yarn-project/circuit-types/src/logs/index.ts b/yarn-project/circuit-types/src/logs/index.ts index af29a4c96770..2f10eb33f605 100644 --- a/yarn-project/circuit-types/src/logs/index.ts +++ b/yarn-project/circuit-types/src/logs/index.ts @@ -1,7 +1,7 @@ export * from './encrypted_l2_note_log.js'; export * from './encrypted_l2_log.js'; export * from './event_metadata.js'; -export * from './get_unencrypted_logs_response.js'; +export * from './get_logs_response.js'; export * from './function_l2_logs.js'; export * from './l2_block_l2_logs.js'; export * from './l2_logs_source.js'; diff --git a/yarn-project/circuit-types/src/logs/l2_logs_source.ts b/yarn-project/circuit-types/src/logs/l2_logs_source.ts index 9c00af874bf3..07ebcfd8eb72 100644 --- a/yarn-project/circuit-types/src/logs/l2_logs_source.ts +++ b/yarn-project/circuit-types/src/logs/l2_logs_source.ts @@ -1,7 +1,6 @@ import { type Fr } from '@aztec/circuits.js'; -import { type EncryptedL2NoteLog } from './encrypted_l2_note_log.js'; -import { type GetUnencryptedLogsResponse } from './get_unencrypted_logs_response.js'; +import { type GetUnencryptedLogsResponse, type TxScopedEncryptedL2NoteLog } from './get_logs_response.js'; import { type L2BlockL2Logs } from './l2_block_l2_logs.js'; import { type LogFilter } from './log_filter.js'; import { type FromLogType, type LogType } from './log_type.js'; @@ -29,7 +28,7 @@ export interface L2LogsSource { * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[]): Promise; + getLogsByTags(tags: Fr[]): Promise; /** * Gets unencrypted logs based on the provided filter. diff --git a/yarn-project/circuit-types/src/tx/tx.ts b/yarn-project/circuit-types/src/tx/tx.ts index 6bb380aa82d8..6b3954af752f 100644 --- a/yarn-project/circuit-types/src/tx/tx.ts +++ b/yarn-project/circuit-types/src/tx/tx.ts @@ -8,7 +8,7 @@ import { type Buffer32 } from '@aztec/foundation/buffer'; import { arraySerializedSizeOfNonEmpty } from '@aztec/foundation/collection'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; -import { type GetUnencryptedLogsResponse } from '../logs/get_unencrypted_logs_response.js'; +import { type GetUnencryptedLogsResponse } from '../logs/get_logs_response.js'; import { type L2LogsSource } from '../logs/l2_logs_source.js'; import { EncryptedNoteTxL2Logs, EncryptedTxL2Logs, UnencryptedTxL2Logs } from '../logs/tx_l2_logs.js'; import { Gossipable } from '../p2p/gossipable.js'; diff --git a/yarn-project/circuit-types/src/tx/tx_hash.ts b/yarn-project/circuit-types/src/tx/tx_hash.ts index 921903d75a00..7781fe4b30e1 100644 --- a/yarn-project/circuit-types/src/tx/tx_hash.ts +++ b/yarn-project/circuit-types/src/tx/tx_hash.ts @@ -1,3 +1,4 @@ +import { Fr } from '@aztec/circuits.js'; import { Buffer32 } from '@aztec/foundation/buffer'; /** @@ -12,4 +13,13 @@ export class TxHash extends Buffer32 { ) { super(hash); } + + /* + * TxHashes are generated from the first nullifier of a transaction, which is a Fr. + * Using Buffer32.random() could potentially generate invalid TxHashes. + * @returns A random TxHash. + */ + static override random() { + return new TxHash(Fr.random().toBuffer()); + } } diff --git a/yarn-project/circuit-types/src/tx_effect.ts b/yarn-project/circuit-types/src/tx_effect.ts index 1115f1b29114..1e327bd42a8c 100644 --- a/yarn-project/circuit-types/src/tx_effect.ts +++ b/yarn-project/circuit-types/src/tx_effect.ts @@ -261,7 +261,7 @@ export class TxEffect { [inspect.custom]() { // print out the non-empty fields - return `TxEffect { + return `TxEffect { revertCode: ${this.revertCode}, transactionFee: ${this.transactionFee}, note hashes: [${this.noteHashes.map(h => h.toString()).join(', ')}], diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index d44ce292c6d7..9c6f25092d08 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -64,6 +64,7 @@ export class KVPxeDatabase implements PxeDatabase { #notesByTxHashAndScope: Map>; #notesByAddressPointAndScope: Map>; + // Stores the last index used for each tagging secret #taggingSecretIndexes: AztecMap; constructor(private db: AztecKVStore) { diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index bbf50bf3e4df..c46ee53537ea 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -91,8 +91,6 @@ export class PXEService implements PXE { // ensures that state is not changed while simulating private jobQueue = new SerialQueue(); - private fakeProofCreator = new TestPrivateKernelProver(); - constructor( private keyStore: KeyStore, private node: AztecNode, diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 541ddefd7dc0..425603d6516e 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -1,11 +1,12 @@ import { type AztecNode, - type EncryptedL2NoteLog, + L1NotePayload, type L2Block, MerkleTreeId, type NoteStatus, type NullifierMembershipWitness, type PublicDataWitness, + type TxScopedEncryptedL2NoteLog, getNonNullifiedL1ToL2MessageWitness, } from '@aztec/circuit-types'; import { @@ -19,16 +20,23 @@ import { type KeyValidationRequest, type L1_TO_L2_MSG_TREE_HEIGHT, TaggingSecret, + computeAddressSecret, + computePoint, computeTaggingSecret, } from '@aztec/circuits.js'; import { type FunctionArtifact, getFunctionArtifact } from '@aztec/foundation/abi'; import { poseidon2Hash } from '@aztec/foundation/crypto'; import { createDebugLogger } from '@aztec/foundation/log'; import { type KeyStore } from '@aztec/key-store'; -import { type DBOracle, MessageLoadOracleInputs } from '@aztec/simulator'; +import { type AcirSimulator, type DBOracle, MessageLoadOracleInputs } from '@aztec/simulator'; import { type ContractDataOracle } from '../contract_data_oracle/index.js'; +import { type DeferredNoteDao } from '../database/deferred_note_dao.js'; +import { type IncomingNoteDao } from '../database/incoming_note_dao.js'; import { type PxeDatabase } from '../database/index.js'; +import { type OutgoingNoteDao } from '../database/outgoing_note_dao.js'; +import { produceNoteDaos } from '../note_processor/utils/produce_note_daos.js'; +import { getAcirSimulator } from '../simulator/index.js'; /** * A data oracle that provides information needed for simulating a transaction. @@ -244,7 +252,7 @@ export class SimulatorOracle implements DBOracle { /** * Returns the tagging secret for a given sender and recipient pair. For this to work, the ivpsk_m of the sender must be known. - * Includes the last known index used for tagging with this secret. + * Includes the next index to be used used for tagging with this secret. * @param contractAddress - The contract address to silo the secret for * @param sender - The address sending the note * @param recipient - The address receiving the note @@ -288,11 +296,14 @@ export class SimulatorOracle implements DBOracle { /** * Returns the siloed tagging secrets for a given recipient and all the senders in the address book + * This method should be exposed as an oracle call to allow aztec.nr to perform the orchestration + * of the syncTaggedLogs and processTaggedLogs methods. However, it is not possible to do so at the moment, + * so we're keeping it private for now. * @param contractAddress - The contract address to silo the secret for * @param recipient - The address receiving the notes * @returns A list of siloed tagging secrets */ - public async getAppTaggingSecretsForSenders( + async #getAppTaggingSecretsForSenders( contractAddress: AztecAddress, recipient: AztecAddress, ): Promise { @@ -320,7 +331,10 @@ export class SimulatorOracle implements DBOracle { * @param recipient - The address of the recipient * @returns A list of encrypted logs tagged with the recipient's address */ - public async syncTaggedLogs(contractAddress: AztecAddress, recipient: AztecAddress): Promise { + public async syncTaggedLogs( + contractAddress: AztecAddress, + recipient: AztecAddress, + ): Promise { // Ideally this algorithm would be implemented in noir, exposing its building blocks as oracles. // However it is impossible at the moment due to the language not supporting nested slices. // This nesting is necessary because for a given set of tags we don't @@ -328,9 +342,9 @@ export class SimulatorOracle implements DBOracle { // length, since we don't really know the note they correspond to until we decrypt them. // 1. Get all the secrets for the recipient and sender pairs (#9365) - let appTaggingSecrets = await this.getAppTaggingSecretsForSenders(contractAddress, recipient); + let appTaggingSecrets = await this.#getAppTaggingSecretsForSenders(contractAddress, recipient); - const logs: EncryptedL2NoteLog[] = []; + const logs: TxScopedEncryptedL2NoteLog[] = []; while (appTaggingSecrets.length > 0) { // 2. Compute tags using the secrets, recipient and index. Obtain logs for each tag (#9380) const currentTags = appTaggingSecrets.map(taggingSecret => taggingSecret.computeTag()); @@ -354,4 +368,145 @@ export class SimulatorOracle implements DBOracle { } return logs; } + + /** + * 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: TxScopedEncryptedL2NoteLog[], + recipient: AztecAddress, + simulator: AcirSimulator, + ) { + const recipientCompleteAddress = await this.getCompleteAddress(recipient); + const ivskM = await this.keyStore.getMasterSecretKey( + recipientCompleteAddress.publicKeys.masterIncomingViewingPublicKey, + ); + const addressSecret = computeAddressSecret(recipientCompleteAddress.getPreaddress(), ivskM); + const ovskM = await this.keyStore.getMasterSecretKey( + recipientCompleteAddress.publicKeys.masterOutgoingViewingPublicKey, + ); + // 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 outgoingNotes: OutgoingNoteDao[] = []; + const deferredIncomingNotes: DeferredNoteDao[] = []; + const deferredOutgoingNotes: DeferredNoteDao[] = []; + for (const scopedLog of scopedLogs) { + const incomingNotePayload = L1NotePayload.decryptAsIncoming(scopedLog.log.data, addressSecret); + const outgoingNotePayload = L1NotePayload.decryptAsOutgoing(scopedLog.log.data, ovskM); + + if (incomingNotePayload || outgoingNotePayload) { + if (incomingNotePayload && outgoingNotePayload && !incomingNotePayload.equals(outgoingNotePayload)) { + this.log.warn( + `Incoming and outgoing note payloads do not match. Incoming: ${JSON.stringify( + incomingNotePayload, + )}, Outgoing: ${JSON.stringify(outgoingNotePayload)}`, + ); + } + + const payload = incomingNotePayload || outgoingNotePayload; + const txEffect = await this.aztecNode.getTxEffect(scopedLog.txHash); + + if (!txEffect) { + this.log.warn(`No tx effect found for ${scopedLog.txHash} while decrypting tagged logs`); + continue; + } + if (!excludedIndices.has(scopedLog.txHash.toString())) { + excludedIndices.set(scopedLog.txHash.toString(), new Set()); + } + const { incomingNote, outgoingNote, incomingDeferredNote, outgoingDeferredNote } = 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 ? computePoint(recipient) : undefined, + outgoingNotePayload ? recipientCompleteAddress.publicKeys.masterOutgoingViewingPublicKey : undefined, + payload!, + txEffect.txHash, + txEffect.noteHashes, + scopedLog.dataStartIndexForTx, + excludedIndices.get(scopedLog.txHash.toString())!, + this.log, + txEffect.unencryptedLogs, + ); + + if (incomingNote) { + incomingNotes.push(incomingNote); + } + if (outgoingNote) { + outgoingNotes.push(outgoingNote); + } + if (incomingDeferredNote) { + deferredIncomingNotes.push(incomingDeferredNote); + } + if (outgoingDeferredNote) { + deferredOutgoingNotes.push(outgoingDeferredNote); + } + } + } + if (deferredIncomingNotes.length || deferredOutgoingNotes.length) { + this.log.warn('Found deferred notes when processing tagged logs. This should not happen.'); + } + + return { incomingNotes, outgoingNotes }; + } + + /** + * Processes the tagged logs returned by syncTaggedLogs by decrypting them and storing them in the database. + * @param logs - The logs to process. + * @param recipient - The recipient of the logs. + */ + public async processTaggedLogs( + logs: TxScopedEncryptedL2NoteLog[], + recipient: AztecAddress, + simulator?: AcirSimulator, + ): Promise { + const { incomingNotes, outgoingNotes } = await this.#decryptTaggedLogs( + logs, + recipient, + simulator ?? getAcirSimulator(this.db, this.aztecNode, this.keyStore, this.contractDataOracle), + ); + if (incomingNotes.length || outgoingNotes.length) { + await this.db.addNotes(incomingNotes, outgoingNotes, recipient); + incomingNotes.forEach(noteDao => { + this.log.verbose( + `Added incoming note for contract ${noteDao.contractAddress} at slot ${ + noteDao.storageSlot + } with nullifier ${noteDao.siloedNullifier.toString()}`, + ); + }); + outgoingNotes.forEach(noteDao => { + this.log.verbose(`Added outgoing note for contract ${noteDao.contractAddress} at slot ${noteDao.storageSlot}`); + }); + } + const nullifiedNotes: IncomingNoteDao[] = []; + for (const incomingNote of incomingNotes) { + // NOTE: this leaks information about the nullifiers I'm interested in to the node. + const found = await this.aztecNode.findLeafIndex( + 'latest', + MerkleTreeId.NULLIFIER_TREE, + incomingNote.siloedNullifier, + ); + if (found) { + nullifiedNotes.push(incomingNote); + } + } + await this.db.removeNullifiedNotes( + nullifiedNotes.map(note => note.siloedNullifier), + computePoint(recipient), + ); + nullifiedNotes.forEach(noteDao => { + this.log.verbose( + `Removed note for contract ${noteDao.contractAddress} at slot ${ + noteDao.storageSlot + } with nullifier ${noteDao.siloedNullifier.toString()}`, + ); + }); + } } diff --git a/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts index af4cfcdcb2bb..a10c21271e09 100644 --- a/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts +++ b/yarn-project/pxe/src/simulator_oracle/simulator_oracle.test.ts @@ -1,25 +1,104 @@ -import { type AztecNode, EncryptedL2NoteLog } from '@aztec/circuit-types'; +import { + type AztecNode, + EncryptedL2NoteLog, + EncryptedLogPayload, + L1NotePayload, + Note, + type TxEffect, + TxHash, + TxScopedEncryptedL2NoteLog, +} from '@aztec/circuit-types'; import { AztecAddress, CompleteAddress, type Fq, Fr, + GrumpkinScalar, + INITIAL_L2_BLOCK_NUM, + KeyValidationRequest, + MAX_NOTE_HASHES_PER_TX, TaggingSecret, computeAddress, + computeOvskApp, computeTaggingSecret, deriveKeys, } from '@aztec/circuits.js'; -import { poseidon2Hash } from '@aztec/foundation/crypto'; +import { pedersenHash, poseidon2Hash } from '@aztec/foundation/crypto'; import { KeyStore } from '@aztec/key-store'; import { openTmpStore } from '@aztec/kv-store/utils'; +import { type AcirSimulator } from '@aztec/simulator'; +import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; import times from 'lodash.times'; +import { type IncomingNoteDao } from '../database/incoming_note_dao.js'; import { type PxeDatabase } from '../database/index.js'; import { KVPxeDatabase } from '../database/kv_pxe_database.js'; +import { type OutgoingNoteDao } from '../database/outgoing_note_dao.js'; import { ContractDataOracle } from '../index.js'; -import { SimulatorOracle } from './index.js'; +import { type SimulatorOracle } from './index.js'; + +const TXS_PER_BLOCK = 4; +const NUM_NOTE_HASHES_PER_BLOCK = TXS_PER_BLOCK * MAX_NOTE_HASHES_PER_TX; + +function getRandomNoteLogPayload(tag = Fr.random(), app = AztecAddress.random()): EncryptedLogPayload { + return new EncryptedLogPayload(tag, app, L1NotePayload.random(app).toIncomingBodyPlaintext()); +} + +/** A wrapper containing info about a note we want to mock and insert into a block. */ +class MockNoteRequest { + constructor( + /** Log payload corresponding to a note we want to insert into a block. */ + public readonly logPayload: EncryptedLogPayload, + /** Block number this note corresponds to. */ + public readonly blockNumber: number, + /** Index of a tx within a block this note corresponds to. */ + public readonly txIndex: number, + /** Index of a note hash within a list of note hashes for 1 tx. */ + public readonly noteHashIndex: number, + /** Address point we use when encrypting a note. */ + public readonly recipient: AztecAddress, + /** ovKeys we use when encrypting a note. */ + public readonly ovKeys: KeyValidationRequest, + ) { + if (blockNumber < INITIAL_L2_BLOCK_NUM) { + throw new Error(`Block number should be greater than or equal to ${INITIAL_L2_BLOCK_NUM}.`); + } + if (noteHashIndex >= MAX_NOTE_HASHES_PER_TX) { + throw new Error(`Data index should be less than ${MAX_NOTE_HASHES_PER_TX}.`); + } + if (txIndex >= TXS_PER_BLOCK) { + throw new Error(`Tx index should be less than ${TXS_PER_BLOCK}.`); + } + } + + encrypt(): EncryptedL2NoteLog { + const ephSk = GrumpkinScalar.random(); + const log = this.logPayload.encrypt(ephSk, this.recipient, this.ovKeys); + return new EncryptedL2NoteLog(log); + } + + get indexWithinNoteHashTree(): bigint { + return BigInt( + (this.blockNumber - 1) * NUM_NOTE_HASHES_PER_BLOCK + this.txIndex * MAX_NOTE_HASHES_PER_TX + this.noteHashIndex, + ); + } + + get snippetOfNoteDao() { + const payload = L1NotePayload.fromIncomingBodyPlaintextContractAndPublicValues( + this.logPayload.incomingBodyPlaintext, + this.logPayload.contractAddress, + [], + )!; + return { + note: new Note(payload.privateNoteValues), + contractAddress: payload.contractAddress, + storageSlot: payload.storageSlot, + noteTypeId: payload.noteTypeId, + }; + } +} function computeTagForIndex( sender: { completeAddress: CompleteAddress; ivsk: Fq }, @@ -40,84 +119,118 @@ describe('Simulator oracle', () => { let keyStore: KeyStore; let recipient: CompleteAddress; + let recipientOvKeys: KeyValidationRequest; let contractAddress: AztecAddress; - const NUM_SENDERS = 10; - let senders: { completeAddress: CompleteAddress; ivsk: Fq }[]; - beforeEach(async () => { const db = openTmpStore(); aztecNode = mock(); database = new KVPxeDatabase(db); contractDataOracle = new ContractDataOracle(database); keyStore = new KeyStore(db); - simulatorOracle = new SimulatorOracle(contractDataOracle, database, keyStore, aztecNode); + const simulatorOracleModule = await import('../simulator_oracle/index.js'); + simulatorOracle = new simulatorOracleModule.SimulatorOracle(contractDataOracle, database, keyStore, aztecNode); // Set up contract address contractAddress = AztecAddress.random(); // Set up recipient account recipient = await keyStore.addAccount(new Fr(69), Fr.random()); + const recipientOvskApp = await keyStore.getAppOutgoingViewingSecretKey(recipient.address, contractAddress); await database.addCompleteAddress(recipient); - // Set up the address book - senders = times(NUM_SENDERS).map((_, index) => { - const keys = deriveKeys(new Fr(index)); - const partialAddress = Fr.random(); - const address = computeAddress(keys.publicKeys, partialAddress); - const completeAddress = new CompleteAddress(address, keys.publicKeys, partialAddress); - return { completeAddress, ivsk: keys.masterIncomingViewingSecretKey }; - }); - for (const sender of senders) { - await database.addContactAddress(sender.completeAddress.address); - } + recipientOvKeys = new KeyValidationRequest(recipient.publicKeys.masterOutgoingViewingPublicKey, recipientOvskApp); + }); - const logs: { [k: string]: EncryptedL2NoteLog[] } = {}; + describe('sync tagged logs', () => { + const NUM_SENDERS = 10; + let senders: { completeAddress: CompleteAddress; ivsk: Fq }[]; - // Add a random note from every address in the address book for our account with index 0 - // Compute the tag as sender (knowledge of preaddress and ivsk) - for (const sender of senders) { - const tag = computeTagForIndex(sender, recipient.address, contractAddress, 0); - const log = EncryptedL2NoteLog.random(tag); - logs[tag.toString()] = [log]; - } - // Accumulated logs intended for recipient: NUM_SENDERS - - // Add a random note from the first sender in the address book, repeating the tag - // Compute the tag as sender (knowledge of preaddress and ivsk) - const firstSender = senders[0]; - const tag = computeTagForIndex(firstSender, recipient.address, contractAddress, 0); - const log = EncryptedL2NoteLog.random(tag); - logs[tag.toString()].push(log); - // Accumulated logs intended for recipient: NUM_SENDERS + 1 - - // Add a random note from half the address book for our account with index 1 - // Compute the tag as sender (knowledge of preaddress and ivsk) - for (let i = NUM_SENDERS / 2; i < NUM_SENDERS; i++) { - const sender = senders[i]; - const tag = computeTagForIndex(sender, recipient.address, contractAddress, 1); - const log = EncryptedL2NoteLog.random(tag); - logs[tag.toString()] = [log]; - } - // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 - - // Add a random note from every address in the address book for a random recipient with index 0 - // Compute the tag as sender (knowledge of preaddress and ivsk) - for (const sender of senders) { - const keys = deriveKeys(Fr.random()); - const partialAddress = Fr.random(); - const randomRecipient = computeAddress(keys.publicKeys, partialAddress); - const tag = computeTagForIndex(sender, randomRecipient, contractAddress, 0); - const log = EncryptedL2NoteLog.random(tag); - logs[tag.toString()] = [log]; - } - // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 + beforeEach(async () => { + // Set up the address book + senders = times(NUM_SENDERS).map((_, index) => { + const keys = deriveKeys(new Fr(index)); + const partialAddress = Fr.random(); + const address = computeAddress(keys.publicKeys, partialAddress); + const completeAddress = new CompleteAddress(address, keys.publicKeys, partialAddress); + return { completeAddress, ivsk: keys.masterIncomingViewingSecretKey }; + }); + for (const sender of senders) { + await database.addContactAddress(sender.completeAddress.address); + } + + const logs: { [k: string]: TxScopedEncryptedL2NoteLog[] } = {}; + + // Add a random note from every address in the address book for our account with index 0 + // Compute the tag as sender (knowledge of preaddress and ivsk) + for (const sender of senders) { + const tag = computeTagForIndex(sender, recipient.address, contractAddress, 0); + const randomNote = new MockNoteRequest( + getRandomNoteLogPayload(tag, contractAddress), + 1, + 1, + 1, + recipient.address, + recipientOvKeys, + ); + const log = new TxScopedEncryptedL2NoteLog(TxHash.random(), 0, randomNote.encrypt()); + logs[tag.toString()] = [log]; + } + // Accumulated logs intended for recipient: NUM_SENDERS + + // Add a random note from the first sender in the address book, repeating the tag + // Compute the tag as sender (knowledge of preaddress and ivsk) + const firstSender = senders[0]; + const tag = computeTagForIndex(firstSender, recipient.address, contractAddress, 0); + const log = new TxScopedEncryptedL2NoteLog(TxHash.random(), 0, EncryptedL2NoteLog.random(tag)); + logs[tag.toString()].push(log); + // Accumulated logs intended for recipient: NUM_SENDERS + 1 - // Set up the getTaggedLogs mock + // Add a random note from half the address book for our account with index 1 + // Compute the tag as sender (knowledge of preaddress and ivsk) + for (let i = NUM_SENDERS / 2; i < NUM_SENDERS; i++) { + const sender = senders[i]; + const tag = computeTagForIndex(sender, recipient.address, contractAddress, 1); + const randomNote = new MockNoteRequest( + getRandomNoteLogPayload(tag, contractAddress), + 1, + 1, + 1, + recipient.address, + recipientOvKeys, + ); + const log = new TxScopedEncryptedL2NoteLog(TxHash.random(), 0, randomNote.encrypt()); + logs[tag.toString()] = [log]; + } + // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 - aztecNode.getLogsByTags.mockImplementation(tags => { - return Promise.resolve(tags.map(tag => logs[tag.toString()] ?? [])); + // Add a random note from every address in the address book for a random recipient with index 0 + // Compute the tag as sender (knowledge of preaddress and ivsk) + for (const sender of senders) { + const keys = deriveKeys(Fr.random()); + const partialAddress = Fr.random(); + const randomRecipient = computeAddress(keys.publicKeys, partialAddress); + const tag = computeTagForIndex(sender, randomRecipient, contractAddress, 0); + const randomNote = new MockNoteRequest( + getRandomNoteLogPayload(tag, contractAddress), + 1, + 1, + 1, + randomRecipient, + new KeyValidationRequest( + keys.publicKeys.masterOutgoingViewingPublicKey, + computeOvskApp(keys.masterOutgoingViewingSecretKey, contractAddress), + ), + ); + const log = new TxScopedEncryptedL2NoteLog(TxHash.random(), 0, randomNote.encrypt()); + logs[tag.toString()] = [log]; + } + // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 + + // Set up the getTaggedLogs mock + + aztecNode.getLogsByTags.mockImplementation(tags => { + return Promise.resolve(tags.map(tag => logs[tag.toString()] ?? [])); + }); }); - }); - describe('sync tagged logs', () => { it('should sync tagged logs', async () => { const syncedLogs = await simulatorOracle.syncTaggedLogs(contractAddress, recipient.address); // We expect to have all logs intended for the recipient, one per sender + 1 with a duplicated tag for the first one + half of the logs for the second index @@ -162,4 +275,274 @@ describe('Simulator oracle', () => { expect(aztecNode.getLogsByTags.mock.calls.length).toBe(2); }); }); + + describe('Process notes', () => { + let addNotesSpy: any; + let simulator: MockProxy; + + beforeEach(() => { + addNotesSpy = jest.spyOn(database, 'addNotes'); + simulator = mock(); + simulator.computeNoteHashAndOptionallyANullifier.mockImplementation((...args: any) => + Promise.resolve({ + noteHash: Fr.random(), + uniqueNoteHash: Fr.random(), + siloedNoteHash: pedersenHash(args[5].items), // args[5] is note + innerNullifier: Fr.random(), + }), + ); + }); + + afterEach(() => { + addNotesSpy.mockReset(); + simulator.computeNoteHashAndOptionallyANullifier.mockReset(); + aztecNode.getTxEffect.mockReset(); + }); + + function mockTaggedLogs(requests: MockNoteRequest[]) { + const txEffectsMap: { [k: string]: { noteHashes: Fr[]; txHash: TxHash } } = {}; + const taggedLogs: TxScopedEncryptedL2NoteLog[] = []; + const groupedByTx = requests.reduce<{ [i: number]: { [j: number]: MockNoteRequest[] } }>((acc, request) => { + if (!acc[request.blockNumber]) { + acc[request.blockNumber] = {}; + } + if (!acc[request.blockNumber][request.txIndex]) { + acc[request.blockNumber][request.txIndex] = []; + } + acc[request.blockNumber][request.txIndex].push(request); + return acc; + }, {}); + Object.keys(groupedByTx).forEach(blockNumberKey => { + const blockNumber = parseInt(blockNumberKey); + Object.keys(groupedByTx[blockNumber]).forEach(txIndexKey => { + const txIndex = parseInt(txIndexKey); + const requestsInTx = groupedByTx[blockNumber][txIndex]; + const maxNoteIndex = Math.max(...requestsInTx.map(request => request.noteHashIndex)); + const txHash = TxHash.random(); + for (const request of requestsInTx) { + if (!txEffectsMap[txHash.toString()]) { + txEffectsMap[txHash.toString()] = { + txHash, + noteHashes: Array(maxNoteIndex + 1) + .fill(0) + .map(() => Fr.random()), + }; + } + const dataStartIndex = + (request.blockNumber - 1) * NUM_NOTE_HASHES_PER_BLOCK + request.txIndex * MAX_NOTE_HASHES_PER_TX; + const taggedLog = new TxScopedEncryptedL2NoteLog(txHash, dataStartIndex, request.encrypt()); + const note = request.snippetOfNoteDao.note; + const noteHash = pedersenHash(note.items); + txEffectsMap[txHash.toString()].noteHashes[request.noteHashIndex] = noteHash; + taggedLogs.push(taggedLog); + } + }); + }); + + aztecNode.getTxEffect.mockImplementation(txHash => { + return Promise.resolve(txEffectsMap[txHash.toString()] as TxEffect); + }); + return taggedLogs; + } + + it('should store an incoming note that belongs to us', async () => { + const request = new MockNoteRequest( + getRandomNoteLogPayload(Fr.random(), contractAddress), + 4, + 0, + 2, + recipient.address, + KeyValidationRequest.random(), + ); + const taggedLogs = mockTaggedLogs([request]); + + await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); + + expect(addNotesSpy).toHaveBeenCalledTimes(1); + expect(addNotesSpy).toHaveBeenCalledWith( + [ + expect.objectContaining({ + ...request.snippetOfNoteDao, + index: request.indexWithinNoteHashTree, + }), + ], + [], + recipient.address, + ); + }, 25_000); + + it('should store an outgoing note that belongs to us', async () => { + const request = new MockNoteRequest( + getRandomNoteLogPayload(Fr.random(), contractAddress), + 4, + 0, + 2, + CompleteAddress.random().address, + recipientOvKeys, + ); + + const taggedLogs = mockTaggedLogs([request]); + + await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); + + expect(addNotesSpy).toHaveBeenCalledTimes(1); + // For outgoing notes, the resulting DAO does not contain index. + expect(addNotesSpy).toHaveBeenCalledWith( + [], + [expect.objectContaining(request.snippetOfNoteDao)], + recipient.address, + ); + }, 25_000); + + it('should store multiple notes that belong to us', async () => { + const requests = [ + new MockNoteRequest( + getRandomNoteLogPayload(Fr.random(), contractAddress), + 1, + 1, + 1, + recipient.address, + recipientOvKeys, + ), + new MockNoteRequest( + getRandomNoteLogPayload(Fr.random(), contractAddress), + 2, + 3, + 0, + CompleteAddress.random().address, + recipientOvKeys, + ), + new MockNoteRequest( + getRandomNoteLogPayload(Fr.random(), contractAddress), + 6, + 3, + 2, + recipient.address, + KeyValidationRequest.random(), + ), + new MockNoteRequest( + getRandomNoteLogPayload(Fr.random(), contractAddress), + 9, + 3, + 2, + CompleteAddress.random().address, + KeyValidationRequest.random(), + ), + new MockNoteRequest( + getRandomNoteLogPayload(Fr.random(), contractAddress), + 12, + 3, + 2, + recipient.address, + recipientOvKeys, + ), + ]; + + const taggedLogs = mockTaggedLogs(requests); + + await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); + + expect(addNotesSpy).toHaveBeenCalledTimes(1); + expect(addNotesSpy).toHaveBeenCalledWith( + // Incoming should contain notes from requests 0, 2, 4 because in those requests we set owner address point. + [ + expect.objectContaining({ + ...requests[0].snippetOfNoteDao, + index: requests[0].indexWithinNoteHashTree, + }), + expect.objectContaining({ + ...requests[2].snippetOfNoteDao, + index: requests[2].indexWithinNoteHashTree, + }), + expect.objectContaining({ + ...requests[4].snippetOfNoteDao, + index: requests[4].indexWithinNoteHashTree, + }), + ], + // Outgoing should contain notes from requests 0, 1, 4 because in those requests we set owner ovKeys. + [ + expect.objectContaining(requests[0].snippetOfNoteDao), + expect.objectContaining(requests[1].snippetOfNoteDao), + expect.objectContaining(requests[4].snippetOfNoteDao), + ], + recipient.address, + ); + }, 30_000); + + it('should not store notes that do not belong to us', async () => { + // Both notes should be ignored because the encryption keys do not belong to owner (they are random). + const requests = [ + new MockNoteRequest( + getRandomNoteLogPayload(), + 2, + 1, + 1, + CompleteAddress.random().address, + KeyValidationRequest.random(), + ), + new MockNoteRequest( + getRandomNoteLogPayload(), + 2, + 3, + 0, + CompleteAddress.random().address, + KeyValidationRequest.random(), + ), + ]; + + const taggedLogs = mockTaggedLogs(requests); + + await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); + + expect(addNotesSpy).toHaveBeenCalledTimes(0); + }); + + it('should be able to recover two note payloads containing the same note', async () => { + const note = getRandomNoteLogPayload(Fr.random(), contractAddress); + const note2 = getRandomNoteLogPayload(Fr.random(), contractAddress); + // All note payloads except one have the same contract address, storage slot, and the actual note. + const requests = [ + new MockNoteRequest(note, 3, 0, 0, recipient.address, recipientOvKeys), + new MockNoteRequest(note, 4, 0, 2, recipient.address, recipientOvKeys), + new MockNoteRequest(note, 4, 2, 0, recipient.address, recipientOvKeys), + new MockNoteRequest(note2, 5, 2, 1, recipient.address, recipientOvKeys), + new MockNoteRequest(note, 6, 2, 3, recipient.address, recipientOvKeys), + ]; + + const taggedLogs = mockTaggedLogs(requests); + + await simulatorOracle.processTaggedLogs(taggedLogs, recipient.address, simulator); + + // First we check incoming + { + const addedIncoming: IncomingNoteDao[] = addNotesSpy.mock.calls[0][0]; + expect(addedIncoming.map(dao => dao)).toEqual([ + expect.objectContaining({ ...requests[0].snippetOfNoteDao, index: requests[0].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[1].snippetOfNoteDao, index: requests[1].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[2].snippetOfNoteDao, index: requests[2].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[3].snippetOfNoteDao, index: requests[3].indexWithinNoteHashTree }), + expect.objectContaining({ ...requests[4].snippetOfNoteDao, index: requests[4].indexWithinNoteHashTree }), + ]); + + // Check that every note has a different nonce. + const nonceSet = new Set(); + addedIncoming.forEach(info => nonceSet.add(info.nonce.value)); + expect(nonceSet.size).toBe(requests.length); + } + + // Then we check outgoing + { + const addedOutgoing: OutgoingNoteDao[] = addNotesSpy.mock.calls[0][1]; + expect(addedOutgoing.map(dao => dao)).toEqual([ + expect.objectContaining(requests[0].snippetOfNoteDao), + expect.objectContaining(requests[1].snippetOfNoteDao), + expect.objectContaining(requests[2].snippetOfNoteDao), + expect.objectContaining(requests[3].snippetOfNoteDao), + expect.objectContaining(requests[4].snippetOfNoteDao), + ]); + + // Outgoing note daos do not have a nonce so we don't check it. + } + }); + }); }); diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 00f413048424..cb4f063cf1bf 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -424,8 +424,7 @@ export class Oracle { ); } - async getAppTaggingSecretsForSenders([recipient]: ACVMField[]): Promise { - const taggingSecrets = await this.typedOracle.getAppTaggingSecretsForSenders(AztecAddress.fromString(recipient)); - return taggingSecrets.flatMap(taggingSecret => taggingSecret.toFields().map(toACVMField)); + async syncNotes([recipient]: ACVMField[]) { + await this.typedOracle.syncNotes(AztecAddress.fromString(recipient)); } } diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 83aec5ed0d3f..76fa4b31f2f0 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -262,7 +262,7 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('incrementAppTaggingSecret'); } - getAppTaggingSecretsForSenders(_recipient: AztecAddress): Promise { - throw new OracleMethodNotAvailableError('getAppTaggingSecretsForSenders'); + syncNotes(_recipient: AztecAddress): Promise { + throw new OracleMethodNotAvailableError('syncNotes'); } } diff --git a/yarn-project/simulator/src/client/client_execution_context.ts b/yarn-project/simulator/src/client/client_execution_context.ts index 773c0449f756..be7e8bcde19d 100644 --- a/yarn-project/simulator/src/client/client_execution_context.ts +++ b/yarn-project/simulator/src/client/client_execution_context.ts @@ -613,4 +613,9 @@ export class ClientExecutionContext extends ViewDataOracle { public override async incrementAppTaggingSecret(sender: AztecAddress, recipient: AztecAddress) { await this.db.incrementAppTaggingSecret(this.contractAddress, sender, recipient); } + + public override async syncNotes(recipient: AztecAddress) { + const taggedLogs = await this.db.syncTaggedLogs(this.contractAddress, recipient); + await this.db.processTaggedLogs(taggedLogs, recipient); + } } diff --git a/yarn-project/simulator/src/client/db_oracle.ts b/yarn-project/simulator/src/client/db_oracle.ts index d94cc95d0b06..304ea84d76c0 100644 --- a/yarn-project/simulator/src/client/db_oracle.ts +++ b/yarn-project/simulator/src/client/db_oracle.ts @@ -4,6 +4,7 @@ import { type NoteStatus, type NullifierMembershipWitness, type PublicDataWitness, + type TxScopedEncryptedL2NoteLog, } from '@aztec/circuit-types'; import { type CompleteAddress, @@ -197,7 +198,7 @@ export interface DBOracle extends CommitmentsDB { /** * Returns the tagging secret for a given sender and recipient pair. For this to work, the ivpsk_m of the sender must be known. - * Includes the last known index used for tagging with this secret. + * Includes the next index to be used used for tagging with this secret. * @param contractAddress - The contract address to silo the secret for * @param sender - The address sending the note * @param recipient - The address receiving the note @@ -222,13 +223,18 @@ export interface DBOracle extends CommitmentsDB { ): Promise; /** - * Returns the siloed tagging secrets for a given recipient and all the senders in the address book - * @param contractAddress - The contract address to silo the secret for - * @param recipient - The address receiving the notes - * @returns A list of siloed tagging secrets + * Synchronizes the logs tagged with the recipient's address and all the senders in the addressbook. + * Returns the unsynched logs and updates the indexes of the secrets used to tag them until there are no more logs to sync. + * @param contractAddress - The address of the contract that the logs are tagged for + * @param recipient - The address of the recipient + * @returns A list of encrypted logs tagged with the recipient's address */ - getAppTaggingSecretsForSenders( - contractAddress: AztecAddress, - recipient: AztecAddress, - ): Promise; + syncTaggedLogs(contractAddress: AztecAddress, recipient: AztecAddress): Promise; + + /** + * Processes the tagged logs returned by syncTaggedLogs by decrypting them and storing them in the database. + * @param logs - The logs to process. + * @param recipient - The recipient of the logs. + */ + processTaggedLogs(logs: TxScopedEncryptedL2NoteLog[], 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 b8ca0266fa4b..57fb6695e236 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -296,7 +296,7 @@ export class ViewDataOracle extends TypedOracle { /** * Returns the tagging secret for a given sender and recipient pair, siloed to the current contract address. - * Includes the last known index used for tagging with this secret. + * Includes the next index to be used used for tagging with this secret. * For this to work, the ivpsk_m of the sender must be known. * @param sender - The address sending the note * @param recipient - The address receiving the note @@ -308,14 +308,4 @@ export class ViewDataOracle extends TypedOracle { ): Promise { return await this.db.getAppTaggingSecret(this.contractAddress, sender, recipient); } - - /** - * Returns the siloed tagging secrets for a given recipient and all the senders in the address book - * @param contractAddress - The contract address to silo the secret for - * @param recipient - The address receiving the notes - * @returns A list of siloed tagging secrets - */ - public override async getAppTaggingSecretsForSenders(recipient: AztecAddress): Promise { - return await this.db.getAppTaggingSecretsForSenders(this.contractAddress, recipient); - } } diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 151a85b6539a..dff3c63ac7d8 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -803,7 +803,7 @@ export class TXE implements TypedOracle { return directionalSecret; } - async getAppTaggingSecretsForSenders(recipient: AztecAddress): Promise { + async #getAppTaggingSecretsForSenders(recipient: AztecAddress): Promise { const recipientCompleteAddress = await this.getCompleteAddress(recipient); const completeAddresses = await this.txeDatabase.getCompleteAddresses(); // Filter out the addresses corresponding to accounts @@ -821,6 +821,11 @@ export class TXE implements TypedOracle { return secrets.map((secret, i) => new IndexedTaggingSecret(secret, recipient, indexes[i])); } + syncNotes(_recipient: AztecAddress) { + // TODO: Implement + return Promise.resolve(); + } + // AVM oracles async avmOpcodeCall( diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 52ceb74f06f5..107211915f87 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -608,11 +608,9 @@ export class TXEService { return toForeignCallResult([toArray(secret.toFields())]); } - async getAppTaggingSecretsForSenders(recipient: ForeignCallSingle) { - const secrets = await this.typedOracle.getAppTaggingSecretsForSenders( - AztecAddress.fromField(fromSingle(recipient)), - ); - return toForeignCallResult([toArray(secrets.flatMap(secret => secret.toFields()))]); + async syncNotes(recipient: ForeignCallSingle) { + await this.typedOracle.syncNotes(AztecAddress.fromField(fromSingle(recipient))); + return toForeignCallResult([]); } // AVM opcodes