@aztec/archiver 0.0.1-commit.d431d1c → 0.0.1-commit.d939eb5aa

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 (116) hide show
  1. package/README.md +21 -6
  2. package/dest/archiver.d.ts +17 -12
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +108 -127
  5. package/dest/config.d.ts +3 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +3 -2
  8. package/dest/errors.d.ts +39 -10
  9. package/dest/errors.d.ts.map +1 -1
  10. package/dest/errors.js +52 -15
  11. package/dest/factory.d.ts +4 -2
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +29 -23
  14. package/dest/index.d.ts +2 -1
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +1 -0
  17. package/dest/l1/bin/retrieve-calldata.js +35 -32
  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 +11 -11
  22. package/dest/l1/data_retrieval.d.ts.map +1 -1
  23. package/dest/l1/data_retrieval.js +38 -37
  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_trace.d.ts +6 -3
  28. package/dest/l1/validate_trace.d.ts.map +1 -1
  29. package/dest/l1/validate_trace.js +13 -9
  30. package/dest/modules/data_source_base.d.ts +29 -23
  31. package/dest/modules/data_source_base.d.ts.map +1 -1
  32. package/dest/modules/data_source_base.js +55 -124
  33. package/dest/modules/data_store_updater.d.ts +43 -26
  34. package/dest/modules/data_store_updater.d.ts.map +1 -1
  35. package/dest/modules/data_store_updater.js +158 -129
  36. package/dest/modules/instrumentation.d.ts +19 -4
  37. package/dest/modules/instrumentation.d.ts.map +1 -1
  38. package/dest/modules/instrumentation.js +53 -18
  39. package/dest/modules/l1_synchronizer.d.ts +7 -9
  40. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  41. package/dest/modules/l1_synchronizer.js +186 -145
  42. package/dest/modules/validation.d.ts +1 -1
  43. package/dest/modules/validation.d.ts.map +1 -1
  44. package/dest/modules/validation.js +2 -2
  45. package/dest/store/block_store.d.ts +86 -34
  46. package/dest/store/block_store.d.ts.map +1 -1
  47. package/dest/store/block_store.js +414 -152
  48. package/dest/store/contract_class_store.d.ts +2 -3
  49. package/dest/store/contract_class_store.d.ts.map +1 -1
  50. package/dest/store/contract_class_store.js +16 -72
  51. package/dest/store/contract_instance_store.d.ts +1 -1
  52. package/dest/store/contract_instance_store.d.ts.map +1 -1
  53. package/dest/store/contract_instance_store.js +6 -2
  54. package/dest/store/kv_archiver_store.d.ts +80 -39
  55. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  56. package/dest/store/kv_archiver_store.js +86 -35
  57. package/dest/store/l2_tips_cache.d.ts +20 -0
  58. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  59. package/dest/store/l2_tips_cache.js +109 -0
  60. package/dest/store/log_store.d.ts +9 -6
  61. package/dest/store/log_store.d.ts.map +1 -1
  62. package/dest/store/log_store.js +151 -56
  63. package/dest/store/message_store.d.ts +5 -1
  64. package/dest/store/message_store.d.ts.map +1 -1
  65. package/dest/store/message_store.js +21 -9
  66. package/dest/test/fake_l1_state.d.ts +24 -4
  67. package/dest/test/fake_l1_state.d.ts.map +1 -1
  68. package/dest/test/fake_l1_state.js +133 -26
  69. package/dest/test/index.js +3 -1
  70. package/dest/test/mock_archiver.d.ts +1 -1
  71. package/dest/test/mock_archiver.d.ts.map +1 -1
  72. package/dest/test/mock_archiver.js +3 -2
  73. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  74. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  75. package/dest/test/mock_l1_to_l2_message_source.js +2 -1
  76. package/dest/test/mock_l2_block_source.d.ts +44 -23
  77. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  78. package/dest/test/mock_l2_block_source.js +185 -115
  79. package/dest/test/mock_structs.d.ts +6 -2
  80. package/dest/test/mock_structs.d.ts.map +1 -1
  81. package/dest/test/mock_structs.js +24 -10
  82. package/dest/test/noop_l1_archiver.d.ts +26 -0
  83. package/dest/test/noop_l1_archiver.d.ts.map +1 -0
  84. package/dest/test/noop_l1_archiver.js +71 -0
  85. package/package.json +14 -13
  86. package/src/archiver.ts +144 -159
  87. package/src/config.ts +9 -2
  88. package/src/errors.ts +82 -26
  89. package/src/factory.ts +46 -22
  90. package/src/index.ts +1 -0
  91. package/src/l1/README.md +25 -68
  92. package/src/l1/bin/retrieve-calldata.ts +45 -33
  93. package/src/l1/calldata_retriever.ts +250 -379
  94. package/src/l1/data_retrieval.ts +35 -41
  95. package/src/l1/spire_proposer.ts +7 -15
  96. package/src/l1/validate_trace.ts +24 -6
  97. package/src/modules/data_source_base.ts +98 -169
  98. package/src/modules/data_store_updater.ts +178 -160
  99. package/src/modules/instrumentation.ts +64 -20
  100. package/src/modules/l1_synchronizer.ts +212 -182
  101. package/src/modules/validation.ts +2 -2
  102. package/src/store/block_store.ts +533 -207
  103. package/src/store/contract_class_store.ts +16 -110
  104. package/src/store/contract_instance_store.ts +8 -5
  105. package/src/store/kv_archiver_store.ts +141 -59
  106. package/src/store/l2_tips_cache.ts +134 -0
  107. package/src/store/log_store.ts +232 -74
  108. package/src/store/message_store.ts +27 -10
  109. package/src/structs/inbox_message.ts +1 -1
  110. package/src/test/fake_l1_state.ts +180 -32
  111. package/src/test/index.ts +3 -0
  112. package/src/test/mock_archiver.ts +3 -2
  113. package/src/test/mock_l1_to_l2_message_source.ts +1 -0
  114. package/src/test/mock_l2_block_source.ts +247 -130
  115. package/src/test/mock_structs.ts +45 -15
  116. package/src/test/noop_l1_archiver.ts +114 -0
@@ -9,16 +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
- L2BlockHash,
16
- L2BlockNew,
17
+ L2Block,
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 {
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';
22
32
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
23
33
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
24
34
  import {
@@ -27,20 +37,23 @@ import {
27
37
  TxEffect,
28
38
  TxHash,
29
39
  TxReceipt,
40
+ TxStatus,
30
41
  deserializeIndexedTxEffect,
31
42
  serializeIndexedTxEffect,
32
43
  } from '@aztec/stdlib/tx';
33
44
 
34
45
  import {
46
+ BlockAlreadyCheckpointedError,
35
47
  BlockArchiveNotConsistentError,
36
48
  BlockIndexNotSequentialError,
37
49
  BlockNotFoundError,
38
50
  BlockNumberNotSequentialError,
51
+ CannotOverwriteCheckpointedBlockError,
39
52
  CheckpointNotFoundError,
40
- CheckpointNumberNotConsistentError,
41
53
  CheckpointNumberNotSequentialError,
42
- InitialBlockNumberNotSequentialError,
43
54
  InitialCheckpointNumberNotSequentialError,
55
+ ProposedCheckpointNotSequentialError,
56
+ ProposedCheckpointStaleError,
44
57
  } from '../errors.js';
45
58
 
46
59
  export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
@@ -55,26 +68,29 @@ type BlockStorage = {
55
68
  indexWithinCheckpoint: number;
56
69
  };
57
70
 
58
- type CheckpointStorage = {
71
+ /** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
72
+ type CommonCheckpointStorage = {
59
73
  header: Buffer;
60
74
  archive: Buffer;
75
+ checkpointOutHash: Buffer;
61
76
  checkpointNumber: number;
62
77
  startBlock: number;
63
- numBlocks: number;
78
+ blockCount: number;
79
+ };
80
+
81
+ type CheckpointStorage = CommonCheckpointStorage & {
64
82
  l1: Buffer;
65
83
  attestations: Buffer[];
66
84
  };
67
85
 
68
- export type CheckpointData = {
69
- checkpointNumber: CheckpointNumber;
70
- header: CheckpointHeader;
71
- archive: AppendOnlyTreeSnapshot;
72
- startBlock: number;
73
- numBlocks: number;
74
- l1: L1PublishedData;
75
- attestations: Buffer[];
86
+ /** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
87
+ type ProposedCheckpointStorage = CommonCheckpointStorage & {
88
+ totalManaUsed: string;
89
+ feeAssetPriceModifier: string;
76
90
  };
77
91
 
92
+ export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
93
+
78
94
  /**
79
95
  * LMDB-based block storage for the archiver.
80
96
  */
@@ -85,6 +101,9 @@ export class BlockStore {
85
101
  /** Map checkpoint number to checkpoint data */
86
102
  #checkpoints: AztecAsyncMap<number, CheckpointStorage>;
87
103
 
104
+ /** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
105
+ #slotToCheckpoint: AztecAsyncMap<number, number>;
106
+
88
107
  /** Map block hash to list of tx hashes */
89
108
  #blockTxs: AztecAsyncMap<string, Buffer>;
90
109
 
@@ -97,6 +116,9 @@ export class BlockStore {
97
116
  /** Stores last proven checkpoint */
98
117
  #lastProvenCheckpoint: AztecAsyncSingleton<number>;
99
118
 
119
+ /** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
120
+ #lastFinalizedCheckpoint: AztecAsyncSingleton<number>;
121
+
100
122
  /** Stores the pending chain validation status */
101
123
  #pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;
102
124
 
@@ -109,6 +131,9 @@ export class BlockStore {
109
131
  /** Index mapping block archive to block number */
110
132
  #blockArchiveIndex: AztecAsyncMap<string, number>;
111
133
 
134
+ /** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
135
+ #proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
136
+
112
137
  #log = createLogger('archiver:block_store');
113
138
 
114
139
  constructor(private db: AztecAsyncKVStore) {
@@ -120,99 +145,114 @@ export class BlockStore {
120
145
  this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
121
146
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
122
147
  this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint');
148
+ this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint');
123
149
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
124
150
  this.#checkpoints = db.openMap('archiver_checkpoints');
151
+ this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
152
+ this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
125
153
  }
126
154
 
127
155
  /**
128
- * Append new blocks to the store's list. All blocks must be for the 'current' checkpoint
129
- * @param blocks - The L2 blocks to be added to the store.
130
- * @returns True if the operation is successful.
156
+ * Returns the finalized L2 block number. An L2 block is finalized when it was proven
157
+ * in an L1 block that has itself been finalized on Ethereum.
158
+ * @returns The finalized block number.
131
159
  */
132
- async addBlocks(blocks: L2BlockNew[], opts: { force?: boolean } = {}): Promise<boolean> {
133
- if (blocks.length === 0) {
134
- return true;
160
+ async getFinalizedL2BlockNumber(): Promise<BlockNumber> {
161
+ const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber();
162
+ if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
163
+ return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
164
+ }
165
+ const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber);
166
+ if (!checkpointStorage) {
167
+ throw new CheckpointNotFoundError(finalizedCheckpointNumber);
135
168
  }
169
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
170
+ }
136
171
 
172
+ /**
173
+ * Append a new proposed block to the store.
174
+ * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
175
+ * For checkpointed blocks (already published to L1), use addCheckpoints() instead.
176
+ * @param block - The proposed L2 block to be added to the store.
177
+ * @returns True if the operation is successful.
178
+ */
179
+ async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
137
180
  return await this.db.transactionAsync(async () => {
138
- // Check that the block immediately before the first block to be added is present in the store.
139
- const firstBlockNumber = blocks[0].number;
140
- const firstBlockCheckpointNumber = blocks[0].checkpointNumber;
141
- const firstBlockIndex = blocks[0].indexWithinCheckpoint;
142
- const firstBlockLastArchive = blocks[0].header.lastArchive.root;
181
+ const blockNumber = block.number;
182
+ const blockCheckpointNumber = block.checkpointNumber;
183
+ const blockIndex = block.indexWithinCheckpoint;
184
+ const blockLastArchive = block.header.lastArchive.root;
143
185
 
144
186
  // Extract the latest block and checkpoint numbers
145
- const previousBlockNumber = await this.getLatestBlockNumber();
187
+ const previousBlockNumber = await this.getLatestL2BlockNumber();
188
+ const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
146
189
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
147
190
 
148
- // Check that the first block number is the expected one
149
- if (!opts.force && previousBlockNumber !== firstBlockNumber - 1) {
150
- throw new InitialBlockNumberNotSequentialError(firstBlockNumber, previousBlockNumber);
191
+ // Verify we're not overwriting checkpointed blocks
192
+ const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
193
+ if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
194
+ // Check if the proposed block matches the already-checkpointed one
195
+ const existingBlock = await this.getBlock(BlockNumber(blockNumber));
196
+ if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
197
+ throw new BlockAlreadyCheckpointedError(blockNumber);
198
+ }
199
+ throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber);
151
200
  }
152
201
 
153
- // The same check as above but for checkpoints
154
- if (!opts.force && previousCheckpointNumber !== firstBlockCheckpointNumber - 1) {
155
- throw new InitialCheckpointNumberNotSequentialError(firstBlockCheckpointNumber, previousCheckpointNumber);
202
+ // Check that the block number is the expected one
203
+ if (!opts.force && previousBlockNumber !== blockNumber - 1) {
204
+ throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
205
+ }
206
+
207
+ // The same check as above but for checkpoints. Accept the block if either the confirmed
208
+ // checkpoint or the pending (locally validated but not yet confirmed) checkpoint matches.
209
+ const expectedCheckpointNumber = blockCheckpointNumber - 1;
210
+ if (
211
+ !opts.force &&
212
+ previousCheckpointNumber !== expectedCheckpointNumber &&
213
+ proposedCheckpointNumber !== expectedCheckpointNumber
214
+ ) {
215
+ const [reported, source]: [CheckpointNumber, 'confirmed' | 'proposed'] =
216
+ proposedCheckpointNumber > previousCheckpointNumber
217
+ ? [proposedCheckpointNumber, 'proposed']
218
+ : [previousCheckpointNumber, 'confirmed'];
219
+ throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, reported, source);
156
220
  }
157
221
 
158
222
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
159
223
  const previousBlockResult = await this.getBlock(previousBlockNumber);
160
224
 
161
- let expectedFirstblockIndex = 0;
225
+ let expectedBlockIndex = 0;
162
226
  let previousBlockIndex: number | undefined = undefined;
163
227
  if (previousBlockResult !== undefined) {
164
- if (previousBlockResult.checkpointNumber === firstBlockCheckpointNumber) {
228
+ if (previousBlockResult.checkpointNumber === blockCheckpointNumber) {
165
229
  // The previous block is for the same checkpoint, therefore our index should follow it
166
230
  previousBlockIndex = previousBlockResult.indexWithinCheckpoint;
167
- expectedFirstblockIndex = previousBlockIndex + 1;
231
+ expectedBlockIndex = previousBlockIndex + 1;
168
232
  }
169
- if (!previousBlockResult.archive.root.equals(firstBlockLastArchive)) {
233
+ if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
170
234
  throw new BlockArchiveNotConsistentError(
171
- firstBlockNumber,
235
+ blockNumber,
172
236
  previousBlockResult.number,
173
- firstBlockLastArchive,
237
+ blockLastArchive,
174
238
  previousBlockResult.archive.root,
175
239
  );
176
240
  }
177
241
  }
178
242
 
179
- // Now check that the first block has the expected index value
180
- if (!opts.force && expectedFirstblockIndex !== firstBlockIndex) {
181
- throw new BlockIndexNotSequentialError(firstBlockIndex, previousBlockIndex);
243
+ // Now check that the block has the expected index value
244
+ if (!opts.force && expectedBlockIndex !== blockIndex) {
245
+ throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex);
182
246
  }
183
247
 
184
- // Iterate over blocks array and insert them, checking that the block numbers and indexes are sequential. Also check they are for the correct checkpoint.
185
- let previousBlock: L2BlockNew | undefined = undefined;
186
- for (const block of blocks) {
187
- if (!opts.force && previousBlock) {
188
- if (previousBlock.number + 1 !== block.number) {
189
- throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
190
- }
191
- if (previousBlock.indexWithinCheckpoint + 1 !== block.indexWithinCheckpoint) {
192
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
193
- }
194
- if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
195
- throw new BlockArchiveNotConsistentError(
196
- block.number,
197
- previousBlock.number,
198
- block.header.lastArchive.root,
199
- previousBlock.archive.root,
200
- );
201
- }
202
- }
203
- if (!opts.force && firstBlockCheckpointNumber !== block.checkpointNumber) {
204
- throw new CheckpointNumberNotConsistentError(block.checkpointNumber, firstBlockCheckpointNumber);
205
- }
206
- previousBlock = block;
207
- await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
208
- }
248
+ await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
209
249
 
210
250
  return true;
211
251
  });
212
252
  }
213
253
 
214
254
  /**
215
- * Append new cheskpoints to the store's list.
255
+ * Append new checkpoints to the store's list.
216
256
  * @param checkpoints - The L2 checkpoints to be added to the store.
217
257
  * @returns True if the operation is successful.
218
258
  */
@@ -222,37 +262,29 @@ export class BlockStore {
222
262
  }
223
263
 
224
264
  return await this.db.transactionAsync(async () => {
225
- // Check that the checkpoint immediately before the first block to be added is present in the store.
226
265
  const firstCheckpointNumber = checkpoints[0].checkpoint.number;
227
266
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
228
267
 
229
- if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
230
- throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
231
- }
232
-
233
- // Extract the previous checkpoint if there is one
234
- let previousCheckpointData: CheckpointData | undefined = undefined;
235
- if (previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1) {
236
- // There should be a previous checkpoint
237
- previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
238
- if (previousCheckpointData === undefined) {
239
- throw new CheckpointNotFoundError(previousCheckpointNumber);
268
+ // Handle already-stored checkpoints at the start of the batch.
269
+ // This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
270
+ // We accept them if archives match (same content) and update their L1 metadata.
271
+ if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
272
+ checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
273
+ if (checkpoints.length === 0) {
274
+ return true;
240
275
  }
241
- }
242
-
243
- let previousBlockNumber: BlockNumber | undefined = undefined;
244
- let previousBlock: L2BlockNew | undefined = undefined;
245
-
246
- // If we have a previous checkpoint then we need to get the previous block number
247
- if (previousCheckpointData !== undefined) {
248
- previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.numBlocks - 1);
249
- previousBlock = await this.getBlock(previousBlockNumber);
250
- if (previousBlock === undefined) {
251
- // We should be able to get the required previous block
252
- throw new BlockNotFoundError(previousBlockNumber);
276
+ // Re-check sequentiality after skipping
277
+ const newFirstNumber = checkpoints[0].checkpoint.number;
278
+ if (previousCheckpointNumber !== newFirstNumber - 1) {
279
+ throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
253
280
  }
281
+ } else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
282
+ throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
254
283
  }
255
284
 
285
+ // Get the last block of the previous checkpoint for archive chaining
286
+ let previousBlock = await this.getPreviousCheckpointBlock(checkpoints[0].checkpoint.number);
287
+
256
288
  // Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
257
289
  let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
258
290
  for (const checkpoint of checkpoints) {
@@ -268,62 +300,148 @@ export class BlockStore {
268
300
  }
269
301
  previousCheckpoint = checkpoint;
270
302
 
271
- // Store every block in the database. the block may already exist, but this has come from chain and is assumed to be correct.
272
- for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
273
- const block = checkpoint.checkpoint.blocks[i];
274
- if (previousBlock) {
275
- // The blocks should have a sequential block number
276
- if (previousBlock.number !== block.number - 1) {
277
- throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
278
- }
279
- // If the blocks are for the same checkpoint then they should have sequential indexes
280
- if (
281
- previousBlock.checkpointNumber === block.checkpointNumber &&
282
- previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1
283
- ) {
284
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
285
- }
286
- if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
287
- throw new BlockArchiveNotConsistentError(
288
- block.number,
289
- previousBlock.number,
290
- block.header.lastArchive.root,
291
- previousBlock.archive.root,
292
- );
293
- }
294
- } else {
295
- // No previous block, must be block 1 at checkpoint index 0
296
- if (block.indexWithinCheckpoint !== 0) {
297
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
298
- }
299
- if (block.number !== INITIAL_L2_BLOCK_NUM) {
300
- throw new BlockNumberNotSequentialError(block.number, undefined);
301
- }
302
- }
303
+ // Validate block sequencing, indexes, and archive chaining
304
+ this.validateCheckpointBlocks(checkpoint.checkpoint.blocks, previousBlock);
303
305
 
304
- previousBlock = block;
305
- await this.addBlockToDatabase(block, checkpoint.checkpoint.number, i);
306
+ // Store every block in the database (may already exist, but L1 data is authoritative)
307
+ for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
308
+ await this.addBlockToDatabase(checkpoint.checkpoint.blocks[i], checkpoint.checkpoint.number, i);
306
309
  }
310
+ previousBlock = checkpoint.checkpoint.blocks.at(-1);
307
311
 
308
312
  // Store the checkpoint in the database
309
313
  await this.#checkpoints.set(checkpoint.checkpoint.number, {
310
314
  header: checkpoint.checkpoint.header.toBuffer(),
311
315
  archive: checkpoint.checkpoint.archive.toBuffer(),
316
+ checkpointOutHash: checkpoint.checkpoint.getCheckpointOutHash().toBuffer(),
312
317
  l1: checkpoint.l1.toBuffer(),
313
318
  attestations: checkpoint.attestations.map(attestation => attestation.toBuffer()),
314
319
  checkpointNumber: checkpoint.checkpoint.number,
315
320
  startBlock: checkpoint.checkpoint.blocks[0].number,
316
- numBlocks: checkpoint.checkpoint.blocks.length,
321
+ blockCount: checkpoint.checkpoint.blocks.length,
317
322
  });
323
+
324
+ // Update slot-to-checkpoint index
325
+ await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
318
326
  }
319
327
 
328
+ // Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
329
+ const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
330
+ await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
331
+
320
332
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
321
333
  return true;
322
334
  });
323
335
  }
324
336
 
325
- private async addBlockToDatabase(block: L2BlockNew, checkpointNumber: number, indexWithinCheckpoint: number) {
326
- const blockHash = L2BlockHash.fromField(await block.hash());
337
+ /**
338
+ * Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
339
+ * Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
340
+ */
341
+ private async skipOrUpdateAlreadyStoredCheckpoints(
342
+ checkpoints: PublishedCheckpoint[],
343
+ latestStored: CheckpointNumber,
344
+ ): Promise<PublishedCheckpoint[]> {
345
+ let i = 0;
346
+ for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
347
+ const incoming = checkpoints[i];
348
+ const stored = await this.getCheckpointData(incoming.checkpoint.number);
349
+ if (!stored) {
350
+ // Should not happen if latestStored is correct, but be safe
351
+ break;
352
+ }
353
+ // Verify the checkpoint content matches (archive root)
354
+ if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
355
+ throw new Error(
356
+ `Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
357
+ `Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
358
+ );
359
+ }
360
+ // Update L1 metadata and attestations for the already-stored checkpoint
361
+ this.#log.warn(
362
+ `Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
363
+ `(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
364
+ );
365
+ await this.#checkpoints.set(incoming.checkpoint.number, {
366
+ header: incoming.checkpoint.header.toBuffer(),
367
+ archive: incoming.checkpoint.archive.toBuffer(),
368
+ checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
369
+ l1: incoming.l1.toBuffer(),
370
+ attestations: incoming.attestations.map(a => a.toBuffer()),
371
+ checkpointNumber: incoming.checkpoint.number,
372
+ startBlock: incoming.checkpoint.blocks[0].number,
373
+ blockCount: incoming.checkpoint.blocks.length,
374
+ });
375
+ // Update the sync point to reflect the new L1 block
376
+ await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
377
+ }
378
+ return checkpoints.slice(i);
379
+ }
380
+
381
+ /**
382
+ * Gets the last block of the checkpoint before the given one.
383
+ * Returns undefined if there is no previous checkpoint (i.e. genesis).
384
+ */
385
+ private async getPreviousCheckpointBlock(checkpointNumber: CheckpointNumber): Promise<L2Block | undefined> {
386
+ const previousCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
387
+ if (previousCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
388
+ return undefined;
389
+ }
390
+
391
+ const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
392
+ if (previousCheckpointData === undefined) {
393
+ throw new CheckpointNotFoundError(previousCheckpointNumber);
394
+ }
395
+
396
+ const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
397
+ const previousBlock = await this.getBlock(previousBlockNumber);
398
+ if (previousBlock === undefined) {
399
+ throw new BlockNotFoundError(previousBlockNumber);
400
+ }
401
+
402
+ return previousBlock;
403
+ }
404
+
405
+ /**
406
+ * Validates that blocks are sequential, have correct indexes, and chain via archive roots.
407
+ * This is the same validation used for both confirmed checkpoints (addCheckpoints) and
408
+ * proposed checkpoints (setProposedCheckpoint).
409
+ */
410
+ private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
411
+ for (const block of blocks) {
412
+ if (previousBlock) {
413
+ if (previousBlock.number !== block.number - 1) {
414
+ throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
415
+ }
416
+ if (previousBlock.checkpointNumber === block.checkpointNumber) {
417
+ if (previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1) {
418
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
419
+ }
420
+ } else if (block.indexWithinCheckpoint !== 0) {
421
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
422
+ }
423
+ if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
424
+ throw new BlockArchiveNotConsistentError(
425
+ block.number,
426
+ previousBlock.number,
427
+ block.header.lastArchive.root,
428
+ previousBlock.archive.root,
429
+ );
430
+ }
431
+ } else {
432
+ if (block.indexWithinCheckpoint !== 0) {
433
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
434
+ }
435
+ if (block.number !== INITIAL_L2_BLOCK_NUM) {
436
+ throw new BlockNumberNotSequentialError(block.number, undefined);
437
+ }
438
+ }
439
+ previousBlock = block;
440
+ }
441
+ }
442
+
443
+ private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
444
+ const blockHash = await block.hash();
327
445
 
328
446
  await this.#blocks.set(block.number, {
329
447
  header: block.header.toBuffer(),
@@ -351,7 +469,7 @@ export class BlockStore {
351
469
  }
352
470
 
353
471
  /** Deletes a block and all associated data (tx effects, indices). */
354
- private async deleteBlock(block: L2BlockNew): Promise<void> {
472
+ private async deleteBlock(block: L2Block): Promise<void> {
355
473
  // Delete the block from the main blocks map
356
474
  await this.#blocks.delete(block.number);
357
475
 
@@ -368,51 +486,59 @@ export class BlockStore {
368
486
  }
369
487
 
370
488
  /**
371
- * Unwinds checkpoints from the database
372
- * @param from - The tip of the chain, passed for verification purposes,
373
- * ensuring that we don't end up deleting something we did not intend
374
- * @param checkpointsToUnwind - The number of checkpoints we are to unwind
375
- * @returns True if the operation is successful
489
+ * Removes all checkpoints with checkpoint number > checkpointNumber.
490
+ * Also removes ALL blocks (both checkpointed and uncheckpointed) after the last block of the given checkpoint.
491
+ * @param checkpointNumber - Remove all checkpoints strictly after this one.
376
492
  */
377
- async unwindCheckpoints(from: CheckpointNumber, checkpointsToUnwind: number) {
493
+ async removeCheckpointsAfter(checkpointNumber: CheckpointNumber): Promise<RemoveCheckpointsResult> {
378
494
  return await this.db.transactionAsync(async () => {
379
- const last = await this.getLatestCheckpointNumber();
380
- if (from !== last) {
381
- throw new Error(`Can only unwind checkpoints from the tip (requested ${from} but current tip is ${last})`);
495
+ const latestCheckpointNumber = await this.getLatestCheckpointNumber();
496
+
497
+ if (checkpointNumber >= latestCheckpointNumber) {
498
+ this.#log.warn(`No checkpoints to remove after ${checkpointNumber} (latest is ${latestCheckpointNumber})`);
499
+ return { blocksRemoved: undefined };
382
500
  }
383
501
 
502
+ // If the proven checkpoint is beyond the target, update it
384
503
  const proven = await this.getProvenCheckpointNumber();
385
- if (from - checkpointsToUnwind < proven) {
386
- await this.setProvenCheckpointNumber(CheckpointNumber(from - checkpointsToUnwind));
504
+ if (proven > checkpointNumber) {
505
+ this.#log.warn(`Updating proven checkpoint ${proven} to last valid checkpoint ${checkpointNumber}`);
506
+ await this.setProvenCheckpointNumber(checkpointNumber);
387
507
  }
388
508
 
389
- for (let i = 0; i < checkpointsToUnwind; i++) {
390
- const checkpointNumber = from - i;
391
- const checkpoint = await this.#checkpoints.getAsync(checkpointNumber);
392
-
393
- if (checkpoint === undefined) {
394
- this.#log.warn(`Cannot remove checkpoint ${checkpointNumber} from the store since we don't have it`);
395
- continue;
509
+ // Find the last block number to keep (last block of the given checkpoint, or 0 if no checkpoint)
510
+ let lastBlockToKeep: BlockNumber;
511
+ if (checkpointNumber <= 0) {
512
+ lastBlockToKeep = BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
513
+ } else {
514
+ const targetCheckpoint = await this.#checkpoints.getAsync(checkpointNumber);
515
+ if (!targetCheckpoint) {
516
+ throw new Error(`Target checkpoint ${checkpointNumber} not found in store`);
396
517
  }
397
- await this.#checkpoints.delete(checkpointNumber);
398
- const maxBlock = checkpoint.startBlock + checkpoint.numBlocks - 1;
518
+ lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.blockCount - 1);
519
+ }
399
520
 
400
- for (let blockNumber = checkpoint.startBlock; blockNumber <= maxBlock; blockNumber++) {
401
- const block = await this.getBlock(BlockNumber(blockNumber));
521
+ // Remove all blocks after lastBlockToKeep (both checkpointed and uncheckpointed)
522
+ const blocksRemoved = await this.removeBlocksAfter(lastBlockToKeep);
402
523
 
403
- if (block === undefined) {
404
- this.#log.warn(`Cannot remove block ${blockNumber} from the store since we don't have it`);
405
- continue;
406
- }
407
-
408
- await this.deleteBlock(block);
409
- this.#log.debug(
410
- `Unwound block ${blockNumber} ${(await block.hash()).toString()} for checkpoint ${checkpointNumber}`,
411
- );
524
+ // Remove all checkpoints after the target
525
+ for (let c = latestCheckpointNumber; c > checkpointNumber; c = CheckpointNumber(c - 1)) {
526
+ const checkpointStorage = await this.#checkpoints.getAsync(c);
527
+ if (checkpointStorage) {
528
+ const slotNumber = CheckpointHeader.fromBuffer(checkpointStorage.header).slotNumber;
529
+ await this.#slotToCheckpoint.delete(slotNumber);
412
530
  }
531
+ await this.#checkpoints.delete(c);
532
+ this.#log.debug(`Removed checkpoint ${c}`);
413
533
  }
414
534
 
415
- return true;
535
+ // Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
536
+ const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
537
+ if (proposedCheckpointNumber > checkpointNumber) {
538
+ await this.#proposedCheckpoint.delete();
539
+ }
540
+
541
+ return { blocksRemoved };
416
542
  });
417
543
  }
418
544
 
@@ -436,20 +562,35 @@ export class BlockStore {
436
562
  return checkpoints;
437
563
  }
438
564
 
439
- private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage) {
440
- const data: CheckpointData = {
565
+ /** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
566
+ async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
567
+ const result: CheckpointData[] = [];
568
+ for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
569
+ start: startSlot,
570
+ end: endSlot + 1,
571
+ })) {
572
+ const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
573
+ if (checkpointStorage) {
574
+ result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
575
+ }
576
+ }
577
+ return result;
578
+ }
579
+
580
+ private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
581
+ return {
441
582
  header: CheckpointHeader.fromBuffer(checkpointStorage.header),
442
583
  archive: AppendOnlyTreeSnapshot.fromBuffer(checkpointStorage.archive),
584
+ checkpointOutHash: Fr.fromBuffer(checkpointStorage.checkpointOutHash),
443
585
  checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
444
- startBlock: checkpointStorage.startBlock,
445
- numBlocks: checkpointStorage.numBlocks,
586
+ startBlock: BlockNumber(checkpointStorage.startBlock),
587
+ blockCount: checkpointStorage.blockCount,
446
588
  l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
447
- attestations: checkpointStorage.attestations,
589
+ attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
448
590
  };
449
- return data;
450
591
  }
451
592
 
452
- async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<L2BlockNew[] | undefined> {
593
+ async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<L2Block[] | undefined> {
453
594
  const checkpoint = await this.#checkpoints.getAsync(checkpointNumber);
454
595
  if (!checkpoint) {
455
596
  return undefined;
@@ -458,7 +599,7 @@ export class BlockStore {
458
599
  const blocksForCheckpoint = await toArray(
459
600
  this.#blocks.entriesAsync({
460
601
  start: checkpoint.startBlock,
461
- end: checkpoint.startBlock + checkpoint.numBlocks,
602
+ end: checkpoint.startBlock + checkpoint.blockCount,
462
603
  }),
463
604
  );
464
605
 
@@ -472,8 +613,8 @@ export class BlockStore {
472
613
  * @param slotNumber - The slot number to search for.
473
614
  * @returns All blocks with the given slot number, in ascending block number order.
474
615
  */
475
- async getBlocksForSlot(slotNumber: SlotNumber): Promise<L2BlockNew[]> {
476
- const blocks: L2BlockNew[] = [];
616
+ async getBlocksForSlot(slotNumber: SlotNumber): Promise<L2Block[]> {
617
+ const blocks: L2Block[] = [];
477
618
 
478
619
  // Iterate backwards through all blocks and filter by slot number
479
620
  // This is more efficient since we usually query for the most recent slot
@@ -493,15 +634,16 @@ export class BlockStore {
493
634
 
494
635
  /**
495
636
  * Removes all blocks with block number > blockNumber.
637
+ * Does not remove any associated checkpoints.
496
638
  * @param blockNumber - The block number to remove after.
497
639
  * @returns The removed blocks (for event emission).
498
640
  */
499
- async unwindBlocksAfter(blockNumber: BlockNumber): Promise<L2BlockNew[]> {
641
+ async removeBlocksAfter(blockNumber: BlockNumber): Promise<L2Block[]> {
500
642
  return await this.db.transactionAsync(async () => {
501
- const removedBlocks: L2BlockNew[] = [];
643
+ const removedBlocks: L2Block[] = [];
502
644
 
503
645
  // Get the latest block number to determine the range
504
- const latestBlockNumber = await this.getLatestBlockNumber();
646
+ const latestBlockNumber = await this.getLatestL2BlockNumber();
505
647
 
506
648
  // Iterate from blockNumber + 1 to latestBlockNumber
507
649
  for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
@@ -530,17 +672,10 @@ export class BlockStore {
530
672
  if (!checkpointStorage) {
531
673
  throw new CheckpointNotFoundError(provenCheckpointNumber);
532
674
  } else {
533
- return BlockNumber(checkpointStorage.startBlock + checkpointStorage.numBlocks - 1);
675
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
534
676
  }
535
677
  }
536
678
 
537
- async getLatestBlockNumber(): Promise<BlockNumber> {
538
- const [latestBlocknumber] = await toArray(this.#blocks.keysAsync({ reverse: true, limit: 1 }));
539
- return typeof latestBlocknumber === 'number'
540
- ? BlockNumber(latestBlocknumber)
541
- : BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
542
- }
543
-
544
679
  async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
545
680
  const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
546
681
  if (latestCheckpointNumber === undefined) {
@@ -549,6 +684,84 @@ export class BlockStore {
549
684
  return CheckpointNumber(latestCheckpointNumber);
550
685
  }
551
686
 
687
+ async hasProposedCheckpoint(): Promise<boolean> {
688
+ const proposed = await this.#proposedCheckpoint.getAsync();
689
+ return proposed !== undefined;
690
+ }
691
+
692
+ /** Deletes the proposed checkpoint from storage. */
693
+ async deleteProposedCheckpoint(): Promise<void> {
694
+ await this.#proposedCheckpoint.delete();
695
+ }
696
+
697
+ /** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
698
+ async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
699
+ const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
700
+ if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
701
+ await this.#proposedCheckpoint.delete();
702
+ }
703
+ }
704
+
705
+ /** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
706
+ async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
707
+ const stored = await this.#proposedCheckpoint.getAsync();
708
+ if (!stored) {
709
+ return undefined;
710
+ }
711
+ return this.convertToProposedCheckpointData(stored);
712
+ }
713
+
714
+ /**
715
+ * Gets the checkpoint at the proposed tip
716
+ * - pending checkpoint if it exists
717
+ * - fallsback to latest confirmed checkpoint otherwise
718
+ * @returns CommonCheckpointData
719
+ */
720
+ async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
721
+ const stored = await this.#proposedCheckpoint.getAsync();
722
+ if (!stored) {
723
+ return this.getCheckpointData(await this.getLatestCheckpointNumber());
724
+ }
725
+ return this.convertToProposedCheckpointData(stored);
726
+ }
727
+
728
+ private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
729
+ return {
730
+ checkpointNumber: CheckpointNumber(stored.checkpointNumber),
731
+ header: CheckpointHeader.fromBuffer(stored.header),
732
+ archive: AppendOnlyTreeSnapshot.fromBuffer(stored.archive),
733
+ checkpointOutHash: Fr.fromBuffer(stored.checkpointOutHash),
734
+ startBlock: BlockNumber(stored.startBlock),
735
+ blockCount: stored.blockCount,
736
+ totalManaUsed: BigInt(stored.totalManaUsed),
737
+ feeAssetPriceModifier: BigInt(stored.feeAssetPriceModifier),
738
+ };
739
+ }
740
+
741
+ /**
742
+ * Attempts to get the proposedCheckpoint's number, if there is not one, then fallback to the latest confirmed checkpoint number.
743
+ * @returns CheckpointNumber
744
+ */
745
+ async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
746
+ const proposed = await this.getProposedCheckpoint();
747
+ if (!proposed) {
748
+ return await this.getLatestCheckpointNumber();
749
+ }
750
+ return CheckpointNumber(proposed.checkpointNumber);
751
+ }
752
+
753
+ /**
754
+ * Attempts to get the proposedCheckpoint's block number, if there is not one, then fallback to the checkpointed block number
755
+ * @returns BlockNumber
756
+ */
757
+ async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
758
+ const proposed = await this.getProposedCheckpoint();
759
+ if (!proposed) {
760
+ return await this.getCheckpointedL2BlockNumber();
761
+ }
762
+ return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
763
+ }
764
+
552
765
  async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
553
766
  const blockStorage = await this.#blocks.getAsync(number);
554
767
  if (!blockStorage) {
@@ -598,7 +811,7 @@ export class BlockStore {
598
811
  }
599
812
  }
600
813
 
601
- async getCheckpointedBlockByHash(blockHash: Fr): Promise<CheckpointedL2Block | undefined> {
814
+ async getCheckpointedBlockByHash(blockHash: BlockHash): Promise<CheckpointedL2Block | undefined> {
602
815
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
603
816
  if (blockNumber === undefined) {
604
817
  return undefined;
@@ -620,7 +833,7 @@ export class BlockStore {
620
833
  * @param limit - The number of blocks to return.
621
834
  * @returns The requested L2 blocks
622
835
  */
623
- async *getBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<L2BlockNew> {
836
+ async *getBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<L2Block> {
624
837
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
625
838
  const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
626
839
  if (block) {
@@ -629,12 +842,38 @@ export class BlockStore {
629
842
  }
630
843
  }
631
844
 
845
+ /**
846
+ * Gets block metadata (without tx data) by block number.
847
+ * @param blockNumber - The number of the block to return.
848
+ * @returns The requested block data.
849
+ */
850
+ async getBlockData(blockNumber: BlockNumber): Promise<BlockData | undefined> {
851
+ const blockStorage = await this.#blocks.getAsync(blockNumber);
852
+ if (!blockStorage || !blockStorage.header) {
853
+ return undefined;
854
+ }
855
+ return this.getBlockDataFromBlockStorage(blockStorage);
856
+ }
857
+
858
+ /**
859
+ * Gets block metadata (without tx data) by archive root.
860
+ * @param archive - The archive root of the block to return.
861
+ * @returns The requested block data.
862
+ */
863
+ async getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
864
+ const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
865
+ if (blockNumber === undefined) {
866
+ return undefined;
867
+ }
868
+ return this.getBlockData(BlockNumber(blockNumber));
869
+ }
870
+
632
871
  /**
633
872
  * Gets an L2 block.
634
873
  * @param blockNumber - The number of the block to return.
635
874
  * @returns The requested L2 block.
636
875
  */
637
- async getBlock(blockNumber: BlockNumber): Promise<L2BlockNew | undefined> {
876
+ async getBlock(blockNumber: BlockNumber): Promise<L2Block | undefined> {
638
877
  const blockStorage = await this.#blocks.getAsync(blockNumber);
639
878
  if (!blockStorage || !blockStorage.header) {
640
879
  return Promise.resolve(undefined);
@@ -647,7 +886,7 @@ export class BlockStore {
647
886
  * @param blockHash - The hash of the block to return.
648
887
  * @returns The requested L2 block.
649
888
  */
650
- async getBlockByHash(blockHash: L2BlockHash): Promise<L2BlockNew | undefined> {
889
+ async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
651
890
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
652
891
  if (blockNumber === undefined) {
653
892
  return undefined;
@@ -660,7 +899,7 @@ export class BlockStore {
660
899
  * @param archive - The archive root of the block to return.
661
900
  * @returns The requested L2 block.
662
901
  */
663
- async getBlockByArchive(archive: Fr): Promise<L2BlockNew | undefined> {
902
+ async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
664
903
  const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
665
904
  if (blockNumber === undefined) {
666
905
  return undefined;
@@ -673,7 +912,7 @@ export class BlockStore {
673
912
  * @param blockHash - The hash of the block to return.
674
913
  * @returns The requested block header.
675
914
  */
676
- async getBlockHeaderByHash(blockHash: L2BlockHash): Promise<BlockHeader | undefined> {
915
+ async getBlockHeaderByHash(blockHash: BlockHash): Promise<BlockHeader | undefined> {
677
916
  const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
678
917
  if (blockNumber === undefined) {
679
918
  return undefined;
@@ -733,15 +972,24 @@ export class BlockStore {
733
972
  }
734
973
  }
735
974
 
975
+ private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
976
+ return {
977
+ header: BlockHeader.fromBuffer(blockStorage.header),
978
+ archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
979
+ blockHash: BlockHash.fromBuffer(blockStorage.blockHash),
980
+ checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
981
+ indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
982
+ };
983
+ }
984
+
736
985
  private async getBlockFromBlockStorage(
737
986
  blockNumber: number,
738
987
  blockStorage: BlockStorage,
739
- ): Promise<L2BlockNew | undefined> {
740
- const header = BlockHeader.fromBuffer(blockStorage.header);
741
- const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive);
742
- const blockHash = blockStorage.blockHash;
743
- header.setHash(Fr.fromBuffer(blockHash));
744
- const blockHashString = bufferToHex(blockHash);
988
+ ): Promise<L2Block | undefined> {
989
+ const { header, archive, blockHash, checkpointNumber, indexWithinCheckpoint } =
990
+ this.getBlockDataFromBlockStorage(blockStorage);
991
+ header.setHash(blockHash);
992
+ const blockHashString = bufferToHex(blockStorage.blockHash);
745
993
  const blockTxsBuffer = await this.#blockTxs.getAsync(blockHashString);
746
994
  if (blockTxsBuffer === undefined) {
747
995
  this.#log.warn(`Could not find body for block ${header.globalVariables.blockNumber} ${blockHash}`);
@@ -760,13 +1008,7 @@ export class BlockStore {
760
1008
  txEffects.push(deserializeIndexedTxEffect(txEffect).data);
761
1009
  }
762
1010
  const body = new Body(txEffects);
763
- const block = new L2BlockNew(
764
- archive,
765
- header,
766
- body,
767
- CheckpointNumber(blockStorage.checkpointNumber!),
768
- IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
769
- );
1011
+ const block = new L2Block(archive, header, body, checkpointNumber, indexWithinCheckpoint);
770
1012
 
771
1013
  if (block.number !== blockNumber) {
772
1014
  throw new Error(
@@ -796,19 +1038,48 @@ export class BlockStore {
796
1038
  * @param txHash - The hash of a tx we try to get the receipt for.
797
1039
  * @returns The requested tx receipt (or undefined if not found).
798
1040
  */
799
- async getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
1041
+ async getSettledTxReceipt(
1042
+ txHash: TxHash,
1043
+ l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
1044
+ ): Promise<TxReceipt | undefined> {
800
1045
  const txEffect = await this.getTxEffect(txHash);
801
1046
  if (!txEffect) {
802
1047
  return undefined;
803
1048
  }
804
1049
 
1050
+ const blockNumber = BlockNumber(txEffect.l2BlockNumber);
1051
+
1052
+ // Use existing archiver methods to determine finalization level
1053
+ const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
1054
+ this.getProvenBlockNumber(),
1055
+ this.getCheckpointedL2BlockNumber(),
1056
+ this.getFinalizedL2BlockNumber(),
1057
+ this.getBlockData(blockNumber),
1058
+ ]);
1059
+
1060
+ let status: TxStatus;
1061
+ if (blockNumber <= finalizedBlockNumber) {
1062
+ status = TxStatus.FINALIZED;
1063
+ } else if (blockNumber <= provenBlockNumber) {
1064
+ status = TxStatus.PROVEN;
1065
+ } else if (blockNumber <= checkpointedBlockNumber) {
1066
+ status = TxStatus.CHECKPOINTED;
1067
+ } else {
1068
+ status = TxStatus.PROPOSED;
1069
+ }
1070
+
1071
+ const epochNumber =
1072
+ blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
1073
+
805
1074
  return new TxReceipt(
806
1075
  txHash,
807
- TxReceipt.statusFromRevertCode(txEffect.data.revertCode),
808
- '',
1076
+ status,
1077
+ TxReceipt.executionResultFromRevertCode(txEffect.data.revertCode),
1078
+ undefined,
809
1079
  txEffect.data.transactionFee.toBigInt(),
810
1080
  txEffect.l2BlockHash,
811
- BlockNumber(txEffect.l2BlockNumber),
1081
+ blockNumber,
1082
+ epochNumber,
812
1083
  );
813
1084
  }
814
1085
 
@@ -845,7 +1116,7 @@ export class BlockStore {
845
1116
  if (!checkpoint) {
846
1117
  return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
847
1118
  }
848
- return BlockNumber(checkpoint.startBlock + checkpoint.numBlocks - 1);
1119
+ return BlockNumber(checkpoint.startBlock + checkpoint.blockCount - 1);
849
1120
  }
850
1121
 
851
1122
  async getLatestL2BlockNumber(): Promise<BlockNumber> {
@@ -865,6 +1136,47 @@ export class BlockStore {
865
1136
  return this.#lastSynchedL1Block.set(l1BlockNumber);
866
1137
  }
867
1138
 
1139
+ /** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
1140
+ * Computes archive and checkpointOutHash from the stored blocks. */
1141
+ async setProposedCheckpoint(proposed: ProposedCheckpointInput) {
1142
+ return await this.db.transactionAsync(async () => {
1143
+ const current = await this.getProposedCheckpointNumber();
1144
+ if (proposed.checkpointNumber <= current) {
1145
+ throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
1146
+ }
1147
+ const confirmed = await this.getLatestCheckpointNumber();
1148
+ if (proposed.checkpointNumber !== confirmed + 1) {
1149
+ throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
1150
+ }
1151
+
1152
+ // Ensure the previous checkpoint + blocks exist
1153
+ const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
1154
+ const blocks: L2Block[] = [];
1155
+ for (let i = 0; i < proposed.blockCount; i++) {
1156
+ const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
1157
+ if (!block) {
1158
+ throw new BlockNotFoundError(proposed.startBlock + i);
1159
+ }
1160
+ blocks.push(block);
1161
+ }
1162
+ this.validateCheckpointBlocks(blocks, previousBlock);
1163
+
1164
+ const archive = blocks[blocks.length - 1].archive;
1165
+ const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
1166
+
1167
+ await this.#proposedCheckpoint.set({
1168
+ header: proposed.header.toBuffer(),
1169
+ archive: archive.toBuffer(),
1170
+ checkpointOutHash: checkpointOutHash.toBuffer(),
1171
+ checkpointNumber: proposed.checkpointNumber,
1172
+ startBlock: proposed.startBlock,
1173
+ blockCount: proposed.blockCount,
1174
+ totalManaUsed: proposed.totalManaUsed.toString(),
1175
+ feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
1176
+ });
1177
+ });
1178
+ }
1179
+
868
1180
  async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
869
1181
  const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
870
1182
  this.getLatestCheckpointNumber(),
@@ -880,6 +1192,20 @@ export class BlockStore {
880
1192
  return result;
881
1193
  }
882
1194
 
1195
+ async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
1196
+ const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
1197
+ this.getLatestCheckpointNumber(),
1198
+ this.#lastFinalizedCheckpoint.getAsync(),
1199
+ ]);
1200
+ return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber
1201
+ ? latestCheckpointNumber
1202
+ : CheckpointNumber(finalizedCheckpointNumber ?? 0);
1203
+ }
1204
+
1205
+ setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
1206
+ return this.#lastFinalizedCheckpoint.set(checkpointNumber);
1207
+ }
1208
+
883
1209
  #computeBlockRange(start: BlockNumber, limit: number): Required<Pick<Range<number>, 'start' | 'limit'>> {
884
1210
  if (limit < 1) {
885
1211
  throw new Error(`Invalid limit: ${limit}`);