@aztec/archiver 0.63.0 → 0.64.0

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.
Files changed (45) hide show
  1. package/dest/archiver/archiver.d.ts +12 -3
  2. package/dest/archiver/archiver.d.ts.map +1 -1
  3. package/dest/archiver/archiver.js +37 -10
  4. package/dest/archiver/archiver_store.d.ts +25 -2
  5. package/dest/archiver/archiver_store.d.ts.map +1 -1
  6. package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
  7. package/dest/archiver/archiver_store_test_suite.js +79 -19
  8. package/dest/archiver/data_retrieval.d.ts.map +1 -1
  9. package/dest/archiver/data_retrieval.js +4 -4
  10. package/dest/archiver/epoch_helpers.d.ts +12 -7
  11. package/dest/archiver/epoch_helpers.d.ts.map +1 -1
  12. package/dest/archiver/epoch_helpers.js +14 -2
  13. package/dest/archiver/instrumentation.d.ts +6 -0
  14. package/dest/archiver/instrumentation.d.ts.map +1 -1
  15. package/dest/archiver/instrumentation.js +15 -2
  16. package/dest/archiver/kv_archiver_store/block_store.d.ts +2 -2
  17. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
  18. package/dest/archiver/kv_archiver_store/block_store.js +18 -8
  19. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +16 -2
  20. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  21. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +24 -3
  22. package/dest/archiver/kv_archiver_store/nullifier_store.d.ts +12 -0
  23. package/dest/archiver/kv_archiver_store/nullifier_store.d.ts.map +1 -0
  24. package/dest/archiver/kv_archiver_store/nullifier_store.js +71 -0
  25. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts +11 -2
  26. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts.map +1 -1
  27. package/dest/archiver/memory_archiver_store/memory_archiver_store.js +51 -6
  28. package/dest/factory.d.ts.map +1 -1
  29. package/dest/factory.js +21 -3
  30. package/dest/test/mock_l2_block_source.d.ts +5 -1
  31. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  32. package/dest/test/mock_l2_block_source.js +9 -3
  33. package/package.json +12 -11
  34. package/src/archiver/archiver.ts +51 -11
  35. package/src/archiver/archiver_store.ts +24 -1
  36. package/src/archiver/archiver_store_test_suite.ts +92 -17
  37. package/src/archiver/data_retrieval.ts +13 -4
  38. package/src/archiver/epoch_helpers.ts +30 -6
  39. package/src/archiver/instrumentation.ts +22 -0
  40. package/src/archiver/kv_archiver_store/block_store.ts +22 -10
  41. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +27 -3
  42. package/src/archiver/kv_archiver_store/nullifier_store.ts +78 -0
  43. package/src/archiver/memory_archiver_store/memory_archiver_store.ts +59 -5
  44. package/src/factory.ts +21 -3
  45. package/src/test/mock_l2_block_source.ts +8 -2
@@ -1,4 +1,4 @@
1
- import { InboxLeaf, L2Block, LogId, LogType, TxHash } from '@aztec/circuit-types';
1
+ import { InboxLeaf, L2Block, LogId, LogType, TxHash, wrapInBlock } from '@aztec/circuit-types';
2
2
  import '@aztec/circuit-types/jest';
3
3
  import {
4
4
  AztecAddress,
@@ -7,6 +7,7 @@ import {
7
7
  Fr,
8
8
  INITIAL_L2_BLOCK_NUM,
9
9
  L1_TO_L2_MSG_SUBTREE_HEIGHT,
10
+ MAX_NULLIFIERS_PER_TX,
10
11
  SerializableContractInstance,
11
12
  } from '@aztec/circuits.js';
12
13
  import {
@@ -37,12 +38,18 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
37
38
  [5, 2, () => blocks.slice(4, 6)],
38
39
  ];
39
40
 
41
+ const makeL1Published = (block: L2Block, l1BlockNumber: number): L1Published<L2Block> => ({
42
+ data: block,
43
+ l1: {
44
+ blockNumber: BigInt(l1BlockNumber),
45
+ blockHash: `0x${l1BlockNumber}`,
46
+ timestamp: BigInt(l1BlockNumber * 1000),
47
+ },
48
+ });
49
+
40
50
  beforeEach(() => {
41
51
  store = getStore();
42
- blocks = times(10, i => ({
43
- data: L2Block.random(i + 1),
44
- l1: { blockNumber: BigInt(i + 10), blockHash: `0x${i}`, timestamp: BigInt(i * 1000) },
45
- }));
52
+ blocks = times(10, i => makeL1Published(L2Block.random(i + 1), i + 10));
46
53
  });
47
54
 
48
55
  describe('addBlocks', () => {
@@ -68,6 +75,21 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
68
75
  expect(await store.getSynchedL2BlockNumber()).toBe(blockNumber - 1);
69
76
  expect(await store.getBlocks(blockNumber, 1)).toEqual([]);
70
77
  });
78
+
79
+ it('can unwind multiple empty blocks', async () => {
80
+ const emptyBlocks = times(10, i => makeL1Published(L2Block.random(i + 1, 0), i + 10));
81
+ await store.addBlocks(emptyBlocks);
82
+ expect(await store.getSynchedL2BlockNumber()).toBe(10);
83
+
84
+ await store.unwindBlocks(10, 3);
85
+ expect(await store.getSynchedL2BlockNumber()).toBe(7);
86
+ expect((await store.getBlocks(1, 10)).map(b => b.data.number)).toEqual([1, 2, 3, 4, 5, 6, 7]);
87
+ });
88
+
89
+ it('refuses to unwind blocks if the tip is not the last block', async () => {
90
+ await store.addBlocks(blocks);
91
+ await expect(store.unwindBlocks(5, 1)).rejects.toThrow(/can only unwind blocks from the tip/i);
92
+ });
71
93
  });
72
94
 
73
95
  describe('getBlocks', () => {
@@ -191,14 +213,14 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
191
213
  });
192
214
 
193
215
  it.each([
194
- () => blocks[0].data.body.txEffects[0],
195
- () => blocks[9].data.body.txEffects[3],
196
- () => blocks[3].data.body.txEffects[1],
197
- () => blocks[5].data.body.txEffects[2],
198
- () => blocks[1].data.body.txEffects[0],
216
+ () => wrapInBlock(blocks[0].data.body.txEffects[0], blocks[0].data),
217
+ () => wrapInBlock(blocks[9].data.body.txEffects[3], blocks[9].data),
218
+ () => wrapInBlock(blocks[3].data.body.txEffects[1], blocks[3].data),
219
+ () => wrapInBlock(blocks[5].data.body.txEffects[2], blocks[5].data),
220
+ () => wrapInBlock(blocks[1].data.body.txEffects[0], blocks[1].data),
199
221
  ])('retrieves a previously stored transaction', async getExpectedTx => {
200
222
  const expectedTx = getExpectedTx();
201
- const actualTx = await store.getTxEffect(expectedTx.txHash);
223
+ const actualTx = await store.getTxEffect(expectedTx.data.txHash);
202
224
  expect(actualTx).toEqual(expectedTx);
203
225
  });
204
226
 
@@ -207,16 +229,16 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
207
229
  });
208
230
 
209
231
  it.each([
210
- () => blocks[0].data.body.txEffects[0],
211
- () => blocks[9].data.body.txEffects[3],
212
- () => blocks[3].data.body.txEffects[1],
213
- () => blocks[5].data.body.txEffects[2],
214
- () => blocks[1].data.body.txEffects[0],
232
+ () => wrapInBlock(blocks[0].data.body.txEffects[0], blocks[0].data),
233
+ () => wrapInBlock(blocks[9].data.body.txEffects[3], blocks[9].data),
234
+ () => wrapInBlock(blocks[3].data.body.txEffects[1], blocks[3].data),
235
+ () => wrapInBlock(blocks[5].data.body.txEffects[2], blocks[5].data),
236
+ () => wrapInBlock(blocks[1].data.body.txEffects[0], blocks[1].data),
215
237
  ])('tries to retrieves a previously stored transaction after deleted', async getExpectedTx => {
216
238
  await store.unwindBlocks(blocks.length, blocks.length);
217
239
 
218
240
  const expectedTx = getExpectedTx();
219
- const actualTx = await store.getTxEffect(expectedTx.txHash);
241
+ const actualTx = await store.getTxEffect(expectedTx.data.txHash);
220
242
  expect(actualTx).toEqual(undefined);
221
243
  });
222
244
 
@@ -705,5 +727,58 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
705
727
  }
706
728
  });
707
729
  });
730
+
731
+ describe('findNullifiersIndexesWithBlock', () => {
732
+ let blocks: L2Block[];
733
+ const numBlocks = 10;
734
+ const nullifiersPerBlock = new Map<number, Fr[]>();
735
+
736
+ beforeEach(() => {
737
+ blocks = times(numBlocks, (index: number) => L2Block.random(index + 1, 1));
738
+
739
+ blocks.forEach((block, blockIndex) => {
740
+ nullifiersPerBlock.set(
741
+ blockIndex,
742
+ block.body.txEffects.flatMap(txEffect => txEffect.nullifiers),
743
+ );
744
+ });
745
+ });
746
+
747
+ it('returns wrapped nullifiers with blocks if they exist', async () => {
748
+ await store.addNullifiers(blocks);
749
+ const nullifiersToRetrieve = [...nullifiersPerBlock.get(0)!, ...nullifiersPerBlock.get(5)!, Fr.random()];
750
+ const blockScopedNullifiers = await store.findNullifiersIndexesWithBlock(10, nullifiersToRetrieve);
751
+
752
+ expect(blockScopedNullifiers).toHaveLength(nullifiersToRetrieve.length);
753
+ const [undefinedNullifier] = blockScopedNullifiers.slice(-1);
754
+ const realNullifiers = blockScopedNullifiers.slice(0, -1);
755
+ realNullifiers.forEach((blockScopedNullifier, index) => {
756
+ expect(blockScopedNullifier).not.toBeUndefined();
757
+ const { data, l2BlockNumber } = blockScopedNullifier!;
758
+ expect(data).toEqual(expect.any(BigInt));
759
+ expect(l2BlockNumber).toEqual(index < MAX_NULLIFIERS_PER_TX ? 1 : 6);
760
+ });
761
+ expect(undefinedNullifier).toBeUndefined();
762
+ });
763
+
764
+ it('returns wrapped nullifiers filtering by blockNumber', async () => {
765
+ await store.addNullifiers(blocks);
766
+ const nullifiersToRetrieve = [...nullifiersPerBlock.get(0)!, ...nullifiersPerBlock.get(5)!];
767
+ const blockScopedNullifiers = await store.findNullifiersIndexesWithBlock(5, nullifiersToRetrieve);
768
+
769
+ expect(blockScopedNullifiers).toHaveLength(nullifiersToRetrieve.length);
770
+ const undefinedNullifiers = blockScopedNullifiers.slice(-MAX_NULLIFIERS_PER_TX);
771
+ const realNullifiers = blockScopedNullifiers.slice(0, -MAX_NULLIFIERS_PER_TX);
772
+ realNullifiers.forEach(blockScopedNullifier => {
773
+ expect(blockScopedNullifier).not.toBeUndefined();
774
+ const { data, l2BlockNumber } = blockScopedNullifier!;
775
+ expect(data).toEqual(expect.any(BigInt));
776
+ expect(l2BlockNumber).toEqual(1);
777
+ });
778
+ undefinedNullifiers.forEach(undefinedNullifier => {
779
+ expect(undefinedNullifier).toBeUndefined();
780
+ });
781
+ });
782
+ });
708
783
  });
709
784
  }
@@ -139,9 +139,18 @@ async function getBlockFromRollupTx(
139
139
  if (!allowedMethods.includes(functionName)) {
140
140
  throw new Error(`Unexpected method called ${functionName}`);
141
141
  }
142
- const [headerHex, archiveRootHex, , , , bodyHex] = args! as readonly [Hex, Hex, Hex, Hex[], ViemSignature[], Hex];
143
-
144
- const header = Header.fromBuffer(Buffer.from(hexToBytes(headerHex)));
142
+ const [decodedArgs, , bodyHex] = args! as readonly [
143
+ {
144
+ header: Hex;
145
+ archive: Hex;
146
+ blockHash: Hex;
147
+ txHashes: Hex[];
148
+ },
149
+ ViemSignature[],
150
+ Hex,
151
+ ];
152
+
153
+ const header = Header.fromBuffer(Buffer.from(hexToBytes(decodedArgs.header)));
145
154
  const blockBody = Body.fromBuffer(Buffer.from(hexToBytes(bodyHex)));
146
155
 
147
156
  const blockNumberFromHeader = header.globalVariables.blockNumber.toBigInt();
@@ -152,7 +161,7 @@ async function getBlockFromRollupTx(
152
161
 
153
162
  const archive = AppendOnlyTreeSnapshot.fromBuffer(
154
163
  Buffer.concat([
155
- Buffer.from(hexToBytes(archiveRootHex)), // L2Block.archive.root
164
+ Buffer.from(hexToBytes(decodedArgs.archive)), // L2Block.archive.root
156
165
  numToUInt32BE(Number(l2BlockNum + 1n)), // L2Block.archive.nextAvailableLeafIndex
157
166
  ]),
158
167
  );
@@ -1,30 +1,54 @@
1
- type TimeConstants = {
1
+ // REFACTOR: This file should go in a package lower in the dependency graph.
2
+
3
+ export type EpochConstants = {
4
+ l1GenesisBlock: bigint;
2
5
  l1GenesisTime: bigint;
3
6
  epochDuration: number;
4
7
  slotDuration: number;
5
8
  };
6
9
 
7
10
  /** Returns the slot number for a given timestamp. */
8
- export function getSlotAtTimestamp(ts: bigint, constants: Pick<TimeConstants, 'l1GenesisTime' | 'slotDuration'>) {
11
+ export function getSlotAtTimestamp(ts: bigint, constants: Pick<EpochConstants, 'l1GenesisTime' | 'slotDuration'>) {
9
12
  return ts < constants.l1GenesisTime ? 0n : (ts - constants.l1GenesisTime) / BigInt(constants.slotDuration);
10
13
  }
11
14
 
12
15
  /** Returns the epoch number for a given timestamp. */
13
- export function getEpochNumberAtTimestamp(ts: bigint, constants: TimeConstants) {
16
+ export function getEpochNumberAtTimestamp(
17
+ ts: bigint,
18
+ constants: Pick<EpochConstants, 'epochDuration' | 'slotDuration' | 'l1GenesisTime'>,
19
+ ) {
14
20
  return getSlotAtTimestamp(ts, constants) / BigInt(constants.epochDuration);
15
21
  }
16
22
 
17
- /** Returns the range of slots (inclusive) for a given epoch number. */
18
- export function getSlotRangeForEpoch(epochNumber: bigint, constants: Pick<TimeConstants, 'epochDuration'>) {
23
+ /** Returns the range of L2 slots (inclusive) for a given epoch number. */
24
+ export function getSlotRangeForEpoch(epochNumber: bigint, constants: Pick<EpochConstants, 'epochDuration'>) {
19
25
  const startSlot = epochNumber * BigInt(constants.epochDuration);
20
26
  return [startSlot, startSlot + BigInt(constants.epochDuration) - 1n];
21
27
  }
22
28
 
23
29
  /** Returns the range of L1 timestamps (inclusive) for a given epoch number. */
24
- export function getTimestampRangeForEpoch(epochNumber: bigint, constants: TimeConstants) {
30
+ export function getTimestampRangeForEpoch(
31
+ epochNumber: bigint,
32
+ constants: Pick<EpochConstants, 'l1GenesisTime' | 'slotDuration' | 'epochDuration'>,
33
+ ) {
25
34
  const [startSlot, endSlot] = getSlotRangeForEpoch(epochNumber, constants);
26
35
  return [
27
36
  constants.l1GenesisTime + startSlot * BigInt(constants.slotDuration),
28
37
  constants.l1GenesisTime + endSlot * BigInt(constants.slotDuration),
29
38
  ];
30
39
  }
40
+
41
+ /**
42
+ * Returns the range of L1 blocks (inclusive) for a given epoch number.
43
+ * @remarks This assumes no time warp has happened.
44
+ */
45
+ export function getL1BlockRangeForEpoch(
46
+ epochNumber: bigint,
47
+ constants: Pick<EpochConstants, 'l1GenesisBlock' | 'epochDuration' | 'slotDuration'>,
48
+ ) {
49
+ const epochDurationInL1Blocks = BigInt(constants.epochDuration) * BigInt(constants.slotDuration);
50
+ return [
51
+ epochNumber * epochDurationInL1Blocks + constants.l1GenesisBlock,
52
+ (epochNumber + 1n) * epochDurationInL1Blocks + constants.l1GenesisBlock - 1n,
53
+ ];
54
+ }
@@ -4,6 +4,7 @@ import {
4
4
  Attributes,
5
5
  type Gauge,
6
6
  type Histogram,
7
+ LmdbMetrics,
7
8
  Metrics,
8
9
  type TelemetryClient,
9
10
  type UpDownCounter,
@@ -18,6 +19,7 @@ export class ArchiverInstrumentation {
18
19
  private syncDuration: Histogram;
19
20
  private proofsSubmittedDelay: Histogram;
20
21
  private proofsSubmittedCount: UpDownCounter;
22
+ private dbMetrics: LmdbMetrics;
21
23
 
22
24
  private log = createDebugLogger('aztec:archiver:instrumentation');
23
25
 
@@ -55,6 +57,26 @@ export class ArchiverInstrumentation {
55
57
  explicitBucketBoundaries: millisecondBuckets(1, 80), // 10ms -> ~3hs
56
58
  },
57
59
  });
60
+
61
+ this.dbMetrics = new LmdbMetrics(
62
+ meter,
63
+ {
64
+ name: Metrics.ARCHIVER_DB_MAP_SIZE,
65
+ description: 'Database map size for the archiver',
66
+ },
67
+ {
68
+ name: Metrics.ARCHIVER_DB_USED_SIZE,
69
+ description: 'Database used size for the archiver',
70
+ },
71
+ {
72
+ name: Metrics.ARCHIVER_DB_NUM_ITEMS,
73
+ description: 'Num items in the archiver database',
74
+ },
75
+ );
76
+ }
77
+
78
+ public recordDBMetrics(metrics: { mappingSize: number; numItems: number; actualSize: number }) {
79
+ this.dbMetrics.recordDBMetrics(metrics);
58
80
  }
59
81
 
60
82
  public isEnabled(): boolean {
@@ -1,4 +1,4 @@
1
- import { Body, L2Block, type TxEffect, type TxHash, TxReceipt } from '@aztec/circuit-types';
1
+ import { Body, type InBlock, L2Block, type TxEffect, type TxHash, TxReceipt } from '@aztec/circuit-types';
2
2
  import { AppendOnlyTreeSnapshot, type AztecAddress, Header, INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js';
3
3
  import { createDebugLogger } from '@aztec/foundation/log';
4
4
  import { type AztecKVStore, type AztecMap, type AztecSingleton, type Range } from '@aztec/kv-store';
@@ -20,7 +20,7 @@ export class BlockStore {
20
20
  /** Map block number to block data */
21
21
  #blocks: AztecMap<number, BlockStorage>;
22
22
 
23
- /** Map block body hash to block body */
23
+ /** Map block hash to block body */
24
24
  #blockBodies: AztecMap<string, Buffer>;
25
25
 
26
26
  /** Stores L1 block number in which the last processed L2 block was included */
@@ -72,7 +72,7 @@ export class BlockStore {
72
72
  void this.#txIndex.set(tx.txHash.toString(), [block.data.number, i]);
73
73
  });
74
74
 
75
- void this.#blockBodies.set(block.data.body.getTxsEffectsHash().toString('hex'), block.data.body.toBuffer());
75
+ void this.#blockBodies.set(block.data.hash().toString(), block.data.body.toBuffer());
76
76
  }
77
77
 
78
78
  void this.#lastSynchedL1Block.set(blocks[blocks.length - 1].l1.blockNumber);
@@ -92,7 +92,7 @@ export class BlockStore {
92
92
  return this.db.transaction(() => {
93
93
  const last = this.getSynchedL2BlockNumber();
94
94
  if (from != last) {
95
- throw new Error(`Can only remove from the tip`);
95
+ throw new Error(`Can only unwind blocks from the tip (requested ${from} but current tip is ${last})`);
96
96
  }
97
97
 
98
98
  for (let i = 0; i < blocksToUnwind; i++) {
@@ -106,7 +106,9 @@ export class BlockStore {
106
106
  block.data.body.txEffects.forEach(tx => {
107
107
  void this.#txIndex.delete(tx.txHash.toString());
108
108
  });
109
- void this.#blockBodies.delete(block.data.body.getTxsEffectsHash().toString('hex'));
109
+ const blockHash = block.data.hash().toString();
110
+ void this.#blockBodies.delete(blockHash);
111
+ this.#log.debug(`Unwound block ${blockNumber} ${blockHash}`);
110
112
  }
111
113
 
112
114
  return true;
@@ -154,10 +156,12 @@ export class BlockStore {
154
156
  private getBlockFromBlockStorage(blockStorage: BlockStorage) {
155
157
  const header = Header.fromBuffer(blockStorage.header);
156
158
  const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive);
157
-
158
- const blockBodyBuffer = this.#blockBodies.get(header.contentCommitment.txsEffectsHash.toString('hex'));
159
+ const blockHash = header.hash().toString();
160
+ const blockBodyBuffer = this.#blockBodies.get(blockHash);
159
161
  if (blockBodyBuffer === undefined) {
160
- throw new Error('Body could not be retrieved');
162
+ throw new Error(
163
+ `Could not retrieve body for block ${header.globalVariables.blockNumber.toNumber()} ${blockHash}`,
164
+ );
161
165
  }
162
166
  const body = Body.fromBuffer(blockBodyBuffer);
163
167
 
@@ -170,14 +174,22 @@ export class BlockStore {
170
174
  * @param txHash - The txHash of the tx corresponding to the tx effect.
171
175
  * @returns The requested tx effect (or undefined if not found).
172
176
  */
173
- getTxEffect(txHash: TxHash): TxEffect | undefined {
177
+ getTxEffect(txHash: TxHash): InBlock<TxEffect> | undefined {
174
178
  const [blockNumber, txIndex] = this.getTxLocation(txHash) ?? [];
175
179
  if (typeof blockNumber !== 'number' || typeof txIndex !== 'number') {
176
180
  return undefined;
177
181
  }
178
182
 
179
183
  const block = this.getBlock(blockNumber);
180
- return block?.data.body.txEffects[txIndex];
184
+ if (!block) {
185
+ return undefined;
186
+ }
187
+
188
+ return {
189
+ data: block.data.body.txEffects[txIndex],
190
+ l2BlockNumber: block.data.number,
191
+ l2BlockHash: block.data.hash().toString(),
192
+ };
181
193
  }
182
194
 
183
195
  /**
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  type FromLogType,
3
3
  type GetUnencryptedLogsResponse,
4
+ type InBlock,
4
5
  type InboxLeaf,
5
6
  type L2Block,
6
7
  type L2BlockL2Logs,
7
8
  type LogFilter,
8
9
  type LogType,
9
- type TxEffect,
10
10
  type TxHash,
11
11
  type TxReceipt,
12
12
  type TxScopedL2Log,
@@ -33,6 +33,7 @@ import { ContractClassStore } from './contract_class_store.js';
33
33
  import { ContractInstanceStore } from './contract_instance_store.js';
34
34
  import { LogStore } from './log_store.js';
35
35
  import { MessageStore } from './message_store.js';
36
+ import { NullifierStore } from './nullifier_store.js';
36
37
 
37
38
  /**
38
39
  * LMDB implementation of the ArchiverDataStore interface.
@@ -40,6 +41,7 @@ import { MessageStore } from './message_store.js';
40
41
  export class KVArchiverDataStore implements ArchiverDataStore {
41
42
  #blockStore: BlockStore;
42
43
  #logStore: LogStore;
44
+ #nullifierStore: NullifierStore;
43
45
  #messageStore: MessageStore;
44
46
  #contractClassStore: ContractClassStore;
45
47
  #contractInstanceStore: ContractInstanceStore;
@@ -47,13 +49,14 @@ export class KVArchiverDataStore implements ArchiverDataStore {
47
49
 
48
50
  #log = createDebugLogger('aztec:archiver:data-store');
49
51
 
50
- constructor(db: AztecKVStore, logsMaxPageSize: number = 1000) {
52
+ constructor(private db: AztecKVStore, logsMaxPageSize: number = 1000) {
51
53
  this.#blockStore = new BlockStore(db);
52
54
  this.#logStore = new LogStore(db, this.#blockStore, logsMaxPageSize);
53
55
  this.#messageStore = new MessageStore(db);
54
56
  this.#contractClassStore = new ContractClassStore(db);
55
57
  this.#contractInstanceStore = new ContractInstanceStore(db);
56
58
  this.#contractArtifactStore = new ContractArtifactsStore(db);
59
+ this.#nullifierStore = new NullifierStore(db);
57
60
  }
58
61
 
59
62
  getContractArtifact(address: AztecAddress): Promise<ContractArtifact | undefined> {
@@ -159,7 +162,7 @@ export class KVArchiverDataStore implements ArchiverDataStore {
159
162
  * @param txHash - The txHash of the tx corresponding to the tx effect.
160
163
  * @returns The requested tx effect (or undefined if not found).
161
164
  */
162
- getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
165
+ getTxEffect(txHash: TxHash) {
163
166
  return Promise.resolve(this.#blockStore.getTxEffect(txHash));
164
167
  }
165
168
 
@@ -185,6 +188,23 @@ export class KVArchiverDataStore implements ArchiverDataStore {
185
188
  return this.#logStore.deleteLogs(blocks);
186
189
  }
187
190
 
191
+ /**
192
+ * Append new nullifiers to the store's list.
193
+ * @param blocks - The blocks for which to add the nullifiers.
194
+ * @returns True if the operation is successful.
195
+ */
196
+ addNullifiers(blocks: L2Block[]): Promise<boolean> {
197
+ return this.#nullifierStore.addNullifiers(blocks);
198
+ }
199
+
200
+ deleteNullifiers(blocks: L2Block[]): Promise<boolean> {
201
+ return this.#nullifierStore.deleteNullifiers(blocks);
202
+ }
203
+
204
+ findNullifiersIndexesWithBlock(blockNumber: number, nullifiers: Fr[]): Promise<(InBlock<bigint> | undefined)[]> {
205
+ return this.#nullifierStore.findNullifiersIndexesWithBlock(blockNumber, nullifiers);
206
+ }
207
+
188
208
  getTotalL1ToL2MessageCount(): Promise<bigint> {
189
209
  return Promise.resolve(this.#messageStore.getTotalL1ToL2MessageCount());
190
210
  }
@@ -324,4 +344,8 @@ export class KVArchiverDataStore implements ArchiverDataStore {
324
344
  messagesSynchedTo: this.#messageStore.getSynchedL1BlockNumber(),
325
345
  });
326
346
  }
347
+
348
+ public estimateSize(): { mappingSize: number; actualSize: number; numItems: number } {
349
+ return this.db.estimateSize();
350
+ }
327
351
  }
@@ -0,0 +1,78 @@
1
+ import { type InBlock, type L2Block } from '@aztec/circuit-types';
2
+ import { type Fr, MAX_NULLIFIERS_PER_TX } from '@aztec/circuits.js';
3
+ import { createDebugLogger } from '@aztec/foundation/log';
4
+ import { type AztecKVStore, type AztecMap } from '@aztec/kv-store';
5
+
6
+ export class NullifierStore {
7
+ #nullifiersToBlockNumber: AztecMap<string, number>;
8
+ #nullifiersToBlockHash: AztecMap<string, string>;
9
+ #nullifiersToIndex: AztecMap<string, number>;
10
+ #log = createDebugLogger('aztec:archiver:log_store');
11
+
12
+ constructor(private db: AztecKVStore) {
13
+ this.#nullifiersToBlockNumber = db.openMap('archiver_nullifiers_to_block_number');
14
+ this.#nullifiersToBlockHash = db.openMap('archiver_nullifiers_to_block_hash');
15
+ this.#nullifiersToIndex = db.openMap('archiver_nullifiers_to_index');
16
+ }
17
+
18
+ async addNullifiers(blocks: L2Block[]): Promise<boolean> {
19
+ await this.db.transaction(() => {
20
+ blocks.forEach(block => {
21
+ const dataStartIndexForBlock =
22
+ block.header.state.partial.nullifierTree.nextAvailableLeafIndex -
23
+ block.body.numberOfTxsIncludingPadded * MAX_NULLIFIERS_PER_TX;
24
+ block.body.txEffects.forEach((txEffects, txIndex) => {
25
+ const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NULLIFIERS_PER_TX;
26
+ txEffects.nullifiers.forEach((nullifier, nullifierIndex) => {
27
+ void this.#nullifiersToBlockNumber.set(nullifier.toString(), block.number);
28
+ void this.#nullifiersToBlockHash.set(nullifier.toString(), block.hash().toString());
29
+ void this.#nullifiersToIndex.set(nullifier.toString(), dataStartIndexForTx + nullifierIndex);
30
+ });
31
+ });
32
+ });
33
+ });
34
+ return true;
35
+ }
36
+
37
+ async deleteNullifiers(blocks: L2Block[]): Promise<boolean> {
38
+ await this.db.transaction(() => {
39
+ for (const block of blocks) {
40
+ for (const nullifier of block.body.txEffects.flatMap(tx => tx.nullifiers)) {
41
+ void this.#nullifiersToBlockNumber.delete(nullifier.toString());
42
+ void this.#nullifiersToBlockHash.delete(nullifier.toString());
43
+ void this.#nullifiersToIndex.delete(nullifier.toString());
44
+ }
45
+ }
46
+ });
47
+ return true;
48
+ }
49
+
50
+ async findNullifiersIndexesWithBlock(
51
+ blockNumber: number,
52
+ nullifiers: Fr[],
53
+ ): Promise<(InBlock<bigint> | undefined)[]> {
54
+ const maybeNullifiers = await this.db.transaction(() => {
55
+ return nullifiers.map(nullifier => ({
56
+ data: this.#nullifiersToIndex.get(nullifier.toString()),
57
+ l2BlockNumber: this.#nullifiersToBlockNumber.get(nullifier.toString()),
58
+ l2BlockHash: this.#nullifiersToBlockHash.get(nullifier.toString()),
59
+ }));
60
+ });
61
+ return maybeNullifiers.map(({ data, l2BlockNumber, l2BlockHash }) => {
62
+ if (
63
+ data === undefined ||
64
+ l2BlockNumber === undefined ||
65
+ l2BlockHash === undefined ||
66
+ l2BlockNumber > blockNumber
67
+ ) {
68
+ return undefined;
69
+ } else {
70
+ return {
71
+ data: BigInt(data),
72
+ l2BlockNumber,
73
+ l2BlockHash,
74
+ } as InBlock<bigint>;
75
+ }
76
+ });
77
+ }
78
+ }
@@ -6,6 +6,7 @@ import {
6
6
  ExtendedUnencryptedL2Log,
7
7
  type FromLogType,
8
8
  type GetUnencryptedLogsResponse,
9
+ type InBlock,
9
10
  type InboxLeaf,
10
11
  type L2Block,
11
12
  type L2BlockL2Logs,
@@ -17,6 +18,7 @@ import {
17
18
  TxReceipt,
18
19
  TxScopedL2Log,
19
20
  type UnencryptedL2BlockL2Logs,
21
+ wrapInBlock,
20
22
  } from '@aztec/circuit-types';
21
23
  import {
22
24
  type ContractClassPublic,
@@ -27,6 +29,7 @@ import {
27
29
  type Header,
28
30
  INITIAL_L2_BLOCK_NUM,
29
31
  MAX_NOTE_HASHES_PER_TX,
32
+ MAX_NULLIFIERS_PER_TX,
30
33
  type UnconstrainedFunctionWithMembershipProof,
31
34
  } from '@aztec/circuits.js';
32
35
  import { type ContractArtifact } from '@aztec/foundation/abi';
@@ -50,7 +53,7 @@ export class MemoryArchiverStore implements ArchiverDataStore {
50
53
  /**
51
54
  * An array containing all the tx effects in the L2 blocks that have been fetched so far.
52
55
  */
53
- private txEffects: TxEffect[] = [];
56
+ private txEffects: InBlock<TxEffect>[] = [];
54
57
 
55
58
  private noteEncryptedLogsPerBlock: Map<number, EncryptedNoteL2BlockL2Logs> = new Map();
56
59
 
@@ -64,6 +67,8 @@ export class MemoryArchiverStore implements ArchiverDataStore {
64
67
 
65
68
  private contractClassLogsPerBlock: Map<number, ContractClass2BlockL2Logs> = new Map();
66
69
 
70
+ private blockScopedNullifiers: Map<string, { blockNumber: number; blockHash: string; index: bigint }> = new Map();
71
+
67
72
  /**
68
73
  * Contains all L1 to L2 messages.
69
74
  */
@@ -181,7 +186,7 @@ export class MemoryArchiverStore implements ArchiverDataStore {
181
186
 
182
187
  this.lastL1BlockNewBlocks = blocks[blocks.length - 1].l1.blockNumber;
183
188
  this.l2Blocks.push(...blocks);
184
- this.txEffects.push(...blocks.flatMap(b => b.data.body.txEffects));
189
+ this.txEffects.push(...blocks.flatMap(b => b.data.body.txEffects.map(txEffect => wrapInBlock(txEffect, b.data))));
185
190
 
186
191
  return Promise.resolve(true);
187
192
  }
@@ -196,7 +201,7 @@ export class MemoryArchiverStore implements ArchiverDataStore {
196
201
  public async unwindBlocks(from: number, blocksToUnwind: number): Promise<boolean> {
197
202
  const last = await this.getSynchedL2BlockNumber();
198
203
  if (from != last) {
199
- throw new Error(`Can only remove the tip`);
204
+ throw new Error(`Can only unwind blocks from the tip (requested ${from} but current tip is ${last})`);
200
205
  }
201
206
 
202
207
  const stopAt = from - blocksToUnwind;
@@ -297,6 +302,51 @@ export class MemoryArchiverStore implements ArchiverDataStore {
297
302
  return Promise.resolve(true);
298
303
  }
299
304
 
305
+ addNullifiers(blocks: L2Block[]): Promise<boolean> {
306
+ blocks.forEach(block => {
307
+ const dataStartIndexForBlock =
308
+ block.header.state.partial.nullifierTree.nextAvailableLeafIndex -
309
+ block.body.numberOfTxsIncludingPadded * MAX_NULLIFIERS_PER_TX;
310
+ block.body.txEffects.forEach((txEffects, txIndex) => {
311
+ const dataStartIndexForTx = dataStartIndexForBlock + txIndex * MAX_NULLIFIERS_PER_TX;
312
+ txEffects.nullifiers.forEach((nullifier, nullifierIndex) => {
313
+ this.blockScopedNullifiers.set(nullifier.toString(), {
314
+ index: BigInt(dataStartIndexForTx + nullifierIndex),
315
+ blockNumber: block.number,
316
+ blockHash: block.hash().toString(),
317
+ });
318
+ });
319
+ });
320
+ });
321
+ return Promise.resolve(true);
322
+ }
323
+
324
+ deleteNullifiers(blocks: L2Block[]): Promise<boolean> {
325
+ blocks.forEach(block => {
326
+ block.body.txEffects.forEach(txEffect => {
327
+ txEffect.nullifiers.forEach(nullifier => {
328
+ this.blockScopedNullifiers.delete(nullifier.toString());
329
+ });
330
+ });
331
+ });
332
+ return Promise.resolve(true);
333
+ }
334
+
335
+ findNullifiersIndexesWithBlock(blockNumber: number, nullifiers: Fr[]): Promise<(InBlock<bigint> | undefined)[]> {
336
+ const blockScopedNullifiers = nullifiers.map(nullifier => {
337
+ const nullifierData = this.blockScopedNullifiers.get(nullifier.toString());
338
+ if (nullifierData !== undefined && nullifierData.blockNumber <= blockNumber) {
339
+ return {
340
+ data: nullifierData.index,
341
+ l2BlockHash: nullifierData.blockHash,
342
+ l2BlockNumber: nullifierData.blockNumber,
343
+ } as InBlock<bigint>;
344
+ }
345
+ return undefined;
346
+ });
347
+ return Promise.resolve(blockScopedNullifiers);
348
+ }
349
+
300
350
  getTotalL1ToL2MessageCount(): Promise<bigint> {
301
351
  return Promise.resolve(this.l1ToL2Messages.getTotalL1ToL2MessageCount());
302
352
  }
@@ -365,8 +415,8 @@ export class MemoryArchiverStore implements ArchiverDataStore {
365
415
  * @param txHash - The txHash of the tx effect.
366
416
  * @returns The requested tx effect.
367
417
  */
368
- public getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
369
- const txEffect = this.txEffects.find(tx => tx.txHash.equals(txHash));
418
+ public getTxEffect(txHash: TxHash): Promise<InBlock<TxEffect> | undefined> {
419
+ const txEffect = this.txEffects.find(tx => tx.data.txHash.equals(txHash));
370
420
  return Promise.resolve(txEffect);
371
421
  }
372
422
 
@@ -686,4 +736,8 @@ export class MemoryArchiverStore implements ArchiverDataStore {
686
736
  public getContractArtifact(address: AztecAddress): Promise<ContractArtifact | undefined> {
687
737
  return Promise.resolve(this.contractArtifacts.get(address.toString()));
688
738
  }
739
+
740
+ public estimateSize(): { mappingSize: number; actualSize: number; numItems: number } {
741
+ return { mappingSize: 0, actualSize: 0, numItems: 0 };
742
+ }
689
743
  }