@aztec/archiver 0.0.1-commit.fffb133c → 0.0.1-dev

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 (100) hide show
  1. package/dest/archiver.d.ts +9 -5
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +87 -111
  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 +4 -3
  8. package/dest/factory.d.ts.map +1 -1
  9. package/dest/factory.js +32 -26
  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 +10 -14
  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/l1/validate_trace.d.ts +6 -3
  24. package/dest/l1/validate_trace.d.ts.map +1 -1
  25. package/dest/l1/validate_trace.js +13 -9
  26. package/dest/modules/data_source_base.d.ts +13 -8
  27. package/dest/modules/data_source_base.d.ts.map +1 -1
  28. package/dest/modules/data_source_base.js +29 -73
  29. package/dest/modules/data_store_updater.d.ts +22 -7
  30. package/dest/modules/data_store_updater.d.ts.map +1 -1
  31. package/dest/modules/data_store_updater.js +114 -40
  32. package/dest/modules/instrumentation.d.ts +4 -2
  33. package/dest/modules/instrumentation.d.ts.map +1 -1
  34. package/dest/modules/instrumentation.js +26 -12
  35. package/dest/modules/l1_synchronizer.d.ts +5 -8
  36. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  37. package/dest/modules/l1_synchronizer.js +42 -13
  38. package/dest/store/block_store.d.ts +38 -29
  39. package/dest/store/block_store.d.ts.map +1 -1
  40. package/dest/store/block_store.js +294 -85
  41. package/dest/store/contract_class_store.d.ts +1 -1
  42. package/dest/store/contract_class_store.d.ts.map +1 -1
  43. package/dest/store/contract_class_store.js +27 -7
  44. package/dest/store/contract_instance_store.d.ts +1 -1
  45. package/dest/store/contract_instance_store.d.ts.map +1 -1
  46. package/dest/store/contract_instance_store.js +17 -2
  47. package/dest/store/kv_archiver_store.d.ts +40 -18
  48. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  49. package/dest/store/kv_archiver_store.js +44 -16
  50. package/dest/store/l2_tips_cache.d.ts +18 -0
  51. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  52. package/dest/store/l2_tips_cache.js +20 -0
  53. package/dest/store/log_store.d.ts +1 -1
  54. package/dest/store/log_store.d.ts.map +1 -1
  55. package/dest/store/log_store.js +125 -56
  56. package/dest/store/message_store.js +1 -1
  57. package/dest/test/fake_l1_state.d.ts +20 -1
  58. package/dest/test/fake_l1_state.d.ts.map +1 -1
  59. package/dest/test/fake_l1_state.js +97 -20
  60. package/dest/test/index.js +3 -1
  61. package/dest/test/mock_archiver.d.ts +1 -1
  62. package/dest/test/mock_archiver.d.ts.map +1 -1
  63. package/dest/test/mock_archiver.js +3 -2
  64. package/dest/test/mock_l2_block_source.d.ts +25 -9
  65. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  66. package/dest/test/mock_l2_block_source.js +133 -87
  67. package/dest/test/mock_structs.d.ts +6 -2
  68. package/dest/test/mock_structs.d.ts.map +1 -1
  69. package/dest/test/mock_structs.js +20 -6
  70. package/dest/test/noop_l1_archiver.d.ts +26 -0
  71. package/dest/test/noop_l1_archiver.d.ts.map +1 -0
  72. package/dest/test/noop_l1_archiver.js +72 -0
  73. package/package.json +14 -13
  74. package/src/archiver.ts +110 -132
  75. package/src/errors.ts +10 -24
  76. package/src/factory.ts +47 -23
  77. package/src/index.ts +1 -0
  78. package/src/l1/README.md +25 -68
  79. package/src/l1/bin/retrieve-calldata.ts +46 -39
  80. package/src/l1/calldata_retriever.ts +249 -379
  81. package/src/l1/data_retrieval.ts +7 -17
  82. package/src/l1/spire_proposer.ts +7 -15
  83. package/src/l1/validate_trace.ts +24 -6
  84. package/src/modules/data_source_base.ts +59 -98
  85. package/src/modules/data_store_updater.ts +124 -43
  86. package/src/modules/instrumentation.ts +24 -12
  87. package/src/modules/l1_synchronizer.ts +49 -17
  88. package/src/store/block_store.ts +359 -115
  89. package/src/store/contract_class_store.ts +27 -7
  90. package/src/store/contract_instance_store.ts +19 -5
  91. package/src/store/kv_archiver_store.ts +71 -25
  92. package/src/store/l2_tips_cache.ts +27 -0
  93. package/src/store/log_store.ts +186 -57
  94. package/src/store/message_store.ts +1 -1
  95. package/src/test/fake_l1_state.ts +125 -21
  96. package/src/test/index.ts +3 -0
  97. package/src/test/mock_archiver.ts +3 -2
  98. package/src/test/mock_l2_block_source.ts +179 -87
  99. package/src/test/mock_structs.ts +42 -12
  100. package/src/test/noop_l1_archiver.ts +115 -0
@@ -9,17 +9,21 @@ 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,
13
+ BlockHash,
12
14
  Body,
13
15
  CheckpointedL2Block,
14
16
  CommitteeAttestation,
17
+ GENESIS_CHECKPOINT_HEADER_HASH,
15
18
  L2Block,
16
- L2BlockHash,
19
+ type L2TipId,
20
+ type L2Tips,
17
21
  type ValidateCheckpointResult,
18
22
  deserializeValidateCheckpointResult,
19
23
  serializeValidateCheckpointResult,
20
24
  } from '@aztec/stdlib/block';
21
- import { L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
22
- import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
25
+ import { type CheckpointData, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
26
+ import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
23
27
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
24
28
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
25
29
  import {
@@ -34,15 +38,14 @@ import {
34
38
  } from '@aztec/stdlib/tx';
35
39
 
36
40
  import {
41
+ BlockAlreadyCheckpointedError,
37
42
  BlockArchiveNotConsistentError,
38
43
  BlockIndexNotSequentialError,
39
44
  BlockNotFoundError,
40
45
  BlockNumberNotSequentialError,
41
46
  CannotOverwriteCheckpointedBlockError,
42
47
  CheckpointNotFoundError,
43
- CheckpointNumberNotConsistentError,
44
48
  CheckpointNumberNotSequentialError,
45
- InitialBlockNumberNotSequentialError,
46
49
  InitialCheckpointNumberNotSequentialError,
47
50
  } from '../errors.js';
48
51
 
@@ -61,23 +64,14 @@ type BlockStorage = {
61
64
  type CheckpointStorage = {
62
65
  header: Buffer;
63
66
  archive: Buffer;
67
+ checkpointOutHash: Buffer;
64
68
  checkpointNumber: number;
65
69
  startBlock: number;
66
- numBlocks: number;
70
+ blockCount: number;
67
71
  l1: Buffer;
68
72
  attestations: Buffer[];
69
73
  };
70
74
 
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
75
  export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
82
76
 
83
77
  /**
@@ -90,6 +84,9 @@ export class BlockStore {
90
84
  /** Map checkpoint number to checkpoint data */
91
85
  #checkpoints: AztecAsyncMap<number, CheckpointStorage>;
92
86
 
87
+ /** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
88
+ #slotToCheckpoint: AztecAsyncMap<number, number>;
89
+
93
90
  /** Map block hash to list of tx hashes */
94
91
  #blockTxs: AztecAsyncMap<string, Buffer>;
95
92
 
@@ -102,6 +99,9 @@ export class BlockStore {
102
99
  /** Stores last proven checkpoint */
103
100
  #lastProvenCheckpoint: AztecAsyncSingleton<number>;
104
101
 
102
+ /** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
103
+ #lastFinalizedCheckpoint: AztecAsyncSingleton<number>;
104
+
105
105
  /** Stores the pending chain validation status */
106
106
  #pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;
107
107
 
@@ -116,10 +116,7 @@ export class BlockStore {
116
116
 
117
117
  #log = createLogger('archiver:block_store');
118
118
 
119
- constructor(
120
- private db: AztecAsyncKVStore,
121
- private l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
122
- ) {
119
+ constructor(private db: AztecAsyncKVStore) {
123
120
  this.#blocks = db.openMap('archiver_blocks');
124
121
  this.#blockTxs = db.openMap('archiver_block_txs');
125
122
  this.#txEffects = db.openMap('archiver_tx_effects');
@@ -128,40 +125,42 @@ export class BlockStore {
128
125
  this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
129
126
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
130
127
  this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint');
128
+ this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint');
131
129
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
132
130
  this.#checkpoints = db.openMap('archiver_checkpoints');
131
+ this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
133
132
  }
134
133
 
135
134
  /**
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
135
+ * Returns the finalized L2 block number. An L2 block is finalized when it was proven
136
+ * in an L1 block that has itself been finalized on Ethereum.
140
137
  * @returns The finalized block number.
141
138
  */
142
139
  async getFinalizedL2BlockNumber(): Promise<BlockNumber> {
143
- const provenBlockNumber = await this.getProvenBlockNumber();
144
- return BlockNumber(Math.max(provenBlockNumber - this.l1Constants.epochDuration * 2, 0));
140
+ const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber();
141
+ if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
142
+ return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
143
+ }
144
+ const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber);
145
+ if (!checkpointStorage) {
146
+ throw new CheckpointNotFoundError(finalizedCheckpointNumber);
147
+ }
148
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
145
149
  }
146
150
 
147
151
  /**
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.
152
+ * Append a new proposed block to the store.
153
+ * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
150
154
  * For checkpointed blocks (already published to L1), use addCheckpoints() instead.
151
- * @param blocks - The proposed L2 blocks to be added to the store.
155
+ * @param block - The proposed L2 block to be added to the store.
152
156
  * @returns True if the operation is successful.
153
157
  */
154
- async addProposedBlocks(blocks: L2Block[], opts: { force?: boolean } = {}): Promise<boolean> {
155
- if (blocks.length === 0) {
156
- return true;
157
- }
158
-
158
+ async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
159
159
  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;
160
+ const blockNumber = block.number;
161
+ const blockCheckpointNumber = block.checkpointNumber;
162
+ const blockIndex = block.indexWithinCheckpoint;
163
+ const blockLastArchive = block.header.lastArchive.root;
165
164
 
166
165
  // Extract the latest block and checkpoint numbers
167
166
  const previousBlockNumber = await this.getLatestBlockNumber();
@@ -169,71 +168,52 @@ export class BlockStore {
169
168
 
170
169
  // Verify we're not overwriting checkpointed blocks
171
170
  const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
172
- if (!opts.force && firstBlockNumber <= lastCheckpointedBlockNumber) {
173
- throw new CannotOverwriteCheckpointedBlockError(firstBlockNumber, lastCheckpointedBlockNumber);
171
+ if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
172
+ // Check if the proposed block matches the already-checkpointed one
173
+ const existingBlock = await this.getBlock(BlockNumber(blockNumber));
174
+ if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
175
+ throw new BlockAlreadyCheckpointedError(blockNumber);
176
+ }
177
+ throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber);
174
178
  }
175
179
 
176
- // Check that the first block number is the expected one
177
- if (!opts.force && previousBlockNumber !== firstBlockNumber - 1) {
178
- throw new InitialBlockNumberNotSequentialError(firstBlockNumber, previousBlockNumber);
180
+ // Check that the block number is the expected one
181
+ if (!opts.force && previousBlockNumber !== blockNumber - 1) {
182
+ throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
179
183
  }
180
184
 
181
185
  // The same check as above but for checkpoints
182
- if (!opts.force && previousCheckpointNumber !== firstBlockCheckpointNumber - 1) {
183
- throw new InitialCheckpointNumberNotSequentialError(firstBlockCheckpointNumber, previousCheckpointNumber);
186
+ if (!opts.force && previousCheckpointNumber !== blockCheckpointNumber - 1) {
187
+ throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, previousCheckpointNumber);
184
188
  }
185
189
 
186
190
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
187
191
  const previousBlockResult = await this.getBlock(previousBlockNumber);
188
192
 
189
- let expectedFirstblockIndex = 0;
193
+ let expectedBlockIndex = 0;
190
194
  let previousBlockIndex: number | undefined = undefined;
191
195
  if (previousBlockResult !== undefined) {
192
- if (previousBlockResult.checkpointNumber === firstBlockCheckpointNumber) {
196
+ if (previousBlockResult.checkpointNumber === blockCheckpointNumber) {
193
197
  // The previous block is for the same checkpoint, therefore our index should follow it
194
198
  previousBlockIndex = previousBlockResult.indexWithinCheckpoint;
195
- expectedFirstblockIndex = previousBlockIndex + 1;
199
+ expectedBlockIndex = previousBlockIndex + 1;
196
200
  }
197
- if (!previousBlockResult.archive.root.equals(firstBlockLastArchive)) {
201
+ if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
198
202
  throw new BlockArchiveNotConsistentError(
199
- firstBlockNumber,
203
+ blockNumber,
200
204
  previousBlockResult.number,
201
- firstBlockLastArchive,
205
+ blockLastArchive,
202
206
  previousBlockResult.archive.root,
203
207
  );
204
208
  }
205
209
  }
206
210
 
207
- // Now check that the first block has the expected index value
208
- if (!opts.force && expectedFirstblockIndex !== firstBlockIndex) {
209
- throw new BlockIndexNotSequentialError(firstBlockIndex, previousBlockIndex);
211
+ // Now check that the block has the expected index value
212
+ if (!opts.force && expectedBlockIndex !== blockIndex) {
213
+ throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex);
210
214
  }
211
215
 
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
- }
216
+ await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
237
217
 
238
218
  return true;
239
219
  });
@@ -250,21 +230,34 @@ export class BlockStore {
250
230
  }
251
231
 
252
232
  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
233
  const firstCheckpointNumber = checkpoints[0].checkpoint.number;
255
234
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
256
235
 
257
- if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
236
+ // Handle already-stored checkpoints at the start of the batch.
237
+ // This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
238
+ // We accept them if archives match (same content) and update their L1 metadata.
239
+ if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
240
+ checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
241
+ if (checkpoints.length === 0) {
242
+ return true;
243
+ }
244
+ // Re-check sequentiality after skipping
245
+ const newFirstNumber = checkpoints[0].checkpoint.number;
246
+ if (previousCheckpointNumber !== newFirstNumber - 1) {
247
+ throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
248
+ }
249
+ } else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
258
250
  throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
259
251
  }
260
252
 
261
253
  // Extract the previous checkpoint if there is one
254
+ const currentFirstCheckpointNumber = checkpoints[0].checkpoint.number;
262
255
  let previousCheckpointData: CheckpointData | undefined = undefined;
263
- if (previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1) {
256
+ if (currentFirstCheckpointNumber - 1 !== INITIAL_CHECKPOINT_NUMBER - 1) {
264
257
  // There should be a previous checkpoint
265
- previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
258
+ previousCheckpointData = await this.getCheckpointData(CheckpointNumber(currentFirstCheckpointNumber - 1));
266
259
  if (previousCheckpointData === undefined) {
267
- throw new CheckpointNotFoundError(previousCheckpointNumber);
260
+ throw new CheckpointNotFoundError(CheckpointNumber(currentFirstCheckpointNumber - 1));
268
261
  }
269
262
  }
270
263
 
@@ -273,7 +266,7 @@ export class BlockStore {
273
266
 
274
267
  // If we have a previous checkpoint then we need to get the previous block number
275
268
  if (previousCheckpointData !== undefined) {
276
- previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.numBlocks - 1);
269
+ previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
277
270
  previousBlock = await this.getBlock(previousBlockNumber);
278
271
  if (previousBlock === undefined) {
279
272
  // We should be able to get the required previous block
@@ -337,12 +330,16 @@ export class BlockStore {
337
330
  await this.#checkpoints.set(checkpoint.checkpoint.number, {
338
331
  header: checkpoint.checkpoint.header.toBuffer(),
339
332
  archive: checkpoint.checkpoint.archive.toBuffer(),
333
+ checkpointOutHash: checkpoint.checkpoint.getCheckpointOutHash().toBuffer(),
340
334
  l1: checkpoint.l1.toBuffer(),
341
335
  attestations: checkpoint.attestations.map(attestation => attestation.toBuffer()),
342
336
  checkpointNumber: checkpoint.checkpoint.number,
343
337
  startBlock: checkpoint.checkpoint.blocks[0].number,
344
- numBlocks: checkpoint.checkpoint.blocks.length,
338
+ blockCount: checkpoint.checkpoint.blocks.length,
345
339
  });
340
+
341
+ // Update slot-to-checkpoint index
342
+ await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
346
343
  }
347
344
 
348
345
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
@@ -350,8 +347,52 @@ export class BlockStore {
350
347
  });
351
348
  }
352
349
 
350
+ /**
351
+ * Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
352
+ * Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
353
+ */
354
+ private async skipOrUpdateAlreadyStoredCheckpoints(
355
+ checkpoints: PublishedCheckpoint[],
356
+ latestStored: CheckpointNumber,
357
+ ): Promise<PublishedCheckpoint[]> {
358
+ let i = 0;
359
+ for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
360
+ const incoming = checkpoints[i];
361
+ const stored = await this.getCheckpointData(incoming.checkpoint.number);
362
+ if (!stored) {
363
+ // Should not happen if latestStored is correct, but be safe
364
+ break;
365
+ }
366
+ // Verify the checkpoint content matches (archive root)
367
+ if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
368
+ throw new Error(
369
+ `Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
370
+ `Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
371
+ );
372
+ }
373
+ // Update L1 metadata and attestations for the already-stored checkpoint
374
+ this.#log.warn(
375
+ `Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
376
+ `(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
377
+ );
378
+ await this.#checkpoints.set(incoming.checkpoint.number, {
379
+ header: incoming.checkpoint.header.toBuffer(),
380
+ archive: incoming.checkpoint.archive.toBuffer(),
381
+ checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
382
+ l1: incoming.l1.toBuffer(),
383
+ attestations: incoming.attestations.map(a => a.toBuffer()),
384
+ checkpointNumber: incoming.checkpoint.number,
385
+ startBlock: incoming.checkpoint.blocks[0].number,
386
+ blockCount: incoming.checkpoint.blocks.length,
387
+ });
388
+ // Update the sync point to reflect the new L1 block
389
+ await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
390
+ }
391
+ return checkpoints.slice(i);
392
+ }
393
+
353
394
  private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
354
- const blockHash = L2BlockHash.fromField(await block.hash());
395
+ const blockHash = await block.hash();
355
396
 
356
397
  await this.#blocks.set(block.number, {
357
398
  header: block.header.toBuffer(),
@@ -425,7 +466,7 @@ export class BlockStore {
425
466
  if (!targetCheckpoint) {
426
467
  throw new Error(`Target checkpoint ${checkpointNumber} not found in store`);
427
468
  }
428
- lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.numBlocks - 1);
469
+ lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.blockCount - 1);
429
470
  }
430
471
 
431
472
  // Remove all blocks after lastBlockToKeep (both checkpointed and uncheckpointed)
@@ -433,6 +474,11 @@ export class BlockStore {
433
474
 
434
475
  // Remove all checkpoints after the target
435
476
  for (let c = latestCheckpointNumber; c > checkpointNumber; c = CheckpointNumber(c - 1)) {
477
+ const checkpointStorage = await this.#checkpoints.getAsync(c);
478
+ if (checkpointStorage) {
479
+ const slotNumber = CheckpointHeader.fromBuffer(checkpointStorage.header).slotNumber;
480
+ await this.#slotToCheckpoint.delete(slotNumber);
481
+ }
436
482
  await this.#checkpoints.delete(c);
437
483
  this.#log.debug(`Removed checkpoint ${c}`);
438
484
  }
@@ -461,17 +507,32 @@ export class BlockStore {
461
507
  return checkpoints;
462
508
  }
463
509
 
464
- private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage) {
465
- const data: CheckpointData = {
510
+ /** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
511
+ async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
512
+ const result: CheckpointData[] = [];
513
+ for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
514
+ start: startSlot,
515
+ end: endSlot + 1,
516
+ })) {
517
+ const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
518
+ if (checkpointStorage) {
519
+ result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
520
+ }
521
+ }
522
+ return result;
523
+ }
524
+
525
+ private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
526
+ return {
466
527
  header: CheckpointHeader.fromBuffer(checkpointStorage.header),
467
528
  archive: AppendOnlyTreeSnapshot.fromBuffer(checkpointStorage.archive),
529
+ checkpointOutHash: Fr.fromBuffer(checkpointStorage.checkpointOutHash),
468
530
  checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
469
- startBlock: checkpointStorage.startBlock,
470
- numBlocks: checkpointStorage.numBlocks,
531
+ startBlock: BlockNumber(checkpointStorage.startBlock),
532
+ blockCount: checkpointStorage.blockCount,
471
533
  l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
472
- attestations: checkpointStorage.attestations,
534
+ attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
473
535
  };
474
- return data;
475
536
  }
476
537
 
477
538
  async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<L2Block[] | undefined> {
@@ -483,7 +544,7 @@ export class BlockStore {
483
544
  const blocksForCheckpoint = await toArray(
484
545
  this.#blocks.entriesAsync({
485
546
  start: checkpoint.startBlock,
486
- end: checkpoint.startBlock + checkpoint.numBlocks,
547
+ end: checkpoint.startBlock + checkpoint.blockCount,
487
548
  }),
488
549
  );
489
550
 
@@ -556,7 +617,7 @@ export class BlockStore {
556
617
  if (!checkpointStorage) {
557
618
  throw new CheckpointNotFoundError(provenCheckpointNumber);
558
619
  } else {
559
- return BlockNumber(checkpointStorage.startBlock + checkpointStorage.numBlocks - 1);
620
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
560
621
  }
561
622
  }
562
623
 
@@ -567,6 +628,138 @@ export class BlockStore {
567
628
  : BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
568
629
  }
569
630
 
631
+ /**
632
+ * Resolves all L2 chain tips in a single read transaction so the snapshot is internally consistent.
633
+ * The result is guaranteed to satisfy finalized <= proven <= checkpointed <= proposed by block number.
634
+ */
635
+ async getL2TipsData(genesisBlockHash: Fr): Promise<L2Tips> {
636
+ return await this.db.transactionAsync(async () => {
637
+ const genesisBlockNumber = BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
638
+ const genesisCheckpointNumber = CheckpointNumber(INITIAL_CHECKPOINT_NUMBER - 1);
639
+ const genesisBlockId = { number: genesisBlockNumber, hash: genesisBlockHash.toString() };
640
+ const genesisCheckpointId = {
641
+ number: genesisCheckpointNumber,
642
+ hash: GENESIS_CHECKPOINT_HEADER_HASH.toString(),
643
+ };
644
+ const genesisTip: L2TipId = { block: genesisBlockId, checkpoint: genesisCheckpointId };
645
+
646
+ const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 }));
647
+ const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 }));
648
+ const latestCheckpointNumber = latestCheckpointEntry
649
+ ? CheckpointNumber(latestCheckpointEntry[0])
650
+ : genesisCheckpointNumber;
651
+
652
+ const [provenRaw, finalizedRaw] = await Promise.all([
653
+ this.#lastProvenCheckpoint.getAsync(),
654
+ this.#lastFinalizedCheckpoint.getAsync(),
655
+ ]);
656
+
657
+ const provenCheckpointNumber = CheckpointNumber(Math.min(provenRaw ?? 0, latestCheckpointNumber));
658
+ const finalizedCheckpointNumber = CheckpointNumber(Math.min(finalizedRaw ?? 0, provenCheckpointNumber));
659
+
660
+ const checkpointStorageCache = new Map<CheckpointNumber, CheckpointStorage>();
661
+ if (latestCheckpointEntry) {
662
+ checkpointStorageCache.set(CheckpointNumber(latestCheckpointEntry[0]), latestCheckpointEntry[1]);
663
+ }
664
+
665
+ const loadCheckpointStorage = async (
666
+ checkpointNumber: CheckpointNumber,
667
+ ): Promise<CheckpointStorage | undefined> => {
668
+ if (checkpointNumber === genesisCheckpointNumber) {
669
+ return undefined;
670
+ }
671
+ if (!checkpointStorageCache.has(checkpointNumber)) {
672
+ const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
673
+ if (!checkpointStorage) {
674
+ throw new CheckpointNotFoundError(checkpointNumber);
675
+ }
676
+ checkpointStorageCache.set(checkpointNumber, checkpointStorage);
677
+ }
678
+ return checkpointStorageCache.get(checkpointNumber)!;
679
+ };
680
+
681
+ const [provenCheckpoint, finalizedCheckpoint] = await Promise.all([
682
+ loadCheckpointStorage(provenCheckpointNumber),
683
+ loadCheckpointStorage(finalizedCheckpointNumber),
684
+ ]);
685
+
686
+ const blockHashCache = new Map<BlockNumber, string>();
687
+ blockHashCache.set(genesisBlockNumber, genesisBlockHash.toString());
688
+ if (latestBlockEntry) {
689
+ blockHashCache.set(
690
+ BlockNumber(latestBlockEntry[0]),
691
+ BlockHash.fromBuffer(latestBlockEntry[1].blockHash).toString(),
692
+ );
693
+ }
694
+
695
+ const loadBlockHash = async (blockNumber: BlockNumber): Promise<string> => {
696
+ if (!blockHashCache.has(blockNumber)) {
697
+ const blockStorage = await this.#blocks.getAsync(blockNumber);
698
+ if (!blockStorage) {
699
+ throw new BlockNotFoundError(blockNumber);
700
+ }
701
+ blockHashCache.set(blockNumber, BlockHash.fromBuffer(blockStorage.blockHash).toString());
702
+ }
703
+ return blockHashCache.get(blockNumber)!;
704
+ };
705
+
706
+ const proposed =
707
+ latestBlockEntry === undefined
708
+ ? genesisBlockId
709
+ : {
710
+ number: BlockNumber(latestBlockEntry[0]),
711
+ hash: BlockHash.fromBuffer(latestBlockEntry[1].blockHash).toString(),
712
+ };
713
+
714
+ const buildTipFromCheckpoint = async (stored: CheckpointStorage | undefined): Promise<L2TipId> => {
715
+ if (!stored) {
716
+ return genesisTip;
717
+ }
718
+ const blockNumber = BlockNumber(stored.startBlock + stored.blockCount - 1);
719
+ return {
720
+ block: { number: blockNumber, hash: await loadBlockHash(blockNumber) },
721
+ checkpoint: {
722
+ number: CheckpointNumber(stored.checkpointNumber),
723
+ hash: CheckpointHeader.fromBuffer(stored.header).hash().toString(),
724
+ },
725
+ };
726
+ };
727
+
728
+ const [checkpointed, proven, finalized] = await Promise.all([
729
+ buildTipFromCheckpoint(latestCheckpointEntry?.[1]),
730
+ buildTipFromCheckpoint(provenCheckpoint),
731
+ buildTipFromCheckpoint(finalizedCheckpoint),
732
+ ]);
733
+
734
+ if (proposed.number < checkpointed.block.number) {
735
+ throw new Error(
736
+ `Inconsistent block store: latest block ${proposed.number} is behind checkpointed block ${checkpointed.block.number}`,
737
+ );
738
+ }
739
+
740
+ if (
741
+ finalized.checkpoint.number > proven.checkpoint.number ||
742
+ proven.checkpoint.number > checkpointed.checkpoint.number
743
+ ) {
744
+ throw new Error(
745
+ `Inconsistent checkpoint numbers in chain tips: finalized=${finalized.checkpoint.number} proven=${proven.checkpoint.number} checkpointed=${checkpointed.checkpoint.number}`,
746
+ );
747
+ }
748
+
749
+ if (
750
+ finalized.block.number > proven.block.number ||
751
+ proven.block.number > checkpointed.block.number ||
752
+ checkpointed.block.number > proposed.number
753
+ ) {
754
+ throw new Error(
755
+ `Inconsistent block numbers in chain tips: finalized=${finalized.block.number} proven=${proven.block.number} checkpointed=${checkpointed.block.number} proposed=${proposed.number}`,
756
+ );
757
+ }
758
+
759
+ return { proposed, checkpointed, proven, finalized };
760
+ });
761
+ }
762
+
570
763
  async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
571
764
  const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
572
765
  if (latestCheckpointNumber === undefined) {
@@ -624,7 +817,7 @@ export class BlockStore {
624
817
  }
625
818
  }
626
819
 
627
- async getCheckpointedBlockByHash(blockHash: Fr): Promise<CheckpointedL2Block | undefined> {
820
+ async getCheckpointedBlockByHash(blockHash: BlockHash): Promise<CheckpointedL2Block | undefined> {
628
821
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
629
822
  if (blockNumber === undefined) {
630
823
  return undefined;
@@ -655,6 +848,32 @@ export class BlockStore {
655
848
  }
656
849
  }
657
850
 
851
+ /**
852
+ * Gets block metadata (without tx data) by block number.
853
+ * @param blockNumber - The number of the block to return.
854
+ * @returns The requested block data.
855
+ */
856
+ async getBlockData(blockNumber: BlockNumber): Promise<BlockData | undefined> {
857
+ const blockStorage = await this.#blocks.getAsync(blockNumber);
858
+ if (!blockStorage || !blockStorage.header) {
859
+ return undefined;
860
+ }
861
+ return this.getBlockDataFromBlockStorage(blockStorage);
862
+ }
863
+
864
+ /**
865
+ * Gets block metadata (without tx data) by archive root.
866
+ * @param archive - The archive root of the block to return.
867
+ * @returns The requested block data.
868
+ */
869
+ async getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
870
+ const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
871
+ if (blockNumber === undefined) {
872
+ return undefined;
873
+ }
874
+ return this.getBlockData(BlockNumber(blockNumber));
875
+ }
876
+
658
877
  /**
659
878
  * Gets an L2 block.
660
879
  * @param blockNumber - The number of the block to return.
@@ -673,7 +892,7 @@ export class BlockStore {
673
892
  * @param blockHash - The hash of the block to return.
674
893
  * @returns The requested L2 block.
675
894
  */
676
- async getBlockByHash(blockHash: L2BlockHash): Promise<L2Block | undefined> {
895
+ async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
677
896
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
678
897
  if (blockNumber === undefined) {
679
898
  return undefined;
@@ -699,7 +918,7 @@ export class BlockStore {
699
918
  * @param blockHash - The hash of the block to return.
700
919
  * @returns The requested block header.
701
920
  */
702
- async getBlockHeaderByHash(blockHash: L2BlockHash): Promise<BlockHeader | undefined> {
921
+ async getBlockHeaderByHash(blockHash: BlockHash): Promise<BlockHeader | undefined> {
703
922
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
704
923
  if (blockNumber === undefined) {
705
924
  return undefined;
@@ -759,15 +978,24 @@ export class BlockStore {
759
978
  }
760
979
  }
761
980
 
981
+ private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
982
+ return {
983
+ header: BlockHeader.fromBuffer(blockStorage.header),
984
+ archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
985
+ blockHash: Fr.fromBuffer(blockStorage.blockHash),
986
+ checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
987
+ indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
988
+ };
989
+ }
990
+
762
991
  private async getBlockFromBlockStorage(
763
992
  blockNumber: number,
764
993
  blockStorage: BlockStorage,
765
994
  ): 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);
995
+ const { header, archive, blockHash, checkpointNumber, indexWithinCheckpoint } =
996
+ this.getBlockDataFromBlockStorage(blockStorage);
997
+ header.setHash(blockHash);
998
+ const blockHashString = bufferToHex(blockStorage.blockHash);
771
999
  const blockTxsBuffer = await this.#blockTxs.getAsync(blockHashString);
772
1000
  if (blockTxsBuffer === undefined) {
773
1001
  this.#log.warn(`Could not find body for block ${header.globalVariables.blockNumber} ${blockHash}`);
@@ -786,13 +1014,7 @@ export class BlockStore {
786
1014
  txEffects.push(deserializeIndexedTxEffect(txEffect).data);
787
1015
  }
788
1016
  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
- );
1017
+ const block = new L2Block(archive, header, body, checkpointNumber, indexWithinCheckpoint);
796
1018
 
797
1019
  if (block.number !== blockNumber) {
798
1020
  throw new Error(
@@ -822,7 +1044,10 @@ export class BlockStore {
822
1044
  * @param txHash - The hash of a tx we try to get the receipt for.
823
1045
  * @returns The requested tx receipt (or undefined if not found).
824
1046
  */
825
- async getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
1047
+ async getSettledTxReceipt(
1048
+ txHash: TxHash,
1049
+ l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
1050
+ ): Promise<TxReceipt | undefined> {
826
1051
  const txEffect = await this.getTxEffect(txHash);
827
1052
  if (!txEffect) {
828
1053
  return undefined;
@@ -831,10 +1056,11 @@ export class BlockStore {
831
1056
  const blockNumber = BlockNumber(txEffect.l2BlockNumber);
832
1057
 
833
1058
  // Use existing archiver methods to determine finalization level
834
- const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([
1059
+ const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
835
1060
  this.getProvenBlockNumber(),
836
1061
  this.getCheckpointedL2BlockNumber(),
837
1062
  this.getFinalizedL2BlockNumber(),
1063
+ this.getBlockData(blockNumber),
838
1064
  ]);
839
1065
 
840
1066
  let status: TxStatus;
@@ -848,6 +1074,9 @@ export class BlockStore {
848
1074
  status = TxStatus.PROPOSED;
849
1075
  }
850
1076
 
1077
+ const epochNumber =
1078
+ blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
1079
+
851
1080
  return new TxReceipt(
852
1081
  txHash,
853
1082
  status,
@@ -856,6 +1085,7 @@ export class BlockStore {
856
1085
  txEffect.data.transactionFee.toBigInt(),
857
1086
  txEffect.l2BlockHash,
858
1087
  blockNumber,
1088
+ epochNumber,
859
1089
  );
860
1090
  }
861
1091
 
@@ -892,7 +1122,7 @@ export class BlockStore {
892
1122
  if (!checkpoint) {
893
1123
  return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
894
1124
  }
895
- return BlockNumber(checkpoint.startBlock + checkpoint.numBlocks - 1);
1125
+ return BlockNumber(checkpoint.startBlock + checkpoint.blockCount - 1);
896
1126
  }
897
1127
 
898
1128
  async getLatestL2BlockNumber(): Promise<BlockNumber> {
@@ -927,6 +1157,20 @@ export class BlockStore {
927
1157
  return result;
928
1158
  }
929
1159
 
1160
+ async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
1161
+ const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
1162
+ this.getLatestCheckpointNumber(),
1163
+ this.#lastFinalizedCheckpoint.getAsync(),
1164
+ ]);
1165
+ return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber
1166
+ ? latestCheckpointNumber
1167
+ : CheckpointNumber(finalizedCheckpointNumber ?? 0);
1168
+ }
1169
+
1170
+ setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
1171
+ return this.#lastFinalizedCheckpoint.set(checkpointNumber);
1172
+ }
1173
+
930
1174
  #computeBlockRange(start: BlockNumber, limit: number): Required<Pick<Range<number>, 'start' | 'limit'>> {
931
1175
  if (limit < 1) {
932
1176
  throw new Error(`Invalid limit: ${limit}`);