@aztec/archiver 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a

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 (106) hide show
  1. package/README.md +7 -5
  2. package/dest/archiver.d.ts +49 -14
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +216 -68
  5. package/dest/config.d.ts +4 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +11 -16
  8. package/dest/errors.d.ts +19 -13
  9. package/dest/errors.d.ts.map +1 -1
  10. package/dest/errors.js +32 -16
  11. package/dest/factory.d.ts +12 -6
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +31 -21
  14. package/dest/index.d.ts +9 -2
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +8 -1
  17. package/dest/l1/calldata_retriever.d.ts +2 -1
  18. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  19. package/dest/l1/calldata_retriever.js +13 -4
  20. package/dest/l1/trace_tx.d.ts +12 -66
  21. package/dest/l1/trace_tx.d.ts.map +1 -1
  22. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  23. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  24. package/dest/modules/contract_data_source_adapter.js +40 -0
  25. package/dest/modules/data_source_base.d.ts +71 -49
  26. package/dest/modules/data_source_base.d.ts.map +1 -1
  27. package/dest/modules/data_source_base.js +269 -143
  28. package/dest/modules/data_store_updater.d.ts +18 -6
  29. package/dest/modules/data_store_updater.d.ts.map +1 -1
  30. package/dest/modules/data_store_updater.js +108 -59
  31. package/dest/modules/instrumentation.js +3 -1
  32. package/dest/modules/l1_synchronizer.d.ts +6 -5
  33. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  34. package/dest/modules/l1_synchronizer.js +203 -100
  35. package/dest/modules/outbox_trees_resolver.d.ts +62 -0
  36. package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
  37. package/dest/modules/outbox_trees_resolver.js +162 -0
  38. package/dest/modules/validation.d.ts +4 -3
  39. package/dest/modules/validation.d.ts.map +1 -1
  40. package/dest/modules/validation.js +4 -4
  41. package/dest/store/block_store.d.ts +155 -98
  42. package/dest/store/block_store.d.ts.map +1 -1
  43. package/dest/store/block_store.js +473 -265
  44. package/dest/store/contract_class_store.d.ts +17 -3
  45. package/dest/store/contract_class_store.d.ts.map +1 -1
  46. package/dest/store/contract_class_store.js +17 -1
  47. package/dest/store/contract_instance_store.d.ts +28 -1
  48. package/dest/store/contract_instance_store.d.ts.map +1 -1
  49. package/dest/store/contract_instance_store.js +31 -0
  50. package/dest/store/data_stores.d.ts +68 -0
  51. package/dest/store/data_stores.d.ts.map +1 -0
  52. package/dest/store/data_stores.js +54 -0
  53. package/dest/store/function_names_cache.d.ts +17 -0
  54. package/dest/store/function_names_cache.d.ts.map +1 -0
  55. package/dest/store/function_names_cache.js +30 -0
  56. package/dest/store/l2_tips_cache.d.ts +13 -8
  57. package/dest/store/l2_tips_cache.d.ts.map +1 -1
  58. package/dest/store/l2_tips_cache.js +13 -96
  59. package/dest/store/log_store.d.ts +42 -40
  60. package/dest/store/log_store.d.ts.map +1 -1
  61. package/dest/store/log_store.js +257 -478
  62. package/dest/store/log_store_codec.d.ts +70 -0
  63. package/dest/store/log_store_codec.d.ts.map +1 -0
  64. package/dest/store/log_store_codec.js +101 -0
  65. package/dest/store/message_store.d.ts +9 -3
  66. package/dest/store/message_store.d.ts.map +1 -1
  67. package/dest/store/message_store.js +31 -1
  68. package/dest/test/fake_l1_state.d.ts +2 -1
  69. package/dest/test/fake_l1_state.d.ts.map +1 -1
  70. package/dest/test/fake_l1_state.js +25 -8
  71. package/dest/test/mock_l2_block_source.d.ts +47 -51
  72. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  73. package/dest/test/mock_l2_block_source.js +223 -185
  74. package/dest/test/noop_l1_archiver.d.ts +10 -6
  75. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  76. package/dest/test/noop_l1_archiver.js +19 -8
  77. package/package.json +14 -14
  78. package/src/archiver.ts +265 -72
  79. package/src/config.ts +12 -19
  80. package/src/errors.ts +46 -23
  81. package/src/factory.ts +38 -20
  82. package/src/index.ts +16 -1
  83. package/src/l1/calldata_retriever.ts +14 -4
  84. package/src/modules/contract_data_source_adapter.ts +55 -0
  85. package/src/modules/data_source_base.ts +335 -187
  86. package/src/modules/data_store_updater.ts +118 -61
  87. package/src/modules/instrumentation.ts +1 -1
  88. package/src/modules/l1_synchronizer.ts +278 -120
  89. package/src/modules/outbox_trees_resolver.ts +199 -0
  90. package/src/modules/validation.ts +8 -7
  91. package/src/store/block_store.ts +566 -323
  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 +104 -0
  95. package/src/store/function_names_cache.ts +37 -0
  96. package/src/store/l2_tips_cache.ts +16 -115
  97. package/src/store/log_store.ts +296 -650
  98. package/src/store/log_store_codec.ts +132 -0
  99. package/src/store/message_store.ts +35 -2
  100. package/src/test/fake_l1_state.ts +24 -14
  101. package/src/test/mock_l2_block_source.ts +271 -239
  102. package/src/test/noop_l1_archiver.ts +39 -8
  103. package/dest/store/kv_archiver_store.d.ts +0 -383
  104. package/dest/store/kv_archiver_store.d.ts.map +0 -1
  105. package/dest/store/kv_archiver_store.js +0 -501
  106. package/src/store/kv_archiver_store.ts +0 -728
@@ -12,9 +12,11 @@ import {
12
12
  type BlockData,
13
13
  BlockHash,
14
14
  Body,
15
- CheckpointedL2Block,
16
15
  CommitteeAttestation,
16
+ GENESIS_CHECKPOINT_HEADER_HASH,
17
17
  L2Block,
18
+ type L2TipId,
19
+ type L2Tips,
18
20
  type ValidateCheckpointResult,
19
21
  deserializeValidateCheckpointResult,
20
22
  serializeValidateCheckpointResult,
@@ -28,7 +30,6 @@ import {
28
30
  type ProposedCheckpointInput,
29
31
  PublishedCheckpoint,
30
32
  } from '@aztec/stdlib/checkpoint';
31
- import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
32
33
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
33
34
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
34
35
  import {
@@ -36,8 +37,6 @@ import {
36
37
  type IndexedTxEffect,
37
38
  TxEffect,
38
39
  TxHash,
39
- TxReceipt,
40
- TxStatus,
41
40
  deserializeIndexedTxEffect,
42
41
  serializeIndexedTxEffect,
43
42
  } from '@aztec/stdlib/tx';
@@ -45,6 +44,7 @@ import {
45
44
  import {
46
45
  BlockAlreadyCheckpointedError,
47
46
  BlockArchiveNotConsistentError,
47
+ BlockCheckpointNumberNotSequentialError,
48
48
  BlockIndexNotSequentialError,
49
49
  BlockNotFoundError,
50
50
  BlockNumberNotSequentialError,
@@ -56,10 +56,9 @@ import {
56
56
  ProposedCheckpointArchiveRootMismatchError,
57
57
  ProposedCheckpointNotSequentialError,
58
58
  ProposedCheckpointPromotionNotSequentialError,
59
- ProposedCheckpointStaleError,
60
59
  } from '../errors.js';
61
60
 
62
- export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
61
+ export type { TxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
63
62
 
64
63
  type BlockIndexValue = [blockNumber: number, index: number];
65
64
 
@@ -71,6 +70,41 @@ type BlockStorage = {
71
70
  indexWithinCheckpoint: number;
72
71
  };
73
72
 
73
+ /** Reason a checkpoint was rejected during sync. */
74
+ export type RejectedCheckpointReason = 'invalid-attestations' | 'descends-from-invalid-attestations';
75
+
76
+ /**
77
+ * A checkpoint observed on L1 that the archiver decided not to ingest, recorded so that
78
+ * any descendant that builds on top of it can also be skipped (rather than throwing
79
+ * `InitialCheckpointNumberNotSequentialError` and looping). An entry is dropped via
80
+ * {@link BlockStore.removeRejectedCheckpointByArchiveRoot} once a checkpoint with the same
81
+ * archive root is later ingested as valid (e.g. it gathered enough attestations), which
82
+ * re-enables its descendants.
83
+ */
84
+ export type RejectedCheckpoint = {
85
+ /** Checkpoint number this entry represents. */
86
+ checkpointNumber: CheckpointNumber;
87
+ /** Archive root produced by this rejected checkpoint (matched against descendants' `lastArchiveRoot`). */
88
+ archiveRoot: Fr;
89
+ /** `lastArchiveRoot` from this checkpoint's header (the ancestor it built on). */
90
+ parentArchiveRoot: Fr;
91
+ /** Slot number of the rejected checkpoint. */
92
+ slotNumber: SlotNumber;
93
+ /** L1 publication data for the rejected checkpoint (block number, hash, timestamp). */
94
+ l1: L1PublishedData;
95
+ /** Why the entry was recorded. */
96
+ reason: RejectedCheckpointReason;
97
+ };
98
+
99
+ type RejectedCheckpointStorage = {
100
+ checkpointNumber: number;
101
+ archiveRoot: Buffer;
102
+ parentArchiveRoot: Buffer;
103
+ slotNumber: number;
104
+ l1: Buffer;
105
+ reason: RejectedCheckpointReason;
106
+ };
107
+
74
108
  /** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
75
109
  type CommonCheckpointStorage = {
76
110
  header: Buffer;
@@ -84,6 +118,7 @@ type CommonCheckpointStorage = {
84
118
  type CheckpointStorage = CommonCheckpointStorage & {
85
119
  l1: Buffer;
86
120
  attestations: Buffer[];
121
+ feeAssetPriceModifier: string;
87
122
  };
88
123
 
89
124
  /** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
@@ -94,6 +129,19 @@ type ProposedCheckpointStorage = CommonCheckpointStorage & {
94
129
 
95
130
  export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
96
131
 
132
+ /**
133
+ * Single-block lookup with the chain-tip `tag` variant of {@link BlockQuery} already resolved
134
+ * to a concrete block number. The `tag` branch is unrepresentable here so storage code does
135
+ * not need to handle it at runtime.
136
+ */
137
+ export type ResolvedBlockQuery = { number: BlockNumber } | { hash: BlockHash } | { archive: Fr };
138
+
139
+ /**
140
+ * Range lookup with the `epoch` variant of {@link BlocksQuery} already resolved to a
141
+ * `{ from, limit }` pair. Storage code never needs to map epoch numbers to block ranges.
142
+ */
143
+ export type ResolvedBlocksQuery = { from: BlockNumber; limit: number; onlyCheckpointed?: boolean };
144
+
97
145
  /**
98
146
  * LMDB-based block storage for the archiver.
99
147
  */
@@ -101,7 +149,10 @@ export class BlockStore {
101
149
  /** Map block number to block data */
102
150
  #blocks: AztecAsyncMap<number, BlockStorage>;
103
151
 
104
- /** Map checkpoint number to checkpoint data */
152
+ /** Map keyed by checkpoint number holding proposed (locally-validated, not yet L1-confirmed) checkpoints. */
153
+ #proposedCheckpoints: AztecAsyncMap<number, ProposedCheckpointStorage>;
154
+
155
+ /** Map checkpoint number to checkpoint data for mined checkpoints only */
105
156
  #checkpoints: AztecAsyncMap<number, CheckpointStorage>;
106
157
 
107
158
  /** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
@@ -134,8 +185,11 @@ export class BlockStore {
134
185
  /** Index mapping block archive to block number */
135
186
  #blockArchiveIndex: AztecAsyncMap<string, number>;
136
187
 
137
- /** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
138
- #proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
188
+ /** Map rejected checkpoints (due to invalid attestations) by archive root */
189
+ #rejectedCheckpoints: AztecAsyncMap<string, RejectedCheckpointStorage>;
190
+
191
+ /** Index mapping a rejected checkpoint's number to its archive root, so the latest can be read in reverse order */
192
+ #rejectedCheckpointsByNumber: AztecAsyncMap<number, string>;
139
193
 
140
194
  #log = createLogger('archiver:block_store');
141
195
 
@@ -152,7 +206,9 @@ export class BlockStore {
152
206
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
153
207
  this.#checkpoints = db.openMap('archiver_checkpoints');
154
208
  this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
155
- this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
209
+ this.#proposedCheckpoints = db.openMap('archiver_proposed_checkpoints');
210
+ this.#rejectedCheckpoints = db.openMap('archiver_rejected_checkpoints');
211
+ this.#rejectedCheckpointsByNumber = db.openMap('archiver_rejected_checkpoints_by_number');
156
212
  }
157
213
 
158
214
  /**
@@ -188,14 +244,13 @@ export class BlockStore {
188
244
 
189
245
  // Extract the latest block and checkpoint numbers
190
246
  const previousBlockNumber = await this.getLatestL2BlockNumber();
191
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
192
- const previousCheckpointNumber = await this.getLatestCheckpointNumber();
247
+ const latestCheckpointNumber = await this.getLatestCheckpointNumber();
193
248
 
194
249
  // Verify we're not overwriting checkpointed blocks
195
250
  const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
196
251
  if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
197
252
  // Check if the proposed block matches the already-checkpointed one
198
- const existingBlock = await this.getBlock(BlockNumber(blockNumber));
253
+ const existingBlock = await this.getBlockData({ number: BlockNumber(blockNumber) });
199
254
  if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
200
255
  throw new BlockAlreadyCheckpointedError(blockNumber);
201
256
  }
@@ -207,23 +262,18 @@ export class BlockStore {
207
262
  throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
208
263
  }
209
264
 
210
- // The same check as above but for checkpoints. Accept the block if either the confirmed
211
- // checkpoint or the pending (locally validated but not yet confirmed) checkpoint matches.
265
+ // Accept the block if either the confirmed checkpoint or a pending checkpoint matches
266
+ // the expected predecessor. We look for a pending entry at exactly blockCheckpointNumber - 1.
212
267
  const expectedCheckpointNumber = blockCheckpointNumber - 1;
213
- if (
214
- !opts.force &&
215
- previousCheckpointNumber !== expectedCheckpointNumber &&
216
- proposedCheckpointNumber !== expectedCheckpointNumber
217
- ) {
218
- const [reported, source]: [CheckpointNumber, 'confirmed' | 'proposed'] =
219
- proposedCheckpointNumber > previousCheckpointNumber
220
- ? [proposedCheckpointNumber, 'proposed']
221
- : [previousCheckpointNumber, 'confirmed'];
222
- throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, reported, source);
268
+ const hasPendingAtExpected = await this.#proposedCheckpoints.hasAsync(expectedCheckpointNumber);
269
+ if (!opts.force && latestCheckpointNumber !== expectedCheckpointNumber && !hasPendingAtExpected) {
270
+ const [latestPendingKey] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
271
+ const previous = CheckpointNumber(Math.max(latestCheckpointNumber, latestPendingKey ?? 0));
272
+ throw new BlockCheckpointNumberNotSequentialError(BlockNumber(blockNumber), blockCheckpointNumber, previous);
223
273
  }
224
274
 
225
275
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
226
- const previousBlockResult = await this.getBlock(previousBlockNumber);
276
+ const previousBlockResult = await this.getBlockData({ number: previousBlockNumber });
227
277
 
228
278
  let expectedBlockIndex = 0;
229
279
  let previousBlockIndex: number | undefined = undefined;
@@ -236,7 +286,7 @@ export class BlockStore {
236
286
  if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
237
287
  throw new BlockArchiveNotConsistentError(
238
288
  blockNumber,
239
- previousBlockResult.number,
289
+ previousBlockResult.header.globalVariables.blockNumber,
240
290
  blockLastArchive,
241
291
  previousBlockResult.archive.root,
242
292
  );
@@ -322,17 +372,21 @@ export class BlockStore {
322
372
  checkpointNumber: checkpoint.checkpoint.number,
323
373
  startBlock: checkpoint.checkpoint.blocks[0].number,
324
374
  blockCount: checkpoint.checkpoint.blocks.length,
375
+ feeAssetPriceModifier: checkpoint.checkpoint.feeAssetPriceModifier.toString(),
325
376
  });
326
377
 
327
378
  // Update slot-to-checkpoint index
328
379
  await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
329
- }
330
380
 
331
- // Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
332
- const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
333
- await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
381
+ // Remove proposed checkpoint if it exists, since L1 is authoritative
382
+ await this.#proposedCheckpoints.delete(checkpoint.checkpoint.number);
334
383
 
335
- await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
384
+ // Drop any rejected entry for this archive root: a checkpoint that was previously rejected
385
+ // (e.g. invalid attestations) is now being ingested as valid, so its descendants are allowed.
386
+ await this.removeRejectedCheckpointByArchiveRoot(checkpoint.checkpoint.archive.root);
387
+ }
388
+
389
+ await this.advanceSynchedL1BlockNumber(checkpoints[checkpoints.length - 1].l1.blockNumber);
336
390
  return true;
337
391
  });
338
392
  }
@@ -374,9 +428,10 @@ export class BlockStore {
374
428
  checkpointNumber: incoming.checkpoint.number,
375
429
  startBlock: incoming.checkpoint.blocks[0].number,
376
430
  blockCount: incoming.checkpoint.blocks.length,
431
+ feeAssetPriceModifier: incoming.checkpoint.feeAssetPriceModifier.toString(),
377
432
  });
378
433
  // Update the sync point to reflect the new L1 block
379
- await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
434
+ await this.advanceSynchedL1BlockNumber(incoming.l1.blockNumber);
380
435
  }
381
436
  return checkpoints.slice(i);
382
437
  }
@@ -391,24 +446,27 @@ export class BlockStore {
391
446
  return undefined;
392
447
  }
393
448
 
394
- const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
395
- if (previousCheckpointData === undefined) {
449
+ // Check across both proposed and mined checkpoints
450
+ const predecessor =
451
+ (await this.getProposedCheckpointByNumber(previousCheckpointNumber)) ??
452
+ (await this.getCheckpointData(previousCheckpointNumber));
453
+
454
+ if (!predecessor) {
396
455
  throw new CheckpointNotFoundError(previousCheckpointNumber);
397
456
  }
398
457
 
399
- const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
400
- const previousBlock = await this.getBlock(previousBlockNumber);
458
+ const previousBlockNumber = BlockNumber(predecessor.startBlock + predecessor.blockCount - 1);
459
+ const previousBlock = await this.getBlock({ number: previousBlockNumber });
401
460
  if (previousBlock === undefined) {
402
461
  throw new BlockNotFoundError(previousBlockNumber);
403
462
  }
404
-
405
463
  return previousBlock;
406
464
  }
407
465
 
408
466
  /**
409
467
  * Validates that blocks are sequential, have correct indexes, and chain via archive roots.
410
468
  * This is the same validation used for both confirmed checkpoints (addCheckpoints) and
411
- * proposed checkpoints (setProposedCheckpoint).
469
+ * proposed checkpoints (addProposedCheckpoint).
412
470
  */
413
471
  private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
414
472
  for (const block of blocks) {
@@ -460,6 +518,7 @@ export class BlockStore {
460
518
  l2BlockNumber: block.number,
461
519
  l2BlockHash: blockHash,
462
520
  txIndexInBlock: i,
521
+ slotNumber: block.header.globalVariables.slotNumber,
463
522
  };
464
523
  await this.#txEffects.set(txEffect.data.txHash.toString(), serializeIndexedTxEffect(txEffect));
465
524
  }
@@ -535,11 +594,8 @@ export class BlockStore {
535
594
  this.#log.debug(`Removed checkpoint ${c}`);
536
595
  }
537
596
 
538
- // Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
539
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
540
- if (proposedCheckpointNumber > checkpointNumber) {
541
- await this.#proposedCheckpoint.delete();
542
- }
597
+ // Evict all pending checkpoints > checkpointNumber (their base chain no longer exists)
598
+ await this.evictProposedCheckpointsFrom(CheckpointNumber(checkpointNumber + 1));
543
599
 
544
600
  return { blocksRemoved };
545
601
  });
@@ -580,6 +636,22 @@ export class BlockStore {
580
636
  return result;
581
637
  }
582
638
 
639
+ /**
640
+ * Returns the checkpoint numbers for all checkpoints whose slot falls within the given range (inclusive).
641
+ * Lighter than {@link getCheckpointDataForSlotRange} when callers only need to identify which
642
+ * checkpoints fall in the range and will fetch full data for at most a few of them.
643
+ */
644
+ async getCheckpointNumbersForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointNumber[]> {
645
+ const result: CheckpointNumber[] = [];
646
+ for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
647
+ start: startSlot,
648
+ end: endSlot + 1,
649
+ })) {
650
+ result.push(CheckpointNumber(checkpointNumber));
651
+ }
652
+ return result;
653
+ }
654
+
583
655
  private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
584
656
  return {
585
657
  header: CheckpointHeader.fromBuffer(checkpointStorage.header),
@@ -588,6 +660,7 @@ export class BlockStore {
588
660
  checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
589
661
  startBlock: BlockNumber(checkpointStorage.startBlock),
590
662
  blockCount: checkpointStorage.blockCount,
663
+ feeAssetPriceModifier: BigInt(checkpointStorage.feeAssetPriceModifier),
591
664
  l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
592
665
  attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
593
666
  };
@@ -650,7 +723,7 @@ export class BlockStore {
650
723
 
651
724
  // Iterate from blockNumber + 1 to latestBlockNumber
652
725
  for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
653
- const block = await this.getBlock(BlockNumber(bn));
726
+ const block = await this.getBlock({ number: BlockNumber(bn) });
654
727
 
655
728
  if (block === undefined) {
656
729
  this.#log.warn(`Cannot remove block ${bn} from the store since we don't have it`);
@@ -688,28 +761,34 @@ export class BlockStore {
688
761
  }
689
762
 
690
763
  async hasProposedCheckpoint(): Promise<boolean> {
691
- const proposed = await this.#proposedCheckpoint.getAsync();
692
- return proposed !== undefined;
764
+ const [key] = await toArray(this.#proposedCheckpoints.keysAsync({ limit: 1 }));
765
+ return key !== undefined;
693
766
  }
694
767
 
695
- /** Deletes the proposed checkpoint from storage. */
696
- async deleteProposedCheckpoint(): Promise<void> {
697
- await this.#proposedCheckpoint.delete();
768
+ /** Deletes all pending proposed checkpoints from storage. */
769
+ async deleteProposedCheckpoints(): Promise<void> {
770
+ for await (const key of this.#proposedCheckpoints.keysAsync()) {
771
+ await this.#proposedCheckpoints.delete(key);
772
+ }
698
773
  }
699
774
 
700
775
  /**
701
- * Promotes the proposed checkpoint singleton to a confirmed checkpoint entry.
702
- * This persists the checkpoint to the store, clears the proposed singleton, and updates the L1 sync point.
703
- * Should only be called after the checkpoint has been validated.
704
- * @param expectedArchiveRoot - The archive root to match against the proposed checkpoint, to guard against races.
776
+ * Promotes a specific pending checkpoint to a confirmed checkpoint entry.
777
+ * This persists the checkpoint to the store, removes only that pending entry, and updates the L1 sync point.
778
+ * Remaining pending entries (e.g. N+1, N+2) are left intact — they chain off the just-promoted one.
779
+ * @param checkpointNumber - The checkpoint number to promote.
780
+ * @param l1 - L1 published data for the checkpoint.
781
+ * @param attestations - Committee attestations.
782
+ * @param expectedArchiveRoot - Archive root guard against races.
705
783
  */
706
784
  async promoteProposedToCheckpointed(
785
+ checkpointNumber: CheckpointNumber,
707
786
  l1: L1PublishedData,
708
787
  attestations: CommitteeAttestation[],
709
788
  expectedArchiveRoot: Fr,
710
789
  ): Promise<void> {
711
790
  return await this.db.transactionAsync(async () => {
712
- const proposed = await this.getProposedCheckpointOnly();
791
+ const proposed = await this.getProposedCheckpointByNumber(checkpointNumber);
713
792
  if (!proposed) {
714
793
  throw new NoProposedCheckpointToPromoteError();
715
794
  }
@@ -733,48 +812,86 @@ export class BlockStore {
733
812
  checkpointNumber: proposed.checkpointNumber,
734
813
  startBlock: proposed.startBlock,
735
814
  blockCount: proposed.blockCount,
815
+ feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
736
816
  });
737
817
 
738
818
  // Update the slot-to-checkpoint index
739
819
  await this.#slotToCheckpoint.set(proposed.header.slotNumber, proposed.checkpointNumber);
740
820
 
741
- // Clear the proposed checkpoint singleton
742
- await this.#proposedCheckpoint.delete();
821
+ // Remove only this pending entry — remaining entries N+1, N+2, ... stay valid
822
+ await this.#proposedCheckpoints.delete(proposed.checkpointNumber);
823
+
824
+ // Drop any rejected entry for this archive root: a checkpoint that was previously rejected
825
+ // (e.g. invalid attestations) is now being promoted as valid, so its descendants are allowed.
826
+ await this.removeRejectedCheckpointByArchiveRoot(proposed.archive.root);
743
827
 
744
828
  // Update the last synced L1 block
745
- await this.#lastSynchedL1Block.set(l1.blockNumber);
829
+ await this.advanceSynchedL1BlockNumber(l1.blockNumber);
746
830
  });
747
831
  }
748
832
 
749
- /** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
750
- async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
751
- const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
752
- if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
753
- await this.#proposedCheckpoint.delete();
833
+ /**
834
+ * Returns the latest pending checkpoint (highest-numbered entry), or undefined if none.
835
+ * No fallback to confirmed.
836
+ */
837
+ async getLastProposedCheckpoint(): Promise<ProposedCheckpointData | undefined> {
838
+ const [key] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
839
+ if (key === undefined) {
840
+ return undefined;
754
841
  }
842
+ const stored = await this.#proposedCheckpoints.getAsync(key);
843
+ return stored ? this.convertToProposedCheckpointData(stored) : undefined;
755
844
  }
756
845
 
757
- /** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
758
- async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
759
- const stored = await this.#proposedCheckpoint.getAsync();
760
- if (!stored) {
761
- return undefined;
846
+ /** Returns the pending checkpoint for a specific checkpoint number, or undefined if not found. */
847
+ async getProposedCheckpointByNumber(n: CheckpointNumber): Promise<ProposedCheckpointData | undefined> {
848
+ const stored = await this.#proposedCheckpoints.getAsync(n);
849
+ return stored ? this.convertToProposedCheckpointData(stored) : undefined;
850
+ }
851
+
852
+ /**
853
+ * Returns the pending checkpoint whose header slot matches the given slot, or undefined if not found.
854
+ * Iterates `#proposedCheckpoints` rather than reading an index because the map carries 0–1 entries
855
+ * in normal operation (bounded by the proposer pipelining window). Returns the first match.
856
+ */
857
+ async getProposedCheckpointBySlot(slot: SlotNumber): Promise<ProposedCheckpointData | undefined> {
858
+ for await (const [, stored] of this.#proposedCheckpoints.entriesAsync()) {
859
+ const header = CheckpointHeader.fromBuffer(stored.header);
860
+ if (header.slotNumber === slot) {
861
+ return this.convertToProposedCheckpointData(stored);
862
+ }
762
863
  }
763
- return this.convertToProposedCheckpointData(stored);
864
+ return undefined;
764
865
  }
765
866
 
766
867
  /**
767
- * Gets the checkpoint at the proposed tip
768
- * - pending checkpoint if it exists
868
+ * Evicts all pending checkpoints with checkpoint number >= fromNumber.
869
+ * Used for divergent-mined-checkpoint cleanup: when L1 mines checkpoint N with a different archive,
870
+ * all pending >= N must be evicted since they chain off the now-invalid pending N.
871
+ */
872
+ async evictProposedCheckpointsFrom(fromNumber: CheckpointNumber): Promise<void> {
873
+ const keysToDelete: number[] = [];
874
+ for await (const key of this.#proposedCheckpoints.keysAsync()) {
875
+ if (key >= fromNumber) {
876
+ keysToDelete.push(key);
877
+ }
878
+ }
879
+ for (const key of keysToDelete) {
880
+ await this.#proposedCheckpoints.delete(key);
881
+ }
882
+ }
883
+
884
+ /**
885
+ * Gets the checkpoint at the proposed tip:
886
+ * - latest pending checkpoint if any exist
769
887
  * - fallsback to latest confirmed checkpoint otherwise
770
- * @returns CommonCheckpointData
771
888
  */
772
- async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
773
- const stored = await this.#proposedCheckpoint.getAsync();
774
- if (!stored) {
889
+ async getLastCheckpoint(): Promise<CommonCheckpointData | undefined> {
890
+ const latest = await this.getLastProposedCheckpoint();
891
+ if (!latest) {
775
892
  return this.getCheckpointData(await this.getLatestCheckpointNumber());
776
893
  }
777
- return this.convertToProposedCheckpointData(stored);
894
+ return latest;
778
895
  }
779
896
 
780
897
  private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
@@ -795,7 +912,7 @@ export class BlockStore {
795
912
  * @returns CheckpointNumber
796
913
  */
797
914
  async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
798
- const proposed = await this.getProposedCheckpoint();
915
+ const proposed = await this.getLastCheckpoint();
799
916
  if (!proposed) {
800
917
  return await this.getLatestCheckpointNumber();
801
918
  }
@@ -807,165 +924,40 @@ export class BlockStore {
807
924
  * @returns BlockNumber
808
925
  */
809
926
  async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
810
- const proposed = await this.getProposedCheckpoint();
927
+ const proposed = await this.getLastCheckpoint();
811
928
  if (!proposed) {
812
929
  return await this.getCheckpointedL2BlockNumber();
813
930
  }
814
931
  return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
815
932
  }
816
933
 
817
- async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
818
- const blockStorage = await this.#blocks.getAsync(number);
819
- if (!blockStorage) {
820
- return undefined;
821
- }
822
- const checkpoint = await this.#checkpoints.getAsync(blockStorage.checkpointNumber);
823
- if (!checkpoint) {
824
- return undefined;
825
- }
826
- const block = await this.getBlockFromBlockStorage(number, blockStorage);
827
- if (!block) {
828
- return undefined;
829
- }
830
- return new CheckpointedL2Block(
831
- CheckpointNumber(checkpoint.checkpointNumber),
832
- block,
833
- L1PublishedData.fromBuffer(checkpoint.l1),
834
- checkpoint.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
835
- );
836
- }
837
-
838
- /**
839
- * Gets up to `limit` amount of Checkpointed L2 blocks starting from `from`.
840
- * @param start - Number of the first block to return (inclusive).
841
- * @param limit - The number of blocks to return.
842
- * @returns The requested L2 blocks
843
- */
844
- async *getCheckpointedBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<CheckpointedL2Block> {
845
- const checkpointCache = new Map<CheckpointNumber, CheckpointStorage>();
846
- for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
847
- const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
848
- if (block) {
849
- const checkpoint =
850
- checkpointCache.get(CheckpointNumber(blockStorage.checkpointNumber)) ??
851
- (await this.#checkpoints.getAsync(blockStorage.checkpointNumber));
852
- if (checkpoint) {
853
- checkpointCache.set(CheckpointNumber(blockStorage.checkpointNumber), checkpoint);
854
- const checkpointedBlock = new CheckpointedL2Block(
855
- CheckpointNumber(checkpoint.checkpointNumber),
856
- block,
857
- L1PublishedData.fromBuffer(checkpoint.l1),
858
- checkpoint.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
859
- );
860
- yield checkpointedBlock;
861
- }
862
- }
863
- }
934
+ /** Returns the checkpoint number that contains the given slot (or undefined if not found). */
935
+ async getCheckpointNumberBySlot(slot: SlotNumber): Promise<CheckpointNumber | undefined> {
936
+ const checkpointNumber = await this.#slotToCheckpoint.getAsync(slot);
937
+ return checkpointNumber === undefined ? undefined : CheckpointNumber(checkpointNumber);
864
938
  }
865
939
 
866
- async getCheckpointedBlockByHash(blockHash: BlockHash): Promise<CheckpointedL2Block | undefined> {
867
- const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
940
+ /** Gets a single L2 block matching the given resolved query. */
941
+ async getBlock(query: ResolvedBlockQuery): Promise<L2Block | undefined> {
942
+ const blockNumber = await this.getBlockNumber(query);
868
943
  if (blockNumber === undefined) {
869
944
  return undefined;
870
945
  }
871
- return this.getCheckpointedBlock(BlockNumber(blockNumber));
872
- }
873
-
874
- async getCheckpointedBlockByArchive(archive: Fr): Promise<CheckpointedL2Block | undefined> {
875
- const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
876
- if (blockNumber === undefined) {
877
- return undefined;
878
- }
879
- return this.getCheckpointedBlock(BlockNumber(blockNumber));
880
- }
881
-
882
- /**
883
- * Gets up to `limit` amount of L2 blocks starting from `from`.
884
- * @param start - Number of the first block to return (inclusive).
885
- * @param limit - The number of blocks to return.
886
- * @returns The requested L2 blocks
887
- */
888
- async *getBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<L2Block> {
889
- for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
890
- const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
891
- if (block) {
892
- yield block;
893
- }
894
- }
895
- }
896
-
897
- /**
898
- * Gets block metadata (without tx data) by block number.
899
- * @param blockNumber - The number of the block to return.
900
- * @returns The requested block data.
901
- */
902
- async getBlockData(blockNumber: BlockNumber): Promise<BlockData | undefined> {
903
946
  const blockStorage = await this.#blocks.getAsync(blockNumber);
904
- if (!blockStorage || !blockStorage.header) {
905
- return undefined;
906
- }
907
- return this.getBlockDataFromBlockStorage(blockStorage);
908
- }
909
-
910
- /**
911
- * Gets block metadata (without tx data) by archive root.
912
- * @param archive - The archive root of the block to return.
913
- * @returns The requested block data.
914
- */
915
- async getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
916
- const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
917
- if (blockNumber === undefined) {
947
+ if (!blockStorage) {
918
948
  return undefined;
919
949
  }
920
- return this.getBlockData(BlockNumber(blockNumber));
921
- }
922
-
923
- /**
924
- * Gets an L2 block.
925
- * @param blockNumber - The number of the block to return.
926
- * @returns The requested L2 block.
927
- */
928
- async getBlock(blockNumber: BlockNumber): Promise<L2Block | undefined> {
929
- const blockStorage = await this.#blocks.getAsync(blockNumber);
930
- if (!blockStorage || !blockStorage.header) {
931
- return Promise.resolve(undefined);
932
- }
933
950
  return this.getBlockFromBlockStorage(blockNumber, blockStorage);
934
951
  }
935
952
 
936
- /**
937
- * Gets an L2 block by its hash.
938
- * @param blockHash - The hash of the block to return.
939
- * @returns The requested L2 block.
940
- */
941
- async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
942
- const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
943
- if (blockNumber === undefined) {
944
- return undefined;
945
- }
946
- return this.getBlock(BlockNumber(blockNumber));
953
+ /** Gets a collection of L2 blocks for a resolved range. */
954
+ getBlocks(query: ResolvedBlocksQuery): Promise<L2Block[]> {
955
+ return toArray(this.iterateBlocks(query));
947
956
  }
948
957
 
949
- /**
950
- * Gets an L2 block by its archive root.
951
- * @param archive - The archive root of the block to return.
952
- * @returns The requested L2 block.
953
- */
954
- async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
955
- const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
956
- if (blockNumber === undefined) {
957
- return undefined;
958
- }
959
- return this.getBlock(BlockNumber(blockNumber));
960
- }
961
-
962
- /**
963
- * Gets a block header by its hash.
964
- * @param blockHash - The hash of the block to return.
965
- * @returns The requested block header.
966
- */
967
- async getBlockHeaderByHash(blockHash: BlockHash): Promise<BlockHeader | undefined> {
968
- const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
958
+ /** Gets single block metadata matching the given resolved query. */
959
+ async getBlockData(query: ResolvedBlockQuery): Promise<BlockData | undefined> {
960
+ const blockNumber = await this.getBlockNumber(query);
969
961
  if (blockNumber === undefined) {
970
962
  return undefined;
971
963
  }
@@ -973,41 +965,36 @@ export class BlockStore {
973
965
  if (!blockStorage || !blockStorage.header) {
974
966
  return undefined;
975
967
  }
976
- return BlockHeader.fromBuffer(blockStorage.header);
968
+ return this.getBlockDataFromBlockStorage(blockStorage);
977
969
  }
978
970
 
979
- /**
980
- * Gets a block header by its archive root.
981
- * @param archive - The archive root of the block to return.
982
- * @returns The requested block header.
983
- */
984
- async getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
985
- const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
986
- if (blockNumber === undefined) {
987
- return undefined;
988
- }
989
- const blockStorage = await this.#blocks.getAsync(blockNumber);
990
- if (!blockStorage || !blockStorage.header) {
991
- return undefined;
971
+ /** Gets a collection of block metadata entries for a resolved range. */
972
+ getBlocksData(query: ResolvedBlocksQuery): Promise<BlockData[]> {
973
+ return toArray(this.iterateBlocksData(query));
974
+ }
975
+
976
+ /** Async iterator over L2 blocks for a resolved range. */
977
+ private async *iterateBlocks(query: ResolvedBlocksQuery): AsyncIterableIterator<L2Block> {
978
+ const cap = query.onlyCheckpointed ? await this.getCheckpointedL2BlockNumber() : undefined;
979
+ for await (const [blockNumber, blockStorage] of this.getBlockStorages(query.from, query.limit)) {
980
+ if (cap !== undefined && blockNumber > cap) {
981
+ break;
982
+ }
983
+ const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
984
+ if (block) {
985
+ yield block;
986
+ }
992
987
  }
993
- return BlockHeader.fromBuffer(blockStorage.header);
994
988
  }
995
989
 
996
- /**
997
- * Gets the headers for a sequence of L2 blocks.
998
- * @param start - Number of the first block to return (inclusive).
999
- * @param limit - The number of blocks to return.
1000
- * @returns The requested L2 block headers
1001
- */
1002
- async *getBlockHeaders(start: BlockNumber, limit: number): AsyncIterableIterator<BlockHeader> {
1003
- for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
1004
- const header = BlockHeader.fromBuffer(blockStorage.header);
1005
- if (header.getBlockNumber() !== blockNumber) {
1006
- throw new Error(
1007
- `Block number mismatch when retrieving block header from archive (expected ${blockNumber} but got ${header.getBlockNumber()})`,
1008
- );
990
+ /** Async iterator over block metadata for a resolved range. */
991
+ private async *iterateBlocksData(query: ResolvedBlocksQuery): AsyncIterableIterator<BlockData> {
992
+ const cap = query.onlyCheckpointed ? await this.getCheckpointedL2BlockNumber() : undefined;
993
+ for await (const [blockNumber, blockStorage] of this.getBlockStorages(query.from, query.limit)) {
994
+ if (cap !== undefined && blockNumber > cap) {
995
+ break;
1009
996
  }
1010
- yield header;
997
+ yield this.getBlockDataFromBlockStorage(blockStorage);
1011
998
  }
1012
999
  }
1013
1000
 
@@ -1024,6 +1011,24 @@ export class BlockStore {
1024
1011
  }
1025
1012
  }
1026
1013
 
1014
+ /** Resolves a ResolvedBlockQuery discriminant to a block number, or undefined if not found. */
1015
+ async getBlockNumber(query: ResolvedBlockQuery): Promise<BlockNumber | undefined> {
1016
+ let blockNumber: BlockNumber | undefined;
1017
+ if ('number' in query) {
1018
+ blockNumber = query.number;
1019
+ } else if ('hash' in query) {
1020
+ const n = await this.#blockHashIndex.getAsync(query.hash.toString());
1021
+ blockNumber = n !== undefined ? BlockNumber(n) : undefined;
1022
+ } else {
1023
+ const n = await this.#blockArchiveIndex.getAsync(query.archive.toString());
1024
+ blockNumber = n !== undefined ? BlockNumber(n) : undefined;
1025
+ }
1026
+ if (blockNumber === undefined) {
1027
+ return undefined;
1028
+ }
1029
+ return blockNumber;
1030
+ }
1031
+
1027
1032
  private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
1028
1033
  return {
1029
1034
  header: BlockHeader.fromBuffer(blockStorage.header),
@@ -1085,56 +1090,6 @@ export class BlockStore {
1085
1090
  return deserializeIndexedTxEffect(buffer);
1086
1091
  }
1087
1092
 
1088
- /**
1089
- * Gets a receipt of a settled tx.
1090
- * @param txHash - The hash of a tx we try to get the receipt for.
1091
- * @returns The requested tx receipt (or undefined if not found).
1092
- */
1093
- async getSettledTxReceipt(
1094
- txHash: TxHash,
1095
- l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
1096
- ): Promise<TxReceipt | undefined> {
1097
- const txEffect = await this.getTxEffect(txHash);
1098
- if (!txEffect) {
1099
- return undefined;
1100
- }
1101
-
1102
- const blockNumber = BlockNumber(txEffect.l2BlockNumber);
1103
-
1104
- // Use existing archiver methods to determine finalization level
1105
- const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
1106
- this.getProvenBlockNumber(),
1107
- this.getCheckpointedL2BlockNumber(),
1108
- this.getFinalizedL2BlockNumber(),
1109
- this.getBlockData(blockNumber),
1110
- ]);
1111
-
1112
- let status: TxStatus;
1113
- if (blockNumber <= finalizedBlockNumber) {
1114
- status = TxStatus.FINALIZED;
1115
- } else if (blockNumber <= provenBlockNumber) {
1116
- status = TxStatus.PROVEN;
1117
- } else if (blockNumber <= checkpointedBlockNumber) {
1118
- status = TxStatus.CHECKPOINTED;
1119
- } else {
1120
- status = TxStatus.PROPOSED;
1121
- }
1122
-
1123
- const epochNumber =
1124
- blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
1125
-
1126
- return new TxReceipt(
1127
- txHash,
1128
- status,
1129
- TxReceipt.executionResultFromRevertCode(txEffect.data.revertCode),
1130
- undefined,
1131
- txEffect.data.transactionFee.toBigInt(),
1132
- txEffect.l2BlockHash,
1133
- blockNumber,
1134
- epochNumber,
1135
- );
1136
- }
1137
-
1138
1093
  /**
1139
1094
  * Looks up which block included the requested tx effect.
1140
1095
  * @param txHash - The txHash of the tx.
@@ -1145,10 +1100,45 @@ export class BlockStore {
1145
1100
  if (!txEffect) {
1146
1101
  return undefined;
1147
1102
  }
1148
- const { l2BlockNumber, txIndexInBlock } = deserializeIndexedTxEffect(txEffect);
1103
+ // Read only the IndexedTxEffect header (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4)`); the
1104
+ // large tail (the full TxEffect with logs etc.) is irrelevant here.
1105
+ const view = Buffer.from(txEffect.buffer, txEffect.byteOffset, txEffect.byteLength);
1106
+ const l2BlockNumber = view.readUInt32BE(32);
1107
+ const txIndexInBlock = view.readUInt32BE(36);
1149
1108
  return [l2BlockNumber, txIndexInBlock];
1150
1109
  }
1151
1110
 
1111
+ /**
1112
+ * Batched, partial deserializer that fetches `noteHashes` and `nullifiers` (all of them) for the given
1113
+ * txs. For each input txHash, returns a `[noteHashes, nullifiers]` tuple. Returns `[[], []]` for any
1114
+ * unknown txHash. Preserves input order. Used by the log read path when `includeEffects` is set to
1115
+ * attach effect data on demand without paying for a full {@link TxEffect} deserialization.
1116
+ *
1117
+ * The on-disk `IndexedTxEffect` layout starts with a fixed-length header
1118
+ * (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4) + slotNumber(4) + revertCode(1) + txHash(32) +
1119
+ * transactionFee(32)` = 109 bytes), followed by `noteHashes` and `nullifiers` (both u8-length-prefixed `Fr`
1120
+ * vectors). We
1121
+ * skip the header, then read the two vectors, and stop — the large tail (`l2ToL1Msgs`,
1122
+ * `publicDataWrites`, `privateLogs`, `publicLogs`, `contractClassLogs`) is never touched.
1123
+ */
1124
+ public getNoteHashesAndNullifiers(txHashes: TxHash[]): Promise<[Fr[], Fr[]][]> {
1125
+ return Promise.all(
1126
+ txHashes.map(async (txHash): Promise<[Fr[], Fr[]]> => {
1127
+ const buffer = await this.#txEffects.getAsync(txHash.toString());
1128
+ if (!buffer) {
1129
+ return [[], []];
1130
+ }
1131
+ const reader = BufferReader.asReader(buffer);
1132
+ // Skip the fixed-length header: blockHash + l2BlockNumber + txIndexInBlock + slotNumber + revertCode +
1133
+ // txHash + transactionFee.
1134
+ reader.readBytes(32 + 4 + 4 + 4 + 1 + 32 + 32);
1135
+ const noteHashes = reader.readVectorUint8Prefix(Fr);
1136
+ const nullifiers = reader.readVectorUint8Prefix(Fr);
1137
+ return [noteHashes, nullifiers];
1138
+ }),
1139
+ );
1140
+ }
1141
+
1152
1142
  /**
1153
1143
  * Looks up which block deployed a particular contract.
1154
1144
  * @param contractAddress - The address of the contract to look up.
@@ -1176,6 +1166,174 @@ export class BlockStore {
1176
1166
  return typeof lastBlockNumber === 'number' ? BlockNumber(lastBlockNumber) : BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
1177
1167
  }
1178
1168
 
1169
+ /**
1170
+ * Resolves all five L2 chain tips (proposed, proposedCheckpoint, checkpointed, proven, finalized)
1171
+ * in a single read-only transaction so the snapshot is internally consistent. Each underlying
1172
+ * record is read at most once: latest block, latest confirmed checkpoint, and latest pending
1173
+ * checkpoint are each loaded directly (no separate "find the number, then look up data" hop),
1174
+ * the proven/finalized checkpoint singletons are read once and their storage entries are
1175
+ * reused if they coincide with the latest checkpoint, and per-tip block hashes are deduped
1176
+ * when two tips land on the same block (e.g. finalized == proven, or proposedCheckpoint falls
1177
+ * back to checkpointed when no pending checkpoint exists).
1178
+ *
1179
+ * The result is guaranteed to satisfy `finalized <= proven <= checkpointed <= proposed` (by
1180
+ * block number). Genesis is represented by `(INITIAL_L2_BLOCK_NUM - 1)` and the supplied
1181
+ * `genesisBlockHash`, paired with the synthetic genesis checkpoint id.
1182
+ *
1183
+ * @param genesisBlockHash - Block hash to report for the synthetic pre-initial block (used when
1184
+ * a tip is still at genesis).
1185
+ */
1186
+ async getL2TipsData(genesisBlockHash: BlockHash): Promise<L2Tips> {
1187
+ return await this.db.transactionAsync(async () => {
1188
+ // Define genesis tips
1189
+ const genesisBlockNumber = BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
1190
+ const genesisCheckpointNumber = CheckpointNumber(INITIAL_CHECKPOINT_NUMBER - 1);
1191
+ const genesisBlockId = { number: genesisBlockNumber, hash: genesisBlockHash.toString() };
1192
+ const genesisCheckpointId = {
1193
+ number: genesisCheckpointNumber,
1194
+ hash: GENESIS_CHECKPOINT_HEADER_HASH.toString(),
1195
+ };
1196
+ const genesisTip: L2TipId = { block: genesisBlockId, checkpoint: genesisCheckpointId };
1197
+
1198
+ // Load latest block and checkpoint entries
1199
+ const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 }));
1200
+ const [proposedCheckpointEntry] = await toArray(
1201
+ this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }),
1202
+ );
1203
+ const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 }));
1204
+ const latestCheckpointNumber = latestCheckpointEntry
1205
+ ? CheckpointNumber(latestCheckpointEntry[0])
1206
+ : genesisCheckpointNumber;
1207
+
1208
+ // Load proven and finalized checkpoint number pointers
1209
+ const [provenRaw, finalizedRaw] = await Promise.all([
1210
+ this.#lastProvenCheckpoint.getAsync(),
1211
+ this.#lastFinalizedCheckpoint.getAsync(),
1212
+ ]);
1213
+
1214
+ // Clamp to enforce finalized <= proven <= checkpointed.
1215
+ const provenCheckpointNumber = CheckpointNumber(Math.min(provenRaw ?? 0, latestCheckpointNumber));
1216
+ const finalizedCheckpointNumber = CheckpointNumber(Math.min(finalizedRaw ?? 0, provenCheckpointNumber));
1217
+
1218
+ // Avoid loading the same checkpoint more than once
1219
+ const checkpointStorageCache = new Map<CheckpointNumber, CheckpointStorage>();
1220
+ if (latestCheckpointEntry) {
1221
+ checkpointStorageCache.set(CheckpointNumber(latestCheckpointEntry[0]), latestCheckpointEntry[1]);
1222
+ }
1223
+ const loadCheckpointStorage = async (n: CheckpointNumber): Promise<CheckpointStorage | undefined> => {
1224
+ if (n === 0) {
1225
+ return undefined;
1226
+ }
1227
+ if (!checkpointStorageCache.has(n)) {
1228
+ const checkpointStorage = await this.#checkpoints.getAsync(n);
1229
+ if (!checkpointStorage) {
1230
+ throw new CheckpointNotFoundError(n);
1231
+ }
1232
+ checkpointStorageCache.set(n, checkpointStorage);
1233
+ }
1234
+ return checkpointStorageCache.get(n)!;
1235
+ };
1236
+
1237
+ // Load proven and finalized checkpoint storage entries
1238
+ const provenCheckpoint = await loadCheckpointStorage(provenCheckpointNumber);
1239
+ const finalizedCheckpoint = await loadCheckpointStorage(finalizedCheckpointNumber);
1240
+
1241
+ // Avoid loading the same block hash multiple times when tips land on the same block
1242
+ const blockHashCache = new Map<number, string>();
1243
+ blockHashCache.set(genesisBlockNumber, genesisBlockHash.toString());
1244
+ if (latestBlockEntry) {
1245
+ blockHashCache.set(latestBlockEntry[0], BlockHash.fromBuffer(latestBlockEntry[1].blockHash).toString());
1246
+ }
1247
+ const loadBlockHash = async (n: BlockNumber): Promise<string> => {
1248
+ if (!blockHashCache.has(n)) {
1249
+ const blockStorage = await this.#blocks.getAsync(n);
1250
+ if (!blockStorage) {
1251
+ throw new BlockNotFoundError(n);
1252
+ }
1253
+ const blockHash = BlockHash.fromBuffer(blockStorage.blockHash).toString();
1254
+ blockHashCache.set(n, blockHash);
1255
+ }
1256
+ return blockHashCache.get(n)!;
1257
+ };
1258
+
1259
+ // Build proposed chain tip (this one has block only, no checkpoint)
1260
+ const proposedBlockId =
1261
+ latestBlockEntry === undefined
1262
+ ? genesisBlockId
1263
+ : {
1264
+ number: BlockNumber(latestBlockEntry[0]),
1265
+ hash: BlockHash.fromBuffer(latestBlockEntry[1].blockHash).toString(),
1266
+ };
1267
+
1268
+ // Build other tips from checkpoint data, reading corresponding block data from the cache
1269
+ const buildTipFromCheckpoint = async (
1270
+ stored: ProposedCheckpointStorage | CheckpointStorage | undefined,
1271
+ ): Promise<L2TipId> => {
1272
+ if (!stored) {
1273
+ return genesisTip;
1274
+ }
1275
+ const blockNumber = BlockNumber(stored.startBlock + stored.blockCount - 1);
1276
+ const blockHash = await loadBlockHash(blockNumber);
1277
+ const header = CheckpointHeader.fromBuffer(stored.header);
1278
+ return {
1279
+ block: { number: blockNumber, hash: blockHash },
1280
+ checkpoint: { number: CheckpointNumber(stored.checkpointNumber), hash: header.hash().toString() },
1281
+ };
1282
+ };
1283
+
1284
+ const checkpointedTip = await buildTipFromCheckpoint(latestCheckpointEntry?.[1]);
1285
+ const provenTip = await buildTipFromCheckpoint(provenCheckpoint);
1286
+ const finalizedTip = await buildTipFromCheckpoint(finalizedCheckpoint);
1287
+
1288
+ // Proposed checkpoint falls back to the checkpoint tip if it's not set. And if local storage is
1289
+ // inconsistent and the proposed checkpoint is behind the checkpointed tip, we patch that and
1290
+ // report the checkpointed tip as the proposed checkpoint to maintain the invariant.
1291
+ const proposedCheckpointTip =
1292
+ proposedCheckpointEntry === undefined || proposedCheckpointEntry[0] <= latestCheckpointNumber
1293
+ ? checkpointedTip
1294
+ : await buildTipFromCheckpoint(proposedCheckpointEntry[1]);
1295
+
1296
+ // A checkpointed block past the latest stored block would mean a checkpoint
1297
+ // references blocks that aren't in blocks.
1298
+ if (proposedBlockId.number < checkpointedTip.block.number) {
1299
+ throw new Error(
1300
+ `Inconsistent block store: latest block ${proposedBlockId.number} is behind checkpointed block ${checkpointedTip.block.number}`,
1301
+ );
1302
+ }
1303
+
1304
+ // Assert that checkpoint numbers are increasing
1305
+ if (
1306
+ finalizedTip.checkpoint.number > provenTip.checkpoint.number ||
1307
+ provenTip.checkpoint.number > checkpointedTip.checkpoint.number ||
1308
+ checkpointedTip.checkpoint.number > proposedCheckpointTip.checkpoint.number
1309
+ ) {
1310
+ throw new Error(
1311
+ `Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number} proposed=${proposedCheckpointTip.checkpoint.number}`,
1312
+ );
1313
+ }
1314
+
1315
+ // Assert block numbers are increasing
1316
+ if (
1317
+ finalizedTip.block.number > provenTip.block.number ||
1318
+ provenTip.block.number > checkpointedTip.block.number ||
1319
+ checkpointedTip.block.number > proposedCheckpointTip.block.number ||
1320
+ proposedCheckpointTip.block.number > proposedBlockId.number
1321
+ ) {
1322
+ throw new Error(
1323
+ `Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposedCheckpoint=${proposedCheckpointTip.block.number} proposed=${proposedBlockId.number}`,
1324
+ );
1325
+ }
1326
+
1327
+ return {
1328
+ proposed: proposedBlockId,
1329
+ proposedCheckpoint: proposedCheckpointTip,
1330
+ checkpointed: checkpointedTip,
1331
+ proven: provenTip,
1332
+ finalized: finalizedTip,
1333
+ };
1334
+ });
1335
+ }
1336
+
1179
1337
  /**
1180
1338
  * Gets the most recent L1 block processed.
1181
1339
  * @returns The L1 block that published the latest L2 block
@@ -1188,24 +1346,29 @@ export class BlockStore {
1188
1346
  return this.#lastSynchedL1Block.set(l1BlockNumber);
1189
1347
  }
1190
1348
 
1191
- /** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
1192
- * Computes archive and checkpointOutHash from the stored blocks. */
1193
- async setProposedCheckpoint(proposed: ProposedCheckpointInput) {
1349
+ /**
1350
+ * Adds a proposed checkpoint to the pending queue.
1351
+ * Accepts proposed.checkpointNumber === latestTip + 1, where latestTip is the highest of
1352
+ * confirmed and the highest pending checkpoint number.
1353
+ * Computes archive and checkpointOutHash from the stored blocks.
1354
+ */
1355
+ async addProposedCheckpoint(proposed: ProposedCheckpointInput) {
1194
1356
  return await this.db.transactionAsync(async () => {
1195
- const current = await this.getProposedCheckpointNumber();
1196
- if (proposed.checkpointNumber <= current) {
1197
- throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
1198
- }
1199
1357
  const confirmed = await this.getLatestCheckpointNumber();
1200
- if (proposed.checkpointNumber !== confirmed + 1) {
1201
- throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
1358
+ const [latestPendingKey] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
1359
+ const latestTip = CheckpointNumber(
1360
+ latestPendingKey !== undefined ? Math.max(latestPendingKey, confirmed) : confirmed,
1361
+ );
1362
+
1363
+ if (proposed.checkpointNumber !== latestTip + 1) {
1364
+ throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, latestTip);
1202
1365
  }
1203
1366
 
1204
- // Ensure the previous checkpoint + blocks exist
1367
+ // Ensure the predecessor block (from pending or confirmed chain) exists
1205
1368
  const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
1206
1369
  const blocks: L2Block[] = [];
1207
1370
  for (let i = 0; i < proposed.blockCount; i++) {
1208
- const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
1371
+ const block = await this.getBlock({ number: BlockNumber(proposed.startBlock + i) });
1209
1372
  if (!block) {
1210
1373
  throw new BlockNotFoundError(proposed.startBlock + i);
1211
1374
  }
@@ -1216,7 +1379,7 @@ export class BlockStore {
1216
1379
  const archive = blocks[blocks.length - 1].archive;
1217
1380
  const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
1218
1381
 
1219
- await this.#proposedCheckpoint.set({
1382
+ await this.#proposedCheckpoints.set(proposed.checkpointNumber, {
1220
1383
  header: proposed.header.toBuffer(),
1221
1384
  archive: archive.toBuffer(),
1222
1385
  checkpointOutHash: checkpointOutHash.toBuffer(),
@@ -1230,13 +1393,15 @@ export class BlockStore {
1230
1393
  }
1231
1394
 
1232
1395
  async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
1233
- const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
1234
- this.getLatestCheckpointNumber(),
1235
- this.#lastProvenCheckpoint.getAsync(),
1236
- ]);
1237
- return (provenCheckpointNumber ?? 0) > latestCheckpointNumber
1238
- ? latestCheckpointNumber
1239
- : CheckpointNumber(provenCheckpointNumber ?? 0);
1396
+ return await this.db.transactionAsync(async () => {
1397
+ const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
1398
+ this.getLatestCheckpointNumber(),
1399
+ this.#lastProvenCheckpoint.getAsync(),
1400
+ ]);
1401
+ return (provenCheckpointNumber ?? 0) > latestCheckpointNumber
1402
+ ? latestCheckpointNumber
1403
+ : CheckpointNumber(provenCheckpointNumber ?? 0);
1404
+ });
1240
1405
  }
1241
1406
 
1242
1407
  async setProvenCheckpointNumber(checkpointNumber: CheckpointNumber) {
@@ -1245,13 +1410,15 @@ export class BlockStore {
1245
1410
  }
1246
1411
 
1247
1412
  async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
1248
- const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
1249
- this.getLatestCheckpointNumber(),
1250
- this.#lastFinalizedCheckpoint.getAsync(),
1251
- ]);
1252
- return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber
1253
- ? latestCheckpointNumber
1254
- : CheckpointNumber(finalizedCheckpointNumber ?? 0);
1413
+ return await this.db.transactionAsync(async () => {
1414
+ const [provenCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
1415
+ this.getProvenCheckpointNumber(),
1416
+ this.#lastFinalizedCheckpoint.getAsync(),
1417
+ ]);
1418
+ return (finalizedCheckpointNumber ?? 0) > provenCheckpointNumber
1419
+ ? provenCheckpointNumber
1420
+ : CheckpointNumber(finalizedCheckpointNumber ?? 0);
1421
+ });
1255
1422
  }
1256
1423
 
1257
1424
  setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
@@ -1294,4 +1461,80 @@ export class BlockStore {
1294
1461
  await this.#pendingChainValidationStatus.delete();
1295
1462
  }
1296
1463
  }
1464
+
1465
+ /** Records a rejected-checkpoint entry, keyed by its own archive root. */
1466
+ async addRejectedCheckpoint(entry: RejectedCheckpoint): Promise<void> {
1467
+ const archiveRootHex = entry.archiveRoot.toString();
1468
+ await this.#rejectedCheckpoints.set(archiveRootHex, {
1469
+ checkpointNumber: entry.checkpointNumber,
1470
+ archiveRoot: entry.archiveRoot.toBuffer(),
1471
+ parentArchiveRoot: entry.parentArchiveRoot.toBuffer(),
1472
+ slotNumber: entry.slotNumber,
1473
+ l1: entry.l1.toBuffer(),
1474
+ reason: entry.reason,
1475
+ });
1476
+ await this.#rejectedCheckpointsByNumber.set(entry.checkpointNumber, archiveRootHex);
1477
+ await this.advanceSynchedL1BlockNumber(entry.l1.blockNumber);
1478
+ }
1479
+
1480
+ /** Returns the rejected-checkpoint entry with the given archive root, or undefined if not present. */
1481
+ async getRejectedCheckpointByArchiveRoot(archiveRoot: Fr): Promise<RejectedCheckpoint | undefined> {
1482
+ const stored = await this.#rejectedCheckpoints.getAsync(archiveRoot.toString());
1483
+ return stored ? this.rejectedCheckpointFromStorage(stored) : undefined;
1484
+ }
1485
+
1486
+ /** Returns the rejected-checkpoint entry recorded for the given checkpoint number, or undefined if none. */
1487
+ async getRejectedCheckpointByNumber(checkpointNumber: CheckpointNumber): Promise<RejectedCheckpoint | undefined> {
1488
+ const archiveRootHex = await this.#rejectedCheckpointsByNumber.getAsync(checkpointNumber);
1489
+ if (archiveRootHex === undefined) {
1490
+ return undefined;
1491
+ }
1492
+ const stored = await this.#rejectedCheckpoints.getAsync(archiveRootHex);
1493
+ return stored ? this.rejectedCheckpointFromStorage(stored) : undefined;
1494
+ }
1495
+
1496
+ /** Returns the highest checkpoint number recorded across all rejected entries, or `INITIAL_CHECKPOINT_NUMBER - 1` if none. */
1497
+ async getLatestRejectedCheckpointNumber(): Promise<CheckpointNumber> {
1498
+ const [latest] = await toArray(this.#rejectedCheckpointsByNumber.keysAsync({ reverse: true, limit: 1 }));
1499
+ return CheckpointNumber(latest ?? INITIAL_CHECKPOINT_NUMBER - 1);
1500
+ }
1501
+
1502
+ /** Removes a rejected-checkpoint entry by its archive root (used when an entry no longer matches L1). */
1503
+ async removeRejectedCheckpointByArchiveRoot(archiveRoot: Fr): Promise<void> {
1504
+ const archiveRootHex = archiveRoot.toString();
1505
+ const stored = await this.#rejectedCheckpoints.getAsync(archiveRootHex);
1506
+ await this.#rejectedCheckpoints.delete(archiveRootHex);
1507
+ if (stored) {
1508
+ // Only clear the by-number index if it still points at this archive root, so a distinct
1509
+ // entry that shares the checkpoint number (e.g. an L1 reorg replacement) is not dropped.
1510
+ const indexed = await this.#rejectedCheckpointsByNumber.getAsync(stored.checkpointNumber);
1511
+ if (indexed === archiveRootHex) {
1512
+ await this.#rejectedCheckpointsByNumber.delete(stored.checkpointNumber);
1513
+ }
1514
+ }
1515
+ }
1516
+
1517
+ /**
1518
+ * Advances the stored last-synched L1 block number to `l1BlockNumber` only if it is strictly
1519
+ * greater than the current value. Use this whenever ingesting checkpoint-shaped data so the
1520
+ * sync pointer never walks backwards on out-of-order writes (e.g. an invalid checkpoint
1521
+ * advance followed by a valid-checkpoint commit landing at an earlier L1 block).
1522
+ */
1523
+ private async advanceSynchedL1BlockNumber(l1BlockNumber: bigint): Promise<void> {
1524
+ const current = await this.#lastSynchedL1Block.getAsync();
1525
+ if (current === undefined || l1BlockNumber > current) {
1526
+ await this.#lastSynchedL1Block.set(l1BlockNumber);
1527
+ }
1528
+ }
1529
+
1530
+ private rejectedCheckpointFromStorage(stored: RejectedCheckpointStorage): RejectedCheckpoint {
1531
+ return {
1532
+ checkpointNumber: CheckpointNumber(stored.checkpointNumber),
1533
+ archiveRoot: Fr.fromBuffer(stored.archiveRoot),
1534
+ parentArchiveRoot: Fr.fromBuffer(stored.parentArchiveRoot),
1535
+ slotNumber: SlotNumber(stored.slotNumber),
1536
+ l1: L1PublishedData.fromBuffer(stored.l1),
1537
+ reason: stored.reason,
1538
+ };
1539
+ }
1297
1540
  }