@aztec/archiver 0.0.1-commit.d1f2d6c → 0.0.1-commit.d20b825a7

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 (120) hide show
  1. package/README.md +12 -6
  2. package/dest/archiver.d.ts +16 -10
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +110 -122
  5. package/dest/config.d.ts +5 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +15 -3
  8. package/dest/errors.d.ts +55 -10
  9. package/dest/errors.d.ts.map +1 -1
  10. package/dest/errors.js +74 -15
  11. package/dest/factory.d.ts +5 -4
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +34 -29
  14. package/dest/index.d.ts +4 -2
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +3 -1
  17. package/dest/l1/bin/retrieve-calldata.js +36 -33
  18. package/dest/l1/calldata_retriever.d.ts +73 -50
  19. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  20. package/dest/l1/calldata_retriever.js +191 -259
  21. package/dest/l1/data_retrieval.d.ts +26 -17
  22. package/dest/l1/data_retrieval.d.ts.map +1 -1
  23. package/dest/l1/data_retrieval.js +43 -48
  24. package/dest/l1/spire_proposer.d.ts +5 -5
  25. package/dest/l1/spire_proposer.d.ts.map +1 -1
  26. package/dest/l1/spire_proposer.js +9 -17
  27. package/dest/l1/validate_historical_logs.d.ts +23 -0
  28. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  29. package/dest/l1/validate_historical_logs.js +108 -0
  30. package/dest/l1/validate_trace.d.ts +6 -3
  31. package/dest/l1/validate_trace.d.ts.map +1 -1
  32. package/dest/l1/validate_trace.js +13 -9
  33. package/dest/modules/data_source_base.d.ts +17 -10
  34. package/dest/modules/data_source_base.d.ts.map +1 -1
  35. package/dest/modules/data_source_base.js +39 -77
  36. package/dest/modules/data_store_updater.d.ts +50 -26
  37. package/dest/modules/data_store_updater.d.ts.map +1 -1
  38. package/dest/modules/data_store_updater.js +169 -130
  39. package/dest/modules/instrumentation.d.ts +21 -3
  40. package/dest/modules/instrumentation.d.ts.map +1 -1
  41. package/dest/modules/instrumentation.js +58 -18
  42. package/dest/modules/l1_synchronizer.d.ts +10 -9
  43. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  44. package/dest/modules/l1_synchronizer.js +285 -157
  45. package/dest/modules/validation.d.ts +1 -1
  46. package/dest/modules/validation.d.ts.map +1 -1
  47. package/dest/modules/validation.js +2 -2
  48. package/dest/store/block_store.d.ts +85 -36
  49. package/dest/store/block_store.d.ts.map +1 -1
  50. package/dest/store/block_store.js +433 -162
  51. package/dest/store/contract_class_store.d.ts +2 -3
  52. package/dest/store/contract_class_store.d.ts.map +1 -1
  53. package/dest/store/contract_class_store.js +16 -72
  54. package/dest/store/contract_instance_store.d.ts +1 -1
  55. package/dest/store/contract_instance_store.d.ts.map +1 -1
  56. package/dest/store/contract_instance_store.js +6 -2
  57. package/dest/store/kv_archiver_store.d.ts +76 -32
  58. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  59. package/dest/store/kv_archiver_store.js +92 -37
  60. package/dest/store/l2_tips_cache.d.ts +20 -0
  61. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  62. package/dest/store/l2_tips_cache.js +109 -0
  63. package/dest/store/log_store.d.ts +6 -3
  64. package/dest/store/log_store.d.ts.map +1 -1
  65. package/dest/store/log_store.js +151 -56
  66. package/dest/store/message_store.d.ts +5 -1
  67. package/dest/store/message_store.d.ts.map +1 -1
  68. package/dest/store/message_store.js +21 -9
  69. package/dest/test/fake_l1_state.d.ts +24 -1
  70. package/dest/test/fake_l1_state.d.ts.map +1 -1
  71. package/dest/test/fake_l1_state.js +145 -28
  72. package/dest/test/index.js +3 -1
  73. package/dest/test/mock_archiver.d.ts +1 -1
  74. package/dest/test/mock_archiver.d.ts.map +1 -1
  75. package/dest/test/mock_archiver.js +3 -2
  76. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  77. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  78. package/dest/test/mock_l1_to_l2_message_source.js +2 -1
  79. package/dest/test/mock_l2_block_source.d.ts +31 -10
  80. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  81. package/dest/test/mock_l2_block_source.js +163 -92
  82. package/dest/test/mock_structs.d.ts +6 -2
  83. package/dest/test/mock_structs.d.ts.map +1 -1
  84. package/dest/test/mock_structs.js +20 -6
  85. package/dest/test/noop_l1_archiver.d.ts +26 -0
  86. package/dest/test/noop_l1_archiver.d.ts.map +1 -0
  87. package/dest/test/noop_l1_archiver.js +74 -0
  88. package/package.json +14 -13
  89. package/src/archiver.ts +150 -146
  90. package/src/config.ts +22 -2
  91. package/src/errors.ts +116 -26
  92. package/src/factory.ts +49 -26
  93. package/src/index.ts +3 -1
  94. package/src/l1/README.md +25 -68
  95. package/src/l1/bin/retrieve-calldata.ts +46 -39
  96. package/src/l1/calldata_retriever.ts +250 -379
  97. package/src/l1/data_retrieval.ts +59 -70
  98. package/src/l1/spire_proposer.ts +7 -15
  99. package/src/l1/validate_historical_logs.ts +140 -0
  100. package/src/l1/validate_trace.ts +24 -6
  101. package/src/modules/data_source_base.ts +81 -101
  102. package/src/modules/data_store_updater.ts +202 -160
  103. package/src/modules/instrumentation.ts +71 -19
  104. package/src/modules/l1_synchronizer.ts +365 -197
  105. package/src/modules/validation.ts +2 -2
  106. package/src/store/block_store.ts +546 -206
  107. package/src/store/contract_class_store.ts +16 -110
  108. package/src/store/contract_instance_store.ts +8 -5
  109. package/src/store/kv_archiver_store.ts +143 -53
  110. package/src/store/l2_tips_cache.ts +134 -0
  111. package/src/store/log_store.ts +225 -67
  112. package/src/store/message_store.ts +27 -10
  113. package/src/structs/inbox_message.ts +1 -1
  114. package/src/test/fake_l1_state.ts +193 -32
  115. package/src/test/index.ts +3 -0
  116. package/src/test/mock_archiver.ts +3 -2
  117. package/src/test/mock_l1_to_l2_message_source.ts +1 -0
  118. package/src/test/mock_l2_block_source.ts +217 -90
  119. package/src/test/mock_structs.ts +42 -12
  120. package/src/test/noop_l1_archiver.ts +117 -0
@@ -9,17 +9,26 @@ 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,
15
17
  L2Block,
16
- L2BlockHash,
17
18
  type ValidateCheckpointResult,
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 {
23
+ Checkpoint,
24
+ type CheckpointData,
25
+ type CommonCheckpointData,
26
+ L1PublishedData,
27
+ type ProposedCheckpointData,
28
+ type ProposedCheckpointInput,
29
+ PublishedCheckpoint,
30
+ } from '@aztec/stdlib/checkpoint';
31
+ import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
23
32
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
24
33
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
25
34
  import {
@@ -34,15 +43,20 @@ import {
34
43
  } from '@aztec/stdlib/tx';
35
44
 
36
45
  import {
46
+ BlockAlreadyCheckpointedError,
37
47
  BlockArchiveNotConsistentError,
38
48
  BlockIndexNotSequentialError,
39
49
  BlockNotFoundError,
40
50
  BlockNumberNotSequentialError,
51
+ CannotOverwriteCheckpointedBlockError,
41
52
  CheckpointNotFoundError,
42
- CheckpointNumberNotConsistentError,
43
53
  CheckpointNumberNotSequentialError,
44
- InitialBlockNumberNotSequentialError,
45
54
  InitialCheckpointNumberNotSequentialError,
55
+ NoProposedCheckpointToPromoteError,
56
+ ProposedCheckpointArchiveRootMismatchError,
57
+ ProposedCheckpointNotSequentialError,
58
+ ProposedCheckpointPromotionNotSequentialError,
59
+ ProposedCheckpointStaleError,
46
60
  } from '../errors.js';
47
61
 
48
62
  export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
@@ -57,26 +71,29 @@ type BlockStorage = {
57
71
  indexWithinCheckpoint: number;
58
72
  };
59
73
 
60
- type CheckpointStorage = {
74
+ /** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
75
+ type CommonCheckpointStorage = {
61
76
  header: Buffer;
62
77
  archive: Buffer;
78
+ checkpointOutHash: Buffer;
63
79
  checkpointNumber: number;
64
80
  startBlock: number;
65
- numBlocks: number;
81
+ blockCount: number;
82
+ };
83
+
84
+ type CheckpointStorage = CommonCheckpointStorage & {
66
85
  l1: Buffer;
67
86
  attestations: Buffer[];
68
87
  };
69
88
 
70
- export type CheckpointData = {
71
- checkpointNumber: CheckpointNumber;
72
- header: CheckpointHeader;
73
- archive: AppendOnlyTreeSnapshot;
74
- startBlock: number;
75
- numBlocks: number;
76
- l1: L1PublishedData;
77
- attestations: Buffer[];
89
+ /** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
90
+ type ProposedCheckpointStorage = CommonCheckpointStorage & {
91
+ totalManaUsed: string;
92
+ feeAssetPriceModifier: string;
78
93
  };
79
94
 
95
+ export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
96
+
80
97
  /**
81
98
  * LMDB-based block storage for the archiver.
82
99
  */
@@ -87,6 +104,9 @@ export class BlockStore {
87
104
  /** Map checkpoint number to checkpoint data */
88
105
  #checkpoints: AztecAsyncMap<number, CheckpointStorage>;
89
106
 
107
+ /** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
108
+ #slotToCheckpoint: AztecAsyncMap<number, number>;
109
+
90
110
  /** Map block hash to list of tx hashes */
91
111
  #blockTxs: AztecAsyncMap<string, Buffer>;
92
112
 
@@ -99,6 +119,9 @@ export class BlockStore {
99
119
  /** Stores last proven checkpoint */
100
120
  #lastProvenCheckpoint: AztecAsyncSingleton<number>;
101
121
 
122
+ /** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
123
+ #lastFinalizedCheckpoint: AztecAsyncSingleton<number>;
124
+
102
125
  /** Stores the pending chain validation status */
103
126
  #pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;
104
127
 
@@ -111,12 +134,12 @@ export class BlockStore {
111
134
  /** Index mapping block archive to block number */
112
135
  #blockArchiveIndex: AztecAsyncMap<string, number>;
113
136
 
137
+ /** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
138
+ #proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
139
+
114
140
  #log = createLogger('archiver:block_store');
115
141
 
116
- constructor(
117
- private db: AztecAsyncKVStore,
118
- private l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
119
- ) {
142
+ constructor(private db: AztecAsyncKVStore) {
120
143
  this.#blocks = db.openMap('archiver_blocks');
121
144
  this.#blockTxs = db.openMap('archiver_block_txs');
122
145
  this.#txEffects = db.openMap('archiver_tx_effects');
@@ -125,111 +148,114 @@ export class BlockStore {
125
148
  this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
126
149
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
127
150
  this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint');
151
+ this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint');
128
152
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
129
153
  this.#checkpoints = db.openMap('archiver_checkpoints');
154
+ this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
155
+ this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
130
156
  }
131
157
 
132
158
  /**
133
- * Computes the finalized block number based on the proven block number.
134
- * A block is considered finalized when it's 2 epochs behind the proven block.
135
- * TODO(#13569): Compute proper finalized block number based on L1 finalized block.
136
- * TODO(palla/mbps): Even the provisional computation is wrong, since it should subtract checkpoints, not blocks
159
+ * Returns the finalized L2 block number. An L2 block is finalized when it was proven
160
+ * in an L1 block that has itself been finalized on Ethereum.
137
161
  * @returns The finalized block number.
138
162
  */
139
163
  async getFinalizedL2BlockNumber(): Promise<BlockNumber> {
140
- const provenBlockNumber = await this.getProvenBlockNumber();
141
- return BlockNumber(Math.max(provenBlockNumber - this.l1Constants.epochDuration * 2, 0));
164
+ const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber();
165
+ if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
166
+ return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
167
+ }
168
+ const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber);
169
+ if (!checkpointStorage) {
170
+ throw new CheckpointNotFoundError(finalizedCheckpointNumber);
171
+ }
172
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
142
173
  }
143
174
 
144
175
  /**
145
- * Append new blocks to the store's list. All blocks must be for the 'current' checkpoint
146
- * @param blocks - The L2 blocks to be added to the store.
176
+ * Append a new proposed block to the store.
177
+ * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
178
+ * For checkpointed blocks (already published to L1), use addCheckpoints() instead.
179
+ * @param block - The proposed L2 block to be added to the store.
147
180
  * @returns True if the operation is successful.
148
181
  */
149
- async addBlocks(blocks: L2Block[], opts: { force?: boolean } = {}): Promise<boolean> {
150
- if (blocks.length === 0) {
151
- return true;
152
- }
153
-
182
+ async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
154
183
  return await this.db.transactionAsync(async () => {
155
- // Check that the block immediately before the first block to be added is present in the store.
156
- const firstBlockNumber = blocks[0].number;
157
- const firstBlockCheckpointNumber = blocks[0].checkpointNumber;
158
- const firstBlockIndex = blocks[0].indexWithinCheckpoint;
159
- const firstBlockLastArchive = blocks[0].header.lastArchive.root;
184
+ const blockNumber = block.number;
185
+ const blockCheckpointNumber = block.checkpointNumber;
186
+ const blockIndex = block.indexWithinCheckpoint;
187
+ const blockLastArchive = block.header.lastArchive.root;
160
188
 
161
189
  // Extract the latest block and checkpoint numbers
162
- const previousBlockNumber = await this.getLatestBlockNumber();
190
+ const previousBlockNumber = await this.getLatestL2BlockNumber();
191
+ const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
163
192
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
164
193
 
165
- // Check that the first block number is the expected one
166
- if (!opts.force && previousBlockNumber !== firstBlockNumber - 1) {
167
- throw new InitialBlockNumberNotSequentialError(firstBlockNumber, previousBlockNumber);
194
+ // Verify we're not overwriting checkpointed blocks
195
+ const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
196
+ if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
197
+ // Check if the proposed block matches the already-checkpointed one
198
+ const existingBlock = await this.getBlock(BlockNumber(blockNumber));
199
+ if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
200
+ throw new BlockAlreadyCheckpointedError(blockNumber);
201
+ }
202
+ throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber);
168
203
  }
169
204
 
170
- // The same check as above but for checkpoints
171
- if (!opts.force && previousCheckpointNumber !== firstBlockCheckpointNumber - 1) {
172
- throw new InitialCheckpointNumberNotSequentialError(firstBlockCheckpointNumber, previousCheckpointNumber);
205
+ // Check that the block number is the expected one
206
+ if (!opts.force && previousBlockNumber !== blockNumber - 1) {
207
+ throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
208
+ }
209
+
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.
212
+ 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);
173
223
  }
174
224
 
175
225
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
176
226
  const previousBlockResult = await this.getBlock(previousBlockNumber);
177
227
 
178
- let expectedFirstblockIndex = 0;
228
+ let expectedBlockIndex = 0;
179
229
  let previousBlockIndex: number | undefined = undefined;
180
230
  if (previousBlockResult !== undefined) {
181
- if (previousBlockResult.checkpointNumber === firstBlockCheckpointNumber) {
231
+ if (previousBlockResult.checkpointNumber === blockCheckpointNumber) {
182
232
  // The previous block is for the same checkpoint, therefore our index should follow it
183
233
  previousBlockIndex = previousBlockResult.indexWithinCheckpoint;
184
- expectedFirstblockIndex = previousBlockIndex + 1;
234
+ expectedBlockIndex = previousBlockIndex + 1;
185
235
  }
186
- if (!previousBlockResult.archive.root.equals(firstBlockLastArchive)) {
236
+ if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
187
237
  throw new BlockArchiveNotConsistentError(
188
- firstBlockNumber,
238
+ blockNumber,
189
239
  previousBlockResult.number,
190
- firstBlockLastArchive,
240
+ blockLastArchive,
191
241
  previousBlockResult.archive.root,
192
242
  );
193
243
  }
194
244
  }
195
245
 
196
- // Now check that the first block has the expected index value
197
- if (!opts.force && expectedFirstblockIndex !== firstBlockIndex) {
198
- throw new BlockIndexNotSequentialError(firstBlockIndex, previousBlockIndex);
246
+ // Now check that the block has the expected index value
247
+ if (!opts.force && expectedBlockIndex !== blockIndex) {
248
+ throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex);
199
249
  }
200
250
 
201
- // Iterate over blocks array and insert them, checking that the block numbers and indexes are sequential. Also check they are for the correct checkpoint.
202
- let previousBlock: L2Block | undefined = undefined;
203
- for (const block of blocks) {
204
- if (!opts.force && previousBlock) {
205
- if (previousBlock.number + 1 !== block.number) {
206
- throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
207
- }
208
- if (previousBlock.indexWithinCheckpoint + 1 !== block.indexWithinCheckpoint) {
209
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
210
- }
211
- if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
212
- throw new BlockArchiveNotConsistentError(
213
- block.number,
214
- previousBlock.number,
215
- block.header.lastArchive.root,
216
- previousBlock.archive.root,
217
- );
218
- }
219
- }
220
- if (!opts.force && firstBlockCheckpointNumber !== block.checkpointNumber) {
221
- throw new CheckpointNumberNotConsistentError(block.checkpointNumber, firstBlockCheckpointNumber);
222
- }
223
- previousBlock = block;
224
- await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
225
- }
251
+ await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
226
252
 
227
253
  return true;
228
254
  });
229
255
  }
230
256
 
231
257
  /**
232
- * Append new cheskpoints to the store's list.
258
+ * Append new checkpoints to the store's list.
233
259
  * @param checkpoints - The L2 checkpoints to be added to the store.
234
260
  * @returns True if the operation is successful.
235
261
  */
@@ -239,37 +265,29 @@ export class BlockStore {
239
265
  }
240
266
 
241
267
  return await this.db.transactionAsync(async () => {
242
- // Check that the checkpoint immediately before the first block to be added is present in the store.
243
268
  const firstCheckpointNumber = checkpoints[0].checkpoint.number;
244
269
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
245
270
 
246
- if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
247
- throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
248
- }
249
-
250
- // Extract the previous checkpoint if there is one
251
- let previousCheckpointData: CheckpointData | undefined = undefined;
252
- if (previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1) {
253
- // There should be a previous checkpoint
254
- previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
255
- if (previousCheckpointData === undefined) {
256
- throw new CheckpointNotFoundError(previousCheckpointNumber);
271
+ // Handle already-stored checkpoints at the start of the batch.
272
+ // This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
273
+ // We accept them if archives match (same content) and update their L1 metadata.
274
+ if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
275
+ checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
276
+ if (checkpoints.length === 0) {
277
+ return true;
257
278
  }
258
- }
259
-
260
- let previousBlockNumber: BlockNumber | undefined = undefined;
261
- let previousBlock: L2Block | undefined = undefined;
262
-
263
- // If we have a previous checkpoint then we need to get the previous block number
264
- if (previousCheckpointData !== undefined) {
265
- previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.numBlocks - 1);
266
- previousBlock = await this.getBlock(previousBlockNumber);
267
- if (previousBlock === undefined) {
268
- // We should be able to get the required previous block
269
- throw new BlockNotFoundError(previousBlockNumber);
279
+ // Re-check sequentiality after skipping
280
+ const newFirstNumber = checkpoints[0].checkpoint.number;
281
+ if (previousCheckpointNumber !== newFirstNumber - 1) {
282
+ throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
270
283
  }
284
+ } else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
285
+ throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
271
286
  }
272
287
 
288
+ // Get the last block of the previous checkpoint for archive chaining
289
+ let previousBlock = await this.getPreviousCheckpointBlock(checkpoints[0].checkpoint.number);
290
+
273
291
  // Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
274
292
  let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
275
293
  for (const checkpoint of checkpoints) {
@@ -285,62 +303,148 @@ export class BlockStore {
285
303
  }
286
304
  previousCheckpoint = checkpoint;
287
305
 
288
- // Store every block in the database. the block may already exist, but this has come from chain and is assumed to be correct.
289
- for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
290
- const block = checkpoint.checkpoint.blocks[i];
291
- if (previousBlock) {
292
- // The blocks should have a sequential block number
293
- if (previousBlock.number !== block.number - 1) {
294
- throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
295
- }
296
- // If the blocks are for the same checkpoint then they should have sequential indexes
297
- if (
298
- previousBlock.checkpointNumber === block.checkpointNumber &&
299
- previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1
300
- ) {
301
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
302
- }
303
- if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
304
- throw new BlockArchiveNotConsistentError(
305
- block.number,
306
- previousBlock.number,
307
- block.header.lastArchive.root,
308
- previousBlock.archive.root,
309
- );
310
- }
311
- } else {
312
- // No previous block, must be block 1 at checkpoint index 0
313
- if (block.indexWithinCheckpoint !== 0) {
314
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
315
- }
316
- if (block.number !== INITIAL_L2_BLOCK_NUM) {
317
- throw new BlockNumberNotSequentialError(block.number, undefined);
318
- }
319
- }
306
+ // Validate block sequencing, indexes, and archive chaining
307
+ this.validateCheckpointBlocks(checkpoint.checkpoint.blocks, previousBlock);
320
308
 
321
- previousBlock = block;
322
- await this.addBlockToDatabase(block, checkpoint.checkpoint.number, i);
309
+ // Store every block in the database (may already exist, but L1 data is authoritative)
310
+ for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
311
+ await this.addBlockToDatabase(checkpoint.checkpoint.blocks[i], checkpoint.checkpoint.number, i);
323
312
  }
313
+ previousBlock = checkpoint.checkpoint.blocks.at(-1);
324
314
 
325
315
  // Store the checkpoint in the database
326
316
  await this.#checkpoints.set(checkpoint.checkpoint.number, {
327
317
  header: checkpoint.checkpoint.header.toBuffer(),
328
318
  archive: checkpoint.checkpoint.archive.toBuffer(),
319
+ checkpointOutHash: checkpoint.checkpoint.getCheckpointOutHash().toBuffer(),
329
320
  l1: checkpoint.l1.toBuffer(),
330
321
  attestations: checkpoint.attestations.map(attestation => attestation.toBuffer()),
331
322
  checkpointNumber: checkpoint.checkpoint.number,
332
323
  startBlock: checkpoint.checkpoint.blocks[0].number,
333
- numBlocks: checkpoint.checkpoint.blocks.length,
324
+ blockCount: checkpoint.checkpoint.blocks.length,
334
325
  });
326
+
327
+ // Update slot-to-checkpoint index
328
+ await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
335
329
  }
336
330
 
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);
334
+
337
335
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
338
336
  return true;
339
337
  });
340
338
  }
341
339
 
340
+ /**
341
+ * Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
342
+ * Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
343
+ */
344
+ private async skipOrUpdateAlreadyStoredCheckpoints(
345
+ checkpoints: PublishedCheckpoint[],
346
+ latestStored: CheckpointNumber,
347
+ ): Promise<PublishedCheckpoint[]> {
348
+ let i = 0;
349
+ for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
350
+ const incoming = checkpoints[i];
351
+ const stored = await this.getCheckpointData(incoming.checkpoint.number);
352
+ if (!stored) {
353
+ // Should not happen if latestStored is correct, but be safe
354
+ break;
355
+ }
356
+ // Verify the checkpoint content matches (archive root)
357
+ if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
358
+ throw new Error(
359
+ `Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
360
+ `Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
361
+ );
362
+ }
363
+ // Update L1 metadata and attestations for the already-stored checkpoint
364
+ this.#log.warn(
365
+ `Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
366
+ `(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
367
+ );
368
+ await this.#checkpoints.set(incoming.checkpoint.number, {
369
+ header: incoming.checkpoint.header.toBuffer(),
370
+ archive: incoming.checkpoint.archive.toBuffer(),
371
+ checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
372
+ l1: incoming.l1.toBuffer(),
373
+ attestations: incoming.attestations.map(a => a.toBuffer()),
374
+ checkpointNumber: incoming.checkpoint.number,
375
+ startBlock: incoming.checkpoint.blocks[0].number,
376
+ blockCount: incoming.checkpoint.blocks.length,
377
+ });
378
+ // Update the sync point to reflect the new L1 block
379
+ await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
380
+ }
381
+ return checkpoints.slice(i);
382
+ }
383
+
384
+ /**
385
+ * Gets the last block of the checkpoint before the given one.
386
+ * Returns undefined if there is no previous checkpoint (i.e. genesis).
387
+ */
388
+ private async getPreviousCheckpointBlock(checkpointNumber: CheckpointNumber): Promise<L2Block | undefined> {
389
+ const previousCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
390
+ if (previousCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
391
+ return undefined;
392
+ }
393
+
394
+ const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
395
+ if (previousCheckpointData === undefined) {
396
+ throw new CheckpointNotFoundError(previousCheckpointNumber);
397
+ }
398
+
399
+ const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
400
+ const previousBlock = await this.getBlock(previousBlockNumber);
401
+ if (previousBlock === undefined) {
402
+ throw new BlockNotFoundError(previousBlockNumber);
403
+ }
404
+
405
+ return previousBlock;
406
+ }
407
+
408
+ /**
409
+ * Validates that blocks are sequential, have correct indexes, and chain via archive roots.
410
+ * This is the same validation used for both confirmed checkpoints (addCheckpoints) and
411
+ * proposed checkpoints (setProposedCheckpoint).
412
+ */
413
+ private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
414
+ for (const block of blocks) {
415
+ if (previousBlock) {
416
+ if (previousBlock.number !== block.number - 1) {
417
+ throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
418
+ }
419
+ if (previousBlock.checkpointNumber === block.checkpointNumber) {
420
+ if (previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1) {
421
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
422
+ }
423
+ } else if (block.indexWithinCheckpoint !== 0) {
424
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
425
+ }
426
+ if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
427
+ throw new BlockArchiveNotConsistentError(
428
+ block.number,
429
+ previousBlock.number,
430
+ block.header.lastArchive.root,
431
+ previousBlock.archive.root,
432
+ );
433
+ }
434
+ } else {
435
+ if (block.indexWithinCheckpoint !== 0) {
436
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
437
+ }
438
+ if (block.number !== INITIAL_L2_BLOCK_NUM) {
439
+ throw new BlockNumberNotSequentialError(block.number, undefined);
440
+ }
441
+ }
442
+ previousBlock = block;
443
+ }
444
+ }
445
+
342
446
  private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
343
- const blockHash = L2BlockHash.fromField(await block.hash());
447
+ const blockHash = await block.hash();
344
448
 
345
449
  await this.#blocks.set(block.number, {
346
450
  header: block.header.toBuffer(),
@@ -385,51 +489,59 @@ export class BlockStore {
385
489
  }
386
490
 
387
491
  /**
388
- * Unwinds checkpoints from the database
389
- * @param from - The tip of the chain, passed for verification purposes,
390
- * ensuring that we don't end up deleting something we did not intend
391
- * @param checkpointsToUnwind - The number of checkpoints we are to unwind
392
- * @returns True if the operation is successful
492
+ * Removes all checkpoints with checkpoint number > checkpointNumber.
493
+ * Also removes ALL blocks (both checkpointed and uncheckpointed) after the last block of the given checkpoint.
494
+ * @param checkpointNumber - Remove all checkpoints strictly after this one.
393
495
  */
394
- async unwindCheckpoints(from: CheckpointNumber, checkpointsToUnwind: number) {
496
+ async removeCheckpointsAfter(checkpointNumber: CheckpointNumber): Promise<RemoveCheckpointsResult> {
395
497
  return await this.db.transactionAsync(async () => {
396
- const last = await this.getLatestCheckpointNumber();
397
- if (from !== last) {
398
- throw new Error(`Can only unwind checkpoints from the tip (requested ${from} but current tip is ${last})`);
498
+ const latestCheckpointNumber = await this.getLatestCheckpointNumber();
499
+
500
+ if (checkpointNumber >= latestCheckpointNumber) {
501
+ this.#log.warn(`No checkpoints to remove after ${checkpointNumber} (latest is ${latestCheckpointNumber})`);
502
+ return { blocksRemoved: undefined };
399
503
  }
400
504
 
505
+ // If the proven checkpoint is beyond the target, update it
401
506
  const proven = await this.getProvenCheckpointNumber();
402
- if (from - checkpointsToUnwind < proven) {
403
- await this.setProvenCheckpointNumber(CheckpointNumber(from - checkpointsToUnwind));
507
+ if (proven > checkpointNumber) {
508
+ this.#log.warn(`Updating proven checkpoint ${proven} to last valid checkpoint ${checkpointNumber}`);
509
+ await this.setProvenCheckpointNumber(checkpointNumber);
404
510
  }
405
511
 
406
- for (let i = 0; i < checkpointsToUnwind; i++) {
407
- const checkpointNumber = from - i;
408
- const checkpoint = await this.#checkpoints.getAsync(checkpointNumber);
409
-
410
- if (checkpoint === undefined) {
411
- this.#log.warn(`Cannot remove checkpoint ${checkpointNumber} from the store since we don't have it`);
412
- continue;
512
+ // Find the last block number to keep (last block of the given checkpoint, or 0 if no checkpoint)
513
+ let lastBlockToKeep: BlockNumber;
514
+ if (checkpointNumber <= 0) {
515
+ lastBlockToKeep = BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
516
+ } else {
517
+ const targetCheckpoint = await this.#checkpoints.getAsync(checkpointNumber);
518
+ if (!targetCheckpoint) {
519
+ throw new Error(`Target checkpoint ${checkpointNumber} not found in store`);
413
520
  }
414
- await this.#checkpoints.delete(checkpointNumber);
415
- const maxBlock = checkpoint.startBlock + checkpoint.numBlocks - 1;
416
-
417
- for (let blockNumber = checkpoint.startBlock; blockNumber <= maxBlock; blockNumber++) {
418
- const block = await this.getBlock(BlockNumber(blockNumber));
521
+ lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.blockCount - 1);
522
+ }
419
523
 
420
- if (block === undefined) {
421
- this.#log.warn(`Cannot remove block ${blockNumber} from the store since we don't have it`);
422
- continue;
423
- }
524
+ // Remove all blocks after lastBlockToKeep (both checkpointed and uncheckpointed)
525
+ const blocksRemoved = await this.removeBlocksAfter(lastBlockToKeep);
424
526
 
425
- await this.deleteBlock(block);
426
- this.#log.debug(
427
- `Unwound block ${blockNumber} ${(await block.hash()).toString()} for checkpoint ${checkpointNumber}`,
428
- );
527
+ // Remove all checkpoints after the target
528
+ for (let c = latestCheckpointNumber; c > checkpointNumber; c = CheckpointNumber(c - 1)) {
529
+ const checkpointStorage = await this.#checkpoints.getAsync(c);
530
+ if (checkpointStorage) {
531
+ const slotNumber = CheckpointHeader.fromBuffer(checkpointStorage.header).slotNumber;
532
+ await this.#slotToCheckpoint.delete(slotNumber);
429
533
  }
534
+ await this.#checkpoints.delete(c);
535
+ this.#log.debug(`Removed checkpoint ${c}`);
430
536
  }
431
537
 
432
- return true;
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
+ }
543
+
544
+ return { blocksRemoved };
433
545
  });
434
546
  }
435
547
 
@@ -453,17 +565,32 @@ export class BlockStore {
453
565
  return checkpoints;
454
566
  }
455
567
 
456
- private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage) {
457
- const data: CheckpointData = {
568
+ /** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
569
+ async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
570
+ const result: CheckpointData[] = [];
571
+ for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
572
+ start: startSlot,
573
+ end: endSlot + 1,
574
+ })) {
575
+ const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
576
+ if (checkpointStorage) {
577
+ result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
578
+ }
579
+ }
580
+ return result;
581
+ }
582
+
583
+ private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
584
+ return {
458
585
  header: CheckpointHeader.fromBuffer(checkpointStorage.header),
459
586
  archive: AppendOnlyTreeSnapshot.fromBuffer(checkpointStorage.archive),
587
+ checkpointOutHash: Fr.fromBuffer(checkpointStorage.checkpointOutHash),
460
588
  checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
461
- startBlock: checkpointStorage.startBlock,
462
- numBlocks: checkpointStorage.numBlocks,
589
+ startBlock: BlockNumber(checkpointStorage.startBlock),
590
+ blockCount: checkpointStorage.blockCount,
463
591
  l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
464
- attestations: checkpointStorage.attestations,
592
+ attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
465
593
  };
466
- return data;
467
594
  }
468
595
 
469
596
  async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<L2Block[] | undefined> {
@@ -475,7 +602,7 @@ export class BlockStore {
475
602
  const blocksForCheckpoint = await toArray(
476
603
  this.#blocks.entriesAsync({
477
604
  start: checkpoint.startBlock,
478
- end: checkpoint.startBlock + checkpoint.numBlocks,
605
+ end: checkpoint.startBlock + checkpoint.blockCount,
479
606
  }),
480
607
  );
481
608
 
@@ -510,15 +637,16 @@ export class BlockStore {
510
637
 
511
638
  /**
512
639
  * Removes all blocks with block number > blockNumber.
640
+ * Does not remove any associated checkpoints.
513
641
  * @param blockNumber - The block number to remove after.
514
642
  * @returns The removed blocks (for event emission).
515
643
  */
516
- async unwindBlocksAfter(blockNumber: BlockNumber): Promise<L2Block[]> {
644
+ async removeBlocksAfter(blockNumber: BlockNumber): Promise<L2Block[]> {
517
645
  return await this.db.transactionAsync(async () => {
518
646
  const removedBlocks: L2Block[] = [];
519
647
 
520
648
  // Get the latest block number to determine the range
521
- const latestBlockNumber = await this.getLatestBlockNumber();
649
+ const latestBlockNumber = await this.getLatestL2BlockNumber();
522
650
 
523
651
  // Iterate from blockNumber + 1 to latestBlockNumber
524
652
  for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
@@ -547,17 +675,10 @@ export class BlockStore {
547
675
  if (!checkpointStorage) {
548
676
  throw new CheckpointNotFoundError(provenCheckpointNumber);
549
677
  } else {
550
- return BlockNumber(checkpointStorage.startBlock + checkpointStorage.numBlocks - 1);
678
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
551
679
  }
552
680
  }
553
681
 
554
- async getLatestBlockNumber(): Promise<BlockNumber> {
555
- const [latestBlocknumber] = await toArray(this.#blocks.keysAsync({ reverse: true, limit: 1 }));
556
- return typeof latestBlocknumber === 'number'
557
- ? BlockNumber(latestBlocknumber)
558
- : BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
559
- }
560
-
561
682
  async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
562
683
  const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
563
684
  if (latestCheckpointNumber === undefined) {
@@ -566,6 +687,133 @@ export class BlockStore {
566
687
  return CheckpointNumber(latestCheckpointNumber);
567
688
  }
568
689
 
690
+ async hasProposedCheckpoint(): Promise<boolean> {
691
+ const proposed = await this.#proposedCheckpoint.getAsync();
692
+ return proposed !== undefined;
693
+ }
694
+
695
+ /** Deletes the proposed checkpoint from storage. */
696
+ async deleteProposedCheckpoint(): Promise<void> {
697
+ await this.#proposedCheckpoint.delete();
698
+ }
699
+
700
+ /**
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.
705
+ */
706
+ async promoteProposedToCheckpointed(
707
+ l1: L1PublishedData,
708
+ attestations: CommitteeAttestation[],
709
+ expectedArchiveRoot: Fr,
710
+ ): Promise<void> {
711
+ return await this.db.transactionAsync(async () => {
712
+ const proposed = await this.getProposedCheckpointOnly();
713
+ if (!proposed) {
714
+ throw new NoProposedCheckpointToPromoteError();
715
+ }
716
+ if (!proposed.archive.root.equals(expectedArchiveRoot)) {
717
+ throw new ProposedCheckpointArchiveRootMismatchError(expectedArchiveRoot, proposed.archive.root);
718
+ }
719
+
720
+ // Verify sequentiality: promoted checkpoint must follow the latest confirmed one
721
+ const latestCheckpointNumber = await this.getLatestCheckpointNumber();
722
+ if (latestCheckpointNumber !== proposed.checkpointNumber - 1) {
723
+ throw new ProposedCheckpointPromotionNotSequentialError(proposed.checkpointNumber, latestCheckpointNumber);
724
+ }
725
+
726
+ // Write the checkpoint entry
727
+ await this.#checkpoints.set(proposed.checkpointNumber, {
728
+ header: proposed.header.toBuffer(),
729
+ archive: proposed.archive.toBuffer(),
730
+ checkpointOutHash: proposed.checkpointOutHash.toBuffer(),
731
+ l1: l1.toBuffer(),
732
+ attestations: attestations.map(attestation => attestation.toBuffer()),
733
+ checkpointNumber: proposed.checkpointNumber,
734
+ startBlock: proposed.startBlock,
735
+ blockCount: proposed.blockCount,
736
+ });
737
+
738
+ // Update the slot-to-checkpoint index
739
+ await this.#slotToCheckpoint.set(proposed.header.slotNumber, proposed.checkpointNumber);
740
+
741
+ // Clear the proposed checkpoint singleton
742
+ await this.#proposedCheckpoint.delete();
743
+
744
+ // Update the last synced L1 block
745
+ await this.#lastSynchedL1Block.set(l1.blockNumber);
746
+ });
747
+ }
748
+
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();
754
+ }
755
+ }
756
+
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;
762
+ }
763
+ return this.convertToProposedCheckpointData(stored);
764
+ }
765
+
766
+ /**
767
+ * Gets the checkpoint at the proposed tip
768
+ * - pending checkpoint if it exists
769
+ * - fallsback to latest confirmed checkpoint otherwise
770
+ * @returns CommonCheckpointData
771
+ */
772
+ async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
773
+ const stored = await this.#proposedCheckpoint.getAsync();
774
+ if (!stored) {
775
+ return this.getCheckpointData(await this.getLatestCheckpointNumber());
776
+ }
777
+ return this.convertToProposedCheckpointData(stored);
778
+ }
779
+
780
+ private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
781
+ return {
782
+ checkpointNumber: CheckpointNumber(stored.checkpointNumber),
783
+ header: CheckpointHeader.fromBuffer(stored.header),
784
+ archive: AppendOnlyTreeSnapshot.fromBuffer(stored.archive),
785
+ checkpointOutHash: Fr.fromBuffer(stored.checkpointOutHash),
786
+ startBlock: BlockNumber(stored.startBlock),
787
+ blockCount: stored.blockCount,
788
+ totalManaUsed: BigInt(stored.totalManaUsed),
789
+ feeAssetPriceModifier: BigInt(stored.feeAssetPriceModifier),
790
+ };
791
+ }
792
+
793
+ /**
794
+ * Attempts to get the proposedCheckpoint's number, if there is not one, then fallback to the latest confirmed checkpoint number.
795
+ * @returns CheckpointNumber
796
+ */
797
+ async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
798
+ const proposed = await this.getProposedCheckpoint();
799
+ if (!proposed) {
800
+ return await this.getLatestCheckpointNumber();
801
+ }
802
+ return CheckpointNumber(proposed.checkpointNumber);
803
+ }
804
+
805
+ /**
806
+ * Attempts to get the proposedCheckpoint's block number, if there is not one, then fallback to the checkpointed block number
807
+ * @returns BlockNumber
808
+ */
809
+ async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
810
+ const proposed = await this.getProposedCheckpoint();
811
+ if (!proposed) {
812
+ return await this.getCheckpointedL2BlockNumber();
813
+ }
814
+ return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
815
+ }
816
+
569
817
  async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
570
818
  const blockStorage = await this.#blocks.getAsync(number);
571
819
  if (!blockStorage) {
@@ -615,7 +863,7 @@ export class BlockStore {
615
863
  }
616
864
  }
617
865
 
618
- async getCheckpointedBlockByHash(blockHash: Fr): Promise<CheckpointedL2Block | undefined> {
866
+ async getCheckpointedBlockByHash(blockHash: BlockHash): Promise<CheckpointedL2Block | undefined> {
619
867
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
620
868
  if (blockNumber === undefined) {
621
869
  return undefined;
@@ -646,6 +894,32 @@ export class BlockStore {
646
894
  }
647
895
  }
648
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
+ 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) {
918
+ return undefined;
919
+ }
920
+ return this.getBlockData(BlockNumber(blockNumber));
921
+ }
922
+
649
923
  /**
650
924
  * Gets an L2 block.
651
925
  * @param blockNumber - The number of the block to return.
@@ -664,7 +938,7 @@ export class BlockStore {
664
938
  * @param blockHash - The hash of the block to return.
665
939
  * @returns The requested L2 block.
666
940
  */
667
- async getBlockByHash(blockHash: L2BlockHash): Promise<L2Block | undefined> {
941
+ async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
668
942
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
669
943
  if (blockNumber === undefined) {
670
944
  return undefined;
@@ -690,7 +964,7 @@ export class BlockStore {
690
964
  * @param blockHash - The hash of the block to return.
691
965
  * @returns The requested block header.
692
966
  */
693
- async getBlockHeaderByHash(blockHash: L2BlockHash): Promise<BlockHeader | undefined> {
967
+ async getBlockHeaderByHash(blockHash: BlockHash): Promise<BlockHeader | undefined> {
694
968
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
695
969
  if (blockNumber === undefined) {
696
970
  return undefined;
@@ -750,15 +1024,24 @@ export class BlockStore {
750
1024
  }
751
1025
  }
752
1026
 
1027
+ private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
1028
+ return {
1029
+ header: BlockHeader.fromBuffer(blockStorage.header),
1030
+ archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
1031
+ blockHash: BlockHash.fromBuffer(blockStorage.blockHash),
1032
+ checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
1033
+ indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
1034
+ };
1035
+ }
1036
+
753
1037
  private async getBlockFromBlockStorage(
754
1038
  blockNumber: number,
755
1039
  blockStorage: BlockStorage,
756
1040
  ): Promise<L2Block | undefined> {
757
- const header = BlockHeader.fromBuffer(blockStorage.header);
758
- const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive);
759
- const blockHash = blockStorage.blockHash;
760
- header.setHash(Fr.fromBuffer(blockHash));
761
- const blockHashString = bufferToHex(blockHash);
1041
+ const { header, archive, blockHash, checkpointNumber, indexWithinCheckpoint } =
1042
+ this.getBlockDataFromBlockStorage(blockStorage);
1043
+ header.setHash(blockHash);
1044
+ const blockHashString = bufferToHex(blockStorage.blockHash);
762
1045
  const blockTxsBuffer = await this.#blockTxs.getAsync(blockHashString);
763
1046
  if (blockTxsBuffer === undefined) {
764
1047
  this.#log.warn(`Could not find body for block ${header.globalVariables.blockNumber} ${blockHash}`);
@@ -777,13 +1060,7 @@ export class BlockStore {
777
1060
  txEffects.push(deserializeIndexedTxEffect(txEffect).data);
778
1061
  }
779
1062
  const body = new Body(txEffects);
780
- const block = new L2Block(
781
- archive,
782
- header,
783
- body,
784
- CheckpointNumber(blockStorage.checkpointNumber!),
785
- IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
786
- );
1063
+ const block = new L2Block(archive, header, body, checkpointNumber, indexWithinCheckpoint);
787
1064
 
788
1065
  if (block.number !== blockNumber) {
789
1066
  throw new Error(
@@ -813,7 +1090,10 @@ export class BlockStore {
813
1090
  * @param txHash - The hash of a tx we try to get the receipt for.
814
1091
  * @returns The requested tx receipt (or undefined if not found).
815
1092
  */
816
- async getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
1093
+ async getSettledTxReceipt(
1094
+ txHash: TxHash,
1095
+ l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
1096
+ ): Promise<TxReceipt | undefined> {
817
1097
  const txEffect = await this.getTxEffect(txHash);
818
1098
  if (!txEffect) {
819
1099
  return undefined;
@@ -822,10 +1102,11 @@ export class BlockStore {
822
1102
  const blockNumber = BlockNumber(txEffect.l2BlockNumber);
823
1103
 
824
1104
  // Use existing archiver methods to determine finalization level
825
- const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([
1105
+ const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
826
1106
  this.getProvenBlockNumber(),
827
1107
  this.getCheckpointedL2BlockNumber(),
828
1108
  this.getFinalizedL2BlockNumber(),
1109
+ this.getBlockData(blockNumber),
829
1110
  ]);
830
1111
 
831
1112
  let status: TxStatus;
@@ -839,6 +1120,9 @@ export class BlockStore {
839
1120
  status = TxStatus.PROPOSED;
840
1121
  }
841
1122
 
1123
+ const epochNumber =
1124
+ blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
1125
+
842
1126
  return new TxReceipt(
843
1127
  txHash,
844
1128
  status,
@@ -847,6 +1131,7 @@ export class BlockStore {
847
1131
  txEffect.data.transactionFee.toBigInt(),
848
1132
  txEffect.l2BlockHash,
849
1133
  blockNumber,
1134
+ epochNumber,
850
1135
  );
851
1136
  }
852
1137
 
@@ -883,7 +1168,7 @@ export class BlockStore {
883
1168
  if (!checkpoint) {
884
1169
  return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
885
1170
  }
886
- return BlockNumber(checkpoint.startBlock + checkpoint.numBlocks - 1);
1171
+ return BlockNumber(checkpoint.startBlock + checkpoint.blockCount - 1);
887
1172
  }
888
1173
 
889
1174
  async getLatestL2BlockNumber(): Promise<BlockNumber> {
@@ -903,6 +1188,47 @@ export class BlockStore {
903
1188
  return this.#lastSynchedL1Block.set(l1BlockNumber);
904
1189
  }
905
1190
 
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) {
1194
+ 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
+ const confirmed = await this.getLatestCheckpointNumber();
1200
+ if (proposed.checkpointNumber !== confirmed + 1) {
1201
+ throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
1202
+ }
1203
+
1204
+ // Ensure the previous checkpoint + blocks exist
1205
+ const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
1206
+ const blocks: L2Block[] = [];
1207
+ for (let i = 0; i < proposed.blockCount; i++) {
1208
+ const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
1209
+ if (!block) {
1210
+ throw new BlockNotFoundError(proposed.startBlock + i);
1211
+ }
1212
+ blocks.push(block);
1213
+ }
1214
+ this.validateCheckpointBlocks(blocks, previousBlock);
1215
+
1216
+ const archive = blocks[blocks.length - 1].archive;
1217
+ const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
1218
+
1219
+ await this.#proposedCheckpoint.set({
1220
+ header: proposed.header.toBuffer(),
1221
+ archive: archive.toBuffer(),
1222
+ checkpointOutHash: checkpointOutHash.toBuffer(),
1223
+ checkpointNumber: proposed.checkpointNumber,
1224
+ startBlock: proposed.startBlock,
1225
+ blockCount: proposed.blockCount,
1226
+ totalManaUsed: proposed.totalManaUsed.toString(),
1227
+ feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
1228
+ });
1229
+ });
1230
+ }
1231
+
906
1232
  async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
907
1233
  const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
908
1234
  this.getLatestCheckpointNumber(),
@@ -918,6 +1244,20 @@ export class BlockStore {
918
1244
  return result;
919
1245
  }
920
1246
 
1247
+ 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);
1255
+ }
1256
+
1257
+ setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
1258
+ return this.#lastFinalizedCheckpoint.set(checkpointNumber);
1259
+ }
1260
+
921
1261
  #computeBlockRange(start: BlockNumber, limit: number): Required<Pick<Range<number>, 'start' | 'limit'>> {
922
1262
  if (limit < 1) {
923
1263
  throw new Error(`Invalid limit: ${limit}`);