@aztec/archiver 3.0.0-nightly.20251223 → 3.0.0-nightly.20251224

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,7 +30,7 @@ import {
30
30
  SerializableContractInstance,
31
31
  computePublicBytecodeCommitment,
32
32
  } from '@aztec/stdlib/contract';
33
- import { ContractClassLog, LogId, PrivateLog, PublicLog } from '@aztec/stdlib/logs';
33
+ import { ContractClassLog, LogId, PrivateLog, PublicLog, SiloedTag, Tag } from '@aztec/stdlib/logs';
34
34
  import { InboxLeaf } from '@aztec/stdlib/messaging';
35
35
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
36
36
  import {
@@ -2198,46 +2198,35 @@ export function describeArchiverDataStore(
2198
2198
  });
2199
2199
  });
2200
2200
 
2201
- describe('getLogsByTags', () => {
2201
+ describe('getPrivateLogsByTags', () => {
2202
2202
  const numBlocksForLogs = 3;
2203
2203
  const numTxsPerBlock = 4;
2204
2204
  const numPrivateLogsPerTx = 3;
2205
- const numPublicLogsPerTx = 2;
2206
2205
 
2207
2206
  let logsCheckpoints: PublishedCheckpoint[];
2208
2207
 
2209
- const makeTag = (blockNumber: number, txIndex: number, logIndex: number, isPublic = false) =>
2210
- blockNumber === 1 && txIndex === 0 && logIndex === 0
2211
- ? Fr.ZERO // Shared tag
2212
- : new Fr((blockNumber * 100 + txIndex * 10 + logIndex) * (isPublic ? 123 : 1));
2208
+ const makePrivateLogTag = (blockNumber: number, txIndex: number, logIndex: number): SiloedTag =>
2209
+ new SiloedTag(
2210
+ blockNumber === 1 && txIndex === 0 && logIndex === 0
2211
+ ? Fr.ZERO // Shared tag
2212
+ : new Fr(blockNumber * 100 + txIndex * 10 + logIndex),
2213
+ );
2213
2214
 
2214
- const makePrivateLog = (tag: Fr) =>
2215
+ const makePrivateLog = (tag: SiloedTag) =>
2215
2216
  PrivateLog.from({
2216
- fields: makeTuple(PRIVATE_LOG_SIZE_IN_FIELDS, i => (!i ? tag : new Fr(tag.toNumber() + i))),
2217
+ fields: makeTuple(PRIVATE_LOG_SIZE_IN_FIELDS, i =>
2218
+ !i ? tag.value : new Fr(tag.value.toBigInt() + BigInt(i)),
2219
+ ),
2217
2220
  emittedLength: PRIVATE_LOG_SIZE_IN_FIELDS,
2218
2221
  });
2219
2222
 
2220
- const makePublicLog = (tag: Fr) =>
2221
- PublicLog.from({
2222
- contractAddress: AztecAddress.fromNumber(1),
2223
- // Arbitrary length
2224
- fields: new Array(10).fill(null).map((_, i) => (!i ? tag : new Fr(tag.toNumber() + i))),
2225
- });
2226
-
2227
2223
  const mockPrivateLogs = (blockNumber: number, txIndex: number) => {
2228
2224
  return times(numPrivateLogsPerTx, (logIndex: number) => {
2229
- const tag = makeTag(blockNumber, txIndex, logIndex);
2225
+ const tag = makePrivateLogTag(blockNumber, txIndex, logIndex);
2230
2226
  return makePrivateLog(tag);
2231
2227
  });
2232
2228
  };
2233
2229
 
2234
- const mockPublicLogs = (blockNumber: number, txIndex: number) => {
2235
- return times(numPublicLogsPerTx, (logIndex: number) => {
2236
- const tag = makeTag(blockNumber, txIndex, logIndex, /* isPublic */ true);
2237
- return makePublicLog(tag);
2238
- });
2239
- };
2240
-
2241
2230
  const mockCheckpointWithLogs = async (
2242
2231
  blockNumber: number,
2243
2232
  previousArchive?: AppendOnlyTreeSnapshot,
@@ -2253,7 +2242,7 @@ export function describeArchiverDataStore(
2253
2242
  block.body.txEffects = await timesParallel(numTxsPerBlock, async (txIndex: number) => {
2254
2243
  const txEffect = await TxEffect.random();
2255
2244
  txEffect.privateLogs = mockPrivateLogs(blockNumber, txIndex);
2256
- txEffect.publicLogs = mockPublicLogs(blockNumber, txIndex);
2245
+ txEffect.publicLogs = []; // No public logs needed for private log tests
2257
2246
  return txEffect;
2258
2247
  });
2259
2248
 
@@ -2279,9 +2268,9 @@ export function describeArchiverDataStore(
2279
2268
  });
2280
2269
 
2281
2270
  it('is possible to batch request private logs via tags', async () => {
2282
- const tags = [makeTag(2, 1, 2), makeTag(1, 2, 0)];
2271
+ const tags = [makePrivateLogTag(2, 1, 2), makePrivateLogTag(1, 2, 0)];
2283
2272
 
2284
- const logsByTags = await store.getLogsByTags(tags);
2273
+ const logsByTags = await store.getPrivateLogsByTags(tags);
2285
2274
 
2286
2275
  expect(logsByTags).toEqual([
2287
2276
  [
@@ -2303,11 +2292,21 @@ export function describeArchiverDataStore(
2303
2292
  ]);
2304
2293
  });
2305
2294
 
2306
- it('is possible to batch request all logs (private and public) via tags', async () => {
2307
- // Tag(1, 0, 0) is shared with the first private log and the first public log.
2308
- const tags = [makeTag(1, 0, 0)];
2295
+ it('is possible to batch request logs that have the same tag but different content', async () => {
2296
+ const tags = [makePrivateLogTag(1, 2, 1)];
2309
2297
 
2310
- const logsByTags = await store.getLogsByTags(tags);
2298
+ // Create a checkpoint containing logs that have the same tag as the checkpoints before.
2299
+ // Chain from the last checkpoint's archive
2300
+ const newBlockNumber = numBlocksForLogs + 1;
2301
+ const previousArchive = logsCheckpoints[logsCheckpoints.length - 1].checkpoint.blocks[0].archive;
2302
+ const newCheckpoint = await mockCheckpointWithLogs(newBlockNumber, previousArchive);
2303
+ const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1];
2304
+ newLog.fields[0] = tags[0].value;
2305
+ newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1] = newLog;
2306
+ await store.addCheckpoints([newCheckpoint]);
2307
+ await store.addLogs([newCheckpoint.checkpoint.blocks[0]]);
2308
+
2309
+ const logsByTags = await store.getPrivateLogsByTags(tags);
2311
2310
 
2312
2311
  expect(logsByTags).toEqual([
2313
2312
  [
@@ -2317,54 +2316,169 @@ export function describeArchiverDataStore(
2317
2316
  log: makePrivateLog(tags[0]),
2318
2317
  isFromPublic: false,
2319
2318
  }),
2319
+ expect.objectContaining({
2320
+ blockNumber: newBlockNumber,
2321
+ blockHash: L2BlockHash.fromField(await newCheckpoint.checkpoint.blocks[0].header.hash()),
2322
+ log: newLog,
2323
+ isFromPublic: false,
2324
+ }),
2325
+ ],
2326
+ ]);
2327
+ });
2328
+
2329
+ it('is possible to request logs for non-existing tags and determine their position', async () => {
2330
+ const tags = [makePrivateLogTag(99, 88, 77), makePrivateLogTag(1, 1, 1)];
2331
+
2332
+ const logsByTags = await store.getPrivateLogsByTags(tags);
2333
+
2334
+ expect(logsByTags).toEqual([
2335
+ [
2336
+ // No logs for the first tag.
2337
+ ],
2338
+ [
2320
2339
  expect.objectContaining({
2321
2340
  blockNumber: 1,
2322
2341
  blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2342
+ log: makePrivateLog(tags[1]),
2343
+ isFromPublic: false,
2344
+ }),
2345
+ ],
2346
+ ]);
2347
+ });
2348
+ });
2349
+
2350
+ describe('getPublicLogsByTagsFromContract', () => {
2351
+ const numBlocksForLogs = 3;
2352
+ const numTxsPerBlock = 4;
2353
+ const numPublicLogsPerTx = 2;
2354
+ const contractAddress = AztecAddress.fromNumber(543254);
2355
+
2356
+ let logsCheckpoints: PublishedCheckpoint[];
2357
+
2358
+ const makePublicLogTag = (blockNumber: number, txIndex: number, logIndex: number): Tag =>
2359
+ new Tag(
2360
+ blockNumber === 1 && txIndex === 0 && logIndex === 0
2361
+ ? Fr.ZERO // Shared tag
2362
+ : new Fr((blockNumber * 100 + txIndex * 10 + logIndex) * 123),
2363
+ );
2364
+
2365
+ const makePublicLog = (tag: Tag) =>
2366
+ PublicLog.from({
2367
+ contractAddress: contractAddress,
2368
+ // Arbitrary length
2369
+ fields: new Array(10).fill(null).map((_, i) => (!i ? tag.value : new Fr(tag.value.toBigInt() + BigInt(i)))),
2370
+ });
2371
+
2372
+ const mockPublicLogs = (blockNumber: number, txIndex: number) => {
2373
+ return times(numPublicLogsPerTx, (logIndex: number) => {
2374
+ const tag = makePublicLogTag(blockNumber, txIndex, logIndex);
2375
+ return makePublicLog(tag);
2376
+ });
2377
+ };
2378
+
2379
+ const mockCheckpointWithLogs = async (
2380
+ blockNumber: number,
2381
+ previousArchive?: AppendOnlyTreeSnapshot,
2382
+ ): Promise<PublishedCheckpoint> => {
2383
+ const block = await L2BlockNew.random(BlockNumber(blockNumber), {
2384
+ checkpointNumber: CheckpointNumber(blockNumber),
2385
+ indexWithinCheckpoint: 0,
2386
+ state: makeStateForBlock(blockNumber, numTxsPerBlock),
2387
+ ...(previousArchive ? { lastArchive: previousArchive } : {}),
2388
+ });
2389
+ block.header.globalVariables.blockNumber = BlockNumber(blockNumber);
2390
+
2391
+ block.body.txEffects = await timesParallel(numTxsPerBlock, async (txIndex: number) => {
2392
+ const txEffect = await TxEffect.random();
2393
+ txEffect.privateLogs = []; // No private logs needed for public log tests
2394
+ txEffect.publicLogs = mockPublicLogs(blockNumber, txIndex);
2395
+ return txEffect;
2396
+ });
2397
+
2398
+ const checkpoint = new Checkpoint(
2399
+ AppendOnlyTreeSnapshot.random(),
2400
+ CheckpointHeader.random(),
2401
+ [block],
2402
+ CheckpointNumber(blockNumber),
2403
+ );
2404
+ return makePublishedCheckpoint(checkpoint, blockNumber);
2405
+ };
2406
+
2407
+ beforeEach(async () => {
2408
+ // Create checkpoints sequentially to chain archive roots
2409
+ logsCheckpoints = [];
2410
+ for (let i = 0; i < numBlocksForLogs; i++) {
2411
+ const previousArchive = i > 0 ? logsCheckpoints[i - 1].checkpoint.blocks[0].archive : undefined;
2412
+ logsCheckpoints.push(await mockCheckpointWithLogs(i + 1, previousArchive));
2413
+ }
2414
+
2415
+ await store.addCheckpoints(logsCheckpoints);
2416
+ await store.addLogs(logsCheckpoints.flatMap(p => p.checkpoint.blocks));
2417
+ });
2418
+
2419
+ it('is possible to batch request public logs via tags', async () => {
2420
+ const tags = [makePublicLogTag(2, 1, 1), makePublicLogTag(1, 2, 0)];
2421
+
2422
+ const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags);
2423
+
2424
+ expect(logsByTags).toEqual([
2425
+ [
2426
+ expect.objectContaining({
2427
+ blockNumber: 2,
2428
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[2 - 1].checkpoint.blocks[0].header.hash()),
2323
2429
  log: makePublicLog(tags[0]),
2324
2430
  isFromPublic: true,
2325
2431
  }),
2326
2432
  ],
2433
+ [
2434
+ expect.objectContaining({
2435
+ blockNumber: 1,
2436
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2437
+ log: makePublicLog(tags[1]),
2438
+ isFromPublic: true,
2439
+ }),
2440
+ ],
2327
2441
  ]);
2328
2442
  });
2329
2443
 
2330
2444
  it('is possible to batch request logs that have the same tag but different content', async () => {
2331
- const tags = [makeTag(1, 2, 1)];
2445
+ const tags = [makePublicLogTag(1, 2, 1)];
2332
2446
 
2333
2447
  // Create a checkpoint containing logs that have the same tag as the checkpoints before.
2334
2448
  // Chain from the last checkpoint's archive
2335
2449
  const newBlockNumber = numBlocksForLogs + 1;
2336
2450
  const previousArchive = logsCheckpoints[logsCheckpoints.length - 1].checkpoint.blocks[0].archive;
2337
2451
  const newCheckpoint = await mockCheckpointWithLogs(newBlockNumber, previousArchive);
2338
- const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1];
2339
- newLog.fields[0] = tags[0];
2340
- newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1] = newLog;
2452
+ const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].publicLogs[1];
2453
+ newLog.fields[0] = tags[0].value;
2454
+ newCheckpoint.checkpoint.blocks[0].body.txEffects[1].publicLogs[1] = newLog;
2341
2455
  await store.addCheckpoints([newCheckpoint]);
2342
2456
  await store.addLogs([newCheckpoint.checkpoint.blocks[0]]);
2343
2457
 
2344
- const logsByTags = await store.getLogsByTags(tags);
2458
+ const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags);
2345
2459
 
2346
2460
  expect(logsByTags).toEqual([
2347
2461
  [
2348
2462
  expect.objectContaining({
2349
2463
  blockNumber: 1,
2350
2464
  blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2351
- log: makePrivateLog(tags[0]),
2352
- isFromPublic: false,
2465
+ log: makePublicLog(tags[0]),
2466
+ isFromPublic: true,
2353
2467
  }),
2354
2468
  expect.objectContaining({
2355
2469
  blockNumber: newBlockNumber,
2356
2470
  blockHash: L2BlockHash.fromField(await newCheckpoint.checkpoint.blocks[0].header.hash()),
2357
2471
  log: newLog,
2358
- isFromPublic: false,
2472
+ isFromPublic: true,
2359
2473
  }),
2360
2474
  ],
2361
2475
  ]);
2362
2476
  });
2363
2477
 
2364
2478
  it('is possible to request logs for non-existing tags and determine their position', async () => {
2365
- const tags = [makeTag(99, 88, 77), makeTag(1, 1, 1)];
2479
+ const tags = [makePublicLogTag(99, 88, 77), makePublicLogTag(1, 1, 0)];
2366
2480
 
2367
- const logsByTags = await store.getLogsByTags(tags);
2481
+ const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags);
2368
2482
 
2369
2483
  expect(logsByTags).toEqual([
2370
2484
  [
@@ -2374,8 +2488,8 @@ export function describeArchiverDataStore(
2374
2488
  expect.objectContaining({
2375
2489
  blockNumber: 1,
2376
2490
  blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2377
- log: makePrivateLog(tags[1]),
2378
- isFromPublic: false,
2491
+ log: makePublicLog(tags[1]),
2492
+ isFromPublic: true,
2379
2493
  }),
2380
2494
  ],
2381
2495
  ]);
@@ -17,7 +17,7 @@ import type {
17
17
  UtilityFunctionWithMembershipProof,
18
18
  } from '@aztec/stdlib/contract';
19
19
  import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client';
20
- import type { LogFilter, TxScopedL2Log } from '@aztec/stdlib/logs';
20
+ import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
21
21
  import type { BlockHeader, TxHash, TxReceipt } from '@aztec/stdlib/tx';
22
22
  import type { UInt64 } from '@aztec/stdlib/types';
23
23
 
@@ -318,16 +318,17 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc
318
318
  return this.#messageStore.getL1ToL2Messages(checkpointNumber);
319
319
  }
320
320
 
321
- /**
322
- * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
323
- * @param tags - The tags to filter the logs by.
324
- * @param logsPerTag - How many logs to return per tag. Default returns everything
325
- * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
326
- * that tag.
327
- */
328
- getLogsByTags(tags: Fr[], logsPerTag?: number): Promise<TxScopedL2Log[][]> {
321
+ getPrivateLogsByTags(tags: SiloedTag[]): Promise<TxScopedL2Log[][]> {
322
+ try {
323
+ return this.#logStore.getPrivateLogsByTags(tags);
324
+ } catch (err) {
325
+ return Promise.reject(err);
326
+ }
327
+ }
328
+
329
+ getPublicLogsByTagsFromContract(contractAddress: AztecAddress, tags: Tag[]): Promise<TxScopedL2Log[][]> {
329
330
  try {
330
- return this.#logStore.getLogsByTags(tags, logsPerTag);
331
+ return this.#logStore.getPublicLogsByTagsFromContract(contractAddress, tags);
331
332
  } catch (err) {
332
333
  return Promise.reject(err);
333
334
  }
@@ -4,6 +4,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
4
4
  import { createLogger } from '@aztec/foundation/log';
5
5
  import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize';
6
6
  import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
7
+ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
7
8
  import { L2BlockHash, L2BlockNew } from '@aztec/stdlib/block';
8
9
  import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client';
9
10
  import {
@@ -13,6 +14,8 @@ import {
13
14
  type LogFilter,
14
15
  LogId,
15
16
  PublicLog,
17
+ type SiloedTag,
18
+ Tag,
16
19
  TxScopedL2Log,
17
20
  } from '@aztec/stdlib/logs';
18
21
 
@@ -22,8 +25,12 @@ import type { BlockStore } from './block_store.js';
22
25
  * A store for logs
23
26
  */
24
27
  export class LogStore {
25
- #logsByTag: AztecAsyncMap<string, Buffer[]>;
26
- #logTagsByBlock: AztecAsyncMap<number, string[]>;
28
+ // `tag` --> private logs
29
+ #privateLogsByTag: AztecAsyncMap<string, Buffer[]>;
30
+ // `{contractAddress}_${tag}` --> public logs
31
+ #publicLogsByContractAndTag: AztecAsyncMap<string, Buffer[]>;
32
+ #privateLogKeysByBlock: AztecAsyncMap<number, string[]>;
33
+ #publicLogKeysByBlock: AztecAsyncMap<number, string[]>;
27
34
  #publicLogsByBlock: AztecAsyncMap<number, Buffer>;
28
35
  #contractClassLogsByBlock: AztecAsyncMap<number, Buffer>;
29
36
  #logsMaxPageSize: number;
@@ -34,29 +41,42 @@ export class LogStore {
34
41
  private blockStore: BlockStore,
35
42
  logsMaxPageSize: number = 1000,
36
43
  ) {
37
- this.#logsByTag = db.openMap('archiver_tagged_logs_by_tag');
38
- this.#logTagsByBlock = db.openMap('archiver_log_tags_by_block');
44
+ this.#privateLogsByTag = db.openMap('archiver_private_tagged_logs_by_tag');
45
+ this.#publicLogsByContractAndTag = db.openMap('archiver_public_tagged_logs_by_tag');
46
+ this.#privateLogKeysByBlock = db.openMap('archiver_private_log_keys_by_block');
47
+ this.#publicLogKeysByBlock = db.openMap('archiver_public_log_keys_by_block');
39
48
  this.#publicLogsByBlock = db.openMap('archiver_public_logs_by_block');
40
49
  this.#contractClassLogsByBlock = db.openMap('archiver_contract_class_logs_by_block');
41
50
 
42
51
  this.#logsMaxPageSize = logsMaxPageSize;
43
52
  }
44
53
 
45
- async #extractTaggedLogs(block: L2BlockNew) {
54
+ /**
55
+ * Extracts tagged logs from a single block, grouping them into private and public maps.
56
+ *
57
+ * @param block - The L2 block to extract logs from.
58
+ * @returns An object containing the private and public tagged logs for the block.
59
+ */
60
+ async #extractTaggedLogsFromBlock(block: L2BlockNew) {
46
61
  const blockHash = L2BlockHash.fromField(await block.hash());
47
- const taggedLogs = new Map<string, Buffer[]>();
62
+ // SiloedTag (as string) -> array of log buffers.
63
+ const privateTaggedLogs = new Map<string, Buffer[]>();
64
+ // "{contractAddress}_{tag}" (as string) -> array of log buffers.
65
+ const publicTaggedLogs = new Map<string, Buffer[]>();
48
66
  const dataStartIndexForBlock =
49
67
  block.header.state.partial.noteHashTree.nextAvailableLeafIndex -
50
68
  block.body.txEffects.length * MAX_NOTE_HASHES_PER_TX;
69
+
51
70
  block.body.txEffects.forEach((txEffect, txIndex) => {
52
71
  const txHash = txEffect.txHash;
53
72
  const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NOTE_HASHES_PER_TX;
54
73
 
55
74
  txEffect.privateLogs.forEach((log, logIndex) => {
75
+ // Private logs use SiloedTag (already siloed by kernel)
56
76
  const tag = log.fields[0];
57
77
  this.#log.debug(`Found private log with tag ${tag.toString()} in block ${block.number}`);
58
78
 
59
- const currentLogs = taggedLogs.get(tag.toString()) ?? [];
79
+ const currentLogs = privateTaggedLogs.get(tag.toString()) ?? [];
60
80
  currentLogs.push(
61
81
  new TxScopedL2Log(
62
82
  txHash,
@@ -68,14 +88,19 @@ export class LogStore {
68
88
  log,
69
89
  ).toBuffer(),
70
90
  );
71
- taggedLogs.set(tag.toString(), currentLogs);
91
+ privateTaggedLogs.set(tag.toString(), currentLogs);
72
92
  });
73
93
 
74
94
  txEffect.publicLogs.forEach((log, logIndex) => {
95
+ // Public logs use Tag directly (not siloed) and are stored with contract address
75
96
  const tag = log.fields[0];
76
- this.#log.debug(`Found public log with tag ${tag.toString()} in block ${block.number}`);
97
+ const contractAddress = log.contractAddress;
98
+ const key = `${contractAddress.toString()}_${tag.toString()}`;
99
+ this.#log.debug(
100
+ `Found public log with tag ${tag.toString()} from contract ${contractAddress.toString()} in block ${block.number}`,
101
+ );
77
102
 
78
- const currentLogs = taggedLogs.get(tag.toString()) ?? [];
103
+ const currentLogs = publicTaggedLogs.get(key) ?? [];
79
104
  currentLogs.push(
80
105
  new TxScopedL2Log(
81
106
  txHash,
@@ -87,49 +112,102 @@ export class LogStore {
87
112
  log,
88
113
  ).toBuffer(),
89
114
  );
90
- taggedLogs.set(tag.toString(), currentLogs);
115
+ publicTaggedLogs.set(key, currentLogs);
91
116
  });
92
117
  });
93
- return taggedLogs;
118
+
119
+ return { privateTaggedLogs, publicTaggedLogs };
94
120
  }
95
121
 
96
122
  /**
97
- * Append new logs to the store's list.
98
- * @param blocks - The blocks for which to add the logs.
99
- * @returns True if the operation is successful.
123
+ * Extracts and aggregates tagged logs from a list of blocks.
124
+ * @param blocks - The blocks to extract logs from.
125
+ * @returns A map from tag (as string) to an array of serialized private logs belonging to that tag, and a map from
126
+ * "{contractAddress}_{tag}" (as string) to an array of serialized public logs belonging to that key.
100
127
  */
101
- async addLogs(blocks: L2BlockNew[]): Promise<boolean> {
102
- const taggedLogsInBlocks = await Promise.all(blocks.map(block => this.#extractTaggedLogs(block)));
103
- const taggedLogsToAdd = taggedLogsInBlocks.reduce((acc, taggedLogs) => {
104
- for (const [tag, logs] of taggedLogs.entries()) {
128
+ async #extractTaggedLogs(
129
+ blocks: L2BlockNew[],
130
+ ): Promise<{ privateTaggedLogs: Map<string, Buffer[]>; publicTaggedLogs: Map<string, Buffer[]> }> {
131
+ const taggedLogsInBlocks = await Promise.all(blocks.map(block => this.#extractTaggedLogsFromBlock(block)));
132
+
133
+ // Now we merge the maps from each block into a single map.
134
+ const privateTaggedLogs = taggedLogsInBlocks.reduce((acc, { privateTaggedLogs }) => {
135
+ for (const [tag, logs] of privateTaggedLogs.entries()) {
105
136
  const currentLogs = acc.get(tag) ?? [];
106
137
  acc.set(tag, currentLogs.concat(logs));
107
138
  }
108
139
  return acc;
109
140
  }, new Map<string, Buffer[]>());
110
- const tagsToUpdate = Array.from(taggedLogsToAdd.keys());
141
+
142
+ const publicTaggedLogs = taggedLogsInBlocks.reduce((acc, { publicTaggedLogs }) => {
143
+ for (const [key, logs] of publicTaggedLogs.entries()) {
144
+ const currentLogs = acc.get(key) ?? [];
145
+ acc.set(key, currentLogs.concat(logs));
146
+ }
147
+ return acc;
148
+ }, new Map<string, Buffer[]>());
149
+
150
+ return { privateTaggedLogs, publicTaggedLogs };
151
+ }
152
+
153
+ /**
154
+ * Append new logs to the store's list.
155
+ * @param blocks - The blocks for which to add the logs.
156
+ * @returns True if the operation is successful.
157
+ */
158
+ async addLogs(blocks: L2BlockNew[]): Promise<boolean> {
159
+ const { privateTaggedLogs, publicTaggedLogs } = await this.#extractTaggedLogs(blocks);
160
+
161
+ const keysOfPrivateLogsToUpdate = Array.from(privateTaggedLogs.keys());
162
+ const keysOfPublicLogsToUpdate = Array.from(publicTaggedLogs.keys());
111
163
 
112
164
  return this.db.transactionAsync(async () => {
113
- const currentTaggedLogs = await Promise.all(
114
- tagsToUpdate.map(async tag => ({ tag, logBuffers: await this.#logsByTag.getAsync(tag) })),
165
+ const currentPrivateTaggedLogs = await Promise.all(
166
+ keysOfPrivateLogsToUpdate.map(async key => ({
167
+ tag: key,
168
+ logBuffers: await this.#privateLogsByTag.getAsync(key),
169
+ })),
115
170
  );
116
- currentTaggedLogs.forEach(taggedLogBuffer => {
171
+ currentPrivateTaggedLogs.forEach(taggedLogBuffer => {
117
172
  if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) {
118
- taggedLogsToAdd.set(
173
+ privateTaggedLogs.set(
119
174
  taggedLogBuffer.tag,
120
- taggedLogBuffer.logBuffers!.concat(taggedLogsToAdd.get(taggedLogBuffer.tag)!),
175
+ taggedLogBuffer.logBuffers!.concat(privateTaggedLogs.get(taggedLogBuffer.tag)!),
176
+ );
177
+ }
178
+ });
179
+
180
+ const currentPublicTaggedLogs = await Promise.all(
181
+ keysOfPublicLogsToUpdate.map(async key => ({
182
+ key,
183
+ logBuffers: await this.#publicLogsByContractAndTag.getAsync(key),
184
+ })),
185
+ );
186
+ currentPublicTaggedLogs.forEach(taggedLogBuffer => {
187
+ if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) {
188
+ publicTaggedLogs.set(
189
+ taggedLogBuffer.key,
190
+ taggedLogBuffer.logBuffers!.concat(publicTaggedLogs.get(taggedLogBuffer.key)!),
121
191
  );
122
192
  }
123
193
  });
194
+
124
195
  for (const block of blocks) {
125
196
  const blockHash = await block.hash();
126
197
 
127
- const tagsInBlock = [];
128
- for (const [tag, logs] of taggedLogsToAdd.entries()) {
129
- await this.#logsByTag.set(tag, logs);
130
- tagsInBlock.push(tag);
198
+ const privateTagsInBlock: string[] = [];
199
+ for (const [tag, logs] of privateTaggedLogs.entries()) {
200
+ await this.#privateLogsByTag.set(tag, logs);
201
+ privateTagsInBlock.push(tag);
131
202
  }
132
- await this.#logTagsByBlock.set(block.number, tagsInBlock);
203
+ await this.#privateLogKeysByBlock.set(block.number, privateTagsInBlock);
204
+
205
+ const publicKeysInBlock: string[] = [];
206
+ for (const [key, logs] of publicTaggedLogs.entries()) {
207
+ await this.#publicLogsByContractAndTag.set(key, logs);
208
+ publicKeysInBlock.push(key);
209
+ }
210
+ await this.#publicLogKeysByBlock.set(block.number, publicKeysInBlock);
133
211
 
134
212
  const publicLogsInBlock = block.body.txEffects
135
213
  .map((txEffect, txIndex) =>
@@ -178,44 +256,55 @@ export class LogStore {
178
256
 
179
257
  deleteLogs(blocks: L2BlockNew[]): Promise<boolean> {
180
258
  return this.db.transactionAsync(async () => {
181
- const tagsToDelete = (
182
- await Promise.all(
183
- blocks.map(async block => {
184
- const tags = await this.#logTagsByBlock.getAsync(block.number);
185
- return tags ?? [];
186
- }),
187
- )
188
- ).flat();
259
+ await Promise.all(
260
+ blocks.map(async block => {
261
+ // Delete private logs
262
+ const privateKeys = (await this.#privateLogKeysByBlock.getAsync(block.number)) ?? [];
263
+ await Promise.all(privateKeys.map(tag => this.#privateLogsByTag.delete(tag)));
264
+
265
+ // Delete public logs
266
+ const publicKeys = (await this.#publicLogKeysByBlock.getAsync(block.number)) ?? [];
267
+ await Promise.all(publicKeys.map(key => this.#publicLogsByContractAndTag.delete(key)));
268
+ }),
269
+ );
189
270
 
190
271
  await Promise.all(
191
272
  blocks.map(block =>
192
273
  Promise.all([
193
274
  this.#publicLogsByBlock.delete(block.number),
194
- this.#logTagsByBlock.delete(block.number),
275
+ this.#privateLogKeysByBlock.delete(block.number),
276
+ this.#publicLogKeysByBlock.delete(block.number),
195
277
  this.#contractClassLogsByBlock.delete(block.number),
196
278
  ]),
197
279
  ),
198
280
  );
199
281
 
200
- await Promise.all(tagsToDelete.map(tag => this.#logsByTag.delete(tag.toString())));
201
282
  return true;
202
283
  });
203
284
  }
204
285
 
205
286
  /**
206
- * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
207
- * @param tags - The tags to filter the logs by.
208
- * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
209
- * that tag.
287
+ * Gets all private logs that match any of the `tags`. For each tag, an array of matching logs is returned. An empty
288
+ * array implies no logs match that tag.
210
289
  */
211
- async getLogsByTags(tags: Fr[], limitPerTag?: number): Promise<TxScopedL2Log[][]> {
212
- if (limitPerTag !== undefined && limitPerTag <= 0) {
213
- throw new TypeError('limitPerTag needs to be greater than 0');
214
- }
215
- const logs = await Promise.all(tags.map(tag => this.#logsByTag.getAsync(tag.toString())));
216
- return logs.map(
217
- logBuffers => logBuffers?.slice(0, limitPerTag).map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? [],
290
+ async getPrivateLogsByTags(tags: SiloedTag[]): Promise<TxScopedL2Log[][]> {
291
+ const logs = await Promise.all(tags.map(tag => this.#privateLogsByTag.getAsync(tag.toString())));
292
+
293
+ return logs.map(logBuffers => logBuffers?.map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? []);
294
+ }
295
+
296
+ /**
297
+ * Gets all public logs that match any of the `tags` from the specified contract. For each tag, an array of matching
298
+ * logs is returned. An empty array implies no logs match that tag.
299
+ */
300
+ async getPublicLogsByTagsFromContract(contractAddress: AztecAddress, tags: Tag[]): Promise<TxScopedL2Log[][]> {
301
+ const logs = await Promise.all(
302
+ tags.map(tag => {
303
+ const key = `${contractAddress.toString()}_${tag.value.toString()}`;
304
+ return this.#publicLogsByContractAndTag.getAsync(key);
305
+ }),
218
306
  );
307
+ return logs.map(logBuffers => logBuffers?.map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? []);
219
308
  }
220
309
 
221
310
  /**
@@ -331,7 +331,7 @@ export async function getCheckpointBlobDataFromBlobs(
331
331
  logger: Logger,
332
332
  isHistoricalSync: boolean,
333
333
  ): Promise<CheckpointBlobData> {
334
- const blobBodies = await blobClient.getBlobSidecar(blockHash, blobHashes, undefined, { isHistoricalSync });
334
+ const blobBodies = await blobClient.getBlobSidecar(blockHash, blobHashes, { isHistoricalSync });
335
335
  if (blobBodies.length === 0) {
336
336
  throw new NoBlobBodiesFoundError(checkpointNumber);
337
337
  }
@@ -339,7 +339,7 @@ export async function getCheckpointBlobDataFromBlobs(
339
339
  let checkpointBlobData: CheckpointBlobData;
340
340
  try {
341
341
  // Attempt to decode the checkpoint blob data.
342
- checkpointBlobData = decodeCheckpointBlobDataFromBlobs(blobBodies.map(b => b.blob));
342
+ checkpointBlobData = decodeCheckpointBlobDataFromBlobs(blobBodies);
343
343
  } catch (err: any) {
344
344
  if (err instanceof BlobDeserializationError) {
345
345
  logger.fatal(err.message);