@aztec/archiver 0.0.1-commit.2b2662070 → 0.0.1-commit.2c0ee1788

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 (95) hide show
  1. package/dest/archiver.d.ts +17 -10
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +92 -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 +4 -1
  35. package/dest/modules/instrumentation.d.ts.map +1 -1
  36. package/dest/modules/instrumentation.js +5 -0
  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 +182 -70
  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 +59 -21
  44. package/dest/store/block_store.d.ts.map +1 -1
  45. package/dest/store/block_store.js +181 -66
  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.js +1 -1
  59. package/dest/test/fake_l1_state.d.ts +7 -3
  60. package/dest/test/fake_l1_state.d.ts.map +1 -1
  61. package/dest/test/fake_l1_state.js +42 -15
  62. package/dest/test/mock_l2_block_source.d.ts +12 -3
  63. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  64. package/dest/test/mock_l2_block_source.js +24 -2
  65. package/dest/test/noop_l1_archiver.d.ts +4 -4
  66. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  67. package/dest/test/noop_l1_archiver.js +9 -6
  68. package/package.json +13 -13
  69. package/src/archiver.ts +108 -50
  70. package/src/config.ts +15 -1
  71. package/src/errors.ts +75 -8
  72. package/src/factory.ts +11 -10
  73. package/src/index.ts +17 -2
  74. package/src/l1/calldata_retriever.ts +15 -4
  75. package/src/l1/data_retrieval.ts +30 -35
  76. package/src/l1/validate_historical_logs.ts +140 -0
  77. package/src/modules/contract_data_source_adapter.ts +59 -0
  78. package/src/modules/data_source_base.ts +75 -57
  79. package/src/modules/data_store_updater.ts +71 -39
  80. package/src/modules/instrumentation.ts +8 -0
  81. package/src/modules/l1_synchronizer.ts +241 -71
  82. package/src/modules/validation.ts +8 -7
  83. package/src/store/block_store.ts +204 -73
  84. package/src/store/contract_class_store.ts +28 -2
  85. package/src/store/contract_instance_store.ts +43 -0
  86. package/src/store/data_stores.ts +108 -0
  87. package/src/store/function_names_cache.ts +37 -0
  88. package/src/store/l2_tips_cache.ts +1 -1
  89. package/src/test/fake_l1_state.ts +47 -24
  90. package/src/test/mock_l2_block_source.ts +23 -2
  91. package/src/test/noop_l1_archiver.ts +9 -6
  92. package/dest/store/kv_archiver_store.d.ts +0 -377
  93. package/dest/store/kv_archiver_store.d.ts.map +0 -1
  94. package/dest/store/kv_archiver_store.js +0 -494
  95. 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
@@ -319,15 +318,15 @@ export class BlockStore {
319
318
  checkpointNumber: checkpoint.checkpoint.number,
320
319
  startBlock: checkpoint.checkpoint.blocks[0].number,
321
320
  blockCount: checkpoint.checkpoint.blocks.length,
321
+ feeAssetPriceModifier: checkpoint.checkpoint.feeAssetPriceModifier.toString(),
322
322
  });
323
323
 
324
324
  // Update slot-to-checkpoint index
325
325
  await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
326
- }
327
326
 
328
- // Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
329
- const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
330
- await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
327
+ // Remove proposed checkpoint if it exists, since L1 is authoritative
328
+ await this.#proposedCheckpoints.delete(checkpoint.checkpoint.number);
329
+ }
331
330
 
332
331
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
333
332
  return true;
@@ -371,6 +370,7 @@ export class BlockStore {
371
370
  checkpointNumber: incoming.checkpoint.number,
372
371
  startBlock: incoming.checkpoint.blocks[0].number,
373
372
  blockCount: incoming.checkpoint.blocks.length,
373
+ feeAssetPriceModifier: incoming.checkpoint.feeAssetPriceModifier.toString(),
374
374
  });
375
375
  // Update the sync point to reflect the new L1 block
376
376
  await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
@@ -388,24 +388,27 @@ export class BlockStore {
388
388
  return undefined;
389
389
  }
390
390
 
391
- const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
392
- 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) {
393
397
  throw new CheckpointNotFoundError(previousCheckpointNumber);
394
398
  }
395
399
 
396
- const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
400
+ const previousBlockNumber = BlockNumber(predecessor.startBlock + predecessor.blockCount - 1);
397
401
  const previousBlock = await this.getBlock(previousBlockNumber);
398
402
  if (previousBlock === undefined) {
399
403
  throw new BlockNotFoundError(previousBlockNumber);
400
404
  }
401
-
402
405
  return previousBlock;
403
406
  }
404
407
 
405
408
  /**
406
409
  * Validates that blocks are sequential, have correct indexes, and chain via archive roots.
407
410
  * This is the same validation used for both confirmed checkpoints (addCheckpoints) and
408
- * proposed checkpoints (setProposedCheckpoint).
411
+ * proposed checkpoints (addProposedCheckpoint).
409
412
  */
410
413
  private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
411
414
  for (const block of blocks) {
@@ -532,11 +535,8 @@ export class BlockStore {
532
535
  this.#log.debug(`Removed checkpoint ${c}`);
533
536
  }
534
537
 
535
- // Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
536
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
537
- if (proposedCheckpointNumber > checkpointNumber) {
538
- await this.#proposedCheckpoint.delete();
539
- }
538
+ // Evict all pending checkpoints > checkpointNumber (their base chain no longer exists)
539
+ await this.evictProposedCheckpointsFrom(CheckpointNumber(checkpointNumber + 1));
540
540
 
541
541
  return { blocksRemoved };
542
542
  });
@@ -585,6 +585,7 @@ export class BlockStore {
585
585
  checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
586
586
  startBlock: BlockNumber(checkpointStorage.startBlock),
587
587
  blockCount: checkpointStorage.blockCount,
588
+ feeAssetPriceModifier: BigInt(checkpointStorage.feeAssetPriceModifier),
588
589
  l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
589
590
  attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
590
591
  };
@@ -685,44 +686,127 @@ export class BlockStore {
685
686
  }
686
687
 
687
688
  async hasProposedCheckpoint(): Promise<boolean> {
688
- const proposed = await this.#proposedCheckpoint.getAsync();
689
- return proposed !== undefined;
689
+ const [key] = await toArray(this.#proposedCheckpoints.keysAsync({ limit: 1 }));
690
+ return key !== undefined;
690
691
  }
691
692
 
692
- /** Deletes the proposed checkpoint from storage. */
693
- async deleteProposedCheckpoint(): Promise<void> {
694
- 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
+ }
695
698
  }
696
699
 
697
- /** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
698
- async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
699
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
700
- if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
701
- await this.#proposedCheckpoint.delete();
702
- }
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
+ });
703
752
  }
704
753
 
705
- /** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
706
- async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
707
- const stored = await this.#proposedCheckpoint.getAsync();
708
- 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) {
709
761
  return undefined;
710
762
  }
711
- 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;
712
780
  }
713
781
 
714
782
  /**
715
- * Gets the checkpoint at the proposed tip
716
- * - 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
717
802
  * - fallsback to latest confirmed checkpoint otherwise
718
- * @returns CommonCheckpointData
719
803
  */
720
- async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
721
- const stored = await this.#proposedCheckpoint.getAsync();
722
- if (!stored) {
804
+ async getLastCheckpoint(): Promise<CommonCheckpointData | undefined> {
805
+ const latest = await this.getLastProposedCheckpoint();
806
+ if (!latest) {
723
807
  return this.getCheckpointData(await this.getLatestCheckpointNumber());
724
808
  }
725
- return this.convertToProposedCheckpointData(stored);
809
+ return latest;
726
810
  }
727
811
 
728
812
  private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
@@ -743,7 +827,7 @@ export class BlockStore {
743
827
  * @returns CheckpointNumber
744
828
  */
745
829
  async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
746
- const proposed = await this.getProposedCheckpoint();
830
+ const proposed = await this.getLastCheckpoint();
747
831
  if (!proposed) {
748
832
  return await this.getLatestCheckpointNumber();
749
833
  }
@@ -755,7 +839,7 @@ export class BlockStore {
755
839
  * @returns BlockNumber
756
840
  */
757
841
  async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
758
- const proposed = await this.getProposedCheckpoint();
842
+ const proposed = await this.getLastCheckpoint();
759
843
  if (!proposed) {
760
844
  return await this.getCheckpointedL2BlockNumber();
761
845
  }
@@ -789,7 +873,12 @@ export class BlockStore {
789
873
  * @param limit - The number of blocks to return.
790
874
  * @returns The requested L2 blocks
791
875
  */
792
- 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> {
793
882
  const checkpointCache = new Map<CheckpointNumber, CheckpointStorage>();
794
883
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
795
884
  const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
@@ -833,7 +922,12 @@ export class BlockStore {
833
922
  * @param limit - The number of blocks to return.
834
923
  * @returns The requested L2 blocks
835
924
  */
836
- 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> {
837
931
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
838
932
  const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
839
933
  if (block) {
@@ -855,6 +949,33 @@ export class BlockStore {
855
949
  return this.getBlockDataFromBlockStorage(blockStorage);
856
950
  }
857
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
+
858
979
  /**
859
980
  * Gets block metadata (without tx data) by archive root.
860
981
  * @param archive - The archive root of the block to return.
@@ -947,7 +1068,12 @@ export class BlockStore {
947
1068
  * @param limit - The number of blocks to return.
948
1069
  * @returns The requested L2 block headers
949
1070
  */
950
- 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> {
951
1077
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
952
1078
  const header = BlockHeader.fromBuffer(blockStorage.header);
953
1079
  if (header.getBlockNumber() !== blockNumber) {
@@ -1136,20 +1262,25 @@ export class BlockStore {
1136
1262
  return this.#lastSynchedL1Block.set(l1BlockNumber);
1137
1263
  }
1138
1264
 
1139
- /** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
1140
- * Computes archive and checkpointOutHash from the stored blocks. */
1141
- 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) {
1142
1272
  return await this.db.transactionAsync(async () => {
1143
- const current = await this.getProposedCheckpointNumber();
1144
- if (proposed.checkpointNumber <= current) {
1145
- throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
1146
- }
1147
1273
  const confirmed = await this.getLatestCheckpointNumber();
1148
- if (proposed.checkpointNumber !== confirmed + 1) {
1149
- 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);
1150
1281
  }
1151
1282
 
1152
- // Ensure the previous checkpoint + blocks exist
1283
+ // Ensure the predecessor block (from pending or confirmed chain) exists
1153
1284
  const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
1154
1285
  const blocks: L2Block[] = [];
1155
1286
  for (let i = 0; i < proposed.blockCount; i++) {
@@ -1164,7 +1295,7 @@ export class BlockStore {
1164
1295
  const archive = blocks[blocks.length - 1].archive;
1165
1296
  const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
1166
1297
 
1167
- await this.#proposedCheckpoint.set({
1298
+ await this.#proposedCheckpoints.set(proposed.checkpointNumber, {
1168
1299
  header: proposed.header.toBuffer(),
1169
1300
  archive: archive.toBuffer(),
1170
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 () => {
@@ -25,6 +25,49 @@ export class ContractInstanceStore {
25
25
  this.#contractInstanceUpdates = db.openMap('archiver_contract_instance_updates');
26
26
  }
27
27
 
28
+ /**
29
+ * Adds multiple contract instances to the store.
30
+ * @param data - Contract instances to add.
31
+ * @param blockNumber - L2 block number where the instances were deployed.
32
+ * @returns True if every insert succeeded.
33
+ */
34
+ async addContractInstances(data: ContractInstanceWithAddress[], blockNumber: number): Promise<boolean> {
35
+ return (await Promise.all(data.map(c => this.addContractInstance(c, blockNumber)))).every(Boolean);
36
+ }
37
+
38
+ /**
39
+ * Removes multiple contract instances from the store.
40
+ * @param data - Contract instances to delete.
41
+ * @returns True if every delete succeeded.
42
+ */
43
+ async deleteContractInstances(data: ContractInstanceWithAddress[]): Promise<boolean> {
44
+ return (await Promise.all(data.map(c => this.deleteContractInstance(c)))).every(Boolean);
45
+ }
46
+
47
+ /**
48
+ * Adds multiple contract instance updates to the store.
49
+ * @param data - Contract instance updates to add.
50
+ * @param timestamp - Timestamp at which the updates were scheduled.
51
+ * @returns True if every insert succeeded.
52
+ */
53
+ async addContractInstanceUpdates(data: ContractInstanceUpdateWithAddress[], timestamp: UInt64): Promise<boolean> {
54
+ return (
55
+ await Promise.all(data.map((update, logIndex) => this.addContractInstanceUpdate(update, timestamp, logIndex)))
56
+ ).every(Boolean);
57
+ }
58
+
59
+ /**
60
+ * Removes multiple contract instance updates from the store.
61
+ * @param data - Contract instance updates to delete.
62
+ * @param timestamp - Timestamp at which the updates were scheduled.
63
+ * @returns True if every delete succeeded.
64
+ */
65
+ async deleteContractInstanceUpdates(data: ContractInstanceUpdateWithAddress[], timestamp: UInt64): Promise<boolean> {
66
+ return (
67
+ await Promise.all(data.map((update, logIndex) => this.deleteContractInstanceUpdate(update, timestamp, logIndex)))
68
+ ).every(Boolean);
69
+ }
70
+
28
71
  addContractInstance(contractInstance: ContractInstanceWithAddress, blockNumber: number): Promise<void> {
29
72
  return this.db.transactionAsync(async () => {
30
73
  const key = contractInstance.address.toString();