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) {