From 02e7c2c836858b36e08838ecf685e4c4dfeab2d5 Mon Sep 17 00:00:00 2001 From: benesjan Date: Mon, 9 Oct 2023 08:46:20 +0000 Subject: [PATCH] WIP --- .../archiver/src/archiver/archiver.ts | 2 +- .../archiver/src/archiver/archiver_store.ts | 29 ++++++++++---- yarn-project/cli/src/index.ts | 23 ++++++----- yarn-project/cli/src/utils.ts | 14 +++++++ .../src/logs/extended_unencrypted_l2_log.ts | 31 ++++++++++++++- yarn-project/types/src/logs/index.ts | 1 + yarn-project/types/src/logs/log_filter.ts | 27 ++++++++++++- yarn-project/types/src/logs/log_id.ts | 38 +++++++++++++++++++ 8 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 yarn-project/types/src/logs/log_id.ts diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 545852422718..823b7b9d5155 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -167,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. * diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index dfcd48832da7..2dd3625e6259 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -14,6 +14,7 @@ import { LogType, TxHash, UnencryptedL2Log, + validateLogFilter, } from '@aztec/types'; import { L1ToL2MessageStore, PendingL1ToL2MessageStore } from './l1_to_l2_message_store.js'; @@ -370,13 +371,17 @@ export class MemoryArchiverStore implements ArchiverDataStore { * @throws If txHash and block range are both defined. */ getUnencryptedLogs(filter: LogFilter): Promise { - if (filter.txHash && (filter.fromBlock || filter.toBlock)) { - throw new Error('Cannot filter by txHash and block range at the same time'); - } + validateLogFilter(filter); const txHash = filter.txHash; - const fromBlockIndex = filter.fromBlock === undefined ? 0 : filter.fromBlock - INITIAL_L2_BLOCK_NUM; + let fromBlockIndex = 0; + if (filter.fromBlock !== undefined) { + fromBlockIndex = filter.fromBlock - INITIAL_L2_BLOCK_NUM; + } else if (filter.fromLog !== undefined) { + fromBlockIndex = filter.fromLog.blockNumber - INITIAL_L2_BLOCK_NUM; + } + if (fromBlockIndex < 0) { throw new Error(`"fromBlock" (${filter.fromBlock}) smaller than genesis block number (${INITIAL_L2_BLOCK_NUM}).`); } @@ -393,26 +398,34 @@ export class MemoryArchiverStore implements ArchiverDataStore { const contractAddress = filter.contractAddress; const selector = filter.selector; + let txIndexInBlock = 0; + if (filter.fromLog !== undefined) { + txIndexInBlock = filter.fromLog.logIndex; + } + const logs: ExtendedUnencryptedL2Log[] = []; for (let i = fromBlockIndex; i < toBlockIndex; i++) { const blockContext = this.l2BlockContexts[i]; const blockLogs = this.unencryptedLogsPerBlock[i]; - for (let j = 0; j < blockLogs.txLogs.length; j++) { - const txLogs = blockLogs.txLogs[j].unrollLogs().map(log => UnencryptedL2Log.fromBuffer(log)); + for (; txIndexInBlock < blockLogs.txLogs.length; txIndexInBlock++) { + const txLogs = blockLogs.txLogs[txIndexInBlock].unrollLogs().map(log => UnencryptedL2Log.fromBuffer(log)); for (const log of txLogs) { if ( - (!txHash || blockContext.getTxHash(j).equals(txHash)) && + (!txHash || blockContext.getTxHash(txIndexInBlock).equals(txHash)) && (!contractAddress || log.contractAddress.equals(contractAddress)) && (!selector || log.selector.equals(selector)) ) { - logs.push(new ExtendedUnencryptedL2Log(blockContext.block.number, blockContext.getTxHash(j), log)); + logs.push( + new ExtendedUnencryptedL2Log(blockContext.block.number, blockContext.getTxHash(txIndexInBlock), log), + ); if (logs.length === this.maxLogs) { return Promise.resolve(logs); } } } } + txIndexInBlock = 0; } return Promise.resolve(logs); diff --git a/yarn-project/cli/src/index.ts b/yarn-project/cli/src/index.ts index feeb5f4f6698..562175a2213a 100644 --- a/yarn-project/cli/src/index.ts +++ b/yarn-project/cli/src/index.ts @@ -14,7 +14,7 @@ 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, LogFilter } from '@aztec/types'; +import { CompleteAddress, ContractData, LogFilter, validateLogFilter } from '@aztec/types'; import { createSecp256k1PeerId } from '@libp2p/peer-id-factory'; import { Command, Option } from 'commander'; @@ -37,6 +37,7 @@ import { parseFields, parseOptionalAztecAddress, parseOptionalInteger, + parseOptionalLogId, parseOptionalSelector, parseOptionalTxHash, parsePartialAddress, @@ -285,27 +286,30 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets all the unencrypted logs from L2 blocks in the range specified.') .option('-tx, --tx-hash ', 'A transaction hash to get the receipt for.', parseOptionalTxHash) .option( - '-f, --from-block ', + '-fb, --from-block ', 'Initial block number for getting logs (defaults to 1).', parseOptionalInteger, ) - .option('-t, --to-block ', 'Up to which block to fetch logs (defaults to latest).', parseOptionalInteger) + .option('-tb, --to-block ', 'Up to which block to fetch logs (defaults to latest).', parseOptionalInteger) + .option('-fl --from-log ', 'Initial log id for getting 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) .option('--follow', 'If set, will keep polling for new logs until interrupted.') - .action(async ({ txHash, fromBlock, toBlock, contractAddress, selector, rpcUrl, follow }) => { - const client = await createCompatibleClient(rpcUrl, debugLogger); + .action(async ({ txHash, fromBlock, toBlock, fromLog, contractAddress, selector, rpcUrl, follow }) => { + const pxe = await createCompatibleClient(rpcUrl, debugLogger); 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, contractAddress, selector }; + const filter: LogFilter = { txHash, fromBlock, toBlock, fromLog, contractAddress, selector }; + + validateLogFilter(filter); const fetchLogs = async () => { - const logs = await client.getUnencryptedLogs(filter); + const logs = await pxe.getUnencryptedLogs(filter); if (!logs.length) { const filterOptions = Object.entries(filter) @@ -316,8 +320,9 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { } else { if (!follow) log('Logs found: \n'); logs.forEach(unencryptedLog => log(unencryptedLog.toHumanReadable())); - // Set `fromBlock` to the block number of the next block after the block of last log we fetched. - filter.fromBlock = logs[logs.length - 1].blockNumber + 1; + // Disable `fromBlock` and continue using the `fromLog` filter. + filter.fromBlock = undefined; + filter.fromLog = await logs[logs.length - 1].getLogId(pxe); } }; diff --git a/yarn-project/cli/src/utils.ts b/yarn-project/cli/src/utils.ts index 71d21393e3c2..07dd0c9ca58b 100644 --- a/yarn-project/cli/src/utils.ts +++ b/yarn-project/cli/src/utils.ts @@ -15,6 +15,7 @@ import { RollupAbi, RollupBytecode, } from '@aztec/l1-artifacts'; +import { LogId, parseLogId } from '@aztec/types'; import { InvalidArgumentError } from 'commander'; import fs from 'fs'; @@ -225,6 +226,19 @@ export function parseOptionalAztecAddress(address: string): AztecAddress | undef 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 parseLogId(logId); +} + /** * Parses a selector from a string. * @param selector - A serialised selector. diff --git a/yarn-project/types/src/logs/extended_unencrypted_l2_log.ts b/yarn-project/types/src/logs/extended_unencrypted_l2_log.ts index 64cb21f4d0c1..7e5a83eff816 100644 --- a/yarn-project/types/src/logs/extended_unencrypted_l2_log.ts +++ b/yarn-project/types/src/logs/extended_unencrypted_l2_log.ts @@ -1,7 +1,9 @@ import { toBufferBE } from '@aztec/foundation/bigint-buffer'; import { BufferReader } from '@aztec/foundation/serialize'; -import { TxHash, UnencryptedL2Log } from '../index.js'; +import isEqual from 'lodash.isequal'; + +import { LogId, PXE, TxHash, UnencryptedL2Log } from '../index.js'; /** * Represents an individual unencrypted log entry extended with info about the block and tx it was emitted in. @@ -40,6 +42,33 @@ export class ExtendedUnencryptedL2Log { return `${this.log.toHumanReadable()} (blockNumber: ${this.blockNumber}, txHash: ${this.txHash.toString()})`; } + /** + * 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); + } + + /** + * Gets a globally unique log id. + * @param pxe - The PXE instance to use for retrieving logs. + * @returns A globally unique log id. + */ + public async getLogId(pxe: PXE): Promise { + const txLogs = await pxe.getUnencryptedLogs({ txHash: this.txHash }); + const logIndex = txLogs.findIndex(log => log.equals(this)); + if (logIndex === -1) { + throw new Error(`Log ${this.toHumanReadable()} not found in tx ${this.txHash.toString()}`); + } + return { + blockNumber: this.blockNumber, + txIndex: this.txHash, + logIndex: logIndex, + }; + } + /** * Deserializes log from a buffer. * @param buffer - The buffer or buffer reader containing the log. diff --git a/yarn-project/types/src/logs/index.ts b/yarn-project/types/src/logs/index.ts index 6acc9289d9b5..d34c300eb272 100644 --- a/yarn-project/types/src/logs/index.ts +++ b/yarn-project/types/src/logs/index.ts @@ -1,6 +1,7 @@ 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'; diff --git a/yarn-project/types/src/logs/log_filter.ts b/yarn-project/types/src/logs/log_filter.ts index aeb7407b1cec..363af301bbb0 100644 --- a/yarn-project/types/src/logs/log_filter.ts +++ b/yarn-project/types/src/logs/log_filter.ts @@ -1,6 +1,6 @@ import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; -import { TxHash } from '../index.js'; +import { LogId, TxHash } from '../index.js'; /** * Log filter used to fetch L2 logs. @@ -11,10 +11,18 @@ export type LogFilter = { * @remarks If this is set, `fromBlock` and `toBlock` can't be defined. */ txHash?: TxHash; - /** The block number from which to start fetching logs (inclusive). */ + /** + * The block number from which to start fetching logs (inclusive). + * @remarks If this is set, `txHash` and `fromLog` can't be defined. + */ fromBlock?: number; /** The block number until which to fetch logs (not inclusive). */ toBlock?: number; + /** + * Log id from which to start fetching logs (inclusive). + * @remarks If this is set, `fromBlock` and `txHash` can't be defined. + */ + fromLog?: LogId; /** The contract address to filter logs by. */ contractAddress?: AztecAddress; /** @@ -23,3 +31,18 @@ export type LogFilter = { */ selector?: FunctionSelector; }; + +/** + * Validates a log filter. + * @param filter - Log filter to validate. + * @throws If the filter is invalid. + */ +export function validateLogFilter(filter: LogFilter) { + if (filter.txHash && (filter.fromBlock || filter.toBlock || filter.fromLog)) { + throw new Error("If txHash is set, fromBlock, toBlock, and fromLog can't be defined."); + } + + if (filter.fromBlock !== undefined && filter.fromLog) { + throw new Error("If fromBlock is set, fromLog can't be defined."); + } +} 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 000000000000..d9ca24493e4f --- /dev/null +++ b/yarn-project/types/src/logs/log_id.ts @@ -0,0 +1,38 @@ + +/** A globally unique log id. */ +export type LogId = { + /** The block number in which the tx containing the log was included. */ + blockNumber: number; + /** The index of a tx in a block. */ + txIndex: number; + /** The index of the log within a transaction. */ + logIndex: number; +}; + +/** + * Parses a log id from a string. + * @param logId - A string representation of a log id. + * @returns A log id. + */ +export function parseLogId(logId: string): LogId { + const [rawBlockNumber, rawTxIndex, rawLogIndex] = logId.split('-'); + const blockNumber = Number(rawBlockNumber); + const txIndex = Number(rawTxIndex); + const logIndex = Number(rawLogIndex); + + if (!Number.isInteger(blockNumber)) { + throw new Error(`Invalid block number in log id: ${logId}`); + } + if (!Number.isInteger(txIndex)) { + throw new Error(`Invalid tx index in log id: ${logId}`); + } + if (!Number.isInteger(logIndex)) { + throw new Error(`Invalid log index in log id: ${logId}`); + } + + return { + blockNumber, + txIndex, + logIndex, + }; +}