@aztec/stdlib 0.84.0-nightly.20250409 → 0.84.0-nightly.20250412

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 (61) hide show
  1. package/dest/avm/avm.d.ts +627 -5
  2. package/dest/avm/avm.d.ts.map +1 -1
  3. package/dest/avm/avm.js +13 -7
  4. package/dest/avm/avm_proving_request.d.ts +331 -0
  5. package/dest/avm/avm_proving_request.d.ts.map +1 -1
  6. package/dest/block/index.d.ts +1 -1
  7. package/dest/block/index.d.ts.map +1 -1
  8. package/dest/block/index.js +1 -1
  9. package/dest/block/l2_block_source.d.ts +2 -0
  10. package/dest/block/l2_block_source.d.ts.map +1 -1
  11. package/dest/block/l2_block_source.js +9 -0
  12. package/dest/block/l2_block_stream/index.d.ts +4 -0
  13. package/dest/block/l2_block_stream/index.d.ts.map +1 -0
  14. package/dest/block/l2_block_stream/index.js +3 -0
  15. package/dest/block/l2_block_stream/interfaces.d.ts +26 -0
  16. package/dest/block/l2_block_stream/interfaces.d.ts.map +1 -0
  17. package/dest/block/l2_block_stream/interfaces.js +1 -0
  18. package/dest/block/{l2_block_downloader → l2_block_stream}/l2_block_stream.d.ts +4 -24
  19. package/dest/block/l2_block_stream/l2_block_stream.d.ts.map +1 -0
  20. package/dest/block/{l2_block_downloader → l2_block_stream}/l2_block_stream.js +29 -10
  21. package/dest/block/l2_block_stream/l2_tips_memory_store.d.ts +18 -0
  22. package/dest/block/l2_block_stream/l2_tips_memory_store.d.ts.map +1 -0
  23. package/dest/block/l2_block_stream/l2_tips_memory_store.js +70 -0
  24. package/dest/block/test/index.d.ts +2 -0
  25. package/dest/block/test/index.d.ts.map +1 -0
  26. package/dest/block/test/index.js +1 -0
  27. package/dest/block/test/l2_tips_store_test_suite.d.ts +3 -0
  28. package/dest/block/test/l2_tips_store_test_suite.d.ts.map +1 -0
  29. package/dest/block/test/l2_tips_store_test_suite.js +107 -0
  30. package/dest/database-version/version_manager.d.ts +21 -5
  31. package/dest/database-version/version_manager.d.ts.map +1 -1
  32. package/dest/database-version/version_manager.js +25 -15
  33. package/dest/interfaces/proving-job.d.ts +331 -0
  34. package/dest/interfaces/proving-job.d.ts.map +1 -1
  35. package/dest/proofs/proof.d.ts +4 -1
  36. package/dest/proofs/proof.d.ts.map +1 -1
  37. package/dest/proofs/proof.js +9 -17
  38. package/dest/tests/factories.d.ts.map +1 -1
  39. package/dest/tests/factories.js +3 -2
  40. package/package.json +8 -6
  41. package/src/avm/avm.ts +19 -2
  42. package/src/block/index.ts +1 -1
  43. package/src/block/l2_block_source.ts +8 -0
  44. package/src/block/l2_block_stream/index.ts +3 -0
  45. package/src/block/l2_block_stream/interfaces.ts +33 -0
  46. package/src/block/{l2_block_downloader → l2_block_stream}/l2_block_stream.ts +34 -44
  47. package/src/block/l2_block_stream/l2_tips_memory_store.ts +75 -0
  48. package/src/block/test/index.ts +1 -0
  49. package/src/block/test/l2_tips_store_test_suite.ts +87 -0
  50. package/src/database-version/version_manager.ts +56 -17
  51. package/src/proofs/proof.ts +9 -19
  52. package/src/tests/factories.ts +3 -0
  53. package/dest/block/l2_block_downloader/index.d.ts +0 -3
  54. package/dest/block/l2_block_downloader/index.d.ts.map +0 -1
  55. package/dest/block/l2_block_downloader/index.js +0 -2
  56. package/dest/block/l2_block_downloader/l2_block_downloader.d.ts +0 -58
  57. package/dest/block/l2_block_downloader/l2_block_downloader.d.ts.map +0 -1
  58. package/dest/block/l2_block_downloader/l2_block_downloader.js +0 -124
  59. package/dest/block/l2_block_downloader/l2_block_stream.d.ts.map +0 -1
  60. package/src/block/l2_block_downloader/index.ts +0 -2
  61. package/src/block/l2_block_downloader/l2_block_downloader.ts +0 -149
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/stdlib",
3
- "version": "0.84.0-nightly.20250409",
3
+ "version": "0.84.0-nightly.20250412",
4
4
  "type": "module",
5
5
  "inherits": [
6
6
  "../package.common.json",
@@ -39,6 +39,7 @@
39
39
  "./stats": "./dest/stats/index.js",
40
40
  "./auth-witness": "./dest/auth_witness/index.js",
41
41
  "./block": "./dest/block/index.js",
42
+ "./block/test": "./dest/block/test/index.js",
42
43
  "./versioning": "./dest/versioning/index.js",
43
44
  "./interfaces/client": "./dest/interfaces/client.js",
44
45
  "./interfaces/server": "./dest/interfaces/server.js",
@@ -67,11 +68,11 @@
67
68
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
68
69
  },
69
70
  "dependencies": {
70
- "@aztec/bb.js": "0.84.0-nightly.20250409",
71
- "@aztec/blob-lib": "0.84.0-nightly.20250409",
72
- "@aztec/constants": "0.84.0-nightly.20250409",
73
- "@aztec/ethereum": "0.84.0-nightly.20250409",
74
- "@aztec/foundation": "0.84.0-nightly.20250409",
71
+ "@aztec/bb.js": "0.84.0-nightly.20250412",
72
+ "@aztec/blob-lib": "0.84.0-nightly.20250412",
73
+ "@aztec/constants": "0.84.0-nightly.20250412",
74
+ "@aztec/ethereum": "0.84.0-nightly.20250412",
75
+ "@aztec/foundation": "0.84.0-nightly.20250412",
75
76
  "@google-cloud/storage": "^7.15.0",
76
77
  "lodash.chunk": "^4.2.0",
77
78
  "lodash.isequal": "^4.5.0",
@@ -84,6 +85,7 @@
84
85
  "zod": "^3.23.8"
85
86
  },
86
87
  "devDependencies": {
88
+ "@jest/expect": "^29.5.0",
87
89
  "@jest/globals": "^29.5.0",
88
90
  "@types/jest": "^29.5.0",
89
91
  "@types/lodash.chunk": "^4.2.9",
package/src/avm/avm.ts CHANGED
@@ -10,7 +10,7 @@ import { AppendOnlyTreeSnapshot } from '../trees/append_only_tree_snapshot.js';
10
10
  import { MerkleTreeId } from '../trees/merkle_tree_id.js';
11
11
  import { NullifierLeafPreimage } from '../trees/nullifier_leaf.js';
12
12
  import { PublicDataTreeLeafPreimage } from '../trees/public_data_leaf.js';
13
- import { TreeSnapshots, type Tx } from '../tx/index.js';
13
+ import { GlobalVariables, TreeSnapshots, type Tx } from '../tx/index.js';
14
14
  import { AvmCircuitPublicInputs } from './avm_circuit_public_inputs.js';
15
15
  import { serializeWithMessagePack } from './message_pack.js';
16
16
 
@@ -402,6 +402,7 @@ export class AvmEnqueuedCallHint {
402
402
  export class AvmTxHint {
403
403
  constructor(
404
404
  public readonly hash: string,
405
+ public readonly globalVariables: GlobalVariables,
405
406
  public readonly nonRevertibleAccumulatedData: {
406
407
  noteHashes: Fr[];
407
408
  nullifiers: Fr[];
@@ -429,6 +430,7 @@ export class AvmTxHint {
429
430
 
430
431
  return new AvmTxHint(
431
432
  txHash.hash.toString(),
433
+ tx.data.constants.historicalHeader.globalVariables,
432
434
  {
433
435
  noteHashes: tx.data.forPublic!.nonRevertibleAccumulatedData.noteHashes.filter(x => !x.isZero()),
434
436
  nullifiers: tx.data.forPublic!.nonRevertibleAccumulatedData.nullifiers.filter(x => !x.isZero()),
@@ -467,13 +469,22 @@ export class AvmTxHint {
467
469
  }
468
470
 
469
471
  static empty() {
470
- return new AvmTxHint('', { noteHashes: [], nullifiers: [] }, { noteHashes: [], nullifiers: [] }, [], [], null);
472
+ return new AvmTxHint(
473
+ '',
474
+ GlobalVariables.empty(),
475
+ { noteHashes: [], nullifiers: [] },
476
+ { noteHashes: [], nullifiers: [] },
477
+ [],
478
+ [],
479
+ null,
480
+ );
471
481
  }
472
482
 
473
483
  static get schema() {
474
484
  return z
475
485
  .object({
476
486
  hash: z.string(),
487
+ globalVariables: GlobalVariables.schema,
477
488
  nonRevertibleAccumulatedData: z.object({
478
489
  noteHashes: schemas.Fr.array(),
479
490
  nullifiers: schemas.Fr.array(),
@@ -489,6 +500,7 @@ export class AvmTxHint {
489
500
  .transform(
490
501
  ({
491
502
  hash,
503
+ globalVariables,
492
504
  nonRevertibleAccumulatedData,
493
505
  revertibleAccumulatedData,
494
506
  setupEnqueuedCalls,
@@ -497,6 +509,7 @@ export class AvmTxHint {
497
509
  }) =>
498
510
  new AvmTxHint(
499
511
  hash,
512
+ globalVariables,
500
513
  nonRevertibleAccumulatedData,
501
514
  revertibleAccumulatedData,
502
515
  setupEnqueuedCalls,
@@ -515,6 +528,7 @@ export class AvmExecutionHints {
515
528
  public readonly contractClasses: AvmContractClassHint[] = [],
516
529
  public readonly bytecodeCommitments: AvmBytecodeCommitmentHint[] = [],
517
530
  // Merkle DB hints.
531
+ public startingTreeRoots: TreeSnapshots = TreeSnapshots.empty(),
518
532
  public readonly getSiblingPathHints: AvmGetSiblingPathHint[] = [],
519
533
  public readonly getPreviousValueIndexHints: AvmGetPreviousValueIndexHint[] = [],
520
534
  public readonly getLeafPreimageHintsPublicDataTree: AvmGetLeafPreimageHintPublicDataTree[] = [],
@@ -539,6 +553,7 @@ export class AvmExecutionHints {
539
553
  contractInstances: AvmContractInstanceHint.schema.array(),
540
554
  contractClasses: AvmContractClassHint.schema.array(),
541
555
  bytecodeCommitments: AvmBytecodeCommitmentHint.schema.array(),
556
+ startingTreeRoots: TreeSnapshots.schema,
542
557
  getSiblingPathHints: AvmGetSiblingPathHint.schema.array(),
543
558
  getPreviousValueIndexHints: AvmGetPreviousValueIndexHint.schema.array(),
544
559
  getLeafPreimageHintsPublicDataTree: AvmGetLeafPreimageHintPublicDataTree.schema.array(),
@@ -557,6 +572,7 @@ export class AvmExecutionHints {
557
572
  contractInstances,
558
573
  contractClasses,
559
574
  bytecodeCommitments,
575
+ startingTreeRoots,
560
576
  getSiblingPathHints,
561
577
  getPreviousValueIndexHints,
562
578
  getLeafPreimageHintsPublicDataTree,
@@ -574,6 +590,7 @@ export class AvmExecutionHints {
574
590
  contractInstances,
575
591
  contractClasses,
576
592
  bytecodeCommitments,
593
+ startingTreeRoots,
577
594
  getSiblingPathHints,
578
595
  getPreviousValueIndexHints,
579
596
  getLeafPreimageHintsPublicDataTree,
@@ -1,5 +1,5 @@
1
1
  export * from './l2_block.js';
2
- export * from './l2_block_downloader/index.js';
2
+ export * from './l2_block_stream/index.js';
3
3
  export * from './in_block.js';
4
4
  export * from './body.js';
5
5
  export * from './l2_block_number.js';
@@ -140,6 +140,14 @@ export type L2Tips = Record<L2BlockTag, L2BlockId>;
140
140
  /** Identifies a block by number and hash. */
141
141
  export type L2BlockId = z.infer<typeof L2BlockIdSchema>;
142
142
 
143
+ /** Creates an L2 block id */
144
+ export function makeL2BlockId(number: number, hash?: string): L2BlockId {
145
+ if (number !== 0 && !hash) {
146
+ throw new Error(`Hash is required for non-genesis blocks (got block number ${number})`);
147
+ }
148
+ return { number, hash: hash! };
149
+ }
150
+
143
151
  // TODO(palla/schemas): This package should know what is the block hash of the genesis block 0.
144
152
  const L2BlockIdSchema = z.union([
145
153
  z.object({
@@ -0,0 +1,3 @@
1
+ export * from './interfaces.js';
2
+ export * from './l2_block_stream.js';
3
+ export * from './l2_tips_memory_store.js';
@@ -0,0 +1,33 @@
1
+ import type { L2BlockId, L2Tips } from '../l2_block_source.js';
2
+ import type { PublishedL2Block } from '../published_l2_block.js';
3
+
4
+ /** Interface to the local view of the chain. Implemented by world-state and l2-tips-store. */
5
+ export interface L2BlockStreamLocalDataProvider {
6
+ getL2BlockHash(number: number): Promise<string | undefined>;
7
+ getL2Tips(): Promise<L2Tips>;
8
+ }
9
+
10
+ /** Interface to a handler of events emitted. */
11
+ export interface L2BlockStreamEventHandler {
12
+ handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void>;
13
+ }
14
+
15
+ export type L2BlockStreamEvent =
16
+ | /** Emits blocks added to the chain. */ {
17
+ type: 'blocks-added';
18
+ blocks: PublishedL2Block[];
19
+ }
20
+ | /** Reports last correct block (new tip of the unproven chain). */ {
21
+ type: 'chain-pruned';
22
+ block: L2BlockId;
23
+ }
24
+ | /** Reports new proven block. */ {
25
+ type: 'chain-proven';
26
+ block: L2BlockId;
27
+ }
28
+ | /** Reports new finalized block (proven and finalized on L1). */ {
29
+ type: 'chain-finalized';
30
+ block: L2BlockId;
31
+ };
32
+
33
+ export type L2TipsStore = L2BlockStreamEventHandler & L2BlockStreamLocalDataProvider;
@@ -2,8 +2,8 @@ import { AbortError } from '@aztec/foundation/error';
2
2
  import { createLogger } from '@aztec/foundation/log';
3
3
  import { RunningPromise } from '@aztec/foundation/running-promise';
4
4
 
5
- import type { L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js';
6
- import type { PublishedL2Block } from '../published_l2_block.js';
5
+ import { type L2BlockId, type L2BlockSource, makeL2BlockId } from '../l2_block_source.js';
6
+ import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js';
7
7
 
8
8
  /** Creates a stream of events for new blocks, chain tips updates, and reorgs, out of polling an archiver or a node. */
9
9
  export class L2BlockStream {
@@ -21,6 +21,8 @@ export class L2BlockStream {
21
21
  pollIntervalMS?: number;
22
22
  batchSize?: number;
23
23
  startingBlock?: number;
24
+ /** Instead of downloading all blocks, only fetch the smallest subset that results in reliable reorg detection. */
25
+ skipFinalized?: boolean;
24
26
  } = {},
25
27
  ) {
26
28
  this.runningPromise = new RunningPromise(() => this.work(), log, this.opts.pollIntervalMS ?? 1000);
@@ -72,14 +74,13 @@ export class L2BlockStream {
72
74
  }
73
75
 
74
76
  if (latestBlockNumber < localTips.latest.number) {
77
+ latestBlockNumber = Math.min(latestBlockNumber, sourceTips.latest.number); // see #13471
78
+ const hash = sourceCache.get(latestBlockNumber) ?? (await this.getBlockHashFromSource(latestBlockNumber));
79
+ if (latestBlockNumber !== 0 && !hash) {
80
+ throw new Error(`Block hash not found in block source for block number ${latestBlockNumber}`);
81
+ }
75
82
  this.log.verbose(`Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.latest.number}.`);
76
- await this.emitEvent({
77
- type: 'chain-pruned',
78
- block: {
79
- number: latestBlockNumber,
80
- hash: sourceCache.get(latestBlockNumber) ?? (await this.getBlockHashFromSource(latestBlockNumber))!,
81
- },
82
- });
83
+ await this.emitEvent({ type: 'chain-pruned', block: makeL2BlockId(latestBlockNumber, hash) });
83
84
  }
84
85
 
85
86
  // If we are just starting, use the starting block number from the options.
@@ -93,17 +94,26 @@ export class L2BlockStream {
93
94
  this.hasStarted = true;
94
95
  }
95
96
 
97
+ let nextBlockNumber = latestBlockNumber + 1;
98
+ if (this.opts.skipFinalized) {
99
+ // When skipping finalized blocks we need to provide reliable reorg detection while fetching as few blocks as
100
+ // possible. Finalized blocks cannot be reorged by definition, so we can skip most of them. We do need the very
101
+ // last finalized block however in order to guarantee that we will eventually find a block in which our local
102
+ // store matches the source.
103
+ // If the last finalized block is behind our local tip, there is nothing to skip.
104
+ nextBlockNumber = Math.max(sourceTips.finalized.number, nextBlockNumber);
105
+ }
106
+
96
107
  // Request new blocks from the source.
97
- while (latestBlockNumber < sourceTips.latest.number) {
98
- const from = latestBlockNumber + 1;
99
- const limit = Math.min(this.opts.batchSize ?? 20, sourceTips.latest.number - from + 1);
100
- this.log.trace(`Requesting blocks from ${from} limit ${limit} proven=${this.opts.proven}`);
101
- const blocks = await this.l2BlockSource.getPublishedBlocks(from, limit, this.opts.proven);
108
+ while (nextBlockNumber <= sourceTips.latest.number) {
109
+ const limit = Math.min(this.opts.batchSize ?? 20, sourceTips.latest.number - nextBlockNumber + 1);
110
+ this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit} proven=${this.opts.proven}`);
111
+ const blocks = await this.l2BlockSource.getPublishedBlocks(nextBlockNumber, limit, this.opts.proven);
102
112
  if (blocks.length === 0) {
103
113
  break;
104
114
  }
105
115
  await this.emitEvent({ type: 'blocks-added', blocks });
106
- latestBlockNumber = blocks.at(-1)!.block.number;
116
+ nextBlockNumber = blocks.at(-1)!.block.number + 1;
107
117
  }
108
118
 
109
119
  // Update the proven and finalized tips.
@@ -134,6 +144,15 @@ export class L2BlockStream {
134
144
  return true;
135
145
  }
136
146
  const localBlockHash = await this.localData.getL2BlockHash(blockNumber);
147
+ if (!localBlockHash && this.opts.skipFinalized) {
148
+ // Failing to find a block hash when skipping finalized blocks can be highly problematic as we'd potentially need
149
+ // to go all the way back to the genesis block to find a block in which we agree with the source (since we've
150
+ // potentially skipped all history). This means that stores that prune old blocks must be careful to leave no gaps
151
+ // when going back from latest block to the last finalized one.
152
+ this.log.error(`No local block hash for block number ${blockNumber}`);
153
+ throw new AbortError();
154
+ }
155
+
137
156
  const sourceBlockHashFromCache = args.sourceCache.get(blockNumber);
138
157
  const sourceBlockHash = args.sourceCache.get(blockNumber) ?? (await this.getBlockHashFromSource(blockNumber));
139
158
  if (!sourceBlockHashFromCache && sourceBlockHash) {
@@ -181,32 +200,3 @@ class BlockHashCache {
181
200
  return this.cache.get(blockNumber);
182
201
  }
183
202
  }
184
-
185
- /** Interface to the local view of the chain. Implemented by world-state and l2-tips-store. */
186
- export interface L2BlockStreamLocalDataProvider {
187
- getL2BlockHash(number: number): Promise<string | undefined>;
188
- getL2Tips(): Promise<L2Tips>;
189
- }
190
-
191
- /** Interface to a handler of events emitted. */
192
- export interface L2BlockStreamEventHandler {
193
- handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void>;
194
- }
195
-
196
- export type L2BlockStreamEvent =
197
- | /** Emits blocks added to the chain. */ {
198
- type: 'blocks-added';
199
- blocks: PublishedL2Block[];
200
- }
201
- | /** Reports last correct block (new tip of the unproven chain). */ {
202
- type: 'chain-pruned';
203
- block: L2BlockId;
204
- }
205
- | /** Reports new proven block. */ {
206
- type: 'chain-proven';
207
- block: L2BlockId;
208
- }
209
- | /** Reports new finalized block (proven and finalized on L1). */ {
210
- type: 'chain-finalized';
211
- block: L2BlockId;
212
- };
@@ -0,0 +1,75 @@
1
+ import type { L2Block } from '../l2_block.js';
2
+ import type { L2BlockId, L2BlockTag, L2Tips } from '../l2_block_source.js';
3
+ import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js';
4
+
5
+ /**
6
+ * Stores currently synced L2 tips and unfinalized block hashes.
7
+ * @dev tests in kv-store/src/stores/l2_tips_memory_store.test.ts
8
+ */
9
+ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider {
10
+ protected readonly l2TipsStore: Map<L2BlockTag, number> = new Map();
11
+ protected readonly l2BlockHashesStore: Map<number, string> = new Map();
12
+
13
+ public getL2BlockHash(number: number): Promise<string | undefined> {
14
+ return Promise.resolve(this.l2BlockHashesStore.get(number));
15
+ }
16
+
17
+ public getL2Tips(): Promise<L2Tips> {
18
+ return Promise.resolve({
19
+ latest: this.getL2Tip('latest'),
20
+ finalized: this.getL2Tip('finalized'),
21
+ proven: this.getL2Tip('proven'),
22
+ });
23
+ }
24
+
25
+ private getL2Tip(tag: L2BlockTag): L2BlockId {
26
+ const blockNumber = this.l2TipsStore.get(tag);
27
+ if (blockNumber === undefined || blockNumber === 0) {
28
+ return { number: 0, hash: undefined };
29
+ }
30
+ const blockHash = this.l2BlockHashesStore.get(blockNumber);
31
+ if (!blockHash) {
32
+ throw new Error(`Block hash not found for block number ${blockNumber}`);
33
+ }
34
+
35
+ return { number: blockNumber, hash: blockHash };
36
+ }
37
+
38
+ public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
39
+ switch (event.type) {
40
+ case 'blocks-added': {
41
+ const blocks = event.blocks.map(b => b.block);
42
+ for (const block of blocks) {
43
+ this.l2BlockHashesStore.set(block.number, await this.computeBlockHash(block));
44
+ }
45
+ this.l2TipsStore.set('latest', blocks.at(-1)!.number);
46
+ break;
47
+ }
48
+ case 'chain-pruned':
49
+ this.saveTag('latest', event.block);
50
+ break;
51
+ case 'chain-proven':
52
+ this.saveTag('proven', event.block);
53
+ break;
54
+ case 'chain-finalized':
55
+ this.saveTag('finalized', event.block);
56
+ for (const key of this.l2BlockHashesStore.keys()) {
57
+ if (key < event.block.number) {
58
+ this.l2BlockHashesStore.delete(key);
59
+ }
60
+ }
61
+ break;
62
+ }
63
+ }
64
+
65
+ protected saveTag(name: L2BlockTag, block: L2BlockId) {
66
+ this.l2TipsStore.set(name, block.number);
67
+ if (block.hash) {
68
+ this.l2BlockHashesStore.set(block.number, block.hash);
69
+ }
70
+ }
71
+
72
+ protected computeBlockHash(block: L2Block) {
73
+ return block.header.hash().then(hash => hash.toString());
74
+ }
75
+ }
@@ -0,0 +1 @@
1
+ export * from './l2_tips_store_test_suite.js';
@@ -0,0 +1,87 @@
1
+ import { times } from '@aztec/foundation/collection';
2
+ import { Fr } from '@aztec/foundation/fields';
3
+ import type { L2Block, L2BlockId, PublishedL2Block } from '@aztec/stdlib/block';
4
+ import type { BlockHeader } from '@aztec/stdlib/tx';
5
+
6
+ import { jestExpect as expect } from '@jest/expect';
7
+
8
+ import type { L2TipsStore } from '../l2_block_stream/index.js';
9
+
10
+ export function testL2TipsStore(makeTipsStore: () => Promise<L2TipsStore>) {
11
+ let tipsStore: L2TipsStore;
12
+
13
+ beforeEach(async () => {
14
+ tipsStore = await makeTipsStore();
15
+ });
16
+
17
+ const makeBlock = (number: number): PublishedL2Block => ({
18
+ block: { number, header: { hash: () => Promise.resolve(new Fr(number)) } as BlockHeader } as L2Block,
19
+ l1: { blockNumber: BigInt(number), blockHash: `0x${number}`, timestamp: BigInt(number) },
20
+ signatures: [],
21
+ });
22
+
23
+ const makeBlockId = (number: number): L2BlockId => ({
24
+ number,
25
+ hash: new Fr(number).toString(),
26
+ });
27
+
28
+ const makeTip = (number: number) => ({ number, hash: number === 0 ? undefined : new Fr(number).toString() });
29
+
30
+ const makeTips = (latest: number, proven: number, finalized: number) => ({
31
+ latest: makeTip(latest),
32
+ proven: makeTip(proven),
33
+ finalized: makeTip(finalized),
34
+ });
35
+
36
+ it('returns zero if no tips are stored', async () => {
37
+ const tips = await tipsStore.getL2Tips();
38
+ expect(tips).toEqual(makeTips(0, 0, 0));
39
+ });
40
+
41
+ it('stores chain tips', async () => {
42
+ await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(20, i => makeBlock(i + 1)) });
43
+
44
+ await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(5) });
45
+ await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(8) });
46
+ await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(10) });
47
+
48
+ const tips = await tipsStore.getL2Tips();
49
+ expect(tips).toEqual(makeTips(10, 8, 5));
50
+ });
51
+
52
+ it('sets latest tip from blocks added', async () => {
53
+ await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(3, i => makeBlock(i + 1)) });
54
+
55
+ const tips = await tipsStore.getL2Tips();
56
+ expect(tips).toEqual(makeTips(3, 0, 0));
57
+
58
+ expect(await tipsStore.getL2BlockHash(1)).toEqual(new Fr(1).toString());
59
+ expect(await tipsStore.getL2BlockHash(2)).toEqual(new Fr(2).toString());
60
+ expect(await tipsStore.getL2BlockHash(3)).toEqual(new Fr(3).toString());
61
+ });
62
+
63
+ it('clears block hashes when setting finalized chain', async () => {
64
+ await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(5, i => makeBlock(i + 1)) });
65
+ await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) });
66
+ await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(3) });
67
+
68
+ const tips = await tipsStore.getL2Tips();
69
+ expect(tips).toEqual(makeTips(5, 3, 3));
70
+
71
+ expect(await tipsStore.getL2BlockHash(1)).toBeUndefined();
72
+ expect(await tipsStore.getL2BlockHash(2)).toBeUndefined();
73
+
74
+ expect(await tipsStore.getL2BlockHash(3)).toEqual(new Fr(3).toString());
75
+ expect(await tipsStore.getL2BlockHash(4)).toEqual(new Fr(4).toString());
76
+ expect(await tipsStore.getL2BlockHash(5)).toEqual(new Fr(5).toString());
77
+ });
78
+
79
+ // Regression test for #13142
80
+ it('does not blow up when setting proven chain on an unseen block number', async () => {
81
+ await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [makeBlock(5)] });
82
+ await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) });
83
+
84
+ const tips = await tipsStore.getL2Tips();
85
+ expect(tips).toEqual(makeTips(5, 3, 0));
86
+ });
87
+ }
@@ -1,6 +1,6 @@
1
1
  import { EthAddress } from '@aztec/foundation/eth-address';
2
2
  import { jsonParseWithSchemaSync, jsonStringify } from '@aztec/foundation/json-rpc';
3
- import { createLogger } from '@aztec/foundation/log';
3
+ import { type Logger, createLogger } from '@aztec/foundation/log';
4
4
 
5
5
  import fs from 'fs/promises';
6
6
  import { inspect } from 'node:util';
@@ -11,7 +11,12 @@ import { z } from 'zod';
11
11
  * Represents a version record for storing in a version file.
12
12
  */
13
13
  export class DatabaseVersion {
14
- constructor(public readonly schemaVersion: number, public readonly rollupAddress: EthAddress) {}
14
+ constructor(
15
+ /** The version of the data on disk. Used to perform upgrades */
16
+ public readonly schemaVersion: number,
17
+ /** The rollup the data pertains to */
18
+ public readonly rollupAddress: EthAddress,
19
+ ) {}
15
20
 
16
21
  public toBuffer(): Buffer {
17
22
  return Buffer.from(jsonStringify(this));
@@ -66,7 +71,7 @@ export class DatabaseVersion {
66
71
  }
67
72
 
68
73
  public toString(): string {
69
- return this.schemaVersion.toString();
74
+ return `DatabaseVersion{schemaVersion=${this.schemaVersion},rollupAddress=${this.rollupAddress}"}`;
70
75
  }
71
76
 
72
77
  /**
@@ -81,6 +86,16 @@ export type DatabaseVersionManagerFs = Pick<typeof fs, 'readFile' | 'writeFile'
81
86
 
82
87
  export const DATABASE_VERSION_FILE_NAME = 'db_version';
83
88
 
89
+ export type DatabaseVersionManagerOptions<T> = {
90
+ schemaVersion: number;
91
+ rollupAddress: EthAddress;
92
+ dataDirectory: string;
93
+ onOpen: (dataDir: string) => Promise<T>;
94
+ onUpgrade?: (dataDir: string, currentVersion: number, latestVersion: number) => Promise<void>;
95
+ fileSystem?: DatabaseVersionManagerFs;
96
+ log?: Logger;
97
+ };
98
+
84
99
  /**
85
100
  * A manager for handling database versioning and migrations.
86
101
  * This class will check the version of data in a directory and either
@@ -92,6 +107,12 @@ export class DatabaseVersionManager<T> {
92
107
  private readonly versionFile: string;
93
108
  private readonly currentVersion: DatabaseVersion;
94
109
 
110
+ private dataDirectory: string;
111
+ private onOpen: (dataDir: string) => Promise<T>;
112
+ private onUpgrade?: (dataDir: string, currentVersion: number, latestVersion: number) => Promise<void>;
113
+ private fileSystem: DatabaseVersionManagerFs;
114
+ private log: Logger;
115
+
95
116
  /**
96
117
  * Create a new version manager
97
118
  *
@@ -104,21 +125,27 @@ export class DatabaseVersionManager<T> {
104
125
  * @param log - Optional custom logger
105
126
  * @param options - Configuration options
106
127
  */
107
- constructor(
108
- schemaVersion: number,
109
- rollupAddress: EthAddress,
110
- private dataDirectory: string,
111
- private onOpen: (dataDir: string) => Promise<T>,
112
- private onUpgrade?: (dataDir: string, currentVersion: number, latestVersion: number) => Promise<void>,
113
- private fileSystem: DatabaseVersionManagerFs = fs,
114
- private log = createLogger(`foundation:version-manager`),
115
- ) {
128
+ constructor({
129
+ schemaVersion,
130
+ rollupAddress,
131
+ dataDirectory,
132
+ onOpen,
133
+ onUpgrade,
134
+ fileSystem = fs,
135
+ log = createLogger(`foundation:version-manager`),
136
+ }: DatabaseVersionManagerOptions<T>) {
116
137
  if (schemaVersion < 1) {
117
138
  throw new TypeError(`Invalid schema version received: ${schemaVersion}`);
118
139
  }
119
140
 
120
- this.versionFile = join(this.dataDirectory, DatabaseVersionManager.VERSION_FILE);
141
+ this.versionFile = join(dataDirectory, DatabaseVersionManager.VERSION_FILE);
121
142
  this.currentVersion = new DatabaseVersion(schemaVersion, rollupAddress);
143
+
144
+ this.dataDirectory = dataDirectory;
145
+ this.onOpen = onOpen;
146
+ this.onUpgrade = onUpgrade;
147
+ this.fileSystem = fileSystem;
148
+ this.log = log;
122
149
  }
123
150
 
124
151
  static async writeVersion(version: DatabaseVersion, dataDir: string, fileSystem: DatabaseVersionManagerFs = fs) {
@@ -137,6 +164,8 @@ export class DatabaseVersionManager<T> {
137
164
  public async open(): Promise<[T, boolean]> {
138
165
  // const storedVersion = await DatabaseVersion.readVersion(this.versionFile);
139
166
  let storedVersion: DatabaseVersion;
167
+ // a flag to suppress logs about 'resetting the data dir' when starting from an empty state
168
+ let shouldLogDataReset = true;
140
169
 
141
170
  try {
142
171
  const versionBuf = await this.fileSystem.readFile(this.versionFile);
@@ -144,6 +173,8 @@ export class DatabaseVersionManager<T> {
144
173
  } catch (err) {
145
174
  if (err && (err as Error & { code: string }).code === 'ENOENT') {
146
175
  storedVersion = DatabaseVersion.empty();
176
+ // only turn off these logs if the data dir didn't exist before
177
+ shouldLogDataReset = false;
147
178
  } else {
148
179
  this.log.warn(`Failed to read stored version information: ${err}. Defaulting to empty version`);
149
180
  storedVersion = DatabaseVersion.empty();
@@ -164,13 +195,21 @@ export class DatabaseVersionManager<T> {
164
195
  needsReset = true;
165
196
  }
166
197
  } else if (cmp !== 0) {
167
- this.log.info(
168
- `Can't upgrade from version ${storedVersion.schemaVersion} to ${this.currentVersion.schemaVersion}. Resetting database at ${this.dataDirectory}`,
169
- );
198
+ if (shouldLogDataReset) {
199
+ this.log.info(
200
+ `Can't upgrade from version ${storedVersion} to ${this.currentVersion}. Resetting database at ${this.dataDirectory}`,
201
+ );
202
+ }
170
203
  needsReset = true;
171
204
  }
172
205
  } else {
173
- this.log.warn('Rollup address changed, resetting data directory', { versionFile: this.versionFile });
206
+ if (shouldLogDataReset) {
207
+ this.log.warn('Rollup address has changed, resetting data directory', {
208
+ versionFile: this.versionFile,
209
+ storedVersion,
210
+ currentVersion: this.currentVersion,
211
+ });
212
+ }
174
213
  needsReset = true;
175
214
  }
176
215