From e8de31f4f0a130b8aec04926468ced28b47edecf Mon Sep 17 00:00:00 2001 From: benesjan <13470840+benesjan@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:15:31 +0000 Subject: [PATCH] refactor!: splitting `getLogsByTags` to private and public versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #14555 ## New PR Description As mentioned in the old PR description I originally planned to just type the arg of `getLogsByTags` but while addressing this PR's feedback I realized that I made a mistake: the `getLogsByTags` endpoint was used for both private and public logs and the issue is that for public logs we use "unsiloed" tag and for private logs we use siloed tag. Having these 2 endpoints merged was just a tech debt: there is not a place where we would call this endpoint expecting only 1 type of logs. This led to us having ugly utilities that filtered out one kind of logs after retrieval. We didn't split this before because we didn't want to introduce a breaking change to the API. Given that I am already breaking backwards compatibility and given the error I made it made sense to fix this by splitting the two endpoints. Now we have `getPrivateLogsByTag` and `getPublicLogsByTagsFromContract` endpoints. ## Old PR Description In this PR I used the `SiloedTag` type for the param in `getLogsByTags`. This allowed me to drop some ugly type conversions. This was just a tech debt. The diff turned out to be quite large because I needed to move the type from `pxe` to `stdlib`. As mentioned in a PR down the stack this is second of 3 PRs in which I clean up the `getLogsByTags` endpoint: 1. First PR - including block timestamp in return value, 2. This is the second PR - I type the tag arg of the function to be `SiloedTag`, 3. in the last PR I will drop pagination from this endpoint. Co-authored-by: Jan Beneš --- .../archiver/src/archiver/archiver.ts | 39 +++- .../archiver/src/archiver/archiver_store.ts | 24 ++- .../src/archiver/archiver_store_test_suite.ts | 200 ++++++++++++++---- .../kv_archiver_store/kv_archiver_store.ts | 25 ++- .../archiver/kv_archiver_store/log_store.ts | 198 +++++++++++++---- .../aztec-node/src/aztec-node/server.ts | 21 +- .../log_retrieval_request.test.ts | 3 +- .../noir-structs/log_retrieval_request.ts | 9 +- .../oracle/interfaces.ts | 3 +- .../oracle/private_execution.test.ts | 3 +- .../oracle/private_execution_oracle.ts | 2 +- yarn-project/pxe/src/logs/log_service.test.ts | 62 +++--- yarn-project/pxe/src/logs/log_service.ts | 48 ++--- yarn-project/pxe/src/pxe.test.ts | 3 +- yarn-project/pxe/src/tagging/index.ts | 4 +- ...ate_logs_for_sender_recipient_pair.test.ts | 30 ++- .../utils/load_logs_for_range.test.ts | 76 ++----- .../utils/load_logs_for_range.ts | 24 +-- yarn-project/pxe/src/tagging/siloed_tag.ts | 22 -- .../sync/sync_sender_tagging_indexes.test.ts | 30 +-- ...load_and_store_new_tagging_indexes.test.ts | 56 +++-- .../load_and_store_new_tagging_indexes.ts | 8 +- yarn-project/pxe/src/tagging/tag.ts | 16 -- yarn-project/stdlib/src/hash/hash.ts | 11 - .../stdlib/src/interfaces/archiver.test.ts | 27 ++- .../stdlib/src/interfaces/archiver.ts | 10 +- .../stdlib/src/interfaces/aztec-node.test.ts | 28 ++- .../stdlib/src/interfaces/aztec-node.ts | 38 +++- .../stdlib/src/interfaces/l2_logs_source.ts | 27 ++- yarn-project/stdlib/src/logs/index.ts | 2 + yarn-project/stdlib/src/logs/siloed_tag.ts | 44 ++++ yarn-project/stdlib/src/logs/tag.ts | 42 ++++ .../stdlib/src/logs/tx_scoped_l2_log.ts | 2 +- 33 files changed, 727 insertions(+), 410 deletions(-) delete mode 100644 yarn-project/pxe/src/tagging/siloed_tag.ts delete mode 100644 yarn-project/pxe/src/tagging/tag.ts create mode 100644 yarn-project/stdlib/src/logs/siloed_tag.ts create mode 100644 yarn-project/stdlib/src/logs/tag.ts diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index b841a6e42577..2f9381503b62 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -64,7 +64,15 @@ import { } from '@aztec/stdlib/epoch-helpers'; import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client'; import type { L2LogsSource } from '@aztec/stdlib/interfaces/server'; -import { ContractClassLog, type LogFilter, type PrivateLog, type PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { + ContractClassLog, + type LogFilter, + type PrivateLog, + type PublicLog, + type SiloedTag, + Tag, + TxScopedL2Log, +} from '@aztec/stdlib/logs'; import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging'; import type { CheckpointHeader } from '@aztec/stdlib/rollup'; import { type BlockHeader, type IndexedTxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx'; @@ -1407,14 +1415,16 @@ export class Archiver return this.store.getSettledTxReceipt(txHash); } - /** - * 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. - */ - getLogsByTags(tags: Fr[]): Promise { - return this.store.getLogsByTags(tags); + getPrivateLogsByTags(tags: SiloedTag[], logsPerTag?: number): Promise { + return this.store.getPrivateLogsByTags(tags, logsPerTag); + } + + getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + logsPerTag?: number, + ): Promise { + return this.store.getPublicLogsByTagsFromContract(contractAddress, tags, logsPerTag); } /** @@ -2072,8 +2082,15 @@ export class ArchiverStoreHelper getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { return this.store.getL1ToL2MessageIndex(l1ToL2Message); } - getLogsByTags(tags: Fr[], logsPerTag?: number): Promise { - return this.store.getLogsByTags(tags, logsPerTag); + getPrivateLogsByTags(tags: SiloedTag[], logsPerTag?: number): Promise { + return this.store.getPrivateLogsByTags(tags, logsPerTag); + } + getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + logsPerTag?: number, + ): Promise { + return this.store.getPublicLogsByTagsFromContract(contractAddress, tags, logsPerTag); } getPublicLogs(filter: LogFilter): Promise { return this.store.getPublicLogs(filter); diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index dbcf98be4770..f39230e8a0fb 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -14,7 +14,7 @@ import type { UtilityFunctionWithMembershipProof, } from '@aztec/stdlib/contract'; import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client'; -import type { LogFilter, TxScopedL2Log } from '@aztec/stdlib/logs'; +import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import { BlockHeader, type IndexedTxEffect, type TxHash, type TxReceipt } from '@aztec/stdlib/tx'; import type { UInt64 } from '@aztec/stdlib/types'; @@ -206,13 +206,27 @@ export interface ArchiverDataStore { getTotalL1ToL2MessageCount(): Promise; /** - * 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. + * Gets all private logs that match any of the received tags (i.e. logs with their first field equal to a SiloedTag). + * @param tags - The SiloedTags to filter the logs by. * @param logsPerTag - The number of logs to return per tag. Defaults to everything - * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match + * @returns For each received tag, an array of matching private logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[], logsPerTag?: number): Promise; + getPrivateLogsByTags(tags: SiloedTag[], logsPerTag?: number): Promise; + + /** + * Gets all public logs that match any of the received tags from the specified contract (i.e. logs with their first field equal to a Tag). + * @param contractAddress - The contract that emitted the public logs. + * @param tags - The Tags to filter the logs by. + * @param logsPerTag - The number of logs to return per tag. Defaults to everything + * @returns For each received tag, an array of matching public logs is returned. An empty array implies no logs match + * that tag. + */ + getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + logsPerTag?: number, + ): Promise; /** * Gets public 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 733e745b99ee..0cd292f299ef 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -30,7 +30,7 @@ import { SerializableContractInstance, computePublicBytecodeCommitment, } from '@aztec/stdlib/contract'; -import { ContractClassLog, LogId, PrivateLog, PublicLog } from '@aztec/stdlib/logs'; +import { ContractClassLog, LogId, PrivateLog, PublicLog, SiloedTag, Tag } from '@aztec/stdlib/logs'; import { InboxLeaf } from '@aztec/stdlib/messaging'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { @@ -2198,46 +2198,35 @@ export function describeArchiverDataStore( }); }); - describe('getLogsByTags', () => { + describe('getPrivateLogsByTags', () => { const numBlocksForLogs = 3; const numTxsPerBlock = 4; const numPrivateLogsPerTx = 3; - const numPublicLogsPerTx = 2; let logsCheckpoints: PublishedCheckpoint[]; - const makeTag = (blockNumber: number, txIndex: number, logIndex: number, isPublic = false) => - blockNumber === 1 && txIndex === 0 && logIndex === 0 - ? Fr.ZERO // Shared tag - : new Fr((blockNumber * 100 + txIndex * 10 + logIndex) * (isPublic ? 123 : 1)); + const makePrivateLogTag = (blockNumber: number, txIndex: number, logIndex: number): SiloedTag => + new SiloedTag( + blockNumber === 1 && txIndex === 0 && logIndex === 0 + ? Fr.ZERO // Shared tag + : new Fr(blockNumber * 100 + txIndex * 10 + logIndex), + ); - const makePrivateLog = (tag: Fr) => + const makePrivateLog = (tag: SiloedTag) => PrivateLog.from({ - fields: makeTuple(PRIVATE_LOG_SIZE_IN_FIELDS, i => (!i ? tag : new Fr(tag.toNumber() + i))), + fields: makeTuple(PRIVATE_LOG_SIZE_IN_FIELDS, i => + !i ? tag.value : new Fr(tag.value.toBigInt() + BigInt(i)), + ), emittedLength: PRIVATE_LOG_SIZE_IN_FIELDS, }); - const makePublicLog = (tag: Fr) => - PublicLog.from({ - contractAddress: AztecAddress.fromNumber(1), - // Arbitrary length - fields: new Array(10).fill(null).map((_, i) => (!i ? tag : new Fr(tag.toNumber() + i))), - }); - const mockPrivateLogs = (blockNumber: number, txIndex: number) => { return times(numPrivateLogsPerTx, (logIndex: number) => { - const tag = makeTag(blockNumber, txIndex, logIndex); + const tag = makePrivateLogTag(blockNumber, txIndex, logIndex); return makePrivateLog(tag); }); }; - const mockPublicLogs = (blockNumber: number, txIndex: number) => { - return times(numPublicLogsPerTx, (logIndex: number) => { - const tag = makeTag(blockNumber, txIndex, logIndex, /* isPublic */ true); - return makePublicLog(tag); - }); - }; - const mockCheckpointWithLogs = async ( blockNumber: number, previousArchive?: AppendOnlyTreeSnapshot, @@ -2253,7 +2242,7 @@ export function describeArchiverDataStore( block.body.txEffects = await timesParallel(numTxsPerBlock, async (txIndex: number) => { const txEffect = await TxEffect.random(); txEffect.privateLogs = mockPrivateLogs(blockNumber, txIndex); - txEffect.publicLogs = mockPublicLogs(blockNumber, txIndex); + txEffect.publicLogs = []; // No public logs needed for private log tests return txEffect; }); @@ -2279,9 +2268,9 @@ export function describeArchiverDataStore( }); it('is possible to batch request private logs via tags', async () => { - const tags = [makeTag(2, 1, 2), makeTag(1, 2, 0)]; + const tags = [makePrivateLogTag(2, 1, 2), makePrivateLogTag(1, 2, 0)]; - const logsByTags = await store.getLogsByTags(tags); + const logsByTags = await store.getPrivateLogsByTags(tags); expect(logsByTags).toEqual([ [ @@ -2303,11 +2292,21 @@ export function describeArchiverDataStore( ]); }); - it('is possible to batch request all logs (private and public) via tags', async () => { - // Tag(1, 0, 0) is shared with the first private log and the first public log. - const tags = [makeTag(1, 0, 0)]; + it('is possible to batch request logs that have the same tag but different content', async () => { + const tags = [makePrivateLogTag(1, 2, 1)]; - const logsByTags = await store.getLogsByTags(tags); + // Create a checkpoint containing logs that have the same tag as the checkpoints before. + // Chain from the last checkpoint's archive + const newBlockNumber = numBlocksForLogs + 1; + const previousArchive = logsCheckpoints[logsCheckpoints.length - 1].checkpoint.blocks[0].archive; + const newCheckpoint = await mockCheckpointWithLogs(newBlockNumber, previousArchive); + const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1]; + newLog.fields[0] = tags[0].value; + newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1] = newLog; + await store.addCheckpoints([newCheckpoint]); + await store.addLogs([newCheckpoint.checkpoint.blocks[0]]); + + const logsByTags = await store.getPrivateLogsByTags(tags); expect(logsByTags).toEqual([ [ @@ -2317,54 +2316,169 @@ export function describeArchiverDataStore( log: makePrivateLog(tags[0]), isFromPublic: false, }), + expect.objectContaining({ + blockNumber: newBlockNumber, + blockHash: L2BlockHash.fromField(await newCheckpoint.checkpoint.blocks[0].header.hash()), + log: newLog, + isFromPublic: false, + }), + ], + ]); + }); + + it('is possible to request logs for non-existing tags and determine their position', async () => { + const tags = [makePrivateLogTag(99, 88, 77), makePrivateLogTag(1, 1, 1)]; + + const logsByTags = await store.getPrivateLogsByTags(tags); + + expect(logsByTags).toEqual([ + [ + // No logs for the first tag. + ], + [ expect.objectContaining({ blockNumber: 1, blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()), + log: makePrivateLog(tags[1]), + isFromPublic: false, + }), + ], + ]); + }); + }); + + describe('getPublicLogsByTagsFromContract', () => { + const numBlocksForLogs = 3; + const numTxsPerBlock = 4; + const numPublicLogsPerTx = 2; + const contractAddress = AztecAddress.fromNumber(543254); + + let logsCheckpoints: PublishedCheckpoint[]; + + const makePublicLogTag = (blockNumber: number, txIndex: number, logIndex: number): Tag => + new Tag( + blockNumber === 1 && txIndex === 0 && logIndex === 0 + ? Fr.ZERO // Shared tag + : new Fr((blockNumber * 100 + txIndex * 10 + logIndex) * 123), + ); + + const makePublicLog = (tag: Tag) => + PublicLog.from({ + contractAddress: contractAddress, + // Arbitrary length + fields: new Array(10).fill(null).map((_, i) => (!i ? tag.value : new Fr(tag.value.toBigInt() + BigInt(i)))), + }); + + const mockPublicLogs = (blockNumber: number, txIndex: number) => { + return times(numPublicLogsPerTx, (logIndex: number) => { + const tag = makePublicLogTag(blockNumber, txIndex, logIndex); + return makePublicLog(tag); + }); + }; + + const mockCheckpointWithLogs = async ( + blockNumber: number, + previousArchive?: AppendOnlyTreeSnapshot, + ): Promise => { + const block = await L2BlockNew.random(BlockNumber(blockNumber), { + checkpointNumber: CheckpointNumber(blockNumber), + indexWithinCheckpoint: 0, + state: makeStateForBlock(blockNumber, numTxsPerBlock), + ...(previousArchive ? { lastArchive: previousArchive } : {}), + }); + block.header.globalVariables.blockNumber = BlockNumber(blockNumber); + + block.body.txEffects = await timesParallel(numTxsPerBlock, async (txIndex: number) => { + const txEffect = await TxEffect.random(); + txEffect.privateLogs = []; // No private logs needed for public log tests + txEffect.publicLogs = mockPublicLogs(blockNumber, txIndex); + return txEffect; + }); + + const checkpoint = new Checkpoint( + AppendOnlyTreeSnapshot.random(), + CheckpointHeader.random(), + [block], + CheckpointNumber(blockNumber), + ); + return makePublishedCheckpoint(checkpoint, blockNumber); + }; + + beforeEach(async () => { + // Create checkpoints sequentially to chain archive roots + logsCheckpoints = []; + for (let i = 0; i < numBlocksForLogs; i++) { + const previousArchive = i > 0 ? logsCheckpoints[i - 1].checkpoint.blocks[0].archive : undefined; + logsCheckpoints.push(await mockCheckpointWithLogs(i + 1, previousArchive)); + } + + await store.addCheckpoints(logsCheckpoints); + await store.addLogs(logsCheckpoints.flatMap(p => p.checkpoint.blocks)); + }); + + it('is possible to batch request public logs via tags', async () => { + const tags = [makePublicLogTag(2, 1, 1), makePublicLogTag(1, 2, 0)]; + + const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags); + + expect(logsByTags).toEqual([ + [ + expect.objectContaining({ + blockNumber: 2, + blockHash: L2BlockHash.fromField(await logsCheckpoints[2 - 1].checkpoint.blocks[0].header.hash()), log: makePublicLog(tags[0]), isFromPublic: true, }), ], + [ + expect.objectContaining({ + blockNumber: 1, + blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()), + log: makePublicLog(tags[1]), + isFromPublic: true, + }), + ], ]); }); it('is possible to batch request logs that have the same tag but different content', async () => { - const tags = [makeTag(1, 2, 1)]; + const tags = [makePublicLogTag(1, 2, 1)]; // Create a checkpoint containing logs that have the same tag as the checkpoints before. // Chain from the last checkpoint's archive const newBlockNumber = numBlocksForLogs + 1; const previousArchive = logsCheckpoints[logsCheckpoints.length - 1].checkpoint.blocks[0].archive; const newCheckpoint = await mockCheckpointWithLogs(newBlockNumber, previousArchive); - const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1]; - newLog.fields[0] = tags[0]; - newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1] = newLog; + const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].publicLogs[1]; + newLog.fields[0] = tags[0].value; + newCheckpoint.checkpoint.blocks[0].body.txEffects[1].publicLogs[1] = newLog; await store.addCheckpoints([newCheckpoint]); await store.addLogs([newCheckpoint.checkpoint.blocks[0]]); - const logsByTags = await store.getLogsByTags(tags); + const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags); expect(logsByTags).toEqual([ [ expect.objectContaining({ blockNumber: 1, blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()), - log: makePrivateLog(tags[0]), - isFromPublic: false, + log: makePublicLog(tags[0]), + isFromPublic: true, }), expect.objectContaining({ blockNumber: newBlockNumber, blockHash: L2BlockHash.fromField(await newCheckpoint.checkpoint.blocks[0].header.hash()), log: newLog, - isFromPublic: false, + isFromPublic: true, }), ], ]); }); it('is possible to request logs for non-existing tags and determine their position', async () => { - const tags = [makeTag(99, 88, 77), makeTag(1, 1, 1)]; + const tags = [makePublicLogTag(99, 88, 77), makePublicLogTag(1, 1, 0)]; - const logsByTags = await store.getLogsByTags(tags); + const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags); expect(logsByTags).toEqual([ [ @@ -2374,8 +2488,8 @@ export function describeArchiverDataStore( expect.objectContaining({ blockNumber: 1, blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()), - log: makePrivateLog(tags[1]), - isFromPublic: false, + log: makePublicLog(tags[1]), + isFromPublic: true, }), ], ]); 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 84f3557169bb..97ea4cadec6d 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 @@ -17,7 +17,7 @@ import type { UtilityFunctionWithMembershipProof, } from '@aztec/stdlib/contract'; import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client'; -import type { LogFilter, TxScopedL2Log } from '@aztec/stdlib/logs'; +import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import type { BlockHeader, TxHash, TxReceipt } from '@aztec/stdlib/tx'; import type { UInt64 } from '@aztec/stdlib/types'; @@ -318,16 +318,21 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc return this.#messageStore.getL1ToL2Messages(checkpointNumber); } - /** - * 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. - * @param logsPerTag - How many logs to return per tag. Default returns everything - * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match - * that tag. - */ - getLogsByTags(tags: Fr[], logsPerTag?: number): Promise { + getPrivateLogsByTags(tags: SiloedTag[], logsPerTag?: number): Promise { + try { + return this.#logStore.getPrivateLogsByTags(tags, logsPerTag); + } catch (err) { + return Promise.reject(err); + } + } + + getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + logsPerTag?: number, + ): Promise { try { - return this.#logStore.getLogsByTags(tags, logsPerTag); + return this.#logStore.getPublicLogsByTagsFromContract(contractAddress, tags, logsPerTag); } catch (err) { return Promise.reject(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 02b627486893..ba131d4c7b84 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 @@ -4,6 +4,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2BlockHash, L2BlockNew } from '@aztec/stdlib/block'; import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client'; import { @@ -13,6 +14,8 @@ import { type LogFilter, LogId, PublicLog, + type SiloedTag, + Tag, TxScopedL2Log, } from '@aztec/stdlib/logs'; @@ -22,8 +25,12 @@ import type { BlockStore } from './block_store.js'; * A store for logs */ export class LogStore { - #logsByTag: AztecAsyncMap; - #logTagsByBlock: AztecAsyncMap; + // `tag` --> private logs + #privateLogsByTag: AztecAsyncMap; + // `{contractAddress}_${tag}` --> public logs + #publicLogsByContractAndTag: AztecAsyncMap; + #privateLogKeysByBlock: AztecAsyncMap; + #publicLogKeysByBlock: AztecAsyncMap; #publicLogsByBlock: AztecAsyncMap; #contractClassLogsByBlock: AztecAsyncMap; #logsMaxPageSize: number; @@ -34,29 +41,42 @@ export class LogStore { private blockStore: BlockStore, logsMaxPageSize: number = 1000, ) { - this.#logsByTag = db.openMap('archiver_tagged_logs_by_tag'); - this.#logTagsByBlock = db.openMap('archiver_log_tags_by_block'); + this.#privateLogsByTag = db.openMap('archiver_private_tagged_logs_by_tag'); + this.#publicLogsByContractAndTag = db.openMap('archiver_public_tagged_logs_by_tag'); + this.#privateLogKeysByBlock = db.openMap('archiver_private_log_keys_by_block'); + this.#publicLogKeysByBlock = db.openMap('archiver_public_log_keys_by_block'); this.#publicLogsByBlock = db.openMap('archiver_public_logs_by_block'); this.#contractClassLogsByBlock = db.openMap('archiver_contract_class_logs_by_block'); this.#logsMaxPageSize = logsMaxPageSize; } - async #extractTaggedLogs(block: L2BlockNew) { + /** + * Extracts tagged logs from a single block, grouping them into private and public maps. + * + * @param block - The L2 block to extract logs from. + * @returns An object containing the private and public tagged logs for the block. + */ + async #extractTaggedLogsFromBlock(block: L2BlockNew) { const blockHash = L2BlockHash.fromField(await block.hash()); - const taggedLogs = new Map(); + // SiloedTag (as string) -> array of log buffers. + const privateTaggedLogs = new Map(); + // "{contractAddress}_{tag}" (as string) -> array of log buffers. + const publicTaggedLogs = new Map(); const dataStartIndexForBlock = block.header.state.partial.noteHashTree.nextAvailableLeafIndex - block.body.txEffects.length * MAX_NOTE_HASHES_PER_TX; + block.body.txEffects.forEach((txEffect, txIndex) => { const txHash = txEffect.txHash; const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX; txEffect.privateLogs.forEach((log, logIndex) => { + // Private logs use SiloedTag (already siloed by kernel) const tag = log.fields[0]; this.#log.debug(`Found private log with tag ${tag.toString()} in block ${block.number}`); - const currentLogs = taggedLogs.get(tag.toString()) ?? []; + const currentLogs = privateTaggedLogs.get(tag.toString()) ?? []; currentLogs.push( new TxScopedL2Log( txHash, @@ -68,14 +88,19 @@ export class LogStore { log, ).toBuffer(), ); - taggedLogs.set(tag.toString(), currentLogs); + privateTaggedLogs.set(tag.toString(), currentLogs); }); txEffect.publicLogs.forEach((log, logIndex) => { + // Public logs use Tag directly (not siloed) and are stored with contract address const tag = log.fields[0]; - this.#log.debug(`Found public log with tag ${tag.toString()} in block ${block.number}`); + const contractAddress = log.contractAddress; + const key = `${contractAddress.toString()}_${tag.toString()}`; + this.#log.debug( + `Found public log with tag ${tag.toString()} from contract ${contractAddress.toString()} in block ${block.number}`, + ); - const currentLogs = taggedLogs.get(tag.toString()) ?? []; + const currentLogs = publicTaggedLogs.get(key) ?? []; currentLogs.push( new TxScopedL2Log( txHash, @@ -87,49 +112,102 @@ export class LogStore { log, ).toBuffer(), ); - taggedLogs.set(tag.toString(), currentLogs); + publicTaggedLogs.set(key, currentLogs); }); }); - return taggedLogs; + + return { privateTaggedLogs, publicTaggedLogs }; } /** - * Append new logs to the store's list. - * @param blocks - The blocks for which to add the logs. - * @returns True if the operation is successful. + * Extracts and aggregates tagged logs from a list of blocks. + * @param blocks - The blocks to extract logs from. + * @returns A map from tag (as string) to an array of serialized private logs belonging to that tag, and a map from + * "{contractAddress}_{tag}" (as string) to an array of serialized public logs belonging to that key. */ - async addLogs(blocks: L2BlockNew[]): Promise { - const taggedLogsInBlocks = await Promise.all(blocks.map(block => this.#extractTaggedLogs(block))); - const taggedLogsToAdd = taggedLogsInBlocks.reduce((acc, taggedLogs) => { - for (const [tag, logs] of taggedLogs.entries()) { + async #extractTaggedLogs( + blocks: L2BlockNew[], + ): Promise<{ privateTaggedLogs: Map; publicTaggedLogs: Map }> { + const taggedLogsInBlocks = await Promise.all(blocks.map(block => this.#extractTaggedLogsFromBlock(block))); + + // Now we merge the maps from each block into a single map. + const privateTaggedLogs = taggedLogsInBlocks.reduce((acc, { privateTaggedLogs }) => { + for (const [tag, logs] of privateTaggedLogs.entries()) { const currentLogs = acc.get(tag) ?? []; acc.set(tag, currentLogs.concat(logs)); } return acc; }, new Map()); - const tagsToUpdate = Array.from(taggedLogsToAdd.keys()); + + const publicTaggedLogs = taggedLogsInBlocks.reduce((acc, { publicTaggedLogs }) => { + for (const [key, logs] of publicTaggedLogs.entries()) { + const currentLogs = acc.get(key) ?? []; + acc.set(key, currentLogs.concat(logs)); + } + return acc; + }, new Map()); + + return { privateTaggedLogs, publicTaggedLogs }; + } + + /** + * Append new logs to the store's list. + * @param blocks - The blocks for which to add the logs. + * @returns True if the operation is successful. + */ + async addLogs(blocks: L2BlockNew[]): Promise { + const { privateTaggedLogs, publicTaggedLogs } = await this.#extractTaggedLogs(blocks); + + const keysOfPrivateLogsToUpdate = Array.from(privateTaggedLogs.keys()); + const keysOfPublicLogsToUpdate = Array.from(publicTaggedLogs.keys()); return this.db.transactionAsync(async () => { - const currentTaggedLogs = await Promise.all( - tagsToUpdate.map(async tag => ({ tag, logBuffers: await this.#logsByTag.getAsync(tag) })), + const currentPrivateTaggedLogs = await Promise.all( + keysOfPrivateLogsToUpdate.map(async key => ({ + tag: key, + logBuffers: await this.#privateLogsByTag.getAsync(key), + })), ); - currentTaggedLogs.forEach(taggedLogBuffer => { + currentPrivateTaggedLogs.forEach(taggedLogBuffer => { if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) { - taggedLogsToAdd.set( + privateTaggedLogs.set( taggedLogBuffer.tag, - taggedLogBuffer.logBuffers!.concat(taggedLogsToAdd.get(taggedLogBuffer.tag)!), + taggedLogBuffer.logBuffers!.concat(privateTaggedLogs.get(taggedLogBuffer.tag)!), ); } }); + + const currentPublicTaggedLogs = await Promise.all( + keysOfPublicLogsToUpdate.map(async key => ({ + key, + logBuffers: await this.#publicLogsByContractAndTag.getAsync(key), + })), + ); + currentPublicTaggedLogs.forEach(taggedLogBuffer => { + if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) { + publicTaggedLogs.set( + taggedLogBuffer.key, + taggedLogBuffer.logBuffers!.concat(publicTaggedLogs.get(taggedLogBuffer.key)!), + ); + } + }); + for (const block of blocks) { const blockHash = await block.hash(); - const tagsInBlock = []; - for (const [tag, logs] of taggedLogsToAdd.entries()) { - await this.#logsByTag.set(tag, logs); - tagsInBlock.push(tag); + const privateTagsInBlock: string[] = []; + for (const [tag, logs] of privateTaggedLogs.entries()) { + await this.#privateLogsByTag.set(tag, logs); + privateTagsInBlock.push(tag); + } + await this.#privateLogKeysByBlock.set(block.number, privateTagsInBlock); + + const publicKeysInBlock: string[] = []; + for (const [key, logs] of publicTaggedLogs.entries()) { + await this.#publicLogsByContractAndTag.set(key, logs); + publicKeysInBlock.push(key); } - await this.#logTagsByBlock.set(block.number, tagsInBlock); + await this.#publicLogKeysByBlock.set(block.number, publicKeysInBlock); const publicLogsInBlock = block.body.txEffects .map((txEffect, txIndex) => @@ -178,41 +256,71 @@ export class LogStore { deleteLogs(blocks: L2BlockNew[]): Promise { return this.db.transactionAsync(async () => { - const tagsToDelete = ( - await Promise.all( - blocks.map(async block => { - const tags = await this.#logTagsByBlock.getAsync(block.number); - return tags ?? []; - }), - ) - ).flat(); + await Promise.all( + blocks.map(async block => { + // Delete private logs + const privateKeys = (await this.#privateLogKeysByBlock.getAsync(block.number)) ?? []; + await Promise.all(privateKeys.map(tag => this.#privateLogsByTag.delete(tag))); + + // Delete public logs + const publicKeys = (await this.#publicLogKeysByBlock.getAsync(block.number)) ?? []; + await Promise.all(publicKeys.map(key => this.#publicLogsByContractAndTag.delete(key))); + }), + ); await Promise.all( blocks.map(block => Promise.all([ this.#publicLogsByBlock.delete(block.number), - this.#logTagsByBlock.delete(block.number), + this.#privateLogKeysByBlock.delete(block.number), + this.#publicLogKeysByBlock.delete(block.number), this.#contractClassLogsByBlock.delete(block.number), ]), ), ); - await Promise.all(tagsToDelete.map(tag => this.#logsByTag.delete(tag.toString()))); return true; }); } /** - * 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 + * Gets all private logs that match any of the received tags (i.e. logs with their first field equal to a SiloedTag). + * @param tags - The SiloedTags to filter the logs by. + * @param limitPerTag - The maximum number of logs to return per tag. + * @returns For each received tag, an array of matching private logs is returned. An empty array implies no logs match * that tag. */ - async getLogsByTags(tags: Fr[], limitPerTag?: number): Promise { + async getPrivateLogsByTags(tags: SiloedTag[], limitPerTag?: number): Promise { if (limitPerTag !== undefined && limitPerTag <= 0) { throw new TypeError('limitPerTag needs to be greater than 0'); } - const logs = await Promise.all(tags.map(tag => this.#logsByTag.getAsync(tag.toString()))); + const logs = await Promise.all(tags.map(tag => this.#privateLogsByTag.getAsync(tag.toString()))); + return logs.map( + logBuffers => logBuffers?.slice(0, limitPerTag).map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? [], + ); + } + + /** + * Gets all public logs that match any of the received tags from the specified contract (i.e. logs with their first field equal to a Tag). + * @param contractAddress - The contract that emitted the public logs. + * @param tags - The Tags to filter the logs by. + * @param limitPerTag - The maximum number of logs to return per tag. + * @returns For each received tag, an array of matching public logs is returned. An empty array implies no logs match that tag. + */ + async getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + limitPerTag?: number, + ): Promise { + if (limitPerTag !== undefined && limitPerTag <= 0) { + throw new TypeError('limitPerTag needs to be greater than 0'); + } + const logs = await Promise.all( + tags.map(tag => { + const key = `${contractAddress.toString()}_${tag.value.toString()}`; + return this.#publicLogsByContractAndTag.getAsync(key); + }), + ); return logs.map( logBuffers => logBuffers?.slice(0, limitPerTag).map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? [], ); diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 8a92e101143f..87f78f581727 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -88,7 +88,7 @@ import { type WorldStateSynchronizer, tryStop, } from '@aztec/stdlib/interfaces/server'; -import type { LogFilter, TxScopedL2Log } from '@aztec/stdlib/logs'; +import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { P2PClientType } from '@aztec/stdlib/p2p'; import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing'; @@ -695,15 +695,16 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return this.contractDataSource.getContract(address); } - /** - * 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. - * @param logsPerTag - The maximum number of logs to return for each tag. By default no limit is set - * @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[], logsPerTag?: number): Promise { - return this.logsSource.getLogsByTags(tags, logsPerTag); + public getPrivateLogsByTags(tags: SiloedTag[], logsPerTag?: number): Promise { + return this.logsSource.getPrivateLogsByTags(tags, logsPerTag); + } + + public getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + logsPerTag?: number, + ): Promise { + return this.logsSource.getPublicLogsByTagsFromContract(contractAddress, tags, logsPerTag); } /** diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.test.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.test.ts index 3673df06b12e..d33ba2470a10 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.test.ts @@ -1,5 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { Tag } from '@aztec/stdlib/logs'; import { LogRetrievalRequest } from './log_retrieval_request.js'; @@ -13,6 +14,6 @@ describe('LogRetrievalRequest', () => { const request = LogRetrievalRequest.fromFields(serialized); expect(request.contractAddress).toEqual(AztecAddress.fromBigInt(1n)); - expect(request.unsiloedTag).toEqual(new Fr(2)); + expect(request.tag).toEqual(new Tag(new Fr(2))); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts index b15448488fd3..51dc571f4b97 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts @@ -1,6 +1,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { FieldReader } from '@aztec/foundation/serialize'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { Tag } from '@aztec/stdlib/logs'; /** * Intermediate struct used to perform batch log retrieval by PXE. The `utilityBulkRetrieveLogs` oracle expects values of this @@ -9,19 +10,19 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; export class LogRetrievalRequest { constructor( public contractAddress: AztecAddress, - public unsiloedTag: Fr, + public tag: Tag, ) {} toFields(): Fr[] { - return [this.contractAddress.toField(), this.unsiloedTag]; + return [this.contractAddress.toField(), this.tag.value]; } static fromFields(fields: Fr[] | FieldReader): LogRetrievalRequest { const reader = FieldReader.asReader(fields); const contractAddress = AztecAddress.fromField(reader.readField()); - const unsiloedTag = reader.readField(); + const tag = new Tag(reader.readField()); - return new LogRetrievalRequest(contractAddress, unsiloedTag); + return new LogRetrievalRequest(contractAddress, tag); } } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index bab0d39ac1d8..bea8ade68f6d 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -6,12 +6,11 @@ import type { FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { CompleteAddress, ContractInstance } from '@aztec/stdlib/contract'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; -import type { ContractClassLog } from '@aztec/stdlib/logs'; +import type { ContractClassLog, Tag } from '@aztec/stdlib/logs'; import type { Note, NoteStatus } from '@aztec/stdlib/note'; import { type MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; import type { BlockHeader } from '@aztec/stdlib/tx'; -import type { Tag } from '../../tagging/tag.js'; import type { UtilityContext } from '../noir-structs/utility_context.js'; import type { MessageLoadOracleInputs } from './message_load_oracle_inputs.js'; diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 96d5cd3c21e3..d1817edd5e88 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -45,6 +45,7 @@ import { computeNoteHashNonce, computeSecretHash, computeUniqueNoteHash, siloNot import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { computeAppNullifierSecretKey, deriveKeys } from '@aztec/stdlib/keys'; +import type { SiloedTag } from '@aztec/stdlib/logs'; import { L1Actor, L1ToL2Message, L2Actor } from '@aztec/stdlib/messaging'; import { Note, NoteDao } from '@aztec/stdlib/note'; import { makeBlockHeader } from '@aztec/stdlib/testing'; @@ -326,7 +327,7 @@ describe('Private Execution test suite', () => { // Mock aztec node methods - the return array needs to have the same length as the number of tags // on the input. - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => Promise.resolve(tags.map(() => []))); + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => Promise.resolve(tags.map(() => []))); // TODO: refactor. Maybe it's worth stubbing a key store // and cleaning up the mess that is setting up keys. diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index 0553f5cadb01..36899c72c6aa 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -17,6 +17,7 @@ import { computeUniqueNoteHash, siloNoteHash, siloNullifier } from '@aztec/stdli import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { PrivateContextInputs } from '@aztec/stdlib/kernel'; import { type ContractClassLog, DirectionalAppTaggingSecret, type PreTag } from '@aztec/stdlib/logs'; +import { Tag } from '@aztec/stdlib/logs'; import { Note, type NoteStatus } from '@aztec/stdlib/note'; import { type BlockHeader, @@ -38,7 +39,6 @@ import type { PrivateEventDataProvider } from '../../storage/private_event_data_ import type { RecipientTaggingDataProvider } from '../../storage/tagging_data_provider/recipient_tagging_data_provider.js'; import type { SenderTaggingDataProvider } from '../../storage/tagging_data_provider/sender_tagging_data_provider.js'; import { syncSenderTaggingIndexes } from '../../tagging/sync/sync_sender_tagging_indexes.js'; -import { Tag } from '../../tagging/tag.js'; import type { ExecutionNoteCache } from '../execution_note_cache.js'; import { ExecutionTaggingIndexCache } from '../execution_tagging_index_cache.js'; import type { HashedValuesCache } from '../hashed_values_cache.js'; diff --git a/yarn-project/pxe/src/logs/log_service.test.ts b/yarn-project/pxe/src/logs/log_service.test.ts index a418b7413c76..573220886d98 100644 --- a/yarn-project/pxe/src/logs/log_service.test.ts +++ b/yarn-project/pxe/src/logs/log_service.test.ts @@ -7,10 +7,9 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2BlockHash, randomDataInBlock } from '@aztec/stdlib/block'; import { CompleteAddress } from '@aztec/stdlib/contract'; -import { siloPrivateLog } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { computeAddress, deriveKeys } from '@aztec/stdlib/keys'; -import { DirectionalAppTaggingSecret, PrivateLog, PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { DirectionalAppTaggingSecret, PrivateLog, PublicLog, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import { BlockHeader, GlobalVariables, TxEffect, TxHash, randomIndexedTxEffect } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -21,8 +20,6 @@ import { AnchorBlockDataProvider } from '../storage/anchor_block_data_provider/a import { CapsuleDataProvider } from '../storage/capsule_data_provider/capsule_data_provider.js'; import { RecipientTaggingDataProvider } from '../storage/tagging_data_provider/recipient_tagging_data_provider.js'; import { WINDOW_HALF_SIZE } from '../tagging/constants.js'; -import { SiloedTag } from '../tagging/siloed_tag.js'; -import { Tag } from '../tagging/tag.js'; import { LogService } from './log_service.js'; async function computeSiloedTagForIndex( @@ -148,7 +145,7 @@ describe('LogService', () => { // Accumulated logs intended for recipient: NUM_SENDERS + 1 + NUM_SENDERS / 2 // Set up the getPrivateLogsByTags mock - aztecNode.getLogsByTags.mockImplementation(tags => { + aztecNode.getPrivateLogsByTags.mockImplementation(tags => { return Promise.resolve(tags.map(tag => logs[tag.toString()] ?? [])); }); } @@ -185,7 +182,7 @@ describe('LogService', () => { for (const sender of senders) { await recipientTaggingDataProvider.addSenderAddress(sender.completeAddress.address); } - aztecNode.getLogsByTags.mockReset(); + aztecNode.getPrivateLogsByTags.mockReset(); aztecNode.getTxEffect.mockResolvedValue({ ...randomDataInBlock(await TxEffect.random({ numNullifiers: 1 })), txIndexInBlock: 0, @@ -235,7 +232,7 @@ describe('LogService', () => { // We should have called the node 2 times: // 2 times: first time during initial request, second time after pushing the edge of the window once - expect(aztecNode.getLogsByTags.mock.calls.length).toBe(2); + expect(aztecNode.getPrivateLogsByTags.mock.calls.length).toBe(2); }); it('should sync tagged logs with a sender index offset', async () => { @@ -271,7 +268,7 @@ describe('LogService', () => { // We should have called the node 2 times: // 2 times: first time during initial request, second time after pushing the edge of the window once - expect(aztecNode.getLogsByTags.mock.calls.length).toBe(2); + expect(aztecNode.getPrivateLogsByTags.mock.calls.length).toBe(2); }); it("should sync tagged logs for which indexes are not updated if they're inside the window", async () => { @@ -311,7 +308,7 @@ describe('LogService', () => { // We should have called the node 2 times: // first time during initial request, second time after pushing the edge of the window once - expect(aztecNode.getLogsByTags.mock.calls.length).toBe(2); + expect(aztecNode.getPrivateLogsByTags.mock.calls.length).toBe(2); }); it("should not sync tagged logs for which indexes are not updated if they're outside the window", async () => { @@ -350,7 +347,7 @@ describe('LogService', () => { expect(indexes).toEqual([index, index, index, index, index, index, index, index, index, index]); // We should have called the node once and that is only for the first window - expect(aztecNode.getLogsByTags.mock.calls.length).toBe(1); + expect(aztecNode.getPrivateLogsByTags.mock.calls.length).toBe(1); }); it('should sync tagged logs from scratch after a DB wipe', async () => { @@ -382,9 +379,9 @@ describe('LogService', () => { // Since no logs were synced, window edge hash not been pushed and for this reason we should have called // the node only once for the initial window - expect(aztecNode.getLogsByTags.mock.calls.length).toBe(1); + expect(aztecNode.getPrivateLogsByTags.mock.calls.length).toBe(1); - aztecNode.getLogsByTags.mockClear(); + aztecNode.getPrivateLogsByTags.mockClear(); // Wipe the database await recipientTaggingDataProvider.resetNoteSyncData(); @@ -401,7 +398,7 @@ describe('LogService', () => { // We should have called the node 2 times: // first time during initial request, second time after pushing the edge of the window once - expect(aztecNode.getLogsByTags.mock.calls.length).toBe(2); + expect(aztecNode.getPrivateLogsByTags.mock.calls.length).toBe(2); }); it('should not sync tagged logs with a blockNumber larger than the block number to which PXE is synced', async () => { @@ -432,16 +429,18 @@ describe('LogService', () => { }); describe('bulkRetrieveLogs', () => { - const unsiloedTag = Fr.random(); + const tag = new Tag(Fr.random()); beforeEach(() => { - aztecNode.getLogsByTags.mockReset(); + aztecNode.getPrivateLogsByTags.mockReset(); + aztecNode.getPublicLogsByTagsFromContract.mockReset(); aztecNode.getTxEffect.mockReset(); }); it('returns no logs if none are found', async () => { - aztecNode.getLogsByTags.mockResolvedValue([[]]); - const request = new LogRetrievalRequest(contractAddress, unsiloedTag); + aztecNode.getPrivateLogsByTags.mockResolvedValue([[]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[]]); + const request = new LogRetrievalRequest(contractAddress, tag); const responses = await logService.bulkRetrieveLogs([request]); expect(responses.length).toEqual(1); expect(responses[0]).toBeNull(); @@ -451,14 +450,15 @@ describe('LogService', () => { const scopedLog = await TxScopedL2Log.random(true); (scopedLog.log as PublicLog).contractAddress = contractAddress; - aztecNode.getLogsByTags.mockResolvedValue([[scopedLog]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[scopedLog]]); + aztecNode.getPrivateLogsByTags.mockResolvedValue([[]]); const indexedTxEffect = await randomIndexedTxEffect(); aztecNode.getTxEffect.mockImplementation((txHash: TxHash) => txHash.equals(scopedLog.txHash) ? Promise.resolve(indexedTxEffect) : Promise.resolve(undefined), ); - const request = new LogRetrievalRequest(contractAddress, scopedLog.log.fields[0]); + const request = new LogRetrievalRequest(contractAddress, new Tag(scopedLog.log.fields[0])); const responses = await logService.bulkRetrieveLogs([request]); @@ -468,9 +468,9 @@ describe('LogService', () => { it('returns a private log if one is found', async () => { const scopedLog = await TxScopedL2Log.random(false); - scopedLog.log.fields[0] = await siloPrivateLog(contractAddress, Fr.random()); - aztecNode.getLogsByTags.mockResolvedValue([[scopedLog]]); + aztecNode.getPrivateLogsByTags.mockResolvedValue([[scopedLog]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[]]); const indexedTxEffect = await randomIndexedTxEffect(); aztecNode.getTxEffect.mockResolvedValue(indexedTxEffect); @@ -478,7 +478,7 @@ describe('LogService', () => { txHash.equals(scopedLog.txHash) ? Promise.resolve(indexedTxEffect) : Promise.resolve(undefined), ); - const request = new LogRetrievalRequest(contractAddress, scopedLog.log.fields[0]); + const request = new LogRetrievalRequest(contractAddress, new Tag(scopedLog.log.fields[0])); const responses = await logService.bulkRetrieveLogs([request]); @@ -488,15 +488,15 @@ describe('LogService', () => { }); describe('getPublicLogByTag', () => { - const tag = Fr.random(); + const tag = new Tag(Fr.random()); beforeEach(() => { - aztecNode.getLogsByTags.mockReset(); + aztecNode.getPublicLogsByTagsFromContract.mockReset(); aztecNode.getTxEffect.mockReset(); }); it('returns null if no logs found for tag', async () => { - aztecNode.getLogsByTags.mockResolvedValue([[]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[]]); const result = await logService.getPublicLogByTag(tag, contractAddress); expect(result).toBeNull(); @@ -506,7 +506,7 @@ describe('LogService', () => { const scopedLog = await TxScopedL2Log.random(true); const logContractAddress = (scopedLog.log as PublicLog).contractAddress; - aztecNode.getLogsByTags.mockResolvedValue([[scopedLog]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[scopedLog]]); const indexedTxEffect = await randomIndexedTxEffect(); aztecNode.getTxEffect.mockImplementation((txHash: TxHash) => txHash.equals(scopedLog.txHash) ? Promise.resolve(indexedTxEffect) : Promise.resolve(undefined), @@ -519,13 +519,13 @@ describe('LogService', () => { expect(result.txHash).toEqual(scopedLog.txHash); expect(result.firstNullifierInTx).toEqual(indexedTxEffect.data.nullifiers[0]); - expect(aztecNode.getLogsByTags).toHaveBeenCalledWith([tag]); + expect(aztecNode.getPublicLogsByTagsFromContract).toHaveBeenCalledWith(logContractAddress, [tag]); expect(aztecNode.getTxEffect).toHaveBeenCalledWith(scopedLog.txHash); }); it('throws if multiple logs found for tag', async () => { const scopedLog = await TxScopedL2Log.random(true); - aztecNode.getLogsByTags.mockResolvedValue([[scopedLog, scopedLog]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[scopedLog, scopedLog]]); const logContractAddress = (scopedLog.log as PublicLog).contractAddress; await expect(logService.getPublicLogByTag(tag, logContractAddress)).rejects.toThrow(/Got 2 logs for tag/); @@ -533,7 +533,7 @@ describe('LogService', () => { it('throws if tx effect not found', async () => { const scopedLog = await TxScopedL2Log.random(true); - aztecNode.getLogsByTags.mockResolvedValue([[scopedLog]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[scopedLog]]); aztecNode.getTxEffect.mockResolvedValue(undefined); const logContractAddress = (scopedLog.log as PublicLog).contractAddress; @@ -545,7 +545,7 @@ describe('LogService', () => { it('returns log fields that are actually emitted', async () => { const logContractAddress = await AztecAddress.random(); const logPlaintext = [Fr.random()]; - const logContent = [tag, ...logPlaintext]; + const logContent = [tag.value, ...logPlaintext]; const log = PublicLog.from({ contractAddress: logContractAddress, @@ -561,7 +561,7 @@ describe('LogService', () => { log, ); - aztecNode.getLogsByTags.mockResolvedValue([[scopedLogWithPadding]]); + aztecNode.getPublicLogsByTagsFromContract.mockResolvedValue([[scopedLogWithPadding]]); aztecNode.getTxEffect.mockResolvedValue(await randomIndexedTxEffect()); const result = await logService.getPublicLogByTag(tag, logContractAddress); diff --git a/yarn-project/pxe/src/logs/log_service.ts b/yarn-project/pxe/src/logs/log_service.ts index 950a35fa325c..c37785c72ffa 100644 --- a/yarn-project/pxe/src/logs/log_service.ts +++ b/yarn-project/pxe/src/logs/log_service.ts @@ -2,14 +2,14 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import type { KeyStore } from '@aztec/key-store'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { CompleteAddress } from '@aztec/stdlib/contract'; -import { siloPrivateLog } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { DirectionalAppTaggingSecret, PendingTaggedLog, PrivateLogWithTxData, - PublicLog, PublicLogWithTxData, + SiloedTag, + Tag, TxScopedL2Log, } from '@aztec/stdlib/logs'; @@ -20,8 +20,6 @@ import { AnchorBlockDataProvider } from '../storage/anchor_block_data_provider/a import { CapsuleDataProvider } from '../storage/capsule_data_provider/capsule_data_provider.js'; import { RecipientTaggingDataProvider } from '../storage/tagging_data_provider/recipient_tagging_data_provider.js'; import { WINDOW_HALF_SIZE } from '../tagging/constants.js'; -import { SiloedTag } from '../tagging/siloed_tag.js'; -import { Tag } from '../tagging/tag.js'; import { getInitialIndexesMap, getPreTagsForTheWindow } from '../tagging/utils.js'; export class LogService { @@ -37,16 +35,16 @@ export class LogService { public async bulkRetrieveLogs(logRetrievalRequests: LogRetrievalRequest[]): Promise<(LogRetrievalResponse | null)[]> { return await Promise.all( logRetrievalRequests.map(async request => { - // TODO(#14555): remove these internal functions and have node endpoints that do this instead + // TODO(F-231): remove these internal functions and have node endpoints that do this instead const [publicLog, privateLog] = await Promise.all([ - this.getPublicLogByTag(request.unsiloedTag, request.contractAddress), - this.getPrivateLogByTag(await siloPrivateLog(request.contractAddress, request.unsiloedTag)), + this.getPublicLogByTag(request.tag, request.contractAddress), + this.getPrivateLogByTag(await SiloedTag.compute(request.tag, request.contractAddress)), ]); if (publicLog !== null) { if (privateLog !== null) { throw new Error( - `Found both a public and private log when searching for tag ${request.unsiloedTag} from contract ${request.contractAddress}`, + `Found both a public and private log when searching for tag ${request.tag} from contract ${request.contractAddress}`, ); } @@ -70,9 +68,9 @@ export class LogService { ); } - // TODO(#14555): delete this function and implement this behavior in the node instead - public async getPublicLogByTag(tag: Fr, contractAddress: AztecAddress): Promise { - const logs = await this.#getPublicLogsByTagsFromContract([tag], contractAddress); + // TODO(F-231): delete this function and implement this behavior in the node instead + public async getPublicLogByTag(tag: Tag, contractAddress: AztecAddress): Promise { + const logs = await this.aztecNode.getPublicLogsByTagsFromContract(contractAddress, [tag]); const logsForTag = logs[0]; if (logsForTag.length == 0) { @@ -102,9 +100,9 @@ export class LogService { ); } - // TODO(#14555): delete this function and implement this behavior in the node instead - public async getPrivateLogByTag(siloedTag: Fr): Promise { - const logs = await this.#getPrivateLogsByTags([siloedTag]); + // TODO(F-231): delete this function and implement this behavior in the node instead + public async getPrivateLogByTag(siloedTag: SiloedTag): Promise { + const logs = await this.aztecNode.getPrivateLogsByTags([siloedTag]); const logsForTag = logs[0]; if (logsForTag.length == 0) { @@ -134,23 +132,6 @@ export class LogService { ); } - // TODO(#12656): Make this a public function on the AztecNode interface and remove the original getLogsByTags. This - // was not done yet as we were unsure about the API and we didn't want to introduce a breaking change. - async #getPublicLogsByTagsFromContract(tags: Fr[], contractAddress: AztecAddress): Promise { - const allLogs = await this.aztecNode.getLogsByTags(tags); - const allPublicLogs = allLogs.map(logs => logs.filter(log => log.isFromPublic)); - return allPublicLogs.map(logs => - logs.filter(log => (log.log as PublicLog).contractAddress.equals(contractAddress)), - ); - } - - // TODO(#12656): Make this a public function on the AztecNode interface and remove the original getLogsByTags. This - // was not done yet as we were unsure about the API and we didn't want to introduce a breaking change. - async #getPrivateLogsByTags(tags: Fr[]): Promise { - const allLogs = await this.aztecNode.getLogsByTags(tags); - return allLogs.map(logs => logs.filter(log => !log.isFromPublic)); - } - // TODO(#17775): Replace this implementation of this function with one implementing an approach similar // to syncSenderTaggingIndexes. Not done yet due to re-prioritization to devex and this doesn't directly affect // devex. @@ -215,10 +196,7 @@ export class LogService { const newLargestIndexMapForIteration: { [k: string]: number } = {}; // Fetch the private logs for the tags and iterate over them - // TODO: The following conversion is unfortunate and we should most likely just type the #getPrivateLogsByTags - // to accept SiloedTag[] instead of Fr[]. That would result in a large change so I didn't do it yet. - const tagsForTheWholeWindowAsFr = tagsForTheWholeWindow.map(tag => tag.value); - const logsByTags = await this.#getPrivateLogsByTags(tagsForTheWholeWindowAsFr); + const logsByTags = await this.aztecNode.getPrivateLogsByTags(tagsForTheWholeWindow); for (let logIndex = 0; logIndex < logsByTags.length; logIndex++) { const logsByTag = logsByTags[logIndex]; diff --git a/yarn-project/pxe/src/pxe.test.ts b/yarn-project/pxe/src/pxe.test.ts index f9ec845de38e..39978449f91a 100644 --- a/yarn-project/pxe/src/pxe.test.ts +++ b/yarn-project/pxe/src/pxe.test.ts @@ -13,6 +13,7 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2BlockHash } from '@aztec/stdlib/block'; import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; +import { SiloedTag } from '@aztec/stdlib/logs'; import { randomContractArtifact, randomContractInstanceWithAddress, @@ -180,7 +181,7 @@ describe('PXE', () => { // Used to sync private logs from the node - the return array needs to have the same length as the number of tags // on the input. - node.getLogsByTags.mockImplementation((tags: Fr[]) => Promise.resolve(tags.map(() => []))); + node.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => Promise.resolve(tags.map(() => []))); // Necessary to sync contract private state await pxe.registerContractClass(TestContractArtifact); diff --git a/yarn-project/pxe/src/tagging/index.ts b/yarn-project/pxe/src/tagging/index.ts index bb5f69bcd641..82b8329348f7 100644 --- a/yarn-project/pxe/src/tagging/index.ts +++ b/yarn-project/pxe/src/tagging/index.ts @@ -1,6 +1,4 @@ -export * from './tag.js'; export * from './constants.js'; -export * from './siloed_tag.js'; export * from './utils.js'; -export { DirectionalAppTaggingSecret } from '@aztec/stdlib/logs'; +export { DirectionalAppTaggingSecret, Tag, SiloedTag } from '@aztec/stdlib/logs'; export { type PreTag } from '@aztec/stdlib/logs'; diff --git a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts index eb16f639fb18..1479ec721fc2 100644 --- a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts +++ b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts @@ -5,15 +5,13 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2BlockHash } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; -import { DirectionalAppTaggingSecret, PrivateLog, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { DirectionalAppTaggingSecret, PrivateLog, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import { makeBlockHeader } from '@aztec/stdlib/testing'; import { TxHash } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { SiloedTag } from '../siloed_tag.js'; import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../sync/sync_sender_tagging_indexes.js'; -import { Tag } from '../tag.js'; import { loadPrivateLogsForSenderRecipientPair } from './load_private_logs_for_sender_recipient_pair.js'; import { NewRecipientTaggingDataProvider } from './new_recipient_tagging_data_provider.js'; @@ -55,7 +53,7 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { }); beforeEach(async () => { - aztecNode.getLogsByTags.mockReset(); + aztecNode.getPrivateLogsByTags.mockReset(); aztecNode.getL2Tips.mockReset(); aztecNode.getBlockHeader.mockReset(); taggingDataProvider = new NewRecipientTaggingDataProvider(await openTmpStore('test')); @@ -69,8 +67,8 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); // no logs found for any tag - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { - return Promise.resolve(tags.map((_tag: Fr) => [])); + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { + return Promise.resolve(tags.map((_tag: SiloedTag) => [])); }); const logs = await loadPrivateLogsForSenderRecipientPair( @@ -101,10 +99,10 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); // The log is finalized - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.all( - tags.map(async (t: Fr) => - t.equals(logTag.value) + tags.map(async (t: SiloedTag) => + t.equals(logTag) ? [makeLog(await logBlockHeader.hash(), finalizedBlockNumber, logBlockTimestamp, logTag.value)] : [], ), @@ -139,10 +137,10 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); // The log is finalized - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.all( - tags.map(async (t: Fr) => - t.equals(logTag.value) + tags.map(async (t: SiloedTag) => + t.equals(logTag) ? [makeLog(await logBlockHeader.hash(), finalizedBlockNumber, logBlockTimestamp, logTag.value)] : [], ), @@ -189,13 +187,13 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { // We record the number of queried tags to be able to verify that the window was moved forward correctly. let numQueriedTags = 0; - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { numQueriedTags += tags.length; return Promise.all( - tags.map(async (t: Fr) => { - if (t.equals(log1Tag.value)) { + tags.map(async (t: SiloedTag) => { + if (t.equals(log1Tag)) { return [makeLog(await log1BlockHeader.hash(), finalizedBlockNumber, log1BlockTimestamp, log1Tag.value)]; - } else if (t.equals(log2Tag.value)) { + } else if (t.equals(log2Tag)) { return [makeLog(await log2BlockHeader.hash(), finalizedBlockNumber, log2BlockTimestamp, log2Tag.value)]; } return []; diff --git a/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.test.ts b/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.test.ts index 96c95d3355d3..1165c2c82eea 100644 --- a/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.test.ts +++ b/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.test.ts @@ -3,14 +3,12 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2BlockHash } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; -import { DirectionalAppTaggingSecret, PrivateLog, PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { DirectionalAppTaggingSecret, PrivateLog, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import { makeBlockHeader } from '@aztec/stdlib/testing'; import { TxHash } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { SiloedTag } from '../../siloed_tag.js'; -import { Tag } from '../../tag.js'; import { loadLogsForRange } from './load_logs_for_range.js'; // In tests where the anchor block behavior is not under examination, we use a high block number to ensure it occurs @@ -48,48 +46,18 @@ describe('loadLogsForRange', () => { }); beforeEach(() => { - aztecNode.getLogsByTags.mockReset(); + aztecNode.getPrivateLogsByTags.mockReset(); }); it('returns empty array when no logs found for the given window', async () => { - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { // No log found for any tag - return Promise.resolve(tags.map((_tag: Fr) => [])); + return Promise.resolve(tags.map((_tag: SiloedTag) => [])); }); expect(await loadLogsForRange(secret, app, aztecNode, 0, 10, NON_INTERFERING_ANCHOR_BLOCK_NUMBER)).toHaveLength(0); }); - it('only returns private logs', async () => { - const txHash = TxHash.random(); - const blockNumber = 5; - const index = 3; - const timestamp = 1000n; - const tag = await computeSiloedTagForIndex(index); - const blockHeader = makeBlockHeader(0, { timestamp }); - - aztecNode.getLogsByTags.mockImplementation(async (tags: Fr[]) => { - const blockHash = await blockHeader.hash(); - const privateLog = makeLog(txHash, blockHash, blockNumber, timestamp, tag); - const publicLog = new TxScopedL2Log( - TxHash.random(), - 0, - 0, - BlockNumber(blockNumber), - L2BlockHash.fromField(blockHash), - timestamp, - await PublicLog.random(), - ); - return tags.map((t: Fr) => (t.equals(tag.value) ? [privateLog, publicLog] : [])); - }); - - const result = await loadLogsForRange(secret, app, aztecNode, 0, 10, NON_INTERFERING_ANCHOR_BLOCK_NUMBER); - - expect(result).toHaveLength(1); - expect(result[0].log.txHash.equals(txHash)).toBe(true); - expect(result[0].log.isFromPublic).toBe(false); - }); - it('handles multiple logs at different indexes', async () => { const txHash1 = TxHash.random(); const txHash2 = TxHash.random(); @@ -104,12 +72,12 @@ describe('loadLogsForRange', () => { const blockHeader1 = makeBlockHeader(0, { timestamp: timestamp1 }); const blockHeader2 = makeBlockHeader(1, { timestamp: timestamp2 }); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.all( - tags.map(async (t: Fr) => { - if (t.equals(tag1.value)) { + tags.map(async (t: SiloedTag) => { + if (t.equals(tag1)) { return [makeLog(txHash1, await blockHeader1.hash(), blockNumber1, timestamp1, tag1)]; - } else if (t.equals(tag2.value)) { + } else if (t.equals(tag2)) { return [makeLog(txHash2, await blockHeader2.hash(), blockNumber2, timestamp2, tag2)]; } return []; @@ -141,10 +109,10 @@ describe('loadLogsForRange', () => { const blockHeader1 = makeBlockHeader(0, { timestamp: timestamp1 }); const blockHeader2 = makeBlockHeader(1, { timestamp: timestamp2 }); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.all( - tags.map(async (t: Fr) => - t.equals(tag.value) + tags.map(async (t: SiloedTag) => + t.equals(tag) ? [ makeLog(txHash1, await blockHeader1.hash(), blockNumber1, timestamp1, tag), makeLog(txHash2, await blockHeader2.hash(), blockNumber2, timestamp2, tag), @@ -175,12 +143,12 @@ describe('loadLogsForRange', () => { const tag2 = await computeSiloedTagForIndex(index2); const blockHeader = makeBlockHeader(0, { timestamp }); - aztecNode.getLogsByTags.mockImplementation(async (tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation(async (tags: SiloedTag[]) => { const blockHash = await blockHeader.hash(); - return tags.map((t: Fr) => { - if (t.equals(tag1.value)) { + return tags.map((t: SiloedTag) => { + if (t.equals(tag1)) { return [makeLog(txHash1, blockHash, blockNumber, timestamp, tag1)]; - } else if (t.equals(tag2.value)) { + } else if (t.equals(tag2)) { return [makeLog(txHash2, blockHash, blockNumber, timestamp, tag2)]; } return []; @@ -209,12 +177,12 @@ describe('loadLogsForRange', () => { const tagAtEnd = await computeSiloedTagForIndex(end); const blockHeader = makeBlockHeader(0, { timestamp }); - aztecNode.getLogsByTags.mockImplementation(async (tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation(async (tags: SiloedTag[]) => { const blockHash = await blockHeader.hash(); - return tags.map((t: Fr) => { - if (t.equals(tagAtStart.value)) { + return tags.map((t: SiloedTag) => { + if (t.equals(tagAtStart)) { return [makeLog(txHashAtStart, blockHash, 5, timestamp, tagAtStart)]; - } else if (t.equals(tagAtEnd.value)) { + } else if (t.equals(tagAtEnd)) { return [makeLog(txHashAtEnd, blockHash, 6, timestamp, tagAtEnd)]; } return []; @@ -239,10 +207,10 @@ describe('loadLogsForRange', () => { const blockHeaderAtAnchor = makeBlockHeader(1, { timestamp }); const blockHeaderAfter = makeBlockHeader(2, { timestamp }); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.all( - tags.map(async (t: Fr) => - t.equals(tag.value) + tags.map(async (t: SiloedTag) => + t.equals(tag) ? [ makeLog(TxHash.random(), await blockHeaderBefore.hash(), anchorBlockNumber - 1, timestamp, tag), makeLog(TxHash.random(), await blockHeaderAtAnchor.hash(), anchorBlockNumber, timestamp, tag), diff --git a/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.ts b/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.ts index 959f0a21c8b9..089f730729d6 100644 --- a/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.ts +++ b/yarn-project/pxe/src/tagging/recipient_sync/utils/load_logs_for_range.ts @@ -2,9 +2,7 @@ import type { BlockNumber } from '@aztec/foundation/branded-types'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { DirectionalAppTaggingSecret, PreTag, TxScopedL2Log } from '@aztec/stdlib/logs'; - -import { SiloedTag } from '../../siloed_tag.js'; -import { Tag } from '../../tag.js'; +import { SiloedTag, Tag } from '@aztec/stdlib/logs'; /** * Gets private logs with their corresponding block timestamps and tagging indexes for the given index range, `app` and @@ -27,21 +25,19 @@ export async function loadLogsForRange( Promise.all(tags.map(tag => SiloedTag.compute(tag, app))), ); - // Get logs for these tags - const tagsAsFr = siloedTags.map(tag => tag.value); - const allLogs = await aztecNode.getLogsByTags(tagsAsFr); + const logs = await aztecNode.getPrivateLogsByTags(siloedTags); - // Collect all private logs with their corresponding tagging indexes - const privateLogsWithIndexes: Array<{ log: TxScopedL2Log; taggingIndex: number }> = []; - for (let i = 0; i < allLogs.length; i++) { - const logs = allLogs[i]; + // Pair logs with their corresponding tagging indexes + const logsWithIndexes: Array<{ log: TxScopedL2Log; taggingIndex: number }> = []; + for (let i = 0; i < logs.length; i++) { + const logsForTag = logs[i]; const taggingIndex = preTags[i].index; - for (const log of logs) { - if (!log.isFromPublic && log.blockNumber <= anchorBlockNumber) { - privateLogsWithIndexes.push({ log, taggingIndex }); + for (const log of logsForTag) { + if (log.blockNumber <= anchorBlockNumber) { + logsWithIndexes.push({ log, taggingIndex }); } } } - return privateLogsWithIndexes; + return logsWithIndexes; } diff --git a/yarn-project/pxe/src/tagging/siloed_tag.ts b/yarn-project/pxe/src/tagging/siloed_tag.ts deleted file mode 100644 index 0e8fe113f343..000000000000 --- a/yarn-project/pxe/src/tagging/siloed_tag.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; -import type { Fr } from '@aztec/foundation/curves/bn254'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; - -import type { Tag } from './tag.js'; - -/** - * Represents a tag used in private log as it "appears on the chain" - that is the tag is siloed with a contract - * address that emitted the log. - */ -export class SiloedTag { - private constructor(public readonly value: Fr) {} - - static async compute(tag: Tag, app: AztecAddress): Promise { - const siloedTag = await poseidon2Hash([app, tag.value]); - return new SiloedTag(siloedTag); - } - - toString(): string { - return this.value.toString(); - } -} diff --git a/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts b/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts index 50c8678b53d8..8f6b1fd39cd2 100644 --- a/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts +++ b/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts @@ -41,9 +41,9 @@ describe('syncSenderTaggingIndexes', () => { it('no new logs found for a given secret', async () => { await setUp(); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { // No log found for any tag - return Promise.resolve(tags.map((_tag: Fr) => [])); + return Promise.resolve(tags.map((_tag: SiloedTag) => [])); }); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); @@ -69,10 +69,10 @@ describe('syncSenderTaggingIndexes', () => { // Create a log with tag index 3 const index3Tag = await computeSiloedTagForIndex(finalizedIndexStep1); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { // Return empty arrays for all tags except the one at index 3 return Promise.resolve( - tags.map((tag: Fr) => (tag.equals(index3Tag.value) ? [makeLog(TxHash.random(), index3Tag.value)] : [])), + tags.map((tag: SiloedTag) => (tag.equals(index3Tag) ? [makeLog(TxHash.random(), index3Tag.value)] : [])), ); }); @@ -100,10 +100,10 @@ describe('syncSenderTaggingIndexes', () => { it('step 2: pending log is synced', async () => { const pendingTag = await computeSiloedTagForIndex(pendingIndexStep2); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { // Return empty arrays for all tags except the one at the pending index return Promise.resolve( - tags.map((tag: Fr) => (tag.equals(pendingTag.value) ? [makeLog(pendingTxHashStep2, pendingTag.value)] : [])), + tags.map((tag: SiloedTag) => (tag.equals(pendingTag) ? [makeLog(pendingTxHashStep2, pendingTag.value)] : [])), ); }); @@ -140,15 +140,15 @@ describe('syncSenderTaggingIndexes', () => { const newHighestFinalizedTag = await computeSiloedTagForIndex(newHighestFinalizedIndex); // New finalized log const newHighestUsedTag = await computeSiloedTagForIndex(newHighestUsedIndex); // New pending log - // Mock getLogsByTags to return logs for multiple indices - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + // Mock getPrivateLogsByTags to return logs for multiple indices + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.resolve( - tags.map((tag: Fr) => { - if (tag.equals(nowFinalizedTag.value)) { + tags.map((tag: SiloedTag) => { + if (tag.equals(nowFinalizedTag)) { return [makeLog(pendingTxHashStep2, nowFinalizedTag.value)]; - } else if (tag.equals(newHighestFinalizedTag.value)) { + } else if (tag.equals(newHighestFinalizedTag)) { return [makeLog(newHighestFinalizedTxHash, newHighestFinalizedTag.value)]; - } else if (tag.equals(newHighestUsedTag.value)) { + } else if (tag.equals(newHighestUsedTag)) { return [makeLog(newHighestUsedTxHash, newHighestUsedTag.value)]; } return []; @@ -208,11 +208,11 @@ describe('syncSenderTaggingIndexes', () => { const index3Tag = await computeSiloedTagForIndex(pendingAndFinalizedIndex); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { // Return both the pending and finalized logs for the tag at index 3 return Promise.resolve( - tags.map((tag: Fr) => - tag.equals(index3Tag.value) + tags.map((tag: SiloedTag) => + tag.equals(index3Tag) ? [makeLog(pendingTxHash, index3Tag.value), makeLog(finalizedTxHash, index3Tag.value)] : [], ), diff --git a/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.test.ts b/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.test.ts index f9f536358377..8ad3b8fc0b3f 100644 --- a/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.test.ts +++ b/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.test.ts @@ -4,14 +4,12 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2BlockHash } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; -import { DirectionalAppTaggingSecret, PrivateLog, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { DirectionalAppTaggingSecret, PrivateLog, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import { TxHash } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; import { SenderTaggingDataProvider } from '../../../storage/tagging_data_provider/sender_tagging_data_provider.js'; -import { SiloedTag } from '../../siloed_tag.js'; -import { Tag } from '../../tag.js'; import { loadAndStoreNewTaggingIndexes } from './load_and_store_new_tagging_indexes.js'; describe('loadAndStoreNewTaggingIndexes', () => { @@ -39,14 +37,14 @@ describe('loadAndStoreNewTaggingIndexes', () => { // Unlike for secret, app address and aztecNode we need a fresh instance of the tagging data provider for each test. beforeEach(async () => { - aztecNode.getLogsByTags.mockReset(); + aztecNode.getPrivateLogsByTags.mockReset(); taggingDataProvider = new SenderTaggingDataProvider(await openTmpStore('test')); }); it('no logs found for the given window', async () => { - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { // No log found for any tag - return Promise.resolve(tags.map((_tag: Fr) => [])); + return Promise.resolve(tags.map((_tag: SiloedTag) => [])); }); await loadAndStoreNewTaggingIndexes(secret, app, 0, 10, aztecNode, taggingDataProvider); @@ -65,8 +63,8 @@ describe('loadAndStoreNewTaggingIndexes', () => { const index = 5; const tag = await computeSiloedTagForIndex(index); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { - return Promise.resolve(tags.map((t: Fr) => (t.equals(tag.value) ? [makeLog(txHash, tag.value)] : []))); + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { + return Promise.resolve(tags.map((t: SiloedTag) => (t.equals(tag) ? [makeLog(txHash, tag.value)] : []))); }); await loadAndStoreNewTaggingIndexes(secret, app, 0, 10, aztecNode, taggingDataProvider); @@ -87,12 +85,12 @@ describe('loadAndStoreNewTaggingIndexes', () => { const tag1 = await computeSiloedTagForIndex(index1); const tag2 = await computeSiloedTagForIndex(index2); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.resolve( - tags.map((t: Fr) => { - if (t.equals(tag1.value)) { + tags.map((t: SiloedTag) => { + if (t.equals(tag1)) { return [makeLog(txHash, tag1.value)]; - } else if (t.equals(tag2.value)) { + } else if (t.equals(tag2)) { return [makeLog(txHash, tag2.value)]; } return []; @@ -123,12 +121,12 @@ describe('loadAndStoreNewTaggingIndexes', () => { const tag1 = await computeSiloedTagForIndex(index1); const tag2 = await computeSiloedTagForIndex(index2); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.resolve( - tags.map((t: Fr) => { - if (t.equals(tag1.value)) { + tags.map((t: SiloedTag) => { + if (t.equals(tag1)) { return [makeLog(txHash1, tag1.value)]; - } else if (t.equals(tag2.value)) { + } else if (t.equals(tag2)) { return [makeLog(txHash2, tag2.value)]; } return []; @@ -158,9 +156,9 @@ describe('loadAndStoreNewTaggingIndexes', () => { const index = 4; const tag = await computeSiloedTagForIndex(index); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.resolve( - tags.map((t: Fr) => (t.equals(tag.value) ? [makeLog(txHash1, tag.value), makeLog(txHash2, tag.value)] : [])), + tags.map((t: SiloedTag) => (t.equals(tag) ? [makeLog(txHash1, tag.value), makeLog(txHash2, tag.value)] : [])), ); }); @@ -191,18 +189,18 @@ describe('loadAndStoreNewTaggingIndexes', () => { const tag8 = await computeSiloedTagForIndex(8); const tag9 = await computeSiloedTagForIndex(9); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.resolve( - tags.map((t: Fr) => { - if (t.equals(tag1.value)) { + tags.map((t: SiloedTag) => { + if (t.equals(tag1)) { return [makeLog(txHash1, tag1.value)]; - } else if (t.equals(tag3.value)) { + } else if (t.equals(tag3)) { return [makeLog(txHash2, tag3.value)]; - } else if (t.equals(tag5.value)) { + } else if (t.equals(tag5)) { return [makeLog(txHash2, tag5.value)]; - } else if (t.equals(tag8.value)) { + } else if (t.equals(tag8)) { return [makeLog(txHash1, tag8.value)]; - } else if (t.equals(tag9.value)) { + } else if (t.equals(tag9)) { return [makeLog(txHash3, tag9.value)]; } return []; @@ -245,12 +243,12 @@ describe('loadAndStoreNewTaggingIndexes', () => { const tagAtStart = await computeSiloedTagForIndex(start); const tagAtEnd = await computeSiloedTagForIndex(end); - aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => { + aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => { return Promise.resolve( - tags.map((t: Fr) => { - if (t.equals(tagAtStart.value)) { + tags.map((t: SiloedTag) => { + if (t.equals(tagAtStart)) { return [makeLog(txHashAtStart, tagAtStart.value)]; - } else if (t.equals(tagAtEnd.value)) { + } else if (t.equals(tagAtEnd)) { return [makeLog(txHashAtEnd, tagAtEnd.value)]; } return []; diff --git a/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.ts b/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.ts index a3229a44c99d..e0202aafac84 100644 --- a/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.ts +++ b/yarn-project/pxe/src/tagging/sync/utils/load_and_store_new_tagging_indexes.ts @@ -1,11 +1,10 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import type { DirectionalAppTaggingSecret, PreTag } from '@aztec/stdlib/logs'; +import { SiloedTag, Tag } from '@aztec/stdlib/logs'; import { TxHash } from '@aztec/stdlib/tx'; import type { SenderTaggingDataProvider } from '../../../storage/tagging_data_provider/sender_tagging_data_provider.js'; -import { SiloedTag } from '../../siloed_tag.js'; -import { Tag } from '../../tag.js'; /** * Loads tagging indexes from the Aztec node and stores them in the tagging data provider. @@ -48,9 +47,8 @@ export async function loadAndStoreNewTaggingIndexes( // Returns txs that used the given tags. A tag might have been used in multiple txs and for this reason we return // an array for each tag. async function getTxsContainingTags(tags: SiloedTag[], aztecNode: AztecNode): Promise { - const tagsAsFr = tags.map(tag => tag.value); - const allLogs = await aztecNode.getLogsByTags(tagsAsFr); - return allLogs.map(logs => logs.filter(log => !log.isFromPublic).map(log => log.txHash)); + const allLogs = await aztecNode.getPrivateLogsByTags(tags); + return allLogs.map(logs => logs.map(log => log.txHash)); } // Returns a map of txHash to the highest index for that txHash. diff --git a/yarn-project/pxe/src/tagging/tag.ts b/yarn-project/pxe/src/tagging/tag.ts deleted file mode 100644 index 46df69f643c8..000000000000 --- a/yarn-project/pxe/src/tagging/tag.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; -import type { Fr } from '@aztec/foundation/curves/bn254'; -import type { PreTag } from '@aztec/stdlib/logs'; - -/** - * Represents a tag of a private log. This is not the tag that "appears" on the chain as this tag is first siloed - * with a contract address by kernels before being included in the final log. - */ -export class Tag { - private constructor(public readonly value: Fr) {} - - static async compute(preTag: PreTag): Promise { - const tag = await poseidon2Hash([preTag.secret.value, preTag.index]); - return new Tag(tag); - } -} diff --git a/yarn-project/stdlib/src/hash/hash.ts b/yarn-project/stdlib/src/hash/hash.ts index b984da0412b6..e8cff035d6c1 100644 --- a/yarn-project/stdlib/src/hash/hash.ts +++ b/yarn-project/stdlib/src/hash/hash.ts @@ -69,17 +69,6 @@ export function computeProtocolNullifier(txRequestHash: Fr): Promise { return siloNullifier(AztecAddress.fromBigInt(NULL_MSG_SENDER_CONTRACT_ADDRESS), txRequestHash); } -/** - * Computes a siloed private log tag, given the contract address and the unsiloed tag. - * A siloed private log tag effectively namespaces a log to a specific contract. - * @param contract - The contract address. - * @param unsiloedTag - The unsiloed tag. - * @returns A siloed private log tag. - */ -export function siloPrivateLog(contract: AztecAddress, unsiloedTag: Fr): Promise { - return poseidon2Hash([contract, unsiloedTag]); -} - /** * Computes a public data tree value ready for insertion. * @param value - Raw public data tree value to hash into a tree-insertion-ready value. diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 5466c7daecd5..052bebf963ea 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -27,6 +27,8 @@ import { PublicKeys } from '../keys/public_keys.js'; import { ExtendedContractClassLog } from '../logs/extended_contract_class_log.js'; import { ExtendedPublicLog } from '../logs/extended_public_log.js'; import type { LogFilter } from '../logs/log_filter.js'; +import { SiloedTag } from '../logs/siloed_tag.js'; +import { Tag } from '../logs/tag.js'; import { TxScopedL2Log } from '../logs/tx_scoped_l2_log.js'; import { getTokenContractArtifact } from '../tests/fixtures.js'; import { BlockHeader } from '../tx/block_header.js'; @@ -197,8 +199,14 @@ describe('ArchiverApiSchema', () => { }); }); - it('getLogsByTags', async () => { - const result = await context.client.getLogsByTags([Fr.random()]); + it('getPrivateLogsByTags', async () => { + const result = await context.client.getPrivateLogsByTags([new SiloedTag(Fr.random())]); + expect(result).toEqual([[expect.any(TxScopedL2Log)]]); + }); + + it('getPublicLogsByTagsFromContract', async () => { + const contractAddress = await AztecAddress.random(); + const result = await context.client.getPublicLogsByTagsFromContract(contractAddress, [new Tag(Fr.random())]); expect(result).toEqual([[expect.any(TxScopedL2Log)]]); }); @@ -441,9 +449,18 @@ class MockArchiver implements ArchiverApi { expect(blockNumber).toEqual(BlockNumber(1)); return Promise.resolve(`0x01`); } - async getLogsByTags(tags: Fr[]): Promise { - expect(tags[0]).toBeInstanceOf(Fr); - return [await Promise.all(tags.map(() => TxScopedL2Log.random()))]; + async getPrivateLogsByTags(tags: SiloedTag[], _logsPerTag?: number): Promise { + expect(tags[0]).toBeInstanceOf(SiloedTag); + return [await Promise.all(tags.map(() => TxScopedL2Log.random(false)))]; + } + async getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + _logsPerTag?: number, + ): Promise { + expect(contractAddress).toBeInstanceOf(AztecAddress); + expect(tags[0]).toBeInstanceOf(Tag); + return [await Promise.all(tags.map(() => TxScopedL2Log.random(true)))]; } async getPublicLogs(filter: LogFilter): Promise { expect(filter.txHash).toBeInstanceOf(TxHash); diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 2990f5d440fe..494501e72480 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -17,6 +17,8 @@ import { } from '../contract/index.js'; import { L1RollupConstantsSchema } from '../epoch-helpers/index.js'; import { LogFilterSchema } from '../logs/log_filter.js'; +import { SiloedTag } from '../logs/siloed_tag.js'; +import { Tag } from '../logs/tag.js'; import { TxScopedL2Log } from '../logs/tx_scoped_l2_log.js'; import type { L1ToL2MessageSource } from '../messaging/l1_to_l2_message_source.js'; import { optional, schemas } from '../schemas/schemas.js'; @@ -111,9 +113,13 @@ export const ArchiverApiSchema: ApiSchemaFor = { getBlockHeadersForEpoch: z.function().args(EpochNumberSchema).returns(z.array(BlockHeader.schema)), isEpochComplete: z.function().args(EpochNumberSchema).returns(z.boolean()), getL2Tips: z.function().args().returns(L2TipsSchema), - getLogsByTags: z + getPrivateLogsByTags: z .function() - .args(z.array(schemas.Fr)) + .args(z.array(SiloedTag.schema), optional(schemas.Integer)) + .returns(z.array(z.array(TxScopedL2Log.schema))), + getPublicLogsByTagsFromContract: z + .function() + .args(schemas.AztecAddress, z.array(Tag.schema), optional(schemas.Integer)) .returns(z.array(z.array(TxScopedL2Log.schema))), getPublicLogs: z.function().args(LogFilterSchema).returns(GetPublicLogsResponseSchema), getContractClassLogs: z.function().args(LogFilterSchema).returns(GetContractClassLogsResponseSchema), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index e1530552d030..7f4da739c97e 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -40,6 +40,8 @@ import { PublicKeys } from '../keys/public_keys.js'; import { ExtendedContractClassLog } from '../logs/extended_contract_class_log.js'; import { ExtendedPublicLog } from '../logs/extended_public_log.js'; import type { LogFilter } from '../logs/log_filter.js'; +import { SiloedTag } from '../logs/siloed_tag.js'; +import { Tag } from '../logs/tag.js'; import { TxScopedL2Log } from '../logs/tx_scoped_l2_log.js'; import { getTokenContractArtifact } from '../tests/fixtures.js'; import { MerkleTreeId } from '../trees/merkle_tree_id.js'; @@ -298,8 +300,14 @@ describe('AztecNodeApiSchema', () => { expect(response).toEqual({ logs: [expect.any(ExtendedContractClassLog)], maxLogsHit: true }); }); - it('getLogsByTags', async () => { - const response = await context.client.getLogsByTags([Fr.random()]); + it('getPrivateLogsByTags', async () => { + const response = await context.client.getPrivateLogsByTags([new SiloedTag(Fr.random())]); + expect(response).toEqual([[expect.any(TxScopedL2Log)]]); + }); + + it('getPublicLogsByTagsFromContract', async () => { + const contractAddress = await AztecAddress.random(); + const response = await context.client.getPublicLogsByTagsFromContract(contractAddress, [new Tag(Fr.random())]); expect(response).toEqual([[expect.any(TxScopedL2Log)]]); }); @@ -713,10 +721,20 @@ class MockAztecNode implements AztecNode { expect(filter.contractAddress).toBeInstanceOf(AztecAddress); return Promise.resolve({ logs: [await ExtendedContractClassLog.random()], maxLogsHit: true }); } - async getLogsByTags(tags: Fr[]): Promise { + async getPrivateLogsByTags(tags: SiloedTag[], _logsPerTag?: number): Promise { + expect(tags).toHaveLength(1); + expect(tags[0]).toBeInstanceOf(SiloedTag); + return [[await TxScopedL2Log.random(false)]]; + } + async getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + _logsPerTag?: number, + ): Promise { + expect(contractAddress).toBeInstanceOf(AztecAddress); expect(tags).toHaveLength(1); - expect(tags[0]).toBeInstanceOf(Fr); - return [[await TxScopedL2Log.random()]]; + expect(tags[0]).toBeInstanceOf(Tag); + return [[await TxScopedL2Log.random(true)]]; } sendTx(tx: Tx): Promise { expect(tx).toBeInstanceOf(Tx); diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 2394c75f3741..72f4da99cf11 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -36,8 +36,8 @@ import { ProtocolContractAddressesSchema, } from '../contract/index.js'; import { GasFees } from '../gas/gas_fees.js'; +import { SiloedTag, Tag, TxScopedL2Log } from '../logs/index.js'; import { type LogFilter, LogFilterSchema } from '../logs/log_filter.js'; -import { TxScopedL2Log } from '../logs/tx_scoped_l2_log.js'; import { type ApiSchemaFor, optional, schemas } from '../schemas/schemas.js'; import { MerkleTreeId } from '../trees/merkle_tree_id.js'; import { NullifierMembershipWitness } from '../trees/nullifier_membership_witness.js'; @@ -338,14 +338,29 @@ export interface AztecNode getContractClassLogs(filter: LogFilter): Promise; /** - * 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. + * Gets all private logs that match any of the received tags (i.e. logs with their first field equal to a SiloedTag). + * @param tags - The SiloedTags to filter the logs by. * @param logsPerTag - How many logs to return per tag. Default 10 logs are returned for each tag - * @returns For each received tag, an array of matching logs and metadata (e.g. tx hash) is returned. An empty + * @returns For each received tag, an array of matching private logs and metadata (e.g. tx hash) is returned. An empty * array implies no logs match that tag. There can be multiple logs for 1 tag because tag reuse can happen * --> e.g. when sending a note from multiple unsynched devices. */ - getLogsByTags(tags: Fr[], logsPerTag?: number): Promise; + getPrivateLogsByTags(tags: SiloedTag[], logsPerTag?: number): Promise; + + /** + * Gets all public logs that match any of the received tags from the specified contract (i.e. logs with their first field equal to a Tag). + * @param contractAddress - The contract that emitted the public logs. + * @param tags - The Tags to filter the logs by. + * @param logsPerTag - How many logs to return per tag. Default 10 logs are returned for each tag + * @returns For each received tag, an array of matching public logs and metadata (e.g. tx hash) is returned. An empty + * array implies no logs match that tag. There can be multiple logs for 1 tag because tag reuse can happen + * --> e.g. when sending a note from multiple unsynched devices. + */ + getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + logsPerTag?: number, + ): Promise; /** * Method to submit a transaction to the p2p pool. @@ -601,10 +616,19 @@ export const AztecNodeApiSchema: ApiSchemaFor = { getContractClassLogs: z.function().args(LogFilterSchema).returns(GetContractClassLogsResponseSchema), - getLogsByTags: z + getPrivateLogsByTags: z + .function() + .args( + z.array(SiloedTag.schema).max(MAX_RPC_LEN), + optional(z.number().gte(1).lte(MAX_LOGS_PER_TAG).default(MAX_LOGS_PER_TAG)), + ) + .returns(z.array(z.array(TxScopedL2Log.schema))), + + getPublicLogsByTagsFromContract: z .function() .args( - z.array(schemas.Fr).max(MAX_RPC_LEN), + schemas.AztecAddress, + z.array(Tag.schema).max(MAX_RPC_LEN), optional(z.number().gte(1).lte(MAX_LOGS_PER_TAG).default(MAX_LOGS_PER_TAG)), ) .returns(z.array(z.array(TxScopedL2Log.schema))), diff --git a/yarn-project/stdlib/src/interfaces/l2_logs_source.ts b/yarn-project/stdlib/src/interfaces/l2_logs_source.ts index e1800d53c799..120ad25fd7b0 100644 --- a/yarn-project/stdlib/src/interfaces/l2_logs_source.ts +++ b/yarn-project/stdlib/src/interfaces/l2_logs_source.ts @@ -1,7 +1,9 @@ import type { BlockNumber } from '@aztec/foundation/branded-types'; -import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { AztecAddress } from '../aztec-address/index.js'; import type { LogFilter } from '../logs/log_filter.js'; +import type { SiloedTag } from '../logs/siloed_tag.js'; +import type { Tag } from '../logs/tag.js'; import type { TxScopedL2Log } from '../logs/tx_scoped_l2_log.js'; import type { GetContractClassLogsResponse, GetPublicLogsResponse } from './get_logs_response.js'; @@ -10,13 +12,28 @@ import type { GetContractClassLogsResponse, GetPublicLogsResponse } from './get_ */ export interface L2LogsSource { /** - * 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. + * Gets all private logs that match any of the received tags (i.e. logs with their first field equal to a SiloedTag). + * @param tags - The SiloedTags to filter the logs by. * @param logsPerTag - The maximum number of logs to return for each tag. Default returns everything - * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match + * @returns For each received tag, an array of matching private logs is returned. An empty array implies no logs match * that tag. */ - getLogsByTags(tags: Fr[], logsPerTag?: number): Promise; + getPrivateLogsByTags(tags: SiloedTag[], logsPerTag?: number): Promise; + + /** + * Gets all public logs that match any of the received tags from the specified contract (i.e. logs with their first + * field equal to a Tag). + * @param contractAddress - The contract that emitted the public logs. + * @param tags - The Tags to filter the logs by. + * @param logsPerTag - The maximum number of logs to return for each tag. Default returns everything + * @returns For each received tag, an array of matching public logs is returned. An empty array implies no logs match + * that tag. + */ + getPublicLogsByTagsFromContract( + contractAddress: AztecAddress, + tags: Tag[], + logsPerTag?: number, + ): Promise; /** * Gets public logs based on the provided filter. diff --git a/yarn-project/stdlib/src/logs/index.ts b/yarn-project/stdlib/src/logs/index.ts index bf70cc41e7c6..0c3f96c87f62 100644 --- a/yarn-project/stdlib/src/logs/index.ts +++ b/yarn-project/stdlib/src/logs/index.ts @@ -13,3 +13,5 @@ export * from './shared_secret_derivation.js'; export * from './tx_scoped_l2_log.js'; export * from './message_context.js'; export * from './debug_log.js'; +export * from './tag.js'; +export * from './siloed_tag.js'; diff --git a/yarn-project/stdlib/src/logs/siloed_tag.ts b/yarn-project/stdlib/src/logs/siloed_tag.ts new file mode 100644 index 000000000000..fbd28efe99c2 --- /dev/null +++ b/yarn-project/stdlib/src/logs/siloed_tag.ts @@ -0,0 +1,44 @@ +import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { ZodFor } from '@aztec/foundation/schemas'; + +import type { AztecAddress } from '../aztec-address/index.js'; +import { schemas } from '../schemas/schemas.js'; +import type { Tag } from './tag.js'; + +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ + +/** Branding to ensure fields are not interchangeable types. */ +export interface SiloedTag { + /** Brand. */ + _branding: 'SiloedTag'; +} + +/** + * Represents a tag used in private log as it "appears on the chain" - that is the tag is siloed with a contract + * address that emitted the log. + */ +export class SiloedTag { + constructor(public readonly value: Fr) {} + + static async compute(tag: Tag, app: AztecAddress): Promise { + const siloedTag = await poseidon2Hash([app, tag.value]); + return new SiloedTag(siloedTag); + } + + toString(): string { + return this.value.toString(); + } + + toJSON(): string { + return this.value.toString(); + } + + equals(other: SiloedTag): boolean { + return this.value.equals(other.value); + } + + static get schema(): ZodFor { + return schemas.Fr.transform((fr: Fr) => new SiloedTag(fr)); + } +} diff --git a/yarn-project/stdlib/src/logs/tag.ts b/yarn-project/stdlib/src/logs/tag.ts new file mode 100644 index 000000000000..ff7e120bc5b2 --- /dev/null +++ b/yarn-project/stdlib/src/logs/tag.ts @@ -0,0 +1,42 @@ +import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { ZodFor } from '@aztec/foundation/schemas'; + +import { schemas } from '../schemas/schemas.js'; +import type { PreTag } from './pre_tag.js'; + +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ + +export interface Tag { + /** Brand. */ + _branding: 'Tag'; +} + +/** + * Represents a tag of a private log. This is not the tag that "appears" on the chain as this tag is first siloed + * with a contract address by kernels before being included in the final log. + */ +export class Tag { + constructor(public readonly value: Fr) {} + + static async compute(preTag: PreTag): Promise { + const tag = await poseidon2Hash([preTag.secret.value, preTag.index]); + return new Tag(tag); + } + + toString(): string { + return this.value.toString(); + } + + toJSON(): string { + return this.value.toString(); + } + + equals(other: Tag): boolean { + return this.value.equals(other.value); + } + + static get schema(): ZodFor { + return schemas.Fr.transform((fr: Fr) => new Tag(fr)); + } +} diff --git a/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts index 2c9b8931c679..41877c08eb5d 100644 --- a/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts +++ b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts @@ -95,7 +95,7 @@ export class TxScopedL2Log { static async random(isFromPublic = Math.random() < 0.5) { const log = isFromPublic ? await PublicLog.random() : PrivateLog.random(); - return new TxScopedL2Log(TxHash.random(), 1, 1, BlockNumber(1), L2BlockHash.random(), BigInt(1), log); + return new TxScopedL2Log(TxHash.random(), 1, 1, BlockNumber(1), L2BlockHash.random(), 1n, log); } equals(other: TxScopedL2Log) {