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.