@aztec/archiver 0.0.1-commit.8afd444 → 0.0.1-commit.8c0b8ff

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 (94) hide show
  1. package/dest/archiver.d.ts +7 -4
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +73 -110
  4. package/dest/errors.d.ts +7 -9
  5. package/dest/errors.d.ts.map +1 -1
  6. package/dest/errors.js +9 -14
  7. package/dest/factory.d.ts +3 -4
  8. package/dest/factory.d.ts.map +1 -1
  9. package/dest/factory.js +31 -24
  10. package/dest/index.d.ts +2 -1
  11. package/dest/index.d.ts.map +1 -1
  12. package/dest/index.js +1 -0
  13. package/dest/l1/bin/retrieve-calldata.js +36 -33
  14. package/dest/l1/calldata_retriever.d.ts +73 -50
  15. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  16. package/dest/l1/calldata_retriever.js +190 -259
  17. package/dest/l1/data_retrieval.d.ts +4 -7
  18. package/dest/l1/data_retrieval.d.ts.map +1 -1
  19. package/dest/l1/data_retrieval.js +9 -13
  20. package/dest/l1/spire_proposer.d.ts +5 -5
  21. package/dest/l1/spire_proposer.d.ts.map +1 -1
  22. package/dest/l1/spire_proposer.js +9 -17
  23. package/dest/modules/data_source_base.d.ts +10 -5
  24. package/dest/modules/data_source_base.d.ts.map +1 -1
  25. package/dest/modules/data_source_base.js +29 -73
  26. package/dest/modules/data_store_updater.d.ts +22 -7
  27. package/dest/modules/data_store_updater.d.ts.map +1 -1
  28. package/dest/modules/data_store_updater.js +113 -40
  29. package/dest/modules/instrumentation.d.ts +4 -2
  30. package/dest/modules/instrumentation.d.ts.map +1 -1
  31. package/dest/modules/instrumentation.js +9 -2
  32. package/dest/modules/l1_synchronizer.d.ts +5 -8
  33. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  34. package/dest/modules/l1_synchronizer.js +40 -10
  35. package/dest/store/block_store.d.ts +30 -26
  36. package/dest/store/block_store.d.ts.map +1 -1
  37. package/dest/store/block_store.js +180 -83
  38. package/dest/store/contract_class_store.d.ts +1 -1
  39. package/dest/store/contract_class_store.d.ts.map +1 -1
  40. package/dest/store/contract_class_store.js +6 -2
  41. package/dest/store/contract_instance_store.d.ts +1 -1
  42. package/dest/store/contract_instance_store.d.ts.map +1 -1
  43. package/dest/store/contract_instance_store.js +6 -2
  44. package/dest/store/kv_archiver_store.d.ts +37 -15
  45. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  46. package/dest/store/kv_archiver_store.js +42 -13
  47. package/dest/store/l2_tips_cache.d.ts +19 -0
  48. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  49. package/dest/store/l2_tips_cache.js +89 -0
  50. package/dest/store/log_store.d.ts +1 -1
  51. package/dest/store/log_store.d.ts.map +1 -1
  52. package/dest/store/log_store.js +103 -45
  53. package/dest/store/message_store.js +1 -1
  54. package/dest/test/fake_l1_state.d.ts +20 -1
  55. package/dest/test/fake_l1_state.d.ts.map +1 -1
  56. package/dest/test/fake_l1_state.js +97 -20
  57. package/dest/test/mock_archiver.d.ts +1 -1
  58. package/dest/test/mock_archiver.d.ts.map +1 -1
  59. package/dest/test/mock_archiver.js +3 -2
  60. package/dest/test/mock_l2_block_source.d.ts +21 -5
  61. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  62. package/dest/test/mock_l2_block_source.js +132 -86
  63. package/dest/test/mock_structs.d.ts +4 -1
  64. package/dest/test/mock_structs.d.ts.map +1 -1
  65. package/dest/test/mock_structs.js +13 -1
  66. package/dest/test/noop_l1_archiver.d.ts +4 -1
  67. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  68. package/dest/test/noop_l1_archiver.js +5 -1
  69. package/package.json +13 -13
  70. package/src/archiver.ts +88 -130
  71. package/src/errors.ts +10 -24
  72. package/src/factory.ts +45 -21
  73. package/src/index.ts +1 -0
  74. package/src/l1/README.md +25 -68
  75. package/src/l1/bin/retrieve-calldata.ts +46 -39
  76. package/src/l1/calldata_retriever.ts +249 -379
  77. package/src/l1/data_retrieval.ts +6 -16
  78. package/src/l1/spire_proposer.ts +7 -15
  79. package/src/modules/data_source_base.ts +56 -95
  80. package/src/modules/data_store_updater.ts +123 -43
  81. package/src/modules/instrumentation.ts +9 -2
  82. package/src/modules/l1_synchronizer.ts +47 -14
  83. package/src/store/block_store.ts +219 -110
  84. package/src/store/contract_class_store.ts +7 -3
  85. package/src/store/contract_instance_store.ts +8 -5
  86. package/src/store/kv_archiver_store.ts +66 -20
  87. package/src/store/l2_tips_cache.ts +89 -0
  88. package/src/store/log_store.ts +159 -43
  89. package/src/store/message_store.ts +1 -1
  90. package/src/test/fake_l1_state.ts +125 -21
  91. package/src/test/mock_archiver.ts +3 -2
  92. package/src/test/mock_l2_block_source.ts +173 -81
  93. package/src/test/mock_structs.ts +20 -6
  94. package/src/test/noop_l1_archiver.ts +7 -1
@@ -9,6 +9,7 @@ import { isDefined } from '@aztec/foundation/types';
9
9
  import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncSingleton, Range } from '@aztec/kv-store';
10
10
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
11
11
  import {
12
+ type BlockData,
12
13
  BlockHash,
13
14
  Body,
14
15
  CheckpointedL2Block,
@@ -18,8 +19,8 @@ import {
18
19
  deserializeValidateCheckpointResult,
19
20
  serializeValidateCheckpointResult,
20
21
  } from '@aztec/stdlib/block';
21
- import { L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
22
- import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
22
+ import { type CheckpointData, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
23
+ import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
23
24
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
24
25
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
25
26
  import {
@@ -34,15 +35,14 @@ import {
34
35
  } from '@aztec/stdlib/tx';
35
36
 
36
37
  import {
38
+ BlockAlreadyCheckpointedError,
37
39
  BlockArchiveNotConsistentError,
38
40
  BlockIndexNotSequentialError,
39
41
  BlockNotFoundError,
40
42
  BlockNumberNotSequentialError,
41
43
  CannotOverwriteCheckpointedBlockError,
42
44
  CheckpointNotFoundError,
43
- CheckpointNumberNotConsistentError,
44
45
  CheckpointNumberNotSequentialError,
45
- InitialBlockNumberNotSequentialError,
46
46
  InitialCheckpointNumberNotSequentialError,
47
47
  } from '../errors.js';
48
48
 
@@ -61,23 +61,14 @@ type BlockStorage = {
61
61
  type CheckpointStorage = {
62
62
  header: Buffer;
63
63
  archive: Buffer;
64
+ checkpointOutHash: Buffer;
64
65
  checkpointNumber: number;
65
66
  startBlock: number;
66
- numBlocks: number;
67
+ blockCount: number;
67
68
  l1: Buffer;
68
69
  attestations: Buffer[];
69
70
  };
70
71
 
71
- export type CheckpointData = {
72
- checkpointNumber: CheckpointNumber;
73
- header: CheckpointHeader;
74
- archive: AppendOnlyTreeSnapshot;
75
- startBlock: number;
76
- numBlocks: number;
77
- l1: L1PublishedData;
78
- attestations: Buffer[];
79
- };
80
-
81
72
  export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
82
73
 
83
74
  /**
@@ -90,6 +81,9 @@ export class BlockStore {
90
81
  /** Map checkpoint number to checkpoint data */
91
82
  #checkpoints: AztecAsyncMap<number, CheckpointStorage>;
92
83
 
84
+ /** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
85
+ #slotToCheckpoint: AztecAsyncMap<number, number>;
86
+
93
87
  /** Map block hash to list of tx hashes */
94
88
  #blockTxs: AztecAsyncMap<string, Buffer>;
95
89
 
@@ -102,6 +96,9 @@ export class BlockStore {
102
96
  /** Stores last proven checkpoint */
103
97
  #lastProvenCheckpoint: AztecAsyncSingleton<number>;
104
98
 
99
+ /** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
100
+ #lastFinalizedCheckpoint: AztecAsyncSingleton<number>;
101
+
105
102
  /** Stores the pending chain validation status */
106
103
  #pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;
107
104
 
@@ -116,10 +113,7 @@ export class BlockStore {
116
113
 
117
114
  #log = createLogger('archiver:block_store');
118
115
 
119
- constructor(
120
- private db: AztecAsyncKVStore,
121
- private l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
122
- ) {
116
+ constructor(private db: AztecAsyncKVStore) {
123
117
  this.#blocks = db.openMap('archiver_blocks');
124
118
  this.#blockTxs = db.openMap('archiver_block_txs');
125
119
  this.#txEffects = db.openMap('archiver_tx_effects');
@@ -128,40 +122,42 @@ export class BlockStore {
128
122
  this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
129
123
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
130
124
  this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint');
125
+ this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint');
131
126
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
132
127
  this.#checkpoints = db.openMap('archiver_checkpoints');
128
+ this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
133
129
  }
134
130
 
135
131
  /**
136
- * Computes the finalized block number based on the proven block number.
137
- * A block is considered finalized when it's 2 epochs behind the proven block.
138
- * TODO(#13569): Compute proper finalized block number based on L1 finalized block.
139
- * TODO(palla/mbps): Even the provisional computation is wrong, since it should subtract checkpoints, not blocks
132
+ * Returns the finalized L2 block number. An L2 block is finalized when it was proven
133
+ * in an L1 block that has itself been finalized on Ethereum.
140
134
  * @returns The finalized block number.
141
135
  */
142
136
  async getFinalizedL2BlockNumber(): Promise<BlockNumber> {
143
- const provenBlockNumber = await this.getProvenBlockNumber();
144
- return BlockNumber(Math.max(provenBlockNumber - this.l1Constants.epochDuration * 2, 0));
137
+ const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber();
138
+ if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
139
+ return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
140
+ }
141
+ const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber);
142
+ if (!checkpointStorage) {
143
+ throw new CheckpointNotFoundError(finalizedCheckpointNumber);
144
+ }
145
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
145
146
  }
146
147
 
147
148
  /**
148
- * Append new proposed blocks to the store's list. All blocks must be for the 'current' checkpoint.
149
- * These are uncheckpointed blocks that have been proposed by the sequencer but not yet included in a checkpoint on L1.
149
+ * Append a new proposed block to the store.
150
+ * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
150
151
  * For checkpointed blocks (already published to L1), use addCheckpoints() instead.
151
- * @param blocks - The proposed L2 blocks to be added to the store.
152
+ * @param block - The proposed L2 block to be added to the store.
152
153
  * @returns True if the operation is successful.
153
154
  */
154
- async addProposedBlocks(blocks: L2Block[], opts: { force?: boolean } = {}): Promise<boolean> {
155
- if (blocks.length === 0) {
156
- return true;
157
- }
158
-
155
+ async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
159
156
  return await this.db.transactionAsync(async () => {
160
- // Check that the block immediately before the first block to be added is present in the store.
161
- const firstBlockNumber = blocks[0].number;
162
- const firstBlockCheckpointNumber = blocks[0].checkpointNumber;
163
- const firstBlockIndex = blocks[0].indexWithinCheckpoint;
164
- const firstBlockLastArchive = blocks[0].header.lastArchive.root;
157
+ const blockNumber = block.number;
158
+ const blockCheckpointNumber = block.checkpointNumber;
159
+ const blockIndex = block.indexWithinCheckpoint;
160
+ const blockLastArchive = block.header.lastArchive.root;
165
161
 
166
162
  // Extract the latest block and checkpoint numbers
167
163
  const previousBlockNumber = await this.getLatestBlockNumber();
@@ -169,71 +165,52 @@ export class BlockStore {
169
165
 
170
166
  // Verify we're not overwriting checkpointed blocks
171
167
  const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
172
- if (!opts.force && firstBlockNumber <= lastCheckpointedBlockNumber) {
173
- throw new CannotOverwriteCheckpointedBlockError(firstBlockNumber, lastCheckpointedBlockNumber);
168
+ if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
169
+ // Check if the proposed block matches the already-checkpointed one
170
+ const existingBlock = await this.getBlock(BlockNumber(blockNumber));
171
+ if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
172
+ throw new BlockAlreadyCheckpointedError(blockNumber);
173
+ }
174
+ throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber);
174
175
  }
175
176
 
176
- // Check that the first block number is the expected one
177
- if (!opts.force && previousBlockNumber !== firstBlockNumber - 1) {
178
- throw new InitialBlockNumberNotSequentialError(firstBlockNumber, previousBlockNumber);
177
+ // Check that the block number is the expected one
178
+ if (!opts.force && previousBlockNumber !== blockNumber - 1) {
179
+ throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
179
180
  }
180
181
 
181
182
  // The same check as above but for checkpoints
182
- if (!opts.force && previousCheckpointNumber !== firstBlockCheckpointNumber - 1) {
183
- throw new InitialCheckpointNumberNotSequentialError(firstBlockCheckpointNumber, previousCheckpointNumber);
183
+ if (!opts.force && previousCheckpointNumber !== blockCheckpointNumber - 1) {
184
+ throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, previousCheckpointNumber);
184
185
  }
185
186
 
186
187
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
187
188
  const previousBlockResult = await this.getBlock(previousBlockNumber);
188
189
 
189
- let expectedFirstblockIndex = 0;
190
+ let expectedBlockIndex = 0;
190
191
  let previousBlockIndex: number | undefined = undefined;
191
192
  if (previousBlockResult !== undefined) {
192
- if (previousBlockResult.checkpointNumber === firstBlockCheckpointNumber) {
193
+ if (previousBlockResult.checkpointNumber === blockCheckpointNumber) {
193
194
  // The previous block is for the same checkpoint, therefore our index should follow it
194
195
  previousBlockIndex = previousBlockResult.indexWithinCheckpoint;
195
- expectedFirstblockIndex = previousBlockIndex + 1;
196
+ expectedBlockIndex = previousBlockIndex + 1;
196
197
  }
197
- if (!previousBlockResult.archive.root.equals(firstBlockLastArchive)) {
198
+ if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
198
199
  throw new BlockArchiveNotConsistentError(
199
- firstBlockNumber,
200
+ blockNumber,
200
201
  previousBlockResult.number,
201
- firstBlockLastArchive,
202
+ blockLastArchive,
202
203
  previousBlockResult.archive.root,
203
204
  );
204
205
  }
205
206
  }
206
207
 
207
- // Now check that the first block has the expected index value
208
- if (!opts.force && expectedFirstblockIndex !== firstBlockIndex) {
209
- throw new BlockIndexNotSequentialError(firstBlockIndex, previousBlockIndex);
208
+ // Now check that the block has the expected index value
209
+ if (!opts.force && expectedBlockIndex !== blockIndex) {
210
+ throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex);
210
211
  }
211
212
 
212
- // Iterate over blocks array and insert them, checking that the block numbers and indexes are sequential. Also check they are for the correct checkpoint.
213
- let previousBlock: L2Block | undefined = undefined;
214
- for (const block of blocks) {
215
- if (!opts.force && previousBlock) {
216
- if (previousBlock.number + 1 !== block.number) {
217
- throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
218
- }
219
- if (previousBlock.indexWithinCheckpoint + 1 !== block.indexWithinCheckpoint) {
220
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
221
- }
222
- if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
223
- throw new BlockArchiveNotConsistentError(
224
- block.number,
225
- previousBlock.number,
226
- block.header.lastArchive.root,
227
- previousBlock.archive.root,
228
- );
229
- }
230
- }
231
- if (!opts.force && firstBlockCheckpointNumber !== block.checkpointNumber) {
232
- throw new CheckpointNumberNotConsistentError(block.checkpointNumber, firstBlockCheckpointNumber);
233
- }
234
- previousBlock = block;
235
- await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
236
- }
213
+ await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
237
214
 
238
215
  return true;
239
216
  });
@@ -250,21 +227,34 @@ export class BlockStore {
250
227
  }
251
228
 
252
229
  return await this.db.transactionAsync(async () => {
253
- // Check that the checkpoint immediately before the first block to be added is present in the store.
254
230
  const firstCheckpointNumber = checkpoints[0].checkpoint.number;
255
231
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
256
232
 
257
- if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
233
+ // Handle already-stored checkpoints at the start of the batch.
234
+ // This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
235
+ // We accept them if archives match (same content) and update their L1 metadata.
236
+ if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
237
+ checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
238
+ if (checkpoints.length === 0) {
239
+ return true;
240
+ }
241
+ // Re-check sequentiality after skipping
242
+ const newFirstNumber = checkpoints[0].checkpoint.number;
243
+ if (previousCheckpointNumber !== newFirstNumber - 1) {
244
+ throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
245
+ }
246
+ } else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
258
247
  throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
259
248
  }
260
249
 
261
250
  // Extract the previous checkpoint if there is one
251
+ const currentFirstCheckpointNumber = checkpoints[0].checkpoint.number;
262
252
  let previousCheckpointData: CheckpointData | undefined = undefined;
263
- if (previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1) {
253
+ if (currentFirstCheckpointNumber - 1 !== INITIAL_CHECKPOINT_NUMBER - 1) {
264
254
  // There should be a previous checkpoint
265
- previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
255
+ previousCheckpointData = await this.getCheckpointData(CheckpointNumber(currentFirstCheckpointNumber - 1));
266
256
  if (previousCheckpointData === undefined) {
267
- throw new CheckpointNotFoundError(previousCheckpointNumber);
257
+ throw new CheckpointNotFoundError(CheckpointNumber(currentFirstCheckpointNumber - 1));
268
258
  }
269
259
  }
270
260
 
@@ -273,7 +263,7 @@ export class BlockStore {
273
263
 
274
264
  // If we have a previous checkpoint then we need to get the previous block number
275
265
  if (previousCheckpointData !== undefined) {
276
- previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.numBlocks - 1);
266
+ previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
277
267
  previousBlock = await this.getBlock(previousBlockNumber);
278
268
  if (previousBlock === undefined) {
279
269
  // We should be able to get the required previous block
@@ -337,12 +327,16 @@ export class BlockStore {
337
327
  await this.#checkpoints.set(checkpoint.checkpoint.number, {
338
328
  header: checkpoint.checkpoint.header.toBuffer(),
339
329
  archive: checkpoint.checkpoint.archive.toBuffer(),
330
+ checkpointOutHash: checkpoint.checkpoint.getCheckpointOutHash().toBuffer(),
340
331
  l1: checkpoint.l1.toBuffer(),
341
332
  attestations: checkpoint.attestations.map(attestation => attestation.toBuffer()),
342
333
  checkpointNumber: checkpoint.checkpoint.number,
343
334
  startBlock: checkpoint.checkpoint.blocks[0].number,
344
- numBlocks: checkpoint.checkpoint.blocks.length,
335
+ blockCount: checkpoint.checkpoint.blocks.length,
345
336
  });
337
+
338
+ // Update slot-to-checkpoint index
339
+ await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
346
340
  }
347
341
 
348
342
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
@@ -350,6 +344,50 @@ export class BlockStore {
350
344
  });
351
345
  }
352
346
 
347
+ /**
348
+ * Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
349
+ * Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
350
+ */
351
+ private async skipOrUpdateAlreadyStoredCheckpoints(
352
+ checkpoints: PublishedCheckpoint[],
353
+ latestStored: CheckpointNumber,
354
+ ): Promise<PublishedCheckpoint[]> {
355
+ let i = 0;
356
+ for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
357
+ const incoming = checkpoints[i];
358
+ const stored = await this.getCheckpointData(incoming.checkpoint.number);
359
+ if (!stored) {
360
+ // Should not happen if latestStored is correct, but be safe
361
+ break;
362
+ }
363
+ // Verify the checkpoint content matches (archive root)
364
+ if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
365
+ throw new Error(
366
+ `Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
367
+ `Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
368
+ );
369
+ }
370
+ // Update L1 metadata and attestations for the already-stored checkpoint
371
+ this.#log.warn(
372
+ `Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
373
+ `(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
374
+ );
375
+ await this.#checkpoints.set(incoming.checkpoint.number, {
376
+ header: incoming.checkpoint.header.toBuffer(),
377
+ archive: incoming.checkpoint.archive.toBuffer(),
378
+ checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
379
+ l1: incoming.l1.toBuffer(),
380
+ attestations: incoming.attestations.map(a => a.toBuffer()),
381
+ checkpointNumber: incoming.checkpoint.number,
382
+ startBlock: incoming.checkpoint.blocks[0].number,
383
+ blockCount: incoming.checkpoint.blocks.length,
384
+ });
385
+ // Update the sync point to reflect the new L1 block
386
+ await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
387
+ }
388
+ return checkpoints.slice(i);
389
+ }
390
+
353
391
  private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
354
392
  const blockHash = await block.hash();
355
393
 
@@ -425,7 +463,7 @@ export class BlockStore {
425
463
  if (!targetCheckpoint) {
426
464
  throw new Error(`Target checkpoint ${checkpointNumber} not found in store`);
427
465
  }
428
- lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.numBlocks - 1);
466
+ lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.blockCount - 1);
429
467
  }
430
468
 
431
469
  // Remove all blocks after lastBlockToKeep (both checkpointed and uncheckpointed)
@@ -433,6 +471,11 @@ export class BlockStore {
433
471
 
434
472
  // Remove all checkpoints after the target
435
473
  for (let c = latestCheckpointNumber; c > checkpointNumber; c = CheckpointNumber(c - 1)) {
474
+ const checkpointStorage = await this.#checkpoints.getAsync(c);
475
+ if (checkpointStorage) {
476
+ const slotNumber = CheckpointHeader.fromBuffer(checkpointStorage.header).slotNumber;
477
+ await this.#slotToCheckpoint.delete(slotNumber);
478
+ }
436
479
  await this.#checkpoints.delete(c);
437
480
  this.#log.debug(`Removed checkpoint ${c}`);
438
481
  }
@@ -461,17 +504,32 @@ export class BlockStore {
461
504
  return checkpoints;
462
505
  }
463
506
 
464
- private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage) {
465
- const data: CheckpointData = {
507
+ /** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
508
+ async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
509
+ const result: CheckpointData[] = [];
510
+ for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
511
+ start: startSlot,
512
+ end: endSlot + 1,
513
+ })) {
514
+ const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
515
+ if (checkpointStorage) {
516
+ result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
517
+ }
518
+ }
519
+ return result;
520
+ }
521
+
522
+ private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
523
+ return {
466
524
  header: CheckpointHeader.fromBuffer(checkpointStorage.header),
467
525
  archive: AppendOnlyTreeSnapshot.fromBuffer(checkpointStorage.archive),
526
+ checkpointOutHash: Fr.fromBuffer(checkpointStorage.checkpointOutHash),
468
527
  checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
469
- startBlock: checkpointStorage.startBlock,
470
- numBlocks: checkpointStorage.numBlocks,
528
+ startBlock: BlockNumber(checkpointStorage.startBlock),
529
+ blockCount: checkpointStorage.blockCount,
471
530
  l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
472
- attestations: checkpointStorage.attestations,
531
+ attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
473
532
  };
474
- return data;
475
533
  }
476
534
 
477
535
  async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<L2Block[] | undefined> {
@@ -483,7 +541,7 @@ export class BlockStore {
483
541
  const blocksForCheckpoint = await toArray(
484
542
  this.#blocks.entriesAsync({
485
543
  start: checkpoint.startBlock,
486
- end: checkpoint.startBlock + checkpoint.numBlocks,
544
+ end: checkpoint.startBlock + checkpoint.blockCount,
487
545
  }),
488
546
  );
489
547
 
@@ -556,7 +614,7 @@ export class BlockStore {
556
614
  if (!checkpointStorage) {
557
615
  throw new CheckpointNotFoundError(provenCheckpointNumber);
558
616
  } else {
559
- return BlockNumber(checkpointStorage.startBlock + checkpointStorage.numBlocks - 1);
617
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
560
618
  }
561
619
  }
562
620
 
@@ -655,6 +713,32 @@ export class BlockStore {
655
713
  }
656
714
  }
657
715
 
716
+ /**
717
+ * Gets block metadata (without tx data) by block number.
718
+ * @param blockNumber - The number of the block to return.
719
+ * @returns The requested block data.
720
+ */
721
+ async getBlockData(blockNumber: BlockNumber): Promise<BlockData | undefined> {
722
+ const blockStorage = await this.#blocks.getAsync(blockNumber);
723
+ if (!blockStorage || !blockStorage.header) {
724
+ return undefined;
725
+ }
726
+ return this.getBlockDataFromBlockStorage(blockStorage);
727
+ }
728
+
729
+ /**
730
+ * Gets block metadata (without tx data) by archive root.
731
+ * @param archive - The archive root of the block to return.
732
+ * @returns The requested block data.
733
+ */
734
+ async getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
735
+ const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
736
+ if (blockNumber === undefined) {
737
+ return undefined;
738
+ }
739
+ return this.getBlockData(BlockNumber(blockNumber));
740
+ }
741
+
658
742
  /**
659
743
  * Gets an L2 block.
660
744
  * @param blockNumber - The number of the block to return.
@@ -759,15 +843,24 @@ export class BlockStore {
759
843
  }
760
844
  }
761
845
 
846
+ private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
847
+ return {
848
+ header: BlockHeader.fromBuffer(blockStorage.header),
849
+ archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
850
+ blockHash: Fr.fromBuffer(blockStorage.blockHash),
851
+ checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
852
+ indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
853
+ };
854
+ }
855
+
762
856
  private async getBlockFromBlockStorage(
763
857
  blockNumber: number,
764
858
  blockStorage: BlockStorage,
765
859
  ): Promise<L2Block | undefined> {
766
- const header = BlockHeader.fromBuffer(blockStorage.header);
767
- const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive);
768
- const blockHash = blockStorage.blockHash;
769
- header.setHash(Fr.fromBuffer(blockHash));
770
- const blockHashString = bufferToHex(blockHash);
860
+ const { header, archive, blockHash, checkpointNumber, indexWithinCheckpoint } =
861
+ this.getBlockDataFromBlockStorage(blockStorage);
862
+ header.setHash(blockHash);
863
+ const blockHashString = bufferToHex(blockStorage.blockHash);
771
864
  const blockTxsBuffer = await this.#blockTxs.getAsync(blockHashString);
772
865
  if (blockTxsBuffer === undefined) {
773
866
  this.#log.warn(`Could not find body for block ${header.globalVariables.blockNumber} ${blockHash}`);
@@ -786,13 +879,7 @@ export class BlockStore {
786
879
  txEffects.push(deserializeIndexedTxEffect(txEffect).data);
787
880
  }
788
881
  const body = new Body(txEffects);
789
- const block = new L2Block(
790
- archive,
791
- header,
792
- body,
793
- CheckpointNumber(blockStorage.checkpointNumber!),
794
- IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
795
- );
882
+ const block = new L2Block(archive, header, body, checkpointNumber, indexWithinCheckpoint);
796
883
 
797
884
  if (block.number !== blockNumber) {
798
885
  throw new Error(
@@ -822,7 +909,10 @@ export class BlockStore {
822
909
  * @param txHash - The hash of a tx we try to get the receipt for.
823
910
  * @returns The requested tx receipt (or undefined if not found).
824
911
  */
825
- async getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
912
+ async getSettledTxReceipt(
913
+ txHash: TxHash,
914
+ l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
915
+ ): Promise<TxReceipt | undefined> {
826
916
  const txEffect = await this.getTxEffect(txHash);
827
917
  if (!txEffect) {
828
918
  return undefined;
@@ -831,10 +921,11 @@ export class BlockStore {
831
921
  const blockNumber = BlockNumber(txEffect.l2BlockNumber);
832
922
 
833
923
  // Use existing archiver methods to determine finalization level
834
- const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([
924
+ const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
835
925
  this.getProvenBlockNumber(),
836
926
  this.getCheckpointedL2BlockNumber(),
837
927
  this.getFinalizedL2BlockNumber(),
928
+ this.getBlockData(blockNumber),
838
929
  ]);
839
930
 
840
931
  let status: TxStatus;
@@ -848,6 +939,9 @@ export class BlockStore {
848
939
  status = TxStatus.PROPOSED;
849
940
  }
850
941
 
942
+ const epochNumber =
943
+ blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
944
+
851
945
  return new TxReceipt(
852
946
  txHash,
853
947
  status,
@@ -856,6 +950,7 @@ export class BlockStore {
856
950
  txEffect.data.transactionFee.toBigInt(),
857
951
  txEffect.l2BlockHash,
858
952
  blockNumber,
953
+ epochNumber,
859
954
  );
860
955
  }
861
956
 
@@ -892,7 +987,7 @@ export class BlockStore {
892
987
  if (!checkpoint) {
893
988
  return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
894
989
  }
895
- return BlockNumber(checkpoint.startBlock + checkpoint.numBlocks - 1);
990
+ return BlockNumber(checkpoint.startBlock + checkpoint.blockCount - 1);
896
991
  }
897
992
 
898
993
  async getLatestL2BlockNumber(): Promise<BlockNumber> {
@@ -927,6 +1022,20 @@ export class BlockStore {
927
1022
  return result;
928
1023
  }
929
1024
 
1025
+ async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
1026
+ const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
1027
+ this.getLatestCheckpointNumber(),
1028
+ this.#lastFinalizedCheckpoint.getAsync(),
1029
+ ]);
1030
+ return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber
1031
+ ? latestCheckpointNumber
1032
+ : CheckpointNumber(finalizedCheckpointNumber ?? 0);
1033
+ }
1034
+
1035
+ setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
1036
+ return this.#lastFinalizedCheckpoint.set(checkpointNumber);
1037
+ }
1038
+
930
1039
  #computeBlockRange(start: BlockNumber, limit: number): Required<Pick<Range<number>, 'start' | 'limit'>> {
931
1040
  if (limit < 1) {
932
1041
  throw new Error(`Invalid limit: ${limit}`);
@@ -29,11 +29,15 @@ export class ContractClassStore {
29
29
  blockNumber: number,
30
30
  ): Promise<void> {
31
31
  await this.db.transactionAsync(async () => {
32
- await this.#contractClasses.setIfNotExists(
33
- contractClass.id.toString(),
32
+ const key = contractClass.id.toString();
33
+ if (await this.#contractClasses.hasAsync(key)) {
34
+ throw new Error(`Contract class ${key} already exists, cannot add again at block ${blockNumber}`);
35
+ }
36
+ await this.#contractClasses.set(
37
+ key,
34
38
  serializeContractClassPublic({ ...contractClass, l2BlockNumber: blockNumber }),
35
39
  );
36
- await this.#bytecodeCommitments.setIfNotExists(contractClass.id.toString(), bytecodeCommitment.toBuffer());
40
+ await this.#bytecodeCommitments.set(key, bytecodeCommitment.toBuffer());
37
41
  });
38
42
  }
39
43
 
@@ -27,11 +27,14 @@ export class ContractInstanceStore {
27
27
 
28
28
  addContractInstance(contractInstance: ContractInstanceWithAddress, blockNumber: number): Promise<void> {
29
29
  return this.db.transactionAsync(async () => {
30
- await this.#contractInstances.set(
31
- contractInstance.address.toString(),
32
- new SerializableContractInstance(contractInstance).toBuffer(),
33
- );
34
- await this.#contractInstancePublishedAt.set(contractInstance.address.toString(), blockNumber);
30
+ const key = contractInstance.address.toString();
31
+ if (await this.#contractInstances.hasAsync(key)) {
32
+ throw new Error(
33
+ `Contract instance at ${key} already exists (deployed at block ${await this.#contractInstancePublishedAt.getAsync(key)}), cannot add again at block ${blockNumber}`,
34
+ );
35
+ }
36
+ await this.#contractInstances.set(key, new SerializableContractInstance(contractInstance).toBuffer());
37
+ await this.#contractInstancePublishedAt.set(key, blockNumber);
35
38
  });
36
39
  }
37
40