@aztec/archiver 0.0.1-commit.e0f15ab9b → 0.0.1-commit.e304674f1

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 (67) hide show
  1. package/README.md +12 -6
  2. package/dest/archiver.d.ts +5 -4
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +6 -3
  5. package/dest/errors.d.ts +14 -2
  6. package/dest/errors.d.ts.map +1 -1
  7. package/dest/errors.js +18 -2
  8. package/dest/l1/calldata_retriever.d.ts +1 -1
  9. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  10. package/dest/l1/calldata_retriever.js +2 -1
  11. package/dest/l1/data_retrieval.d.ts +3 -3
  12. package/dest/l1/data_retrieval.d.ts.map +1 -1
  13. package/dest/l1/data_retrieval.js +14 -15
  14. package/dest/modules/data_source_base.d.ts +4 -2
  15. package/dest/modules/data_source_base.d.ts.map +1 -1
  16. package/dest/modules/data_source_base.js +6 -0
  17. package/dest/modules/data_store_updater.d.ts +3 -2
  18. package/dest/modules/data_store_updater.d.ts.map +1 -1
  19. package/dest/modules/data_store_updater.js +9 -0
  20. package/dest/modules/l1_synchronizer.d.ts +3 -2
  21. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  22. package/dest/modules/l1_synchronizer.js +128 -123
  23. package/dest/modules/validation.d.ts +1 -1
  24. package/dest/modules/validation.d.ts.map +1 -1
  25. package/dest/modules/validation.js +2 -2
  26. package/dest/store/block_store.d.ts +39 -4
  27. package/dest/store/block_store.d.ts.map +1 -1
  28. package/dest/store/block_store.js +230 -61
  29. package/dest/store/kv_archiver_store.d.ts +21 -8
  30. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  31. package/dest/store/kv_archiver_store.js +25 -8
  32. package/dest/store/l2_tips_cache.d.ts +2 -1
  33. package/dest/store/l2_tips_cache.d.ts.map +1 -1
  34. package/dest/store/l2_tips_cache.js +25 -5
  35. package/dest/store/message_store.d.ts +3 -3
  36. package/dest/store/message_store.d.ts.map +1 -1
  37. package/dest/store/message_store.js +9 -10
  38. package/dest/test/fake_l1_state.d.ts +9 -1
  39. package/dest/test/fake_l1_state.d.ts.map +1 -1
  40. package/dest/test/fake_l1_state.js +41 -6
  41. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  42. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  43. package/dest/test/mock_l1_to_l2_message_source.js +2 -1
  44. package/dest/test/mock_l2_block_source.d.ts +7 -2
  45. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  46. package/dest/test/mock_l2_block_source.js +28 -3
  47. package/dest/test/noop_l1_archiver.d.ts +1 -1
  48. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  49. package/dest/test/noop_l1_archiver.js +0 -1
  50. package/package.json +13 -13
  51. package/src/archiver.ts +13 -7
  52. package/src/errors.ts +30 -2
  53. package/src/l1/calldata_retriever.ts +2 -1
  54. package/src/l1/data_retrieval.ts +8 -12
  55. package/src/modules/data_source_base.ts +15 -1
  56. package/src/modules/data_store_updater.ts +14 -1
  57. package/src/modules/l1_synchronizer.ts +138 -147
  58. package/src/modules/validation.ts +2 -2
  59. package/src/store/block_store.ts +300 -73
  60. package/src/store/kv_archiver_store.ts +43 -12
  61. package/src/store/l2_tips_cache.ts +50 -11
  62. package/src/store/message_store.ts +10 -12
  63. package/src/structs/inbox_message.ts +1 -1
  64. package/src/test/fake_l1_state.ts +56 -7
  65. package/src/test/mock_l1_to_l2_message_source.ts +1 -0
  66. package/src/test/mock_l2_block_source.ts +37 -2
  67. package/src/test/noop_l1_archiver.ts +0 -1
@@ -9,7 +9,7 @@ import {
9
9
  getAttestationInfoFromPayload,
10
10
  } from '@aztec/stdlib/block';
11
11
  import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
12
- import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
12
+ import { type L1RollupConstants, computeQuorum, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
13
13
  import { ConsensusPayload } from '@aztec/stdlib/p2p';
14
14
 
15
15
  export type { ValidateCheckpointResult };
@@ -66,7 +66,7 @@ export async function validateCheckpointAttestations(
66
66
  return { valid: true };
67
67
  }
68
68
 
69
- const requiredAttestationCount = Math.floor((committee.length * 2) / 3) + 1;
69
+ const requiredAttestationCount = computeQuorum(committee.length);
70
70
 
71
71
  const failedValidationResult = <TReason extends ValidateCheckpointNegativeResult['reason']>(reason: TReason) => ({
72
72
  valid: false as const,
@@ -19,7 +19,15 @@ import {
19
19
  deserializeValidateCheckpointResult,
20
20
  serializeValidateCheckpointResult,
21
21
  } from '@aztec/stdlib/block';
22
- import { type CheckpointData, 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';
23
31
  import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
24
32
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
25
33
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
@@ -44,6 +52,8 @@ import {
44
52
  CheckpointNotFoundError,
45
53
  CheckpointNumberNotSequentialError,
46
54
  InitialCheckpointNumberNotSequentialError,
55
+ ProposedCheckpointNotSequentialError,
56
+ ProposedCheckpointStaleError,
47
57
  } from '../errors.js';
48
58
 
49
59
  export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
@@ -58,17 +68,27 @@ type BlockStorage = {
58
68
  indexWithinCheckpoint: number;
59
69
  };
60
70
 
61
- type CheckpointStorage = {
71
+ /** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
72
+ type CommonCheckpointStorage = {
62
73
  header: Buffer;
63
74
  archive: Buffer;
64
75
  checkpointOutHash: Buffer;
65
76
  checkpointNumber: number;
66
77
  startBlock: number;
67
78
  blockCount: number;
79
+ };
80
+
81
+ type CheckpointStorage = CommonCheckpointStorage & {
68
82
  l1: Buffer;
69
83
  attestations: Buffer[];
70
84
  };
71
85
 
86
+ /** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
87
+ type ProposedCheckpointStorage = CommonCheckpointStorage & {
88
+ totalManaUsed: string;
89
+ feeAssetPriceModifier: string;
90
+ };
91
+
72
92
  export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
73
93
 
74
94
  /**
@@ -111,6 +131,9 @@ export class BlockStore {
111
131
  /** Index mapping block archive to block number */
112
132
  #blockArchiveIndex: AztecAsyncMap<string, number>;
113
133
 
134
+ /** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
135
+ #proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
136
+
114
137
  #log = createLogger('archiver:block_store');
115
138
 
116
139
  constructor(private db: AztecAsyncKVStore) {
@@ -126,6 +149,7 @@ export class BlockStore {
126
149
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
127
150
  this.#checkpoints = db.openMap('archiver_checkpoints');
128
151
  this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
152
+ this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
129
153
  }
130
154
 
131
155
  /**
@@ -160,7 +184,8 @@ export class BlockStore {
160
184
  const blockLastArchive = block.header.lastArchive.root;
161
185
 
162
186
  // Extract the latest block and checkpoint numbers
163
- const previousBlockNumber = await this.getLatestBlockNumber();
187
+ const previousBlockNumber = await this.getLatestL2BlockNumber();
188
+ const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
164
189
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
165
190
 
166
191
  // Verify we're not overwriting checkpointed blocks
@@ -179,9 +204,19 @@ export class BlockStore {
179
204
  throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
180
205
  }
181
206
 
182
- // The same check as above but for checkpoints
183
- if (!opts.force && previousCheckpointNumber !== blockCheckpointNumber - 1) {
184
- throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, previousCheckpointNumber);
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);
185
220
  }
186
221
 
187
222
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
@@ -217,7 +252,7 @@ export class BlockStore {
217
252
  }
218
253
 
219
254
  /**
220
- * Append new cheskpoints to the store's list.
255
+ * Append new checkpoints to the store's list.
221
256
  * @param checkpoints - The L2 checkpoints to be added to the store.
222
257
  * @returns True if the operation is successful.
223
258
  */
@@ -227,37 +262,29 @@ export class BlockStore {
227
262
  }
228
263
 
229
264
  return await this.db.transactionAsync(async () => {
230
- // Check that the checkpoint immediately before the first block to be added is present in the store.
231
265
  const firstCheckpointNumber = checkpoints[0].checkpoint.number;
232
266
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
233
267
 
234
- if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
235
- throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
236
- }
237
-
238
- // Extract the previous checkpoint if there is one
239
- let previousCheckpointData: CheckpointData | undefined = undefined;
240
- if (previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1) {
241
- // There should be a previous checkpoint
242
- previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
243
- if (previousCheckpointData === undefined) {
244
- 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;
245
275
  }
246
- }
247
-
248
- let previousBlockNumber: BlockNumber | undefined = undefined;
249
- let previousBlock: L2Block | undefined = undefined;
250
-
251
- // If we have a previous checkpoint then we need to get the previous block number
252
- if (previousCheckpointData !== undefined) {
253
- previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
254
- previousBlock = await this.getBlock(previousBlockNumber);
255
- if (previousBlock === undefined) {
256
- // We should be able to get the required previous block
257
- 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);
258
280
  }
281
+ } else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
282
+ throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
259
283
  }
260
284
 
285
+ // Get the last block of the previous checkpoint for archive chaining
286
+ let previousBlock = await this.getPreviousCheckpointBlock(checkpoints[0].checkpoint.number);
287
+
261
288
  // Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
262
289
  let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
263
290
  for (const checkpoint of checkpoints) {
@@ -273,42 +300,14 @@ export class BlockStore {
273
300
  }
274
301
  previousCheckpoint = checkpoint;
275
302
 
276
- // Store every block in the database. the block may already exist, but this has come from chain and is assumed to be correct.
277
- for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
278
- const block = checkpoint.checkpoint.blocks[i];
279
- if (previousBlock) {
280
- // The blocks should have a sequential block number
281
- if (previousBlock.number !== block.number - 1) {
282
- throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
283
- }
284
- // If the blocks are for the same checkpoint then they should have sequential indexes
285
- if (
286
- previousBlock.checkpointNumber === block.checkpointNumber &&
287
- previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1
288
- ) {
289
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
290
- }
291
- if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
292
- throw new BlockArchiveNotConsistentError(
293
- block.number,
294
- previousBlock.number,
295
- block.header.lastArchive.root,
296
- previousBlock.archive.root,
297
- );
298
- }
299
- } else {
300
- // No previous block, must be block 1 at checkpoint index 0
301
- if (block.indexWithinCheckpoint !== 0) {
302
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
303
- }
304
- if (block.number !== INITIAL_L2_BLOCK_NUM) {
305
- throw new BlockNumberNotSequentialError(block.number, undefined);
306
- }
307
- }
303
+ // Validate block sequencing, indexes, and archive chaining
304
+ this.validateCheckpointBlocks(checkpoint.checkpoint.blocks, previousBlock);
308
305
 
309
- previousBlock = block;
310
- 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);
311
309
  }
310
+ previousBlock = checkpoint.checkpoint.blocks.at(-1);
312
311
 
313
312
  // Store the checkpoint in the database
314
313
  await this.#checkpoints.set(checkpoint.checkpoint.number, {
@@ -326,11 +325,121 @@ export class BlockStore {
326
325
  await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
327
326
  }
328
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
+
329
332
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
330
333
  return true;
331
334
  });
332
335
  }
333
336
 
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
+
334
443
  private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
335
444
  const blockHash = await block.hash();
336
445
 
@@ -423,6 +532,12 @@ export class BlockStore {
423
532
  this.#log.debug(`Removed checkpoint ${c}`);
424
533
  }
425
534
 
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
+
426
541
  return { blocksRemoved };
427
542
  });
428
543
  }
@@ -528,7 +643,7 @@ export class BlockStore {
528
643
  const removedBlocks: L2Block[] = [];
529
644
 
530
645
  // Get the latest block number to determine the range
531
- const latestBlockNumber = await this.getLatestBlockNumber();
646
+ const latestBlockNumber = await this.getLatestL2BlockNumber();
532
647
 
533
648
  // Iterate from blockNumber + 1 to latestBlockNumber
534
649
  for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
@@ -561,13 +676,6 @@ export class BlockStore {
561
676
  }
562
677
  }
563
678
 
564
- async getLatestBlockNumber(): Promise<BlockNumber> {
565
- const [latestBlocknumber] = await toArray(this.#blocks.keysAsync({ reverse: true, limit: 1 }));
566
- return typeof latestBlocknumber === 'number'
567
- ? BlockNumber(latestBlocknumber)
568
- : BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
569
- }
570
-
571
679
  async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
572
680
  const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
573
681
  if (latestCheckpointNumber === undefined) {
@@ -576,6 +684,84 @@ export class BlockStore {
576
684
  return CheckpointNumber(latestCheckpointNumber);
577
685
  }
578
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
+
579
765
  async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
580
766
  const blockStorage = await this.#blocks.getAsync(number);
581
767
  if (!blockStorage) {
@@ -950,6 +1136,47 @@ export class BlockStore {
950
1136
  return this.#lastSynchedL1Block.set(l1BlockNumber);
951
1137
  }
952
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
+
953
1180
  async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
954
1181
  const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
955
1182
  this.getLatestCheckpointNumber(),
@@ -13,7 +13,13 @@ import {
13
13
  L2Block,
14
14
  type ValidateCheckpointResult,
15
15
  } from '@aztec/stdlib/block';
16
- import type { CheckpointData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
16
+ import type {
17
+ CheckpointData,
18
+ CommonCheckpointData,
19
+ ProposedCheckpointData,
20
+ ProposedCheckpointInput,
21
+ PublishedCheckpoint,
22
+ } from '@aztec/stdlib/checkpoint';
17
23
  import type {
18
24
  ContractClassPublic,
19
25
  ContractClassPublicWithCommitment,
@@ -254,7 +260,7 @@ export class KVArchiverDataStore implements ContractDataSource {
254
260
  * @returns The number of the latest block
255
261
  */
256
262
  getLatestBlockNumber(): Promise<BlockNumber> {
257
- return this.#blockStore.getLatestBlockNumber();
263
+ return this.#blockStore.getLatestL2BlockNumber();
258
264
  }
259
265
 
260
266
  /**
@@ -552,13 +558,6 @@ export class KVArchiverDataStore implements ContractDataSource {
552
558
  await this.#blockStore.setSynchedL1BlockNumber(l1BlockNumber);
553
559
  }
554
560
 
555
- /**
556
- * Stores the l1 block that messages have been synched until
557
- */
558
- async setMessageSynchedL1Block(l1Block: L1BlockId) {
559
- await this.#messageStore.setSynchedL1Block(l1Block);
560
- }
561
-
562
561
  /**
563
562
  * Returns the number of the most recent proven block
564
563
  * @returns The number of the most recent proven block
@@ -591,9 +590,9 @@ export class KVArchiverDataStore implements ContractDataSource {
591
590
  return this.#messageStore.rollbackL1ToL2MessagesToCheckpoint(targetCheckpointNumber);
592
591
  }
593
592
 
594
- /** Persists the inbox tree-in-progress checkpoint number from L1 state. */
595
- public setInboxTreeInProgress(value: bigint): Promise<void> {
596
- return this.#messageStore.setInboxTreeInProgress(value);
593
+ /** Atomically updates the message sync state: the L1 sync point and the inbox tree-in-progress marker. */
594
+ public setMessageSyncState(l1Block: L1BlockId, treeInProgress: bigint | undefined): Promise<void> {
595
+ return this.#messageStore.setMessageSyncState(l1Block, treeInProgress);
597
596
  }
598
597
 
599
598
  /** Returns an async iterator to all L1 to L2 messages on the range. */
@@ -616,6 +615,38 @@ export class KVArchiverDataStore implements ContractDataSource {
616
615
  return this.#blockStore.setPendingChainValidationStatus(status);
617
616
  }
618
617
 
618
+ /**
619
+ * Gets the L2 block number of the proposed checkpoint.
620
+ * @returns The block number of the proposed checkpoint, or the checkpointed block number if none.
621
+ */
622
+ public getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
623
+ return this.#blockStore.getProposedCheckpointL2BlockNumber();
624
+ }
625
+
626
+ /** Returns the checkpoint data at the proposed tip */
627
+ public getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
628
+ return this.#blockStore.getProposedCheckpoint();
629
+ }
630
+
631
+ /** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
632
+ public getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
633
+ return this.#blockStore.getProposedCheckpointOnly();
634
+ }
635
+
636
+ /**
637
+ * Set proposed checkpoint
638
+ * @param proposedCheckpoint
639
+ * @returns
640
+ */
641
+ public setProposedCheckpoint(proposedCheckpoint: ProposedCheckpointInput): Promise<void> {
642
+ return this.#blockStore.setProposedCheckpoint(proposedCheckpoint);
643
+ }
644
+
645
+ /** Deletes the proposed checkpoint from storage. */
646
+ public deleteProposedCheckpoint(): Promise<void> {
647
+ return this.#blockStore.deleteProposedCheckpoint();
648
+ }
649
+
619
650
  /**
620
651
  * Gets the number of the latest L2 block processed.
621
652
  * @returns The number of the latest L2 block processed.