@aztec/archiver 0.0.1-commit.e588bc7e5 → 0.0.1-commit.e5a3663dd

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 (105) hide show
  1. package/dest/archiver.d.ts +19 -11
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +96 -53
  4. package/dest/config.d.ts +3 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +14 -3
  7. package/dest/errors.d.ts +32 -5
  8. package/dest/errors.d.ts.map +1 -1
  9. package/dest/errors.js +51 -6
  10. package/dest/factory.d.ts +4 -4
  11. package/dest/factory.d.ts.map +1 -1
  12. package/dest/factory.js +13 -10
  13. package/dest/index.d.ts +10 -3
  14. package/dest/index.d.ts.map +1 -1
  15. package/dest/index.js +9 -2
  16. package/dest/l1/calldata_retriever.d.ts +2 -1
  17. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  18. package/dest/l1/calldata_retriever.js +9 -4
  19. package/dest/l1/data_retrieval.d.ts +18 -9
  20. package/dest/l1/data_retrieval.d.ts.map +1 -1
  21. package/dest/l1/data_retrieval.js +13 -19
  22. package/dest/l1/validate_historical_logs.d.ts +23 -0
  23. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  24. package/dest/l1/validate_historical_logs.js +108 -0
  25. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  26. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  27. package/dest/modules/contract_data_source_adapter.js +42 -0
  28. package/dest/modules/data_source_base.d.ts +16 -10
  29. package/dest/modules/data_source_base.d.ts.map +1 -1
  30. package/dest/modules/data_source_base.js +71 -60
  31. package/dest/modules/data_store_updater.d.ts +16 -9
  32. package/dest/modules/data_store_updater.d.ts.map +1 -1
  33. package/dest/modules/data_store_updater.js +52 -40
  34. package/dest/modules/instrumentation.d.ts +7 -2
  35. package/dest/modules/instrumentation.d.ts.map +1 -1
  36. package/dest/modules/instrumentation.js +22 -6
  37. package/dest/modules/l1_synchronizer.d.ts +8 -4
  38. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  39. package/dest/modules/l1_synchronizer.js +212 -79
  40. package/dest/modules/validation.d.ts +4 -3
  41. package/dest/modules/validation.d.ts.map +1 -1
  42. package/dest/modules/validation.js +4 -4
  43. package/dest/store/block_store.d.ts +60 -21
  44. package/dest/store/block_store.d.ts.map +1 -1
  45. package/dest/store/block_store.js +229 -70
  46. package/dest/store/contract_class_store.d.ts +17 -3
  47. package/dest/store/contract_class_store.d.ts.map +1 -1
  48. package/dest/store/contract_class_store.js +17 -1
  49. package/dest/store/contract_instance_store.d.ts +28 -1
  50. package/dest/store/contract_instance_store.d.ts.map +1 -1
  51. package/dest/store/contract_instance_store.js +31 -0
  52. package/dest/store/data_stores.d.ts +68 -0
  53. package/dest/store/data_stores.d.ts.map +1 -0
  54. package/dest/store/data_stores.js +50 -0
  55. package/dest/store/function_names_cache.d.ts +17 -0
  56. package/dest/store/function_names_cache.d.ts.map +1 -0
  57. package/dest/store/function_names_cache.js +30 -0
  58. package/dest/store/l2_tips_cache.d.ts +1 -1
  59. package/dest/store/l2_tips_cache.d.ts.map +1 -1
  60. package/dest/store/l2_tips_cache.js +3 -3
  61. package/dest/store/log_store.d.ts +1 -1
  62. package/dest/store/log_store.d.ts.map +1 -1
  63. package/dest/store/log_store.js +2 -4
  64. package/dest/store/message_store.d.ts +9 -3
  65. package/dest/store/message_store.d.ts.map +1 -1
  66. package/dest/store/message_store.js +31 -1
  67. package/dest/test/fake_l1_state.d.ts +14 -3
  68. package/dest/test/fake_l1_state.d.ts.map +1 -1
  69. package/dest/test/fake_l1_state.js +55 -15
  70. package/dest/test/mock_l2_block_source.d.ts +12 -3
  71. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  72. package/dest/test/mock_l2_block_source.js +24 -2
  73. package/dest/test/noop_l1_archiver.d.ts +4 -4
  74. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  75. package/dest/test/noop_l1_archiver.js +9 -7
  76. package/package.json +13 -13
  77. package/src/archiver.ts +113 -52
  78. package/src/config.ts +15 -1
  79. package/src/errors.ts +75 -8
  80. package/src/factory.ts +11 -10
  81. package/src/index.ts +17 -2
  82. package/src/l1/calldata_retriever.ts +15 -4
  83. package/src/l1/data_retrieval.ts +30 -35
  84. package/src/l1/validate_historical_logs.ts +140 -0
  85. package/src/modules/contract_data_source_adapter.ts +59 -0
  86. package/src/modules/data_source_base.ts +75 -57
  87. package/src/modules/data_store_updater.ts +71 -39
  88. package/src/modules/instrumentation.ts +27 -7
  89. package/src/modules/l1_synchronizer.ts +301 -83
  90. package/src/modules/validation.ts +8 -7
  91. package/src/store/block_store.ts +264 -77
  92. package/src/store/contract_class_store.ts +28 -2
  93. package/src/store/contract_instance_store.ts +43 -0
  94. package/src/store/data_stores.ts +108 -0
  95. package/src/store/function_names_cache.ts +37 -0
  96. package/src/store/l2_tips_cache.ts +9 -3
  97. package/src/store/log_store.ts +2 -5
  98. package/src/store/message_store.ts +35 -2
  99. package/src/test/fake_l1_state.ts +62 -24
  100. package/src/test/mock_l2_block_source.ts +23 -2
  101. package/src/test/noop_l1_archiver.ts +9 -7
  102. package/dest/store/kv_archiver_store.d.ts +0 -377
  103. package/dest/store/kv_archiver_store.d.ts.map +0 -1
  104. package/dest/store/kv_archiver_store.js +0 -494
  105. package/src/store/kv_archiver_store.ts +0 -713
@@ -10,7 +10,7 @@ import {
10
10
  } from '@aztec/stdlib/block';
11
11
  import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
12
12
  import { type L1RollupConstants, computeQuorum, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
13
- import { ConsensusPayload } from '@aztec/stdlib/p2p';
13
+ import { ConsensusPayload, type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
14
14
 
15
15
  export type { ValidateCheckpointResult };
16
16
 
@@ -18,11 +18,11 @@ export type { ValidateCheckpointResult };
18
18
  * Extracts attestation information from a published checkpoint.
19
19
  * Returns info for each attestation, preserving array indices.
20
20
  */
21
- export function getAttestationInfoFromPublishedCheckpoint({
22
- checkpoint,
23
- attestations,
24
- }: PublishedCheckpoint): AttestationInfo[] {
25
- const payload = ConsensusPayload.fromCheckpoint(checkpoint);
21
+ export function getAttestationInfoFromPublishedCheckpoint(
22
+ { checkpoint, attestations }: PublishedCheckpoint,
23
+ signatureContext: CoordinationSignatureContext,
24
+ ): AttestationInfo[] {
25
+ const payload = ConsensusPayload.fromCheckpoint(checkpoint, signatureContext);
26
26
  return getAttestationInfoFromPayload(payload, attestations);
27
27
  }
28
28
 
@@ -34,9 +34,10 @@ export async function validateCheckpointAttestations(
34
34
  publishedCheckpoint: PublishedCheckpoint,
35
35
  epochCache: EpochCache,
36
36
  constants: Pick<L1RollupConstants, 'epochDuration'>,
37
+ signatureContext: CoordinationSignatureContext,
37
38
  logger?: Logger,
38
39
  ): Promise<ValidateCheckpointResult> {
39
- const attestorInfos = getAttestationInfoFromPublishedCheckpoint(publishedCheckpoint);
40
+ const attestorInfos = getAttestationInfoFromPublishedCheckpoint(publishedCheckpoint, signatureContext);
40
41
  const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
41
42
  const { checkpoint, attestations } = publishedCheckpoint;
42
43
  const headerHash = checkpoint.header.hash();
@@ -10,6 +10,7 @@ import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncSingleton, Range } fro
10
10
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
11
11
  import {
12
12
  type BlockData,
13
+ type BlockDataWithCheckpointContext,
13
14
  BlockHash,
14
15
  Body,
15
16
  CheckpointedL2Block,
@@ -45,6 +46,7 @@ import {
45
46
  import {
46
47
  BlockAlreadyCheckpointedError,
47
48
  BlockArchiveNotConsistentError,
49
+ BlockCheckpointNumberNotSequentialError,
48
50
  BlockIndexNotSequentialError,
49
51
  BlockNotFoundError,
50
52
  BlockNumberNotSequentialError,
@@ -52,8 +54,10 @@ import {
52
54
  CheckpointNotFoundError,
53
55
  CheckpointNumberNotSequentialError,
54
56
  InitialCheckpointNumberNotSequentialError,
57
+ NoProposedCheckpointToPromoteError,
58
+ ProposedCheckpointArchiveRootMismatchError,
55
59
  ProposedCheckpointNotSequentialError,
56
- ProposedCheckpointStaleError,
60
+ ProposedCheckpointPromotionNotSequentialError,
57
61
  } from '../errors.js';
58
62
 
59
63
  export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
@@ -81,6 +85,7 @@ type CommonCheckpointStorage = {
81
85
  type CheckpointStorage = CommonCheckpointStorage & {
82
86
  l1: Buffer;
83
87
  attestations: Buffer[];
88
+ feeAssetPriceModifier: string;
84
89
  };
85
90
 
86
91
  /** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
@@ -98,7 +103,10 @@ export class BlockStore {
98
103
  /** Map block number to block data */
99
104
  #blocks: AztecAsyncMap<number, BlockStorage>;
100
105
 
101
- /** Map checkpoint number to checkpoint data */
106
+ /** Map keyed by checkpoint number holding proposed (locally-validated, not yet L1-confirmed) checkpoints. */
107
+ #proposedCheckpoints: AztecAsyncMap<number, ProposedCheckpointStorage>;
108
+
109
+ /** Map checkpoint number to checkpoint data for mined checkpoints only */
102
110
  #checkpoints: AztecAsyncMap<number, CheckpointStorage>;
103
111
 
104
112
  /** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
@@ -131,9 +139,6 @@ export class BlockStore {
131
139
  /** Index mapping block archive to block number */
132
140
  #blockArchiveIndex: AztecAsyncMap<string, number>;
133
141
 
134
- /** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
135
- #proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
136
-
137
142
  #log = createLogger('archiver:block_store');
138
143
 
139
144
  constructor(private db: AztecAsyncKVStore) {
@@ -149,7 +154,7 @@ export class BlockStore {
149
154
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
150
155
  this.#checkpoints = db.openMap('archiver_checkpoints');
151
156
  this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
152
- this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
157
+ this.#proposedCheckpoints = db.openMap('archiver_proposed_checkpoints');
153
158
  }
154
159
 
155
160
  /**
@@ -185,8 +190,7 @@ export class BlockStore {
185
190
 
186
191
  // Extract the latest block and checkpoint numbers
187
192
  const previousBlockNumber = await this.getLatestL2BlockNumber();
188
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
189
- const previousCheckpointNumber = await this.getLatestCheckpointNumber();
193
+ const latestCheckpointNumber = await this.getLatestCheckpointNumber();
190
194
 
191
195
  // Verify we're not overwriting checkpointed blocks
192
196
  const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
@@ -204,19 +208,14 @@ export class BlockStore {
204
208
  throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
205
209
  }
206
210
 
207
- // The same check as above but for checkpoints. Accept the block if either the confirmed
208
- // checkpoint or the pending (locally validated but not yet confirmed) checkpoint matches.
211
+ // Accept the block if either the confirmed checkpoint or a pending checkpoint matches
212
+ // the expected predecessor. We look for a pending entry at exactly blockCheckpointNumber - 1.
209
213
  const expectedCheckpointNumber = blockCheckpointNumber - 1;
210
- if (
211
- !opts.force &&
212
- previousCheckpointNumber !== expectedCheckpointNumber &&
213
- proposedCheckpointNumber !== expectedCheckpointNumber
214
- ) {
215
- const [reported, source]: [CheckpointNumber, 'confirmed' | 'proposed'] =
216
- proposedCheckpointNumber > previousCheckpointNumber
217
- ? [proposedCheckpointNumber, 'proposed']
218
- : [previousCheckpointNumber, 'confirmed'];
219
- throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, reported, source);
214
+ const hasPendingAtExpected = await this.#proposedCheckpoints.hasAsync(expectedCheckpointNumber);
215
+ if (!opts.force && latestCheckpointNumber !== expectedCheckpointNumber && !hasPendingAtExpected) {
216
+ const [latestPendingKey] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
217
+ const previous = CheckpointNumber(Math.max(latestCheckpointNumber, latestPendingKey ?? 0));
218
+ throw new BlockCheckpointNumberNotSequentialError(blockNumber, blockCheckpointNumber, previous);
220
219
  }
221
220
 
222
221
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
@@ -262,16 +261,28 @@ export class BlockStore {
262
261
  }
263
262
 
264
263
  return await this.db.transactionAsync(async () => {
265
- // Check that the checkpoint immediately before the first block to be added is present in the store.
266
264
  const firstCheckpointNumber = checkpoints[0].checkpoint.number;
267
265
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
268
266
 
269
- if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
267
+ // Handle already-stored checkpoints at the start of the batch.
268
+ // This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
269
+ // We accept them if archives match (same content) and update their L1 metadata.
270
+ if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
271
+ checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
272
+ if (checkpoints.length === 0) {
273
+ return true;
274
+ }
275
+ // Re-check sequentiality after skipping
276
+ const newFirstNumber = checkpoints[0].checkpoint.number;
277
+ if (previousCheckpointNumber !== newFirstNumber - 1) {
278
+ throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
279
+ }
280
+ } else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
270
281
  throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
271
282
  }
272
283
 
273
284
  // Get the last block of the previous checkpoint for archive chaining
274
- let previousBlock = await this.getPreviousCheckpointBlock(firstCheckpointNumber);
285
+ let previousBlock = await this.getPreviousCheckpointBlock(checkpoints[0].checkpoint.number);
275
286
 
276
287
  // Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
277
288
  let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
@@ -307,21 +318,66 @@ export class BlockStore {
307
318
  checkpointNumber: checkpoint.checkpoint.number,
308
319
  startBlock: checkpoint.checkpoint.blocks[0].number,
309
320
  blockCount: checkpoint.checkpoint.blocks.length,
321
+ feeAssetPriceModifier: checkpoint.checkpoint.feeAssetPriceModifier.toString(),
310
322
  });
311
323
 
312
324
  // Update slot-to-checkpoint index
313
325
  await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
314
- }
315
326
 
316
- // Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
317
- const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
318
- await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
327
+ // Remove proposed checkpoint if it exists, since L1 is authoritative
328
+ await this.#proposedCheckpoints.delete(checkpoint.checkpoint.number);
329
+ }
319
330
 
320
331
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
321
332
  return true;
322
333
  });
323
334
  }
324
335
 
336
+ /**
337
+ * Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
338
+ * Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
339
+ */
340
+ private async skipOrUpdateAlreadyStoredCheckpoints(
341
+ checkpoints: PublishedCheckpoint[],
342
+ latestStored: CheckpointNumber,
343
+ ): Promise<PublishedCheckpoint[]> {
344
+ let i = 0;
345
+ for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
346
+ const incoming = checkpoints[i];
347
+ const stored = await this.getCheckpointData(incoming.checkpoint.number);
348
+ if (!stored) {
349
+ // Should not happen if latestStored is correct, but be safe
350
+ break;
351
+ }
352
+ // Verify the checkpoint content matches (archive root)
353
+ if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
354
+ throw new Error(
355
+ `Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
356
+ `Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
357
+ );
358
+ }
359
+ // Update L1 metadata and attestations for the already-stored checkpoint
360
+ this.#log.warn(
361
+ `Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
362
+ `(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
363
+ );
364
+ await this.#checkpoints.set(incoming.checkpoint.number, {
365
+ header: incoming.checkpoint.header.toBuffer(),
366
+ archive: incoming.checkpoint.archive.toBuffer(),
367
+ checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
368
+ l1: incoming.l1.toBuffer(),
369
+ attestations: incoming.attestations.map(a => a.toBuffer()),
370
+ checkpointNumber: incoming.checkpoint.number,
371
+ startBlock: incoming.checkpoint.blocks[0].number,
372
+ blockCount: incoming.checkpoint.blocks.length,
373
+ feeAssetPriceModifier: incoming.checkpoint.feeAssetPriceModifier.toString(),
374
+ });
375
+ // Update the sync point to reflect the new L1 block
376
+ await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
377
+ }
378
+ return checkpoints.slice(i);
379
+ }
380
+
325
381
  /**
326
382
  * Gets the last block of the checkpoint before the given one.
327
383
  * Returns undefined if there is no previous checkpoint (i.e. genesis).
@@ -332,24 +388,27 @@ export class BlockStore {
332
388
  return undefined;
333
389
  }
334
390
 
335
- const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
336
- if (previousCheckpointData === undefined) {
391
+ // Check across both proposed and mined checkpoints
392
+ const predecessor =
393
+ (await this.getProposedCheckpointByNumber(previousCheckpointNumber)) ??
394
+ (await this.getCheckpointData(previousCheckpointNumber));
395
+
396
+ if (!predecessor) {
337
397
  throw new CheckpointNotFoundError(previousCheckpointNumber);
338
398
  }
339
399
 
340
- const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
400
+ const previousBlockNumber = BlockNumber(predecessor.startBlock + predecessor.blockCount - 1);
341
401
  const previousBlock = await this.getBlock(previousBlockNumber);
342
402
  if (previousBlock === undefined) {
343
403
  throw new BlockNotFoundError(previousBlockNumber);
344
404
  }
345
-
346
405
  return previousBlock;
347
406
  }
348
407
 
349
408
  /**
350
409
  * Validates that blocks are sequential, have correct indexes, and chain via archive roots.
351
410
  * This is the same validation used for both confirmed checkpoints (addCheckpoints) and
352
- * proposed checkpoints (setProposedCheckpoint).
411
+ * proposed checkpoints (addProposedCheckpoint).
353
412
  */
354
413
  private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
355
414
  for (const block of blocks) {
@@ -476,11 +535,8 @@ export class BlockStore {
476
535
  this.#log.debug(`Removed checkpoint ${c}`);
477
536
  }
478
537
 
479
- // Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
480
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
481
- if (proposedCheckpointNumber > checkpointNumber) {
482
- await this.#proposedCheckpoint.delete();
483
- }
538
+ // Evict all pending checkpoints > checkpointNumber (their base chain no longer exists)
539
+ await this.evictProposedCheckpointsFrom(CheckpointNumber(checkpointNumber + 1));
484
540
 
485
541
  return { blocksRemoved };
486
542
  });
@@ -529,6 +585,7 @@ export class BlockStore {
529
585
  checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
530
586
  startBlock: BlockNumber(checkpointStorage.startBlock),
531
587
  blockCount: checkpointStorage.blockCount,
588
+ feeAssetPriceModifier: BigInt(checkpointStorage.feeAssetPriceModifier),
532
589
  l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
533
590
  attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
534
591
  };
@@ -629,44 +686,127 @@ export class BlockStore {
629
686
  }
630
687
 
631
688
  async hasProposedCheckpoint(): Promise<boolean> {
632
- const proposed = await this.#proposedCheckpoint.getAsync();
633
- return proposed !== undefined;
689
+ const [key] = await toArray(this.#proposedCheckpoints.keysAsync({ limit: 1 }));
690
+ return key !== undefined;
634
691
  }
635
692
 
636
- /** Deletes the proposed checkpoint from storage. */
637
- async deleteProposedCheckpoint(): Promise<void> {
638
- await this.#proposedCheckpoint.delete();
693
+ /** Deletes all pending proposed checkpoints from storage. */
694
+ async deleteProposedCheckpoints(): Promise<void> {
695
+ for await (const key of this.#proposedCheckpoints.keysAsync()) {
696
+ await this.#proposedCheckpoints.delete(key);
697
+ }
639
698
  }
640
699
 
641
- /** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
642
- async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
643
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
644
- if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
645
- await this.#proposedCheckpoint.delete();
646
- }
700
+ /**
701
+ * Promotes a specific pending checkpoint to a confirmed checkpoint entry.
702
+ * This persists the checkpoint to the store, removes only that pending entry, and updates the L1 sync point.
703
+ * Remaining pending entries (e.g. N+1, N+2) are left intact — they chain off the just-promoted one.
704
+ * @param checkpointNumber - The checkpoint number to promote.
705
+ * @param l1 - L1 published data for the checkpoint.
706
+ * @param attestations - Committee attestations.
707
+ * @param expectedArchiveRoot - Archive root guard against races.
708
+ */
709
+ async promoteProposedToCheckpointed(
710
+ checkpointNumber: CheckpointNumber,
711
+ l1: L1PublishedData,
712
+ attestations: CommitteeAttestation[],
713
+ expectedArchiveRoot: Fr,
714
+ ): Promise<void> {
715
+ return await this.db.transactionAsync(async () => {
716
+ const proposed = await this.getProposedCheckpointByNumber(checkpointNumber);
717
+ if (!proposed) {
718
+ throw new NoProposedCheckpointToPromoteError();
719
+ }
720
+ if (!proposed.archive.root.equals(expectedArchiveRoot)) {
721
+ throw new ProposedCheckpointArchiveRootMismatchError(expectedArchiveRoot, proposed.archive.root);
722
+ }
723
+
724
+ // Verify sequentiality: promoted checkpoint must follow the latest confirmed one
725
+ const latestCheckpointNumber = await this.getLatestCheckpointNumber();
726
+ if (latestCheckpointNumber !== proposed.checkpointNumber - 1) {
727
+ throw new ProposedCheckpointPromotionNotSequentialError(proposed.checkpointNumber, latestCheckpointNumber);
728
+ }
729
+
730
+ // Write the checkpoint entry
731
+ await this.#checkpoints.set(proposed.checkpointNumber, {
732
+ header: proposed.header.toBuffer(),
733
+ archive: proposed.archive.toBuffer(),
734
+ checkpointOutHash: proposed.checkpointOutHash.toBuffer(),
735
+ l1: l1.toBuffer(),
736
+ attestations: attestations.map(attestation => attestation.toBuffer()),
737
+ checkpointNumber: proposed.checkpointNumber,
738
+ startBlock: proposed.startBlock,
739
+ blockCount: proposed.blockCount,
740
+ feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
741
+ });
742
+
743
+ // Update the slot-to-checkpoint index
744
+ await this.#slotToCheckpoint.set(proposed.header.slotNumber, proposed.checkpointNumber);
745
+
746
+ // Remove only this pending entry — remaining entries N+1, N+2, ... stay valid
747
+ await this.#proposedCheckpoints.delete(proposed.checkpointNumber);
748
+
749
+ // Update the last synced L1 block
750
+ await this.#lastSynchedL1Block.set(l1.blockNumber);
751
+ });
647
752
  }
648
753
 
649
- /** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
650
- async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
651
- const stored = await this.#proposedCheckpoint.getAsync();
652
- if (!stored) {
754
+ /**
755
+ * Returns the latest pending checkpoint (highest-numbered entry), or undefined if none.
756
+ * No fallback to confirmed.
757
+ */
758
+ async getLastProposedCheckpoint(): Promise<ProposedCheckpointData | undefined> {
759
+ const [key] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
760
+ if (key === undefined) {
653
761
  return undefined;
654
762
  }
655
- return this.convertToProposedCheckpointData(stored);
763
+ const stored = await this.#proposedCheckpoints.getAsync(key);
764
+ return stored ? this.convertToProposedCheckpointData(stored) : undefined;
765
+ }
766
+
767
+ /** Returns the pending checkpoint for a specific checkpoint number, or undefined if not found. */
768
+ async getProposedCheckpointByNumber(n: CheckpointNumber): Promise<ProposedCheckpointData | undefined> {
769
+ const stored = await this.#proposedCheckpoints.getAsync(n);
770
+ return stored ? this.convertToProposedCheckpointData(stored) : undefined;
771
+ }
772
+
773
+ /** Returns all pending checkpoints in ascending checkpoint-number order. */
774
+ async getProposedCheckpoints(): Promise<ProposedCheckpointData[]> {
775
+ const results: ProposedCheckpointData[] = [];
776
+ for await (const [, stored] of this.#proposedCheckpoints.entriesAsync()) {
777
+ results.push(this.convertToProposedCheckpointData(stored));
778
+ }
779
+ return results;
656
780
  }
657
781
 
658
782
  /**
659
- * Gets the checkpoint at the proposed tip
660
- * - pending checkpoint if it exists
783
+ * Evicts all pending checkpoints with checkpoint number >= fromNumber.
784
+ * Used for divergent-mined-checkpoint cleanup: when L1 mines checkpoint N with a different archive,
785
+ * all pending >= N must be evicted since they chain off the now-invalid pending N.
786
+ */
787
+ async evictProposedCheckpointsFrom(fromNumber: CheckpointNumber): Promise<void> {
788
+ const keysToDelete: number[] = [];
789
+ for await (const key of this.#proposedCheckpoints.keysAsync()) {
790
+ if (key >= fromNumber) {
791
+ keysToDelete.push(key);
792
+ }
793
+ }
794
+ for (const key of keysToDelete) {
795
+ await this.#proposedCheckpoints.delete(key);
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Gets the checkpoint at the proposed tip:
801
+ * - latest pending checkpoint if any exist
661
802
  * - fallsback to latest confirmed checkpoint otherwise
662
- * @returns CommonCheckpointData
663
803
  */
664
- async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
665
- const stored = await this.#proposedCheckpoint.getAsync();
666
- if (!stored) {
804
+ async getLastCheckpoint(): Promise<CommonCheckpointData | undefined> {
805
+ const latest = await this.getLastProposedCheckpoint();
806
+ if (!latest) {
667
807
  return this.getCheckpointData(await this.getLatestCheckpointNumber());
668
808
  }
669
- return this.convertToProposedCheckpointData(stored);
809
+ return latest;
670
810
  }
671
811
 
672
812
  private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
@@ -687,7 +827,7 @@ export class BlockStore {
687
827
  * @returns CheckpointNumber
688
828
  */
689
829
  async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
690
- const proposed = await this.getProposedCheckpoint();
830
+ const proposed = await this.getLastCheckpoint();
691
831
  if (!proposed) {
692
832
  return await this.getLatestCheckpointNumber();
693
833
  }
@@ -699,7 +839,7 @@ export class BlockStore {
699
839
  * @returns BlockNumber
700
840
  */
701
841
  async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
702
- const proposed = await this.getProposedCheckpoint();
842
+ const proposed = await this.getLastCheckpoint();
703
843
  if (!proposed) {
704
844
  return await this.getCheckpointedL2BlockNumber();
705
845
  }
@@ -733,7 +873,12 @@ export class BlockStore {
733
873
  * @param limit - The number of blocks to return.
734
874
  * @returns The requested L2 blocks
735
875
  */
736
- async *getCheckpointedBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<CheckpointedL2Block> {
876
+ getCheckpointedBlocks(start: BlockNumber, limit: number): Promise<CheckpointedL2Block[]> {
877
+ return toArray(this.iterateCheckpointedBlocks(start, limit));
878
+ }
879
+
880
+ /** Async iterator variant of {@link getCheckpointedBlocks}. */
881
+ async *iterateCheckpointedBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<CheckpointedL2Block> {
737
882
  const checkpointCache = new Map<CheckpointNumber, CheckpointStorage>();
738
883
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
739
884
  const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
@@ -777,7 +922,12 @@ export class BlockStore {
777
922
  * @param limit - The number of blocks to return.
778
923
  * @returns The requested L2 blocks
779
924
  */
780
- async *getBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<L2Block> {
925
+ getBlocks(start: BlockNumber, limit: number): Promise<L2Block[]> {
926
+ return toArray(this.iterateBlocks(start, limit));
927
+ }
928
+
929
+ /** Async iterator variant of {@link getBlocks}. */
930
+ async *iterateBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<L2Block> {
781
931
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
782
932
  const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
783
933
  if (block) {
@@ -799,6 +949,33 @@ export class BlockStore {
799
949
  return this.getBlockDataFromBlockStorage(blockStorage);
800
950
  }
801
951
 
952
+ /**
953
+ * Gets block metadata plus checkpoint-derived context (L1 publish info, attestations) without
954
+ * deserializing tx bodies. When the block's containing checkpoint has not yet been L1-confirmed,
955
+ * `checkpoint` and `l1` are `undefined` and `attestations` is empty.
956
+ */
957
+ async getBlockDataWithCheckpointContext(
958
+ blockNumber: BlockNumber,
959
+ ): Promise<BlockDataWithCheckpointContext | undefined> {
960
+ const blockStorage = await this.#blocks.getAsync(blockNumber);
961
+ if (!blockStorage || !blockStorage.header) {
962
+ return undefined;
963
+ }
964
+ const data = this.getBlockDataFromBlockStorage(blockStorage);
965
+ const checkpointStorage = await this.#checkpoints.getAsync(blockStorage.checkpointNumber);
966
+ if (!checkpointStorage) {
967
+ return { data, checkpoint: undefined, l1: undefined, attestations: [] };
968
+ }
969
+ const checkpoint = this.checkpointDataFromCheckpointStorage(checkpointStorage);
970
+ return { data, checkpoint, l1: checkpoint.l1, attestations: checkpoint.attestations };
971
+ }
972
+
973
+ /** Returns the checkpoint number that contains the given slot (or undefined if not found). */
974
+ async getCheckpointNumberBySlot(slot: SlotNumber): Promise<CheckpointNumber | undefined> {
975
+ const checkpointNumber = await this.#slotToCheckpoint.getAsync(slot);
976
+ return checkpointNumber === undefined ? undefined : CheckpointNumber(checkpointNumber);
977
+ }
978
+
802
979
  /**
803
980
  * Gets block metadata (without tx data) by archive root.
804
981
  * @param archive - The archive root of the block to return.
@@ -891,7 +1068,12 @@ export class BlockStore {
891
1068
  * @param limit - The number of blocks to return.
892
1069
  * @returns The requested L2 block headers
893
1070
  */
894
- async *getBlockHeaders(start: BlockNumber, limit: number): AsyncIterableIterator<BlockHeader> {
1071
+ getBlockHeaders(start: BlockNumber, limit: number): Promise<BlockHeader[]> {
1072
+ return toArray(this.iterateBlockHeaders(start, limit));
1073
+ }
1074
+
1075
+ /** Async iterator variant of {@link getBlockHeaders}. */
1076
+ async *iterateBlockHeaders(start: BlockNumber, limit: number): AsyncIterableIterator<BlockHeader> {
895
1077
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
896
1078
  const header = BlockHeader.fromBuffer(blockStorage.header);
897
1079
  if (header.getBlockNumber() !== blockNumber) {
@@ -920,7 +1102,7 @@ export class BlockStore {
920
1102
  return {
921
1103
  header: BlockHeader.fromBuffer(blockStorage.header),
922
1104
  archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
923
- blockHash: Fr.fromBuffer(blockStorage.blockHash),
1105
+ blockHash: BlockHash.fromBuffer(blockStorage.blockHash),
924
1106
  checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
925
1107
  indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
926
1108
  };
@@ -1080,20 +1262,25 @@ export class BlockStore {
1080
1262
  return this.#lastSynchedL1Block.set(l1BlockNumber);
1081
1263
  }
1082
1264
 
1083
- /** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
1084
- * Computes archive and checkpointOutHash from the stored blocks. */
1085
- async setProposedCheckpoint(proposed: ProposedCheckpointInput) {
1265
+ /**
1266
+ * Adds a proposed checkpoint to the pending queue.
1267
+ * Accepts proposed.checkpointNumber === latestTip + 1, where latestTip is the highest of
1268
+ * confirmed and the highest pending checkpoint number.
1269
+ * Computes archive and checkpointOutHash from the stored blocks.
1270
+ */
1271
+ async addProposedCheckpoint(proposed: ProposedCheckpointInput) {
1086
1272
  return await this.db.transactionAsync(async () => {
1087
- const current = await this.getProposedCheckpointNumber();
1088
- if (proposed.checkpointNumber <= current) {
1089
- throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
1090
- }
1091
1273
  const confirmed = await this.getLatestCheckpointNumber();
1092
- if (proposed.checkpointNumber !== confirmed + 1) {
1093
- throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
1274
+ const [latestPendingKey] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
1275
+ const latestTip = CheckpointNumber(
1276
+ latestPendingKey !== undefined ? Math.max(latestPendingKey, confirmed) : confirmed,
1277
+ );
1278
+
1279
+ if (proposed.checkpointNumber !== latestTip + 1) {
1280
+ throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, latestTip);
1094
1281
  }
1095
1282
 
1096
- // Ensure the previous checkpoint + blocks exist
1283
+ // Ensure the predecessor block (from pending or confirmed chain) exists
1097
1284
  const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
1098
1285
  const blocks: L2Block[] = [];
1099
1286
  for (let i = 0; i < proposed.blockCount; i++) {
@@ -1108,7 +1295,7 @@ export class BlockStore {
1108
1295
  const archive = blocks[blocks.length - 1].archive;
1109
1296
  const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
1110
1297
 
1111
- await this.#proposedCheckpoint.set({
1298
+ await this.#proposedCheckpoints.set(proposed.checkpointNumber, {
1112
1299
  header: proposed.header.toBuffer(),
1113
1300
  archive: archive.toBuffer(),
1114
1301
  checkpointOutHash: checkpointOutHash.toBuffer(),
@@ -2,7 +2,11 @@ import { Fr } from '@aztec/foundation/curves/bn254';
2
2
  import { toArray } from '@aztec/foundation/iterable';
3
3
  import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize';
4
4
  import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
5
- import type { ContractClassPublic, ContractClassPublicWithBlockNumber } from '@aztec/stdlib/contract';
5
+ import type {
6
+ ContractClassPublic,
7
+ ContractClassPublicWithBlockNumber,
8
+ ContractClassPublicWithCommitment,
9
+ } from '@aztec/stdlib/contract';
6
10
 
7
11
  /**
8
12
  * LMDB-based contract class storage for the archiver.
@@ -16,6 +20,28 @@ export class ContractClassStore {
16
20
  this.#bytecodeCommitments = db.openMap('archiver_bytecode_commitments');
17
21
  }
18
22
 
23
+ /**
24
+ * Adds multiple contract classes to the store.
25
+ * @param data - Contract classes (with bytecode commitments) to add.
26
+ * @param blockNumber - L2 block number where the classes were registered.
27
+ * @returns True if every insert succeeded.
28
+ */
29
+ async addContractClasses(data: ContractClassPublicWithCommitment[], blockNumber: number): Promise<boolean> {
30
+ return (await Promise.all(data.map(c => this.addContractClass(c, c.publicBytecodeCommitment, blockNumber)))).every(
31
+ Boolean,
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Removes multiple contract classes from the store, but only if they were registered at or after the given block.
37
+ * @param data - Contract classes to delete.
38
+ * @param blockNumber - Lower bound on the block number at which the classes were registered.
39
+ * @returns True if every delete succeeded.
40
+ */
41
+ async deleteContractClasses(data: ContractClassPublic[], blockNumber: number): Promise<boolean> {
42
+ return (await Promise.all(data.map(c => this.deleteContractClass(c, blockNumber)))).every(Boolean);
43
+ }
44
+
19
45
  async addContractClass(
20
46
  contractClass: ContractClassPublic,
21
47
  bytecodeCommitment: Fr,
@@ -34,7 +60,7 @@ export class ContractClassStore {
34
60
  });
35
61
  }
36
62
 
37
- async deleteContractClasses(contractClass: ContractClassPublic, blockNumber: number): Promise<void> {
63
+ async deleteContractClass(contractClass: ContractClassPublic, blockNumber: number): Promise<void> {
38
64
  const restoredContractClass = await this.#contractClasses.getAsync(contractClass.id.toString());
39
65
  if (restoredContractClass && deserializeContractClassPublic(restoredContractClass).l2BlockNumber >= blockNumber) {
40
66
  await this.db.transactionAsync(async () => {