diff --git a/docs/docs/dev_docs/contracts/syntax/events.md b/docs/docs/dev_docs/contracts/syntax/events.md index 77bd761d968..97c24de0c42 100644 --- a/docs/docs/dev_docs/contracts/syntax/events.md +++ b/docs/docs/dev_docs/contracts/syntax/events.md @@ -111,7 +111,7 @@ Once emitted, unencrypted events are stored in AztecNode and can be queried by a ```bash -aztec-cli get-logs --from 5 --limit 1 +aztec-cli get-logs --fromBlock 5 ``` @@ -122,6 +122,13 @@ aztec-cli get-logs --from 5 --limit 1 +Get logs functionality provides a variety of filtering options. +To display them run: + +```bash +aztec-cli get-logs --help +``` + ## Costs All event data is pushed to Ethereum as calldata by the sequencer and for this reason the cost of emitting an event is non-trivial. diff --git a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts index 14c2d9736a3..c0dfdc3e214 100644 --- a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts @@ -165,7 +165,7 @@ export class Oracle { const logPayload = Buffer.concat(message.map(charBuffer => convertACVMFieldToBuffer(charBuffer).subarray(-1))); const log = new UnencryptedL2Log( AztecAddress.fromString(contractAddress), - FunctionSelector.fromField(fromACVMField(eventSelector)), + FunctionSelector.fromField(fromACVMField(eventSelector)), // TODO https://github.com/AztecProtocol/aztec-packages/issues/2632 logPayload, ); diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index 89dd1a0a433..5b19fbc742d 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -22,7 +22,7 @@ describe('Archiver', () => { beforeEach(() => { publicClient = mock>(); - archiverStore = new MemoryArchiverStore(); + archiverStore = new MemoryArchiverStore(1000); }); it('can start, sync and stop and handle l1 to l2 messages and logs', async () => { diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index ea9c82d3822..82087dc9fa9 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -10,6 +10,7 @@ import { ContractDataSource, EncodedContractFunction, ExtendedContractData, + GetUnencryptedLogsResponse, INITIAL_L2_BLOCK_NUM, L1ToL2Message, L1ToL2MessageSource, @@ -18,6 +19,7 @@ import { L2BlockSource, L2LogsSource, L2Tx, + LogFilter, LogType, TxHash, } from '@aztec/types'; @@ -100,7 +102,7 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource transport: http(chain.rpcUrl), pollingInterval: config.viemPollingIntervalMS, }); - const archiverStore = new MemoryArchiverStore(); + const archiverStore = new MemoryArchiverStore(config.maxLogs ?? 1000); const archiver = new Archiver( publicClient, config.l1Contracts.rollupAddress, @@ -165,7 +167,7 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource * This is a problem for example when setting the last block number marker for L1 to L2 messages - * this.lastProcessedBlockNumber = currentBlockNumber; * It's possible that we actually received messages in block currentBlockNumber + 1 meaning the next time - * we do this sync we get the same message again. Addtionally, the call to get cancelled L1 to L2 messages + * we do this sync we get the same message again. Additionally, the call to get cancelled L1 to L2 messages * could read from a block not present when retrieving pending messages. If a message was added and cancelled * in the same eth block then we could try and cancel a non-existent pending message. * @@ -389,6 +391,15 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource return this.store.getLogs(from, limit, logType); } + /** + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + */ + getUnencryptedLogs(filter: LogFilter): Promise { + return this.store.getUnencryptedLogs(filter); + } + /** * Gets the number of the latest L2 block processed by the block source implementation. * @returns The number of the latest L2 block processed by the block source implementation. diff --git a/yarn-project/archiver/src/archiver/archiver_store.test.ts b/yarn-project/archiver/src/archiver/archiver_store.test.ts index 036af5976f8..d97a813650f 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.test.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.test.ts @@ -1,4 +1,15 @@ -import { INITIAL_L2_BLOCK_NUM, L2Block, L2BlockL2Logs, LogType } from '@aztec/types'; +import { + INITIAL_L2_BLOCK_NUM, + L2Block, + L2BlockContext, + L2BlockL2Logs, + LogId, + LogType, + TxHash, + UnencryptedL2Log, +} from '@aztec/types'; + +import { randomBytes } from 'crypto'; import { ArchiverDataStore, MemoryArchiverStore } from './archiver_store.js'; @@ -6,7 +17,7 @@ describe('Archiver Memory Store', () => { let archiverStore: ArchiverDataStore; beforeEach(() => { - archiverStore = new MemoryArchiverStore(); + archiverStore = new MemoryArchiverStore(1000); }); it('can store and retrieve blocks', async () => { @@ -14,7 +25,7 @@ describe('Archiver Memory Store', () => { .fill(0) .map((_, index) => L2Block.random(index)); await archiverStore.addL2Blocks(blocks); - // Offset indices by INTIAL_L2_BLOCK_NUM to ensure we are correctly aligned + // Offset indices by INITIAL_L2_BLOCK_NUM to ensure we are correctly aligned for (const [from, limit] of [ [0 + INITIAL_L2_BLOCK_NUM, 10], [3 + INITIAL_L2_BLOCK_NUM, 3], @@ -34,7 +45,7 @@ describe('Archiver Memory Store', () => { .fill(0) .map(_ => L2BlockL2Logs.random(6, 3, 2)); await archiverStore.addLogs(logs, logType); - // Offset indices by INTIAL_L2_BLOCK_NUM to ensure we are correctly aligned + // Offset indices by INITIAL_L2_BLOCK_NUM to ensure we are correctly aligned for (const [from, limit] of [ [0 + INITIAL_L2_BLOCK_NUM, 10], [3 + INITIAL_L2_BLOCK_NUM, 3], @@ -71,4 +82,223 @@ describe('Archiver Memory Store', () => { ); }, ); + + describe('getUnencryptedLogs config', () => { + it('does not return more than "maxLogs" logs', async () => { + const maxLogs = 5; + archiverStore = new MemoryArchiverStore(maxLogs); + const blocks = Array(10) + .fill(0) + .map((_, index: number) => L2Block.random(index + 1, 4, 2, 3, 2, 2)); + + await archiverStore.addL2Blocks(blocks); + await archiverStore.addLogs( + blocks.map(block => block.newUnencryptedLogs!), + LogType.UNENCRYPTED, + ); + + const response = await archiverStore.getUnencryptedLogs({}); + + expect(response.maxLogsHit).toBeTruthy(); + expect(response.logs.length).toEqual(maxLogs); + }); + }); + + describe('getUnencryptedLogs filtering', () => { + const txsPerBlock = 4; + const numPublicFunctionCalls = 3; + const numUnencryptedLogs = 4; + const numBlocks = 10; + let blocks: L2Block[]; + + beforeEach(async () => { + blocks = Array(numBlocks) + .fill(0) + .map((_, index: number) => + L2Block.random(index + 1, txsPerBlock, 2, numPublicFunctionCalls, 2, numUnencryptedLogs), + ); + + await archiverStore.addL2Blocks(blocks); + await archiverStore.addLogs( + blocks.map(block => block.newUnencryptedLogs!), + LogType.UNENCRYPTED, + ); + }); + + it('"txHash" filter param is respected', async () => { + // get random tx + const targetBlockIndex = Math.floor(Math.random() * numBlocks); + const targetTxIndex = Math.floor(Math.random() * txsPerBlock); + const targetTxHash = new L2BlockContext(blocks[targetBlockIndex]).getTxHash(targetTxIndex); + + const response = await archiverStore.getUnencryptedLogs({ txHash: targetTxHash }); + const logs = response.logs; + + expect(response.maxLogsHit).toBeFalsy(); + + const expectedNumLogs = numPublicFunctionCalls * numUnencryptedLogs; + expect(logs.length).toEqual(expectedNumLogs); + + const targeBlockNumber = targetBlockIndex + INITIAL_L2_BLOCK_NUM; + for (const log of logs) { + expect(log.id.blockNumber).toEqual(targeBlockNumber); + expect(log.id.txIndex).toEqual(targetTxIndex); + } + }); + + it('"fromBlock" and "toBlock" filter params are respected', async () => { + // Set "fromBlock" and "toBlock" + const fromBlock = 3; + const toBlock = 7; + + const response = await archiverStore.getUnencryptedLogs({ fromBlock, toBlock }); + const logs = response.logs; + + expect(response.maxLogsHit).toBeFalsy(); + + const expectedNumLogs = txsPerBlock * numPublicFunctionCalls * numUnencryptedLogs * (toBlock - fromBlock); + expect(logs.length).toEqual(expectedNumLogs); + + for (const log of logs) { + const blockNumber = log.id.blockNumber; + expect(blockNumber).toBeGreaterThanOrEqual(fromBlock); + expect(blockNumber).toBeLessThan(toBlock); + } + }); + + it('"afterLog" filter param is respected', async () => { + // Get a random log as reference + const targetBlockIndex = Math.floor(Math.random() * numBlocks); + const targetTxIndex = Math.floor(Math.random() * txsPerBlock); + const targetLogIndex = Math.floor(Math.random() * numUnencryptedLogs); + + const afterLog = new LogId(targetBlockIndex + INITIAL_L2_BLOCK_NUM, targetTxIndex, targetLogIndex); + + const response = await archiverStore.getUnencryptedLogs({ afterLog }); + const logs = response.logs; + + expect(response.maxLogsHit).toBeFalsy(); + + for (const log of logs) { + const logId = log.id; + expect(logId.blockNumber).toBeGreaterThanOrEqual(afterLog.blockNumber); + if (logId.blockNumber === afterLog.blockNumber) { + expect(logId.txIndex).toBeGreaterThanOrEqual(afterLog.txIndex); + if (logId.txIndex === afterLog.txIndex) { + expect(logId.logIndex).toBeGreaterThan(afterLog.logIndex); + } + } + } + }); + + it('"contractAddress" filter param is respected', async () => { + // Get a random contract address from the logs + const targetBlockIndex = Math.floor(Math.random() * numBlocks); + const targetTxIndex = Math.floor(Math.random() * txsPerBlock); + const targetFunctionLogIndex = Math.floor(Math.random() * numPublicFunctionCalls); + const targetLogIndex = Math.floor(Math.random() * numUnencryptedLogs); + const targetContractAddress = UnencryptedL2Log.fromBuffer( + blocks[targetBlockIndex].newUnencryptedLogs!.txLogs[targetTxIndex].functionLogs[targetFunctionLogIndex].logs[ + targetLogIndex + ], + ).contractAddress; + + const response = await archiverStore.getUnencryptedLogs({ contractAddress: targetContractAddress }); + + expect(response.maxLogsHit).toBeFalsy(); + + for (const extendedLog of response.logs) { + expect(extendedLog.log.contractAddress.equals(targetContractAddress)).toBeTruthy(); + } + }); + + it('"selector" filter param is respected', async () => { + // Get a random selector from the logs + const targetBlockIndex = Math.floor(Math.random() * numBlocks); + const targetTxIndex = Math.floor(Math.random() * txsPerBlock); + const targetFunctionLogIndex = Math.floor(Math.random() * numPublicFunctionCalls); + const targetLogIndex = Math.floor(Math.random() * numUnencryptedLogs); + const targetSelector = UnencryptedL2Log.fromBuffer( + blocks[targetBlockIndex].newUnencryptedLogs!.txLogs[targetTxIndex].functionLogs[targetFunctionLogIndex].logs[ + targetLogIndex + ], + ).selector; + + const response = await archiverStore.getUnencryptedLogs({ selector: targetSelector }); + + expect(response.maxLogsHit).toBeFalsy(); + + for (const extendedLog of response.logs) { + expect(extendedLog.log.selector.equals(targetSelector)).toBeTruthy(); + } + }); + + it('"txHash" filter param is ignored when "afterLog" is set', async () => { + // Get random txHash + const txHash = new TxHash(randomBytes(TxHash.SIZE)); + const afterLog = new LogId(1, 0, 0); + + const response = await archiverStore.getUnencryptedLogs({ txHash, afterLog }); + expect(response.logs.length).toBeGreaterThan(1); + }); + + it('intersecting works', async () => { + let logs = (await archiverStore.getUnencryptedLogs({ fromBlock: -10, toBlock: -5 })).logs; + expect(logs.length).toBe(0); + + // "fromBlock" gets correctly trimmed to range and "toBlock" is exclusive + logs = (await archiverStore.getUnencryptedLogs({ fromBlock: -10, toBlock: 5 })).logs; + let blockNumbers = new Set(logs.map(log => log.id.blockNumber)); + expect(blockNumbers).toEqual(new Set([1, 2, 3, 4])); + + // "toBlock" should be exclusive + logs = (await archiverStore.getUnencryptedLogs({ fromBlock: 1, toBlock: 1 })).logs; + expect(logs.length).toBe(0); + + logs = (await archiverStore.getUnencryptedLogs({ fromBlock: 10, toBlock: 5 })).logs; + expect(logs.length).toBe(0); + + // both "fromBlock" and "toBlock" get correctly capped to range and logs from all blocks are returned + logs = (await archiverStore.getUnencryptedLogs({ fromBlock: -100, toBlock: +100 })).logs; + blockNumbers = new Set(logs.map(log => log.id.blockNumber)); + expect(blockNumbers.size).toBe(numBlocks); + + // intersecting with "afterLog" works + logs = (await archiverStore.getUnencryptedLogs({ fromBlock: 2, toBlock: 5, afterLog: new LogId(4, 0, 0) })).logs; + blockNumbers = new Set(logs.map(log => log.id.blockNumber)); + expect(blockNumbers).toEqual(new Set([4])); + + logs = (await archiverStore.getUnencryptedLogs({ toBlock: 5, afterLog: new LogId(5, 1, 0) })).logs; + expect(logs.length).toBe(0); + + logs = (await archiverStore.getUnencryptedLogs({ fromBlock: 2, toBlock: 5, afterLog: new LogId(100, 0, 0) })) + .logs; + expect(logs.length).toBe(0); + }); + + it('"txIndex" and "logIndex" are respected when "afterLog.blockNumber" is equal to "fromBlock"', async () => { + // Get a random log as reference + const targetBlockIndex = Math.floor(Math.random() * numBlocks); + const targetTxIndex = Math.floor(Math.random() * txsPerBlock); + const targetLogIndex = Math.floor(Math.random() * numUnencryptedLogs); + + const afterLog = new LogId(targetBlockIndex + INITIAL_L2_BLOCK_NUM, targetTxIndex, targetLogIndex); + + const response = await archiverStore.getUnencryptedLogs({ afterLog, fromBlock: afterLog.blockNumber }); + const logs = response.logs; + + expect(response.maxLogsHit).toBeFalsy(); + + for (const log of logs) { + const logId = log.id; + expect(logId.blockNumber).toBeGreaterThanOrEqual(afterLog.blockNumber); + if (logId.blockNumber === afterLog.blockNumber) { + expect(logId.txIndex).toBeGreaterThanOrEqual(afterLog.txIndex); + if (logId.txIndex === afterLog.txIndex) { + expect(logId.logIndex).toBeGreaterThan(afterLog.logIndex); + } + } + } + }); + }); }); diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index b64a973f677..f24db5057f8 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -3,13 +3,19 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { ContractData, ExtendedContractData, + ExtendedUnencryptedL2Log, + GetUnencryptedLogsResponse, INITIAL_L2_BLOCK_NUM, L1ToL2Message, L2Block, + L2BlockContext, L2BlockL2Logs, L2Tx, + LogFilter, + LogId, LogType, TxHash, + UnencryptedL2Log, } from '@aztec/types'; import { L1ToL2MessageStore, PendingL1ToL2MessageStore } from './l1_to_l2_message_store.js'; @@ -94,6 +100,13 @@ export interface ArchiverDataStore { */ getLogs(from: number, limit: number, logType: LogType): Promise; + /** + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + */ + getUnencryptedLogs(filter: LogFilter): Promise; + /** * Add new extended contract data from an L2 block to the store's list. * @param data - List of contracts' data to be added. @@ -152,7 +165,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { /** * An array containing all the L2 blocks that have been fetched so far. */ - private l2Blocks: L2Block[] = []; + private l2BlockContexts: L2BlockContext[] = []; /** * An array containing all the L2 Txs in the L2 blocks that have been fetched so far. @@ -163,13 +176,13 @@ export class MemoryArchiverStore implements ArchiverDataStore { * An array containing all the encrypted logs that have been fetched so far. * Note: Index in the "outer" array equals to (corresponding L2 block's number - INITIAL_L2_BLOCK_NUM). */ - private encryptedLogs: L2BlockL2Logs[] = []; + private encryptedLogsPerBlock: L2BlockL2Logs[] = []; /** * An array containing all the unencrypted logs that have been fetched so far. * Note: Index in the "outer" array equals to (corresponding L2 block's number - INITIAL_L2_BLOCK_NUM). */ - private unencryptedLogs: L2BlockL2Logs[] = []; + private unencryptedLogsPerBlock: L2BlockL2Logs[] = []; /** * A sparse array containing all the extended contract data that have been fetched so far. @@ -192,7 +205,10 @@ export class MemoryArchiverStore implements ArchiverDataStore { */ private pendingL1ToL2Messages: PendingL1ToL2MessageStore = new PendingL1ToL2MessageStore(); - constructor() {} + constructor( + /** The max number of logs that can be obtained in 1 "getUnencryptedLogs" call. */ + public readonly maxLogs: number, + ) {} /** * Append new blocks to the store's list. @@ -200,7 +216,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @returns True if the operation is successful (always in this implementation). */ public addL2Blocks(blocks: L2Block[]): Promise { - this.l2Blocks.push(...blocks); + this.l2BlockContexts.push(...blocks.map(block => new L2BlockContext(block))); this.l2Txs.push(...blocks.flatMap(b => b.getTxs())); return Promise.resolve(true); } @@ -212,7 +228,9 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @returns True if the operation is successful. */ addLogs(data: L2BlockL2Logs[], logType: LogType): Promise { - logType === LogType.ENCRYPTED ? this.encryptedLogs.push(...data) : this.unencryptedLogs.push(...data); + logType === LogType.ENCRYPTED + ? this.encryptedLogsPerBlock.push(...data) + : this.unencryptedLogsPerBlock.push(...data); return Promise.resolve(true); } @@ -287,12 +305,12 @@ export class MemoryArchiverStore implements ArchiverDataStore { if (limit < 1) { throw new Error(`Invalid block range from: ${from}, limit: ${limit}`); } - if (from < INITIAL_L2_BLOCK_NUM || from > this.l2Blocks.length) { + if (from < INITIAL_L2_BLOCK_NUM || from > this.l2BlockContexts.length) { return Promise.resolve([]); } const startIndex = from - INITIAL_L2_BLOCK_NUM; const endIndex = startIndex + limit; - return Promise.resolve(this.l2Blocks.slice(startIndex, endIndex)); + return Promise.resolve(this.l2BlockContexts.slice(startIndex, endIndex).map(blockContext => blockContext.block)); } /** @@ -338,7 +356,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { if (from < INITIAL_L2_BLOCK_NUM || limit < 1) { throw new Error(`Invalid block range from: ${from}, limit: ${limit}`); } - const logs = logType === LogType.ENCRYPTED ? this.encryptedLogs : this.unencryptedLogs; + const logs = logType === LogType.ENCRYPTED ? this.encryptedLogsPerBlock : this.unencryptedLogsPerBlock; if (from > logs.length) { return Promise.resolve([]); } @@ -347,6 +365,90 @@ export class MemoryArchiverStore implements ArchiverDataStore { return Promise.resolve(logs.slice(startIndex, endIndex)); } + /** + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + * @remarks Works by doing an intersection of all params in the filter. + */ + getUnencryptedLogs(filter: LogFilter): Promise { + let txHash: TxHash | undefined; + let fromBlockIndex = 0; + let toBlockIndex = this.unencryptedLogsPerBlock.length; + let txIndexInBlock = 0; + let logIndexInTx = 0; + + if (filter.afterLog) { + // Continuation parameter is set --> tx hash is ignored + if (filter.fromBlock == undefined || filter.fromBlock <= filter.afterLog.blockNumber) { + fromBlockIndex = filter.afterLog.blockNumber - INITIAL_L2_BLOCK_NUM; + txIndexInBlock = filter.afterLog.txIndex; + logIndexInTx = filter.afterLog.logIndex + 1; // We want to start from the next log + } else { + fromBlockIndex = filter.fromBlock - INITIAL_L2_BLOCK_NUM; + } + } else { + txHash = filter.txHash; + + if (filter.fromBlock !== undefined) { + fromBlockIndex = filter.fromBlock - INITIAL_L2_BLOCK_NUM; + } + } + + if (filter.toBlock !== undefined) { + toBlockIndex = filter.toBlock - INITIAL_L2_BLOCK_NUM; + } + + // Ensure the indices are within block array bounds + fromBlockIndex = Math.max(fromBlockIndex, 0); + toBlockIndex = Math.min(toBlockIndex, this.unencryptedLogsPerBlock.length); + + if (fromBlockIndex > this.unencryptedLogsPerBlock.length || toBlockIndex < fromBlockIndex || toBlockIndex <= 0) { + return Promise.resolve({ + logs: [], + maxLogsHit: false, + }); + } + + const contractAddress = filter.contractAddress; + const selector = filter.selector; + + const logs: ExtendedUnencryptedL2Log[] = []; + + for (; fromBlockIndex < toBlockIndex; fromBlockIndex++) { + const blockContext = this.l2BlockContexts[fromBlockIndex]; + const blockLogs = this.unencryptedLogsPerBlock[fromBlockIndex]; + for (; txIndexInBlock < blockLogs.txLogs.length; txIndexInBlock++) { + const txLogs = blockLogs.txLogs[txIndexInBlock].unrollLogs().map(log => UnencryptedL2Log.fromBuffer(log)); + for (; logIndexInTx < txLogs.length; logIndexInTx++) { + const log = txLogs[logIndexInTx]; + if ( + (!txHash || blockContext.getTxHash(txIndexInBlock).equals(txHash)) && + (!contractAddress || log.contractAddress.equals(contractAddress)) && + (!selector || log.selector.equals(selector)) + ) { + logs.push( + new ExtendedUnencryptedL2Log(new LogId(blockContext.block.number, txIndexInBlock, logIndexInTx), log), + ); + if (logs.length === this.maxLogs) { + return Promise.resolve({ + logs, + maxLogsHit: true, + }); + } + } + } + logIndexInTx = 0; + } + txIndexInBlock = 0; + } + + return Promise.resolve({ + logs, + maxLogsHit: false, + }); + } + /** * Get the extended contract data for this contract. * @param contractAddress - The contract data address. @@ -363,7 +465,7 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @returns All extended contract data in the block (if found). */ public getExtendedContractDataInBlock(blockNum: number): Promise { - if (blockNum > this.l2Blocks.length) { + if (blockNum > this.l2BlockContexts.length) { return Promise.resolve([]); } return Promise.resolve(this.extendedContractDataByBlock[blockNum] || []); @@ -379,8 +481,8 @@ export class MemoryArchiverStore implements ArchiverDataStore { if (contractAddress.isZero()) { return Promise.resolve(undefined); } - for (const block of this.l2Blocks) { - for (const contractData of block.newContractData) { + for (const blockContext of this.l2BlockContexts) { + for (const contractData of blockContext.block.newContractData) { if (contractData.contractAddress.equals(contractAddress)) { return Promise.resolve(contractData); } @@ -396,10 +498,10 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @returns ContractData with the portal address (if we didn't throw an error). */ public getContractDataInBlock(l2BlockNum: number): Promise { - if (l2BlockNum > this.l2Blocks.length) { + if (l2BlockNum > this.l2BlockContexts.length) { return Promise.resolve([]); } - const block = this.l2Blocks[l2BlockNum]; + const block = this.l2BlockContexts[l2BlockNum].block; return Promise.resolve(block.newContractData); } @@ -408,8 +510,8 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @returns The number of the latest L2 block processed. */ public getBlockNumber(): Promise { - if (this.l2Blocks.length === 0) return Promise.resolve(INITIAL_L2_BLOCK_NUM - 1); - return Promise.resolve(this.l2Blocks[this.l2Blocks.length - 1].number); + if (this.l2BlockContexts.length === 0) return Promise.resolve(INITIAL_L2_BLOCK_NUM - 1); + return Promise.resolve(this.l2BlockContexts[this.l2BlockContexts.length - 1].block.number); } /** @@ -417,6 +519,6 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @returns The length of L2 Blocks array. */ public getBlocksLength(): number { - return this.l2Blocks.length; + return this.l2BlockContexts.length; } } diff --git a/yarn-project/archiver/src/archiver/config.ts b/yarn-project/archiver/src/archiver/config.ts index 292475dcdfb..61b91368f15 100644 --- a/yarn-project/archiver/src/archiver/config.ts +++ b/yarn-project/archiver/src/archiver/config.ts @@ -46,6 +46,9 @@ export interface ArchiverConfig { * Optional dir to store data. If omitted will store in memory. */ dataDirectory?: string; + + /** The max number of logs that can be obtained in 1 "getUnencryptedLogs" call. */ + maxLogs?: number; } /** diff --git a/yarn-project/archiver/src/index.ts b/yarn-project/archiver/src/index.ts index b327c5f9bb4..0f206b45ca0 100644 --- a/yarn-project/archiver/src/index.ts +++ b/yarn-project/archiver/src/index.ts @@ -24,7 +24,7 @@ async function main() { transport: http(rpcUrl), }); - const archiverStore = new MemoryArchiverStore(); + const archiverStore = new MemoryArchiverStore(1000); const archiver = new Archiver( publicClient, diff --git a/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts b/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts index 51205c979af..03f94ab8252 100644 --- a/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts +++ b/yarn-project/aztec-node/src/aztec-node/http_rpc_server.ts @@ -1,4 +1,4 @@ -import { HistoricBlockData } from '@aztec/circuits.js'; +import { FunctionSelector, HistoricBlockData } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; @@ -7,10 +7,12 @@ import { AztecNode, ContractData, ExtendedContractData, + ExtendedUnencryptedL2Log, L1ToL2MessageAndIndex, L2Block, L2BlockL2Logs, L2Tx, + LogId, SiblingPath, Tx, TxHash, @@ -28,11 +30,14 @@ export function createAztecNodeRpcServer(node: AztecNode) { AztecAddress, EthAddress, ExtendedContractData, + ExtendedUnencryptedL2Log, ContractData, Fr, + FunctionSelector, HistoricBlockData, L2Block, L2Tx, + LogId, TxHash, SiblingPath, L1ToL2MessageAndIndex, diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index f9b828e89df..08bab77942a 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -24,6 +24,7 @@ import { ContractData, ContractDataSource, ExtendedContractData, + GetUnencryptedLogsResponse, L1ToL2MessageAndIndex, L1ToL2MessageSource, L2Block, @@ -31,6 +32,7 @@ import { L2BlockSource, L2LogsSource, L2Tx, + LogFilter, LogType, MerkleTreeId, SiblingPath, @@ -225,6 +227,15 @@ export class AztecNodeService implements AztecNode { return logSource.getLogs(from, limit, logType); } + /** + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + */ + getUnencryptedLogs(filter: LogFilter): Promise { + return this.unencryptedLogsSource.getUnencryptedLogs(filter); + } + /** * Method to submit a transaction to the p2p pool. * @param tx - The transaction to be submitted. diff --git a/yarn-project/aztec.js/src/contract/sent_tx.ts b/yarn-project/aztec.js/src/contract/sent_tx.ts index 9ab94c115e3..8f920177d28 100644 --- a/yarn-project/aztec.js/src/contract/sent_tx.ts +++ b/yarn-project/aztec.js/src/contract/sent_tx.ts @@ -1,6 +1,6 @@ import { FieldsOf } from '@aztec/circuits.js'; import { retryUntil } from '@aztec/foundation/retry'; -import { PXE, TxHash, TxReceipt, TxStatus } from '@aztec/types'; +import { GetUnencryptedLogsResponse, PXE, TxHash, TxReceipt, TxStatus } from '@aztec/types'; import every from 'lodash.every'; @@ -80,6 +80,16 @@ export class SentTx { return receipt.status === TxStatus.MINED; } + /** + * Gets unencrypted logs emitted by this tx. + * @remarks This function will wait for the tx to be mined if it hasn't been already. + * @returns The requested logs. + */ + public async getUnencryptedLogs(): Promise { + await this.wait(); + return this.pxe.getUnencryptedLogs({ txHash: await this.getTxHash() }); + } + protected async waitForReceipt(opts?: WaitOpts): Promise { const txHash = await this.getTxHash(); return await retryUntil( diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index eade673bd6a..dd8d2ecc845 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -7,7 +7,9 @@ export * from './contract_deployer/deploy_method.js'; export * from './sandbox/index.js'; export * from './wallet/index.js'; -export { AztecAddress, EthAddress, Point, Fr, GrumpkinScalar } from '@aztec/circuits.js'; +// TODO https://github.com/AztecProtocol/aztec-packages/issues/2632 --> FunctionSelector might not need to be exposed +// here once the issue is resolved. +export { AztecAddress, EthAddress, Point, Fr, FunctionSelector, GrumpkinScalar } from '@aztec/circuits.js'; export { PXE, ContractData, @@ -15,6 +17,7 @@ export { DeployedContract, FunctionCall, L2BlockL2Logs, + LogFilter, UnencryptedL2Log, NodeInfo, NotePreimage, diff --git a/yarn-project/aztec.js/src/pxe_client.ts b/yarn-project/aztec.js/src/pxe_client.ts index 0514626c1a9..4e03af4ffbc 100644 --- a/yarn-project/aztec.js/src/pxe_client.ts +++ b/yarn-project/aztec.js/src/pxe_client.ts @@ -1,11 +1,21 @@ -import { AztecAddress, CompleteAddress, EthAddress, Fr, GrumpkinScalar, Point } from '@aztec/circuits.js'; +import { + AztecAddress, + CompleteAddress, + EthAddress, + Fr, + FunctionSelector, + GrumpkinScalar, + Point, +} from '@aztec/circuits.js'; import { createJsonRpcClient, makeFetch } from '@aztec/foundation/json-rpc/client'; import { AuthWitness, ContractData, ExtendedContractData, + ExtendedUnencryptedL2Log, L2BlockL2Logs, L2Tx, + LogId, NotePreimage, PXE, Tx, @@ -21,10 +31,12 @@ export const createPXEClient = (url: string, fetch = makeFetch([1, 2, 3], true)) url, { CompleteAddress, + FunctionSelector, AztecAddress, TxExecutionRequest, ContractData, ExtendedContractData, + ExtendedUnencryptedL2Log, TxHash, EthAddress, Point, @@ -33,6 +45,7 @@ export const createPXEClient = (url: string, fetch = makeFetch([1, 2, 3], true)) NotePreimage, AuthWitness, L2Tx, + LogId, }, { Tx, TxReceipt, L2BlockL2Logs }, false, diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index fb7949db8b3..6b215ee3ea6 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -5,8 +5,9 @@ import { DeployedContract, ExtendedContractData, FunctionCall, - L2BlockL2Logs, + GetUnencryptedLogsResponse, L2Tx, + LogFilter, NodeInfo, NotePreimage, PXE, @@ -96,8 +97,8 @@ export abstract class BaseWallet implements Wallet { getContractData(contractAddress: AztecAddress): Promise { return this.pxe.getContractData(contractAddress); } - getUnencryptedLogs(from: number, limit: number): Promise { - return this.pxe.getUnencryptedLogs(from, limit); + getUnencryptedLogs(filter: LogFilter): Promise { + return this.pxe.getUnencryptedLogs(filter); } getBlockNumber(): Promise { return this.pxe.getBlockNumber(); diff --git a/yarn-project/canary/tsconfig.json b/yarn-project/canary/tsconfig.json index 7621a947004..2e42dbb2f9a 100644 --- a/yarn-project/canary/tsconfig.json +++ b/yarn-project/canary/tsconfig.json @@ -24,7 +24,7 @@ }, { "path": "../end-to-end" - }, + } ], "include": ["src"] } diff --git a/yarn-project/cli/README.md b/yarn-project/cli/README.md index 0ae7ead9837..70007f28da5 100644 --- a/yarn-project/cli/README.md +++ b/yarn-project/cli/README.md @@ -387,29 +387,29 @@ aztec-cli parse-parameter-struct 0xabcdef1234567890abcdef1234567890abcdef1234567 ### get-logs -Gets all the unencrypted logs from L2 blocks in the specified range. +Applies filter and returns the resulting unencrypted logs. +The filter is applied by doing an intersection of all its params. Syntax: ```shell -aztec-cli get-logs --from --limit [options] +aztec-cli get-logs --fromBlock ``` - -- `from`: Block number to start fetching logs from. -- `limit`: Maximum number of block logs to obtain. - Options: - `-u, --rpc-url `: URL of PXE Service. Default: `http://localhost:8080`. -This command retrieves and displays all the unencrypted logs from L2 blocks in the specified range. It shows the logs found in the blocks and unrolls them for readability. - +This command retrieves and displays all the unencrypted logs from L2 blocks in the specified range or from a specific transaction. Example usage: ```shell -aztec-cli get-logs --from 1000 --limit 10 +aztec-cli get-logs --txHash 21fef567e01f8508e30843ebcef9c5f6ff27b29d66783cfcdbd070c3a9174234 +aztec-cli get-logs --fromBlock 4 --toBlock 5 --contractAddress 0x1db5f68861c5960c37205d3d5b23466240359c115c49e45982865ea7ace69a02 +aztec-cli get-logs --fromBlock 4 --toBlock 5 --contractAddress 0x1db5f68861c5960c37205d3d5b23466240359c115c49e45982865ea7ace69a02 --selector 00000005 ``` +Run `aztec-cli get-logs --help` for more information on the filtering options. + ### block-number Gets the current Aztec L2 block number. diff --git a/yarn-project/cli/src/index.ts b/yarn-project/cli/src/index.ts index 8e60c31a4a7..6ac81cd4af2 100644 --- a/yarn-project/cli/src/index.ts +++ b/yarn-project/cli/src/index.ts @@ -11,9 +11,10 @@ import { import { StructType, decodeFunctionSignatureWithParameterNames } from '@aztec/foundation/abi'; import { JsonStringify } from '@aztec/foundation/json-rpc'; import { DebugLogger, LogFn } from '@aztec/foundation/log'; +import { sleep } from '@aztec/foundation/sleep'; import { fileURLToPath } from '@aztec/foundation/url'; import { compileContract, generateNoirInterface, generateTypescriptInterface } from '@aztec/noir-compiler/cli'; -import { CompleteAddress, ContractData, L2BlockL2Logs } from '@aztec/types'; +import { CompleteAddress, ContractData, LogFilter } from '@aztec/types'; import { createSecp256k1PeerId } from '@libp2p/peer-id-factory'; import { Command, Option } from 'commander'; @@ -34,6 +35,11 @@ import { parseAztecAddress, parseField, parseFields, + parseOptionalAztecAddress, + parseOptionalInteger, + parseOptionalLogId, + parseOptionalSelector, + parseOptionalTxHash, parsePartialAddress, parsePrivateKey, parsePublicKey, @@ -288,22 +294,58 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { program .command('get-logs') - .description('Gets all the unencrypted logs from L2 blocks in the range specified.') - .option('-f, --from ', 'Initial block number for getting logs (defaults to 1).') - .option('-l, --limit ', 'How many blocks to fetch (defaults to 100).') + .description('Gets all the unencrypted logs from an intersection of all the filter params.') + .option('-tx, --tx-hash ', 'A transaction hash to get the receipt for.', parseOptionalTxHash) + .option( + '-fb, --from-block ', + 'Initial block number for getting logs (defaults to 1).', + parseOptionalInteger, + ) + .option('-tb, --to-block ', 'Up to which block to fetch logs (defaults to latest).', parseOptionalInteger) + .option('-al --after-log ', 'ID of a log after which to fetch the logs.', parseOptionalLogId) + .option('-ca, --contract-address
', 'Contract address to filter logs by.', parseOptionalAztecAddress) + .option('-s, --selector ', 'Event selector to filter logs by.', parseOptionalSelector) .addOption(pxeOption) - .action(async options => { - const { from, limit } = options; - const fromBlock = from ? parseInt(from) : 1; - const limitCount = limit ? parseInt(limit) : 100; + .option('--follow', 'If set, will keep polling for new logs until interrupted.') + .action(async ({ txHash, fromBlock, toBlock, afterLog, contractAddress, selector, rpcUrl, follow }) => { + const pxe = await createCompatibleClient(rpcUrl, debugLogger); - const client = await createCompatibleClient(options.rpcUrl, debugLogger); - const logs = await client.getUnencryptedLogs(fromBlock, limitCount); - if (!logs.length) { - log(`No logs found in blocks ${fromBlock} to ${fromBlock + limitCount}`); + if (follow) { + if (txHash) throw Error('Cannot use --follow with --tx-hash'); + if (toBlock) throw Error('Cannot use --follow with --to-block'); + } + + const filter: LogFilter = { txHash, fromBlock, toBlock, afterLog, contractAddress, selector }; + + const fetchLogs = async () => { + const response = await pxe.getUnencryptedLogs(filter); + const logs = response.logs; + + if (!logs.length) { + const filterOptions = Object.entries(filter) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + if (!follow) log(`No logs found for filter: {${filterOptions}}`); + } else { + if (!follow && !filter.afterLog) log('Logs found: \n'); + logs.forEach(unencryptedLog => log(unencryptedLog.toHumanReadable())); + // Set the continuation parameter for the following requests + filter.afterLog = logs[logs.length - 1].id; + } + return response.maxLogsHit; + }; + + if (follow) { + log('Fetching logs...'); + while (true) { + const maxLogsHit = await fetchLogs(); + if (!maxLogsHit) await sleep(1000); + } } else { - log('Logs found: \n'); - L2BlockL2Logs.unrollLogs(logs).forEach(fnLog => log(`${fnLog.toString('ascii')}\n`)); + while (await fetchLogs()) { + // Keep fetching logs until we reach the end. + } } }); diff --git a/yarn-project/cli/src/utils.ts b/yarn-project/cli/src/utils.ts index 0937bed88eb..66b6012e54f 100644 --- a/yarn-project/cli/src/utils.ts +++ b/yarn-project/cli/src/utils.ts @@ -1,4 +1,4 @@ -import { AztecAddress, Fr, GrumpkinScalar, PXE, Point, TxHash } from '@aztec/aztec.js'; +import { AztecAddress, Fr, FunctionSelector, GrumpkinScalar, PXE, Point, TxHash } from '@aztec/aztec.js'; import { L1ContractArtifactsForDeployment, createEthereumChain, deployL1Contracts } from '@aztec/ethereum'; import { ContractArtifact } from '@aztec/foundation/abi'; import { DebugLogger, LogFn } from '@aztec/foundation/log'; @@ -14,6 +14,7 @@ import { RollupAbi, RollupBytecode, } from '@aztec/l1-artifacts'; +import { LogId } from '@aztec/types'; import { InvalidArgumentError } from 'commander'; import fs from 'fs'; @@ -197,9 +198,10 @@ export function parseSaltFromHexString(str: string): Fr { } /** - * Parses an AztecAddress from a string. Throws InvalidArgumentError if the string is not a valid. + * Parses an AztecAddress from a string. * @param address - A serialised Aztec address * @returns An Aztec address + * @throws InvalidArgumentError if the input string is not valid. */ export function parseAztecAddress(address: string): AztecAddress { try { @@ -210,9 +212,71 @@ export function parseAztecAddress(address: string): AztecAddress { } /** - * Parses a TxHash from a string. Throws InvalidArgumentError if the string is not a valid. + * Parses an AztecAddress from a string. + * @param address - A serialised Aztec address + * @returns An Aztec address + * @throws InvalidArgumentError if the input string is not valid. + */ +export function parseOptionalAztecAddress(address: string): AztecAddress | undefined { + if (!address) { + return undefined; + } + return parseAztecAddress(address); +} + +/** + * Parses an optional log ID string into a LogId object. + * + * @param logId - The log ID string to parse. + * @returns The parsed LogId object, or undefined if the log ID is missing or empty. + */ +export function parseOptionalLogId(logId: string): LogId | undefined { + if (!logId) { + return undefined; + } + return LogId.fromString(logId); +} + +/** + * Parses a selector from a string. + * @param selector - A serialised selector. + * @returns A selector. + * @throws InvalidArgumentError if the input string is not valid. + */ +export function parseOptionalSelector(selector: string): FunctionSelector | undefined { + if (!selector) { + return undefined; + } + try { + return FunctionSelector.fromString(selector); + } catch { + throw new InvalidArgumentError(`Invalid selector: ${selector}`); + } +} + +/** + * Parses a string into an integer or returns undefined if the input is falsy. + * + * @param value - The string to parse into an integer. + * @returns The parsed integer, or undefined if the input string is falsy. + * @throws If the input is not a valid integer. + */ +export function parseOptionalInteger(value: string): number | undefined { + if (!value) { + return undefined; + } + const parsed = Number(value); + if (!Number.isInteger(parsed)) { + throw new InvalidArgumentError('Invalid integer.'); + } + return parsed; +} + +/** + * Parses a TxHash from a string. * @param txHash - A transaction hash * @returns A TxHash instance + * @throws InvalidArgumentError if the input string is not valid. */ export function parseTxHash(txHash: string): TxHash { try { @@ -223,9 +287,24 @@ export function parseTxHash(txHash: string): TxHash { } /** - * Parses a public key from a string. Throws InvalidArgumentError if the string is not a valid. + * Parses an optional TxHash from a string. + * Calls parseTxHash internally. + * @param txHash - A transaction hash + * @returns A TxHash instance, or undefined if the input string is falsy. + * @throws InvalidArgumentError if the input string is not valid. + */ +export function parseOptionalTxHash(txHash: string): TxHash | undefined { + if (!txHash) { + return undefined; + } + return parseTxHash(txHash); +} + +/** + * Parses a public key from a string. * @param publicKey - A public key * @returns A Point instance + * @throws InvalidArgumentError if the input string is not valid. */ export function parsePublicKey(publicKey: string): Point { try { @@ -236,9 +315,10 @@ export function parsePublicKey(publicKey: string): Point { } /** - * Parses a partial address from a string. Throws InvalidArgumentError if the string is not a valid. + * Parses a partial address from a string. * @param address - A partial address * @returns A Fr instance + * @throws InvalidArgumentError if the input string is not valid. */ export function parsePartialAddress(address: string): Fr { try { @@ -249,9 +329,10 @@ export function parsePartialAddress(address: string): Fr { } /** - * Parses a private key from a string. Throws InvalidArgumentError if the string is not a valid. + * Parses a private key from a string. * @param privateKey - A string * @returns A private key + * @throws InvalidArgumentError if the input string is not valid. */ export function parsePrivateKey(privateKey: string): GrumpkinScalar { try { @@ -268,9 +349,10 @@ export function parsePrivateKey(privateKey: string): GrumpkinScalar { } /** - * Parses a field from a string. Throws InvalidArgumentError if the string is not a valid field value. + * Parses a field from a string. * @param field - A string representing the field. * @returns A field. + * @throws InvalidArgumentError if the input string is not valid. */ export function parseField(field: string): Fr { try { diff --git a/yarn-project/end-to-end/src/e2e_nested_contract.test.ts b/yarn-project/end-to-end/src/e2e_nested_contract.test.ts index 8136b5f60a8..0abd5912844 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract.test.ts @@ -3,7 +3,7 @@ import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { DebugLogger } from '@aztec/foundation/log'; import { toBigInt } from '@aztec/foundation/serialize'; import { ChildContract, ImportTestContract, ParentContract, TestContract } from '@aztec/noir-contracts/types'; -import { L2BlockL2Logs, PXE, UnencryptedL2Log } from '@aztec/types'; +import { PXE } from '@aztec/types'; import { setup } from './fixtures/utils.js'; @@ -114,10 +114,13 @@ describe('e2e_nested_contract', () => { ]; const tx = await new BatchCall(wallet, actions).send().wait(); - const logs = L2BlockL2Logs.unrollLogs(await wallet.getUnencryptedLogs(tx.blockNumber!, 1)).map(log => - toBigIntBE(UnencryptedL2Log.fromBuffer(log).data), - ); - expect(logs).toEqual([20n, 40n]); + const extendedLogs = ( + await wallet.getUnencryptedLogs({ + fromBlock: tx.blockNumber!, + }) + ).logs; + const processedLogs = extendedLogs.map(extendedLog => toBigIntBE(extendedLog.log.data)); + expect(processedLogs).toEqual([20n, 40n]); expect(await getChildStoredValue(childContract)).toEqual(40n); }); }); diff --git a/yarn-project/end-to-end/src/e2e_ordering.test.ts b/yarn-project/end-to-end/src/e2e_ordering.test.ts index f53fa53d544..7f3b722125c 100644 --- a/yarn-project/end-to-end/src/e2e_ordering.test.ts +++ b/yarn-project/end-to-end/src/e2e_ordering.test.ts @@ -5,7 +5,7 @@ import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { Fr } from '@aztec/foundation/fields'; import { toBigInt } from '@aztec/foundation/serialize'; import { ChildContract, ParentContract } from '@aztec/noir-contracts/types'; -import { L2BlockL2Logs, PXE, TxStatus, UnencryptedL2Log } from '@aztec/types'; +import { PXE, TxStatus } from '@aztec/types'; import { setup } from './fixtures/utils.js'; @@ -16,12 +16,13 @@ describe('e2e_ordering', () => { let teardown: () => Promise; const expectLogsFromLastBlockToBe = async (logMessages: bigint[]) => { - const l2BlockNum = await pxe.getBlockNumber(); - const unencryptedLogs = await pxe.getUnencryptedLogs(l2BlockNum, 1); - const unrolledLogs = L2BlockL2Logs.unrollLogs(unencryptedLogs) - .map(log => UnencryptedL2Log.fromBuffer(log)) - .map(log => log.data); - const bigintLogs = unrolledLogs.map((log: Buffer) => toBigIntBE(log)); + const fromBlock = await pxe.getBlockNumber(); + const logFilter = { + fromBlock, + toBlock: fromBlock + 1, + }; + const unencryptedLogs = (await pxe.getUnencryptedLogs(logFilter)).logs; + const bigintLogs = unencryptedLogs.map(extendedLog => toBigIntBE(extendedLog.log.data)); expect(bigintLogs).toStrictEqual(logMessages); }; diff --git a/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts index 32b85181195..2707988a2fc 100644 --- a/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts @@ -5,7 +5,7 @@ import { CompleteAddress, PXE, TxStatus } from '@aztec/types'; import times from 'lodash.times'; -import { expectUnencryptedLogsFromLastBlockToBe, setup } from './fixtures/utils.js'; +import { expectUnencryptedLogsFromLastBlockToBe, expectUnencryptedLogsInTxToBe, setup } from './fixtures/utils.js'; describe('e2e_public_token_contract', () => { let pxe: PXE; @@ -52,7 +52,7 @@ describe('e2e_public_token_contract', () => { const balance = await contract.methods.publicBalanceOf(recipient.toField()).view({ from: recipient }); expect(balance).toBe(mintAmount); - await expectUnencryptedLogsFromLastBlockToBe(pxe, ['Coins minted']); + await expectUnencryptedLogsInTxToBe(tx, ['Coins minted']); }, 45_000); // Regression for https://github.com/AztecProtocol/aztec-packages/issues/640 diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index be53fabf8c2..db7cfde63df 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -6,6 +6,7 @@ import { CompleteAddress, EthAddress, EthCheatCodes, + SentTx, Wallet, createAccounts, createPXEClient, @@ -39,15 +40,7 @@ import { } from '@aztec/l1-artifacts'; import { NonNativeTokenContract, TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; import { PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; -import { - AztecNode, - L2BlockL2Logs, - LogType, - PXE, - TxStatus, - UnencryptedL2Log, - createAztecNodeRpcClient, -} from '@aztec/types'; +import { AztecNode, L2BlockL2Logs, LogType, PXE, TxStatus, createAztecNodeRpcClient } from '@aztec/types'; import * as path from 'path'; import { @@ -522,18 +515,32 @@ export const expectsNumOfEncryptedLogsInTheLastBlockToBe = async ( /** * Checks that the last block contains the given expected unencrypted log messages. - * @param pxe - The instance of PXE for retrieving the logs. + * @param tx - An instance of SentTx for which to retrieve the logs. + * @param logMessages - The set of expected log messages. + */ +export const expectUnencryptedLogsInTxToBe = async (tx: SentTx, logMessages: string[]) => { + const unencryptedLogs = (await tx.getUnencryptedLogs()).logs; + const asciiLogs = unencryptedLogs.map(extendedLog => extendedLog.log.data.toString('ascii')); + + expect(asciiLogs).toStrictEqual(logMessages); +}; + +/** + * Checks that the last block contains the given expected unencrypted log messages. + * @param pxe - An instance of PXE for retrieving the logs. * @param logMessages - The set of expected log messages. */ export const expectUnencryptedLogsFromLastBlockToBe = async (pxe: PXE, logMessages: string[]) => { // docs:start:get_logs - // Get the latest block number to retrieve logs from - const l2BlockNum = await pxe.getBlockNumber(); // Get the unencrypted logs from the last block - const unencryptedLogs = await pxe.getUnencryptedLogs(l2BlockNum, 1); + const fromBlock = await pxe.getBlockNumber(); + const logFilter = { + fromBlock, + toBlock: fromBlock + 1, + }; + const unencryptedLogs = (await pxe.getUnencryptedLogs(logFilter)).logs; // docs:end:get_logs - const unrolledLogs = L2BlockL2Logs.unrollLogs(unencryptedLogs).map(log => UnencryptedL2Log.fromBuffer(log)); - const asciiLogs = unrolledLogs.map(log => log.data.toString('ascii')); + const asciiLogs = unencryptedLogs.map(extendedLog => extendedLog.log.data.toString('ascii')); expect(asciiLogs).toStrictEqual(logMessages); }; diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index d5adfd24890..fcc21d8a756 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -3,10 +3,8 @@ import { AccountWallet, CheatCodes, Fr, - L2BlockL2Logs, NotePreimage, PXE, - UnencryptedL2Log, computeMessageSecretHash, createAccount, createPXEClient, @@ -208,9 +206,12 @@ describe('guides/dapp/testing', () => { // docs:start:unencrypted-logs const value = Fr.fromString('ef'); // Only 1 bytes will make its way in there :( so no larger stuff const tx = await testContract.methods.emit_unencrypted(value).send().wait(); - const logs = await pxe.getUnencryptedLogs(tx.blockNumber!, 1); - const log = UnencryptedL2Log.fromBuffer(L2BlockL2Logs.unrollLogs(logs)[0]); - expect(Fr.fromBuffer(log.data)).toEqual(value); + const filter = { + fromBlock: tx.blockNumber!, + limit: 1, // 1 log expected + }; + const logs = (await pxe.getUnencryptedLogs(filter)).logs; + expect(Fr.fromBuffer(logs[0].log.data)).toEqual(value); // docs:end:unencrypted-logs }); diff --git a/yarn-project/end-to-end/src/sample-dapp/index.mjs b/yarn-project/end-to-end/src/sample-dapp/index.mjs index 449eedace47..dc6ba0c08be 100644 --- a/yarn-project/end-to-end/src/sample-dapp/index.mjs +++ b/yarn-project/end-to-end/src/sample-dapp/index.mjs @@ -98,7 +98,7 @@ async function mintPublicFunds(pxe) { // docs:start:showLogs const blockNumber = await pxe.getBlockNumber(); - const logs = await pxe.getUnencryptedLogs(blockNumber, 1); + const logs = (await pxe.getUnencryptedLogs(blockNumber, 1)).logs; const textLogs = L2BlockL2Logs.unrollLogs(logs).map(log => log.toString('ascii')); for (const log of textLogs) console.log(`Log emitted: ${log}`); // docs:end:showLogs diff --git a/yarn-project/foundation/src/abi/function_selector.ts b/yarn-project/foundation/src/abi/function_selector.ts index 88c7919e4a5..fbfc56f79b6 100644 --- a/yarn-project/foundation/src/abi/function_selector.ts +++ b/yarn-project/foundation/src/abi/function_selector.ts @@ -104,6 +104,22 @@ export class FunctionSelector { return selector; } + /** + * Create an AztecAddress instance from a hex-encoded string. + * The input 'address' should be prefixed with '0x' or not, and have exactly 64 hex characters. + * Throws an error if the input length is invalid or address value is out of range. + * + * @param selector - The hex-encoded string representing the Aztec address. + * @returns An AztecAddress instance. + */ + static fromString(selector: string) { + const buf = Buffer.from(selector.replace(/^0x/i, ''), 'hex'); + if (buf.length !== FunctionSelector.SIZE) { + throw new Error(`Invalid length ${buf.length}.`); + } + return FunctionSelector.fromBuffer(buf); + } + /** * Creates an empty function selector. * @returns An empty function selector. diff --git a/yarn-project/pxe/src/pxe_http/pxe_http_server.ts b/yarn-project/pxe/src/pxe_http/pxe_http_server.ts index e8eb1a864c1..64eb01c1f56 100644 --- a/yarn-project/pxe/src/pxe_http/pxe_http_server.ts +++ b/yarn-project/pxe/src/pxe_http/pxe_http_server.ts @@ -1,3 +1,4 @@ +import { FunctionSelector } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr, GrumpkinScalar, Point } from '@aztec/foundation/fields'; import { JsonRpcServer } from '@aztec/foundation/json-rpc/server'; @@ -6,9 +7,11 @@ import { CompleteAddress, ContractData, ExtendedContractData, + ExtendedUnencryptedL2Log, L2Block, L2BlockL2Logs, L2Tx, + LogId, NotePreimage, PXE, Tx, @@ -37,6 +40,8 @@ export function createPXERpcServer(pxeService: PXE): JsonRpcServer { TxExecutionRequest, ContractData, ExtendedContractData, + ExtendedUnencryptedL2Log, + FunctionSelector, TxHash, EthAddress, Point, @@ -46,6 +51,7 @@ export function createPXERpcServer(pxeService: PXE): JsonRpcServer { AuthWitness, L2Block, L2Tx, + LogId, }, { Tx, TxReceipt, L2BlockL2Logs }, false, diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index 83177e46e96..e911469fe8f 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -32,11 +32,11 @@ import { DeployedContract, ExtendedContractData, FunctionCall, + GetUnencryptedLogsResponse, KeyStore, L2Block, - L2BlockL2Logs, L2Tx, - LogType, + LogFilter, MerkleTreeId, NodeInfo, NotePreimage, @@ -384,8 +384,13 @@ export class PXEService implements PXE { return await this.node.getContractData(contractAddress); } - public async getUnencryptedLogs(from: number, limit: number): Promise { - return await this.node.getLogs(from, limit, LogType.UNENCRYPTED); + /** + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + */ + public getUnencryptedLogs(filter: LogFilter): Promise { + return this.node.getUnencryptedLogs(filter); } async #getFunctionCall(functionName: string, args: any[], to: AztecAddress): Promise { diff --git a/yarn-project/types/src/aztec_node/rpc/http_rpc_client.ts b/yarn-project/types/src/aztec_node/rpc/http_rpc_client.ts index 96cdfb6676e..0dd96e503a7 100644 --- a/yarn-project/types/src/aztec_node/rpc/http_rpc_client.ts +++ b/yarn-project/types/src/aztec_node/rpc/http_rpc_client.ts @@ -1,4 +1,4 @@ -import { HistoricBlockData } from '@aztec/circuits.js'; +import { FunctionSelector, HistoricBlockData } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; @@ -7,10 +7,12 @@ import { AztecNode, ContractData, ExtendedContractData, + ExtendedUnencryptedL2Log, L1ToL2MessageAndIndex, L2Block, L2BlockL2Logs, L2Tx, + LogId, SiblingPath, Tx, TxHash, @@ -29,11 +31,14 @@ export function createAztecNodeRpcClient(url: string, fetch = defaultFetch): Azt AztecAddress, EthAddress, ExtendedContractData, + ExtendedUnencryptedL2Log, ContractData, Fr, + FunctionSelector, HistoricBlockData, L2Block, L2Tx, + LogId, TxHash, SiblingPath, L1ToL2MessageAndIndex, diff --git a/yarn-project/types/src/interfaces/aztec-node.ts b/yarn-project/types/src/interfaces/aztec-node.ts index 231e1ea9446..287e4df2f53 100644 --- a/yarn-project/types/src/interfaces/aztec-node.ts +++ b/yarn-project/types/src/interfaces/aztec-node.ts @@ -6,9 +6,11 @@ import { Fr } from '@aztec/foundation/fields'; import { ContractData, ExtendedContractData, + GetUnencryptedLogsResponse, L2Block, L2BlockL2Logs, L2Tx, + LogFilter, LogType, MerkleTreeId, StateInfoProvider, @@ -90,6 +92,13 @@ export interface AztecNode extends StateInfoProvider { */ getLogs(from: number, limit: number, logType: LogType): Promise; + /** + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. + */ + getUnencryptedLogs(filter: LogFilter): Promise; + /** * Method to submit a transaction to the p2p pool. * @param tx - The transaction to be submitted. diff --git a/yarn-project/types/src/interfaces/pxe.ts b/yarn-project/types/src/interfaces/pxe.ts index b3907ebab6d..6f733a4116f 100644 --- a/yarn-project/types/src/interfaces/pxe.ts +++ b/yarn-project/types/src/interfaces/pxe.ts @@ -4,8 +4,9 @@ import { CompleteAddress, ContractData, ExtendedContractData, - L2BlockL2Logs, + GetUnencryptedLogsResponse, L2Tx, + LogFilter, NotePreimage, Tx, TxExecutionRequest, @@ -231,14 +232,11 @@ export interface PXE { getContractData(contractAddress: AztecAddress): Promise; /** - * Gets unencrypted public logs from the specified block range. Logs are grouped by block and then by - * transaction. Use the `L2BlockL2Logs.unrollLogs` helper function to get an flattened array of logs instead. - * - * @param from - Number of the L2 block to which corresponds the first unencrypted logs to be returned. - * @param limit - The maximum number of unencrypted logs to return. - * @returns The requested unencrypted logs. + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. */ - getUnencryptedLogs(from: number, limit: number): Promise; + getUnencryptedLogs(filter: LogFilter): Promise; /** * Fetches the current block number. diff --git a/yarn-project/types/src/l2_block.ts b/yarn-project/types/src/l2_block.ts index 3890bbf1817..e42e87de978 100644 --- a/yarn-project/types/src/l2_block.ts +++ b/yarn-project/types/src/l2_block.ts @@ -158,19 +158,19 @@ export class L2Block { * Creates an L2 block containing random data. * @param l2BlockNum - The number of the L2 block. * @param txsPerBlock - The number of transactions to include in the block. - * @param numPrivateFunctionCalls - The number of private function calls to include in each transaction. - * @param numPublicFunctionCalls - The number of public function calls to include in each transaction. - * @param numEncryptedLogs - The number of encrypted logs to include in each transaction. - * @param numUnencryptedLogs - The number of unencrypted logs to include in each transaction. + * @param numPrivateCallsPerTx - The number of private function calls to include in each transaction. + * @param numPublicCallsPerTx - The number of public function calls to include in each transaction. + * @param numEncryptedLogsPerCall - The number of encrypted logs per 1 private function invocation. + * @param numUnencryptedLogsPerCall - The number of unencrypted logs per 1 public function invocation. * @returns The L2 block. */ static random( l2BlockNum: number, txsPerBlock = 4, - numPrivateFunctionCalls = 2, - numPublicFunctionCalls = 3, - numEncryptedLogs = 2, - numUnencryptedLogs = 1, + numPrivateCallsPerTx = 2, + numPublicCallsPerTx = 3, + numEncryptedLogsPerCall = 2, + numUnencryptedLogsPerCall = 1, ): L2Block { const newNullifiers = times(MAX_NEW_NULLIFIERS_PER_TX * txsPerBlock, Fr.random); const newCommitments = times(MAX_NEW_COMMITMENTS_PER_TX * txsPerBlock, Fr.random); @@ -179,8 +179,18 @@ export class L2Block { const newPublicDataWrites = times(MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX * txsPerBlock, PublicDataWrite.random); const newL1ToL2Messages = times(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, Fr.random); const newL2ToL1Msgs = times(MAX_NEW_L2_TO_L1_MSGS_PER_TX, Fr.random); - const newEncryptedLogs = L2BlockL2Logs.random(txsPerBlock, numPrivateFunctionCalls, numEncryptedLogs); - const newUnencryptedLogs = L2BlockL2Logs.random(txsPerBlock, numPublicFunctionCalls, numUnencryptedLogs); + const newEncryptedLogs = L2BlockL2Logs.random( + txsPerBlock, + numPrivateCallsPerTx, + numEncryptedLogsPerCall, + LogType.ENCRYPTED, + ); + const newUnencryptedLogs = L2BlockL2Logs.random( + txsPerBlock, + numPublicCallsPerTx, + numUnencryptedLogsPerCall, + LogType.UNENCRYPTED, + ); return L2Block.fromFields({ number: l2BlockNum, diff --git a/yarn-project/types/src/logs/extended_unencrypted_l2_log.ts b/yarn-project/types/src/logs/extended_unencrypted_l2_log.ts new file mode 100644 index 00000000000..e5cc7d46a33 --- /dev/null +++ b/yarn-project/types/src/logs/extended_unencrypted_l2_log.ts @@ -0,0 +1,74 @@ +import { BufferReader } from '@aztec/foundation/serialize'; + +import isEqual from 'lodash.isequal'; + +import { LogId, UnencryptedL2Log } from '../index.js'; + +/** + * Represents an individual unencrypted log entry extended with info about the block and tx it was emitted in. + */ +export class ExtendedUnencryptedL2Log { + constructor( + /** Globally unique id of the log. */ + public readonly id: LogId, + /** The data contents of the log. */ + public readonly log: UnencryptedL2Log, + ) {} + + /** + * Serializes log to a buffer. + * @returns A buffer containing the serialized log. + */ + public toBuffer(): Buffer { + return Buffer.concat([this.id.toBuffer(), this.log.toBuffer()]); + } + + /** + * Serializes log to a string. + * @returns A string containing the serialized log. + */ + public toString(): string { + return this.toBuffer().toString('hex'); + } + + /** + * Serializes log to a human readable string. + * @returns A human readable representation of the log. + */ + public toHumanReadable(): string { + return `${this.id.toHumanReadable()}, ${this.log.toHumanReadable()}`; + } + + /** + * Checks if two ExtendedUnencryptedL2Log objects are equal. + * @param other - Another ExtendedUnencryptedL2Log object to compare with. + * @returns True if the two objects are equal, false otherwise. + */ + public equals(other: ExtendedUnencryptedL2Log): boolean { + return isEqual(this, other); + } + + /** + * Deserializes log from a buffer. + * @param buffer - The buffer or buffer reader containing the log. + * @returns Deserialized instance of `Log`. + */ + public static fromBuffer(buffer: Buffer | BufferReader): ExtendedUnencryptedL2Log { + const reader = BufferReader.asReader(buffer); + + const logId = LogId.fromBuffer(reader); + const log = UnencryptedL2Log.fromBuffer(reader); + + return new ExtendedUnencryptedL2Log(logId, log); + } + + /** + * Deserializes `ExtendedUnencryptedL2Log` object from a hex string representation. + * @param data - A hex string representation of the log. + * @returns An `ExtendedUnencryptedL2Log` object. + */ + public static fromString(data: string): ExtendedUnencryptedL2Log { + const buffer = Buffer.from(data, 'hex'); + return ExtendedUnencryptedL2Log.fromBuffer(buffer); + } +} diff --git a/yarn-project/types/src/logs/function_l2_logs.ts b/yarn-project/types/src/logs/function_l2_logs.ts index b504490537e..ae7608b0e07 100644 --- a/yarn-project/types/src/logs/function_l2_logs.ts +++ b/yarn-project/types/src/logs/function_l2_logs.ts @@ -4,6 +4,9 @@ import { BufferReader, prefixBufferWithLength } from '@aztec/foundation/serializ import { randomBytes } from 'crypto'; +import { LogType } from './log_type.js'; +import { UnencryptedL2Log } from './unencrypted_l2_log.js'; + /** * Data container of logs emitted in 1 function invocation (corresponds to 1 kernel iteration). */ @@ -65,14 +68,19 @@ export class FunctionL2Logs { /** * Creates a new L2Logs object with `numLogs` logs. * @param numLogs - The number of logs to create. + * @param logType - The type of logs to generate. * @returns A new FunctionL2Logs object. */ - public static random(numLogs: number): FunctionL2Logs { + public static random(numLogs: number, logType = LogType.ENCRYPTED): FunctionL2Logs { const logs: Buffer[] = []; for (let i = 0; i < numLogs; i++) { - const randomEphPubKey = Point.random(); - const randomLogContent = randomBytes(144 - Point.SIZE_IN_BYTES); - logs.push(Buffer.concat([randomLogContent, randomEphPubKey.toBuffer()])); + if (logType === LogType.ENCRYPTED) { + const randomEphPubKey = Point.random(); + const randomLogContent = randomBytes(144 - Point.SIZE_IN_BYTES); + logs.push(Buffer.concat([randomLogContent, randomEphPubKey.toBuffer()])); + } else { + logs.push(UnencryptedL2Log.random().toBuffer()); + } } return new FunctionL2Logs(logs); } diff --git a/yarn-project/types/src/logs/get_unencrypted_logs_response.ts b/yarn-project/types/src/logs/get_unencrypted_logs_response.ts new file mode 100644 index 00000000000..d50c7280a9b --- /dev/null +++ b/yarn-project/types/src/logs/get_unencrypted_logs_response.ts @@ -0,0 +1,16 @@ +import { ExtendedUnencryptedL2Log } from './extended_unencrypted_l2_log.js'; + +/** + * It provides documentation for the GetUnencryptedLogsResponse type. + */ +export type GetUnencryptedLogsResponse = { + /** + * An array of ExtendedUnencryptedL2Log elements. + */ + logs: ExtendedUnencryptedL2Log[]; + + /** + * Indicates if a limit has been reached. + */ + maxLogsHit: boolean; +}; diff --git a/yarn-project/types/src/logs/index.ts b/yarn-project/types/src/logs/index.ts index cea46901df1..a495dfb6a7b 100644 --- a/yarn-project/types/src/logs/index.ts +++ b/yarn-project/types/src/logs/index.ts @@ -1,7 +1,11 @@ +export * from './get_unencrypted_logs_response.js'; export * from './function_l2_logs.js'; export * from './l2_block_l2_logs.js'; export * from './l2_logs_source.js'; +export * from './log_id.js'; export * from './log_type.js'; +export * from './log_filter.js'; export * from './note_spending_info/index.js'; export * from './tx_l2_logs.js'; export * from './unencrypted_l2_log.js'; +export * from './extended_unencrypted_l2_log.js'; diff --git a/yarn-project/types/src/logs/l2_block_l2_logs.ts b/yarn-project/types/src/logs/l2_block_l2_logs.ts index 9c8702bfb02..20ceb324e2e 100644 --- a/yarn-project/types/src/logs/l2_block_l2_logs.ts +++ b/yarn-project/types/src/logs/l2_block_l2_logs.ts @@ -2,6 +2,7 @@ import { BufferReader, prefixBufferWithLength } from '@aztec/foundation/serializ import isEqual from 'lodash.isequal'; +import { LogType } from './log_type.js'; import { TxL2Logs } from './tx_l2_logs.js'; /** @@ -62,17 +63,23 @@ export class L2BlockL2Logs { } /** - * Creates a new `L2BlockL2Logs` object with `numFunctionInvocations` function logs and `numLogsIn1Invocation` logs - * in each invocation. + * Creates a new `L2BlockL2Logs` object with `numCalls` function logs and `numLogsPerCall` logs in each function + * call. * @param numTxs - The number of txs in the block. - * @param numFunctionInvocations - The number of function invocations in the tx. - * @param numLogsIn1Invocation - The number of logs emitted in each function invocation. + * @param numCalls - The number of function calls in the tx. + * @param numLogsPerCall - The number of logs emitted in each function call. + * @param logType - The type of logs to generate. * @returns A new `L2BlockL2Logs` object. */ - public static random(numTxs: number, numFunctionInvocations: number, numLogsIn1Invocation: number): L2BlockL2Logs { + public static random( + numTxs: number, + numCalls: number, + numLogsPerCall: number, + logType = LogType.ENCRYPTED, + ): L2BlockL2Logs { const txLogs: TxL2Logs[] = []; for (let i = 0; i < numTxs; i++) { - txLogs.push(TxL2Logs.random(numFunctionInvocations, numLogsIn1Invocation)); + txLogs.push(TxL2Logs.random(numCalls, numLogsPerCall, logType)); } return new L2BlockL2Logs(txLogs); } @@ -86,9 +93,7 @@ export class L2BlockL2Logs { const logs: Buffer[] = []; for (const blockLog of blockLogs) { for (const txLog of blockLog.txLogs) { - for (const functionLog of txLog.functionLogs) { - logs.push(...functionLog.logs); - } + logs.push(...txLog.unrollLogs()); } } return logs; diff --git a/yarn-project/types/src/logs/l2_logs_source.ts b/yarn-project/types/src/logs/l2_logs_source.ts index a21d5ccedd3..a3566a803fc 100644 --- a/yarn-project/types/src/logs/l2_logs_source.ts +++ b/yarn-project/types/src/logs/l2_logs_source.ts @@ -1,8 +1,10 @@ +import { GetUnencryptedLogsResponse } from './get_unencrypted_logs_response.js'; import { L2BlockL2Logs } from './l2_block_l2_logs.js'; +import { LogFilter } from './log_filter.js'; import { LogType } from './log_type.js'; /** - * Interface of classes allowing for the retrieval of encrypted logs. + * Interface of classes allowing for the retrieval of logs. */ export interface L2LogsSource { /** @@ -15,17 +17,11 @@ export interface L2LogsSource { getLogs(from: number, limit: number, logType: LogType): Promise; /** - * Starts the encrypted logs source. - * @param blockUntilSynced - If true, blocks until the data source has fully synced. - * @returns A promise signalling completion of the start process. - */ - start(blockUntilSynced: boolean): Promise; - - /** - * Stops the encrypted logs source. - * @returns A promise signalling completion of the stop process. + * Gets unencrypted logs based on the provided filter. + * @param filter - The filter to apply to the logs. + * @returns The requested logs. */ - stop(): Promise; + getUnencryptedLogs(filter: LogFilter): Promise; /** * Gets the number of the latest L2 block processed by the implementation. diff --git a/yarn-project/types/src/logs/log_filter.ts b/yarn-project/types/src/logs/log_filter.ts new file mode 100644 index 00000000000..34dca97343e --- /dev/null +++ b/yarn-project/types/src/logs/log_filter.ts @@ -0,0 +1,31 @@ +import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; + +import { LogId, TxHash } from '../index.js'; + +/** + * Log filter used to fetch L2 logs. + * @remarks This filter is applied as an intersection of all it's params. + */ +export type LogFilter = { + /** + * Hash of a transaction from which to fetch the logs. + */ + txHash?: TxHash; + /** + * The block number from which to start fetching logs (inclusive). + */ + fromBlock?: number; + /** The block number until which to fetch logs (not inclusive). */ + toBlock?: number; + /** + * Log id after which to start fetching logs. + */ + afterLog?: LogId; + /** The contract address to filter logs by. */ + contractAddress?: AztecAddress; + /** + * Selector of the event/log topic. + * TODO: https://github.com/AztecProtocol/aztec-packages/issues/2632 + */ + selector?: FunctionSelector; +}; diff --git a/yarn-project/types/src/logs/log_id.test.ts b/yarn-project/types/src/logs/log_id.test.ts new file mode 100644 index 00000000000..dd01771262c --- /dev/null +++ b/yarn-project/types/src/logs/log_id.test.ts @@ -0,0 +1,25 @@ +import { LogId } from './log_id.js'; + +describe('LogId', () => { + let logId: LogId; + beforeEach(() => { + const blockNumber = Math.floor(Math.random() * 1000); + const txIndex = Math.floor(Math.random() * 1000); + const logIndex = Math.floor(Math.random() * 1000); + logId = new LogId(blockNumber, txIndex, logIndex); + }); + + it('toBuffer and fromBuffer works', () => { + const buffer = logId.toBuffer(); + const parsedLogId = LogId.fromBuffer(buffer); + + expect(parsedLogId).toEqual(logId); + }); + + it('toBuffer and fromBuffer works', () => { + const buffer = logId.toBuffer(); + const parsedLogId = LogId.fromBuffer(buffer); + + expect(parsedLogId).toEqual(logId); + }); +}); diff --git a/yarn-project/types/src/logs/log_id.ts b/yarn-project/types/src/logs/log_id.ts new file mode 100644 index 00000000000..00ab59c2c52 --- /dev/null +++ b/yarn-project/types/src/logs/log_id.ts @@ -0,0 +1,89 @@ +import { toBufferBE } from '@aztec/foundation/bigint-buffer'; +import { BufferReader } from '@aztec/foundation/serialize'; + +import { INITIAL_L2_BLOCK_NUM } from '../constants.js'; + +/** A globally unique log id. */ +export class LogId { + /** + * Parses a log id from a string. + * @param blockNumber - The block number. + * @param txIndex - The transaction index. + * @param logIndex - The log index. + */ + constructor( + /** The block number the log was emitted in. */ + public readonly blockNumber: number, + /** The index of a tx in a block the log was emitted in. */ + public readonly txIndex: number, + /** The index of a log the tx was emitted in. */ + public readonly logIndex: number, + ) { + if (!Number.isInteger(blockNumber) || blockNumber < INITIAL_L2_BLOCK_NUM) { + throw new Error(`Invalid block number: ${blockNumber}`); + } + if (!Number.isInteger(txIndex)) { + throw new Error(`Invalid tx index: ${txIndex}`); + } + if (!Number.isInteger(logIndex)) { + throw new Error(`Invalid log index: ${logIndex}`); + } + } + + /** + * Serializes log id to a buffer. + * @returns A buffer containing the serialized log id. + */ + public toBuffer(): Buffer { + return Buffer.concat([ + toBufferBE(BigInt(this.blockNumber), 4), + toBufferBE(BigInt(this.txIndex), 4), + toBufferBE(BigInt(this.logIndex), 4), + ]); + } + + /** + * Creates a LogId from a buffer. + * @param buffer - A buffer containing the serialized log id. + * @returns A log id. + */ + static fromBuffer(buffer: Buffer | BufferReader): LogId { + const reader = BufferReader.asReader(buffer); + + const blockNumber = reader.readNumber(); + const txIndex = reader.readNumber(); + const logIndex = reader.readNumber(); + + return new LogId(blockNumber, txIndex, logIndex); + } + + /** + * Converts the LogId instance to a string. + * @returns A string representation of the log id. + */ + public toString(): string { + return `${this.blockNumber}-${this.txIndex}-${this.logIndex}`; + } + + /** + * Creates a LogId from a string. + * @param data - A string representation of a log id. + * @returns A log id. + */ + static fromString(data: string): LogId { + const [rawBlockNumber, rawTxIndex, rawLogIndex] = data.split('-'); + const blockNumber = Number(rawBlockNumber); + const txIndex = Number(rawTxIndex); + const logIndex = Number(rawLogIndex); + + return new LogId(blockNumber, txIndex, logIndex); + } + + /** + * Serializes log id to a human readable string. + * @returns A human readable representation of the log id. + */ + public toHumanReadable(): string { + return `logId: (blockNumber: ${this.blockNumber}, txIndex: ${this.txIndex}, logIndex: ${this.logIndex})`; + } +} diff --git a/yarn-project/types/src/logs/tx_l2_logs.ts b/yarn-project/types/src/logs/tx_l2_logs.ts index 98fb94d1be6..2cf264841f9 100644 --- a/yarn-project/types/src/logs/tx_l2_logs.ts +++ b/yarn-project/types/src/logs/tx_l2_logs.ts @@ -1,6 +1,7 @@ import { BufferReader, prefixBufferWithLength } from '@aztec/foundation/serialize'; import { FunctionL2Logs } from './function_l2_logs.js'; +import { LogType } from './log_type.js'; /** * Data container of logs emitted in 1 tx. @@ -58,16 +59,16 @@ export class TxL2Logs { } /** - * Creates a new `TxL2Logs` object with `numFunctionInvocations` function logs and `numLogsIn1Invocation` logs - * in each invocation. - * @param numFunctionInvocations - The number of function invocations in the tx. - * @param numLogsIn1Invocation - The number of logs emitted in each function invocation. + * Creates a new `TxL2Logs` object with `numCalls` function logs and `numLogsPerCall` logs in each invocation. + * @param numCalls - The number of function calls in the tx. + * @param numLogsPerCall - The number of logs emitted in each function call. + * @param logType - The type of logs to generate. * @returns A new `TxL2Logs` object. */ - public static random(numFunctionInvocations: number, numLogsIn1Invocation: number): TxL2Logs { + public static random(numCalls: number, numLogsPerCall: number, logType = LogType.ENCRYPTED): TxL2Logs { const functionLogs: FunctionL2Logs[] = []; - for (let i = 0; i < numFunctionInvocations; i++) { - functionLogs.push(FunctionL2Logs.random(numLogsIn1Invocation)); + for (let i = 0; i < numCalls; i++) { + functionLogs.push(FunctionL2Logs.random(numLogsPerCall, logType)); } return new TxL2Logs(functionLogs); } @@ -82,6 +83,14 @@ export class TxL2Logs { }; } + /** + * Unrolls logs from this tx. + * @returns Unrolled logs. + */ + public unrollLogs(): Buffer[] { + return this.functionLogs.flatMap(functionLog => functionLog.logs); + } + /** * Convert a plain JSON object to a TxL2Logs class object. * @param obj - A plain TxL2Logs JSON object. diff --git a/yarn-project/types/src/logs/unencrypted_l2_log.ts b/yarn-project/types/src/logs/unencrypted_l2_log.ts index e4707f3ad34..777c9811db4 100644 --- a/yarn-project/types/src/logs/unencrypted_l2_log.ts +++ b/yarn-project/types/src/logs/unencrypted_l2_log.ts @@ -17,7 +17,10 @@ export class UnencryptedL2Log { * TODO: Optimize this once it makes sense. */ public readonly contractAddress: AztecAddress, - /** Selector of the event/log topic. */ + /** + * Selector of the event/log topic. + * TODO: https://github.com/AztecProtocol/aztec-packages/issues/2632 + */ public readonly selector: FunctionSelector, /** The data contents of the log. */ public readonly data: Buffer, @@ -45,7 +48,7 @@ export class UnencryptedL2Log { */ public toHumanReadable(): string { return `UnencryptedL2Log(contractAddress: ${this.contractAddress.toString()}, selector: ${this.selector.toString()}, data: ${this.data.toString( - 'hex', + 'ascii', )})`; } diff --git a/yarn-project/types/src/tx/tx.ts b/yarn-project/types/src/tx/tx.ts index 4486aa8b8ab..253fcddc68d 100644 --- a/yarn-project/types/src/tx/tx.ts +++ b/yarn-project/types/src/tx/tx.ts @@ -10,6 +10,8 @@ import { arrayNonEmptyLength } from '@aztec/foundation/collection'; import { BufferReader, Tuple } from '@aztec/foundation/serialize'; import { ExtendedContractData } from '../contract_data.js'; +import { L2LogsSource } from '../index.js'; +import { GetUnencryptedLogsResponse } from '../logs/get_unencrypted_logs_response.js'; import { TxL2Logs } from '../logs/tx_l2_logs.js'; import { TxHash } from './tx_hash.js'; @@ -112,6 +114,15 @@ export class Tx { }; } + /** + * Gets unencrypted logs emitted by this tx. + * @param logsSource - An instance of `L2LogsSource` which can be used to obtain the logs. + * @returns The requested logs. + */ + public async getUnencryptedLogs(logsSource: L2LogsSource): Promise { + return logsSource.getUnencryptedLogs({ txHash: await this.getTxHash() }); + } + /** * Convert a plain JSON object to a Tx class object. * @param obj - A plain Tx JSON object.