@aztec/archiver 0.0.1-commit.3f296a7d2 → 0.0.1-commit.42ee6df9b

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 (63) hide show
  1. package/README.md +12 -6
  2. package/dest/archiver.d.ts +3 -2
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +5 -2
  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 +2 -2
  12. package/dest/l1/data_retrieval.d.ts.map +1 -1
  13. package/dest/l1/data_retrieval.js +13 -14
  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 +38 -4
  27. package/dest/store/block_store.d.ts.map +1 -1
  28. package/dest/store/block_store.js +185 -60
  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 +2 -1
  39. package/dest/test/fake_l1_state.d.ts.map +1 -1
  40. package/dest/test/fake_l1_state.js +28 -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/package.json +13 -13
  48. package/src/archiver.ts +9 -2
  49. package/src/errors.ts +30 -2
  50. package/src/l1/calldata_retriever.ts +2 -1
  51. package/src/l1/data_retrieval.ts +7 -11
  52. package/src/modules/data_source_base.ts +15 -1
  53. package/src/modules/data_store_updater.ts +14 -1
  54. package/src/modules/l1_synchronizer.ts +137 -147
  55. package/src/modules/validation.ts +2 -2
  56. package/src/store/block_store.ts +242 -71
  57. package/src/store/kv_archiver_store.ts +43 -12
  58. package/src/store/l2_tips_cache.ts +50 -11
  59. package/src/store/message_store.ts +10 -12
  60. package/src/structs/inbox_message.ts +1 -1
  61. package/src/test/fake_l1_state.ts +41 -7
  62. package/src/test/mock_l1_to_l2_message_source.ts +1 -0
  63. package/src/test/mock_l2_block_source.ts +37 -2
@@ -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
  */
@@ -235,28 +270,8 @@ export class BlockStore {
235
270
  throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
236
271
  }
237
272
 
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);
245
- }
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);
258
- }
259
- }
273
+ // Get the last block of the previous checkpoint for archive chaining
274
+ let previousBlock = await this.getPreviousCheckpointBlock(firstCheckpointNumber);
260
275
 
261
276
  // Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
262
277
  let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
@@ -273,42 +288,14 @@ export class BlockStore {
273
288
  }
274
289
  previousCheckpoint = checkpoint;
275
290
 
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
- }
291
+ // Validate block sequencing, indexes, and archive chaining
292
+ this.validateCheckpointBlocks(checkpoint.checkpoint.blocks, previousBlock);
308
293
 
309
- previousBlock = block;
310
- await this.addBlockToDatabase(block, checkpoint.checkpoint.number, i);
294
+ // Store every block in the database (may already exist, but L1 data is authoritative)
295
+ for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
296
+ await this.addBlockToDatabase(checkpoint.checkpoint.blocks[i], checkpoint.checkpoint.number, i);
311
297
  }
298
+ previousBlock = checkpoint.checkpoint.blocks.at(-1);
312
299
 
313
300
  // Store the checkpoint in the database
314
301
  await this.#checkpoints.set(checkpoint.checkpoint.number, {
@@ -326,11 +313,77 @@ export class BlockStore {
326
313
  await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
327
314
  }
328
315
 
316
+ // Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
317
+ const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
318
+ await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
319
+
329
320
  await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
330
321
  return true;
331
322
  });
332
323
  }
333
324
 
325
+ /**
326
+ * Gets the last block of the checkpoint before the given one.
327
+ * Returns undefined if there is no previous checkpoint (i.e. genesis).
328
+ */
329
+ private async getPreviousCheckpointBlock(checkpointNumber: CheckpointNumber): Promise<L2Block | undefined> {
330
+ const previousCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
331
+ if (previousCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
332
+ return undefined;
333
+ }
334
+
335
+ const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
336
+ if (previousCheckpointData === undefined) {
337
+ throw new CheckpointNotFoundError(previousCheckpointNumber);
338
+ }
339
+
340
+ const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
341
+ const previousBlock = await this.getBlock(previousBlockNumber);
342
+ if (previousBlock === undefined) {
343
+ throw new BlockNotFoundError(previousBlockNumber);
344
+ }
345
+
346
+ return previousBlock;
347
+ }
348
+
349
+ /**
350
+ * Validates that blocks are sequential, have correct indexes, and chain via archive roots.
351
+ * This is the same validation used for both confirmed checkpoints (addCheckpoints) and
352
+ * proposed checkpoints (setProposedCheckpoint).
353
+ */
354
+ private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
355
+ for (const block of blocks) {
356
+ if (previousBlock) {
357
+ if (previousBlock.number !== block.number - 1) {
358
+ throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
359
+ }
360
+ if (previousBlock.checkpointNumber === block.checkpointNumber) {
361
+ if (previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1) {
362
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
363
+ }
364
+ } else if (block.indexWithinCheckpoint !== 0) {
365
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
366
+ }
367
+ if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
368
+ throw new BlockArchiveNotConsistentError(
369
+ block.number,
370
+ previousBlock.number,
371
+ block.header.lastArchive.root,
372
+ previousBlock.archive.root,
373
+ );
374
+ }
375
+ } else {
376
+ if (block.indexWithinCheckpoint !== 0) {
377
+ throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
378
+ }
379
+ if (block.number !== INITIAL_L2_BLOCK_NUM) {
380
+ throw new BlockNumberNotSequentialError(block.number, undefined);
381
+ }
382
+ }
383
+ previousBlock = block;
384
+ }
385
+ }
386
+
334
387
  private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
335
388
  const blockHash = await block.hash();
336
389
 
@@ -423,6 +476,12 @@ export class BlockStore {
423
476
  this.#log.debug(`Removed checkpoint ${c}`);
424
477
  }
425
478
 
479
+ // Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
480
+ const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
481
+ if (proposedCheckpointNumber > checkpointNumber) {
482
+ await this.#proposedCheckpoint.delete();
483
+ }
484
+
426
485
  return { blocksRemoved };
427
486
  });
428
487
  }
@@ -528,7 +587,7 @@ export class BlockStore {
528
587
  const removedBlocks: L2Block[] = [];
529
588
 
530
589
  // Get the latest block number to determine the range
531
- const latestBlockNumber = await this.getLatestBlockNumber();
590
+ const latestBlockNumber = await this.getLatestL2BlockNumber();
532
591
 
533
592
  // Iterate from blockNumber + 1 to latestBlockNumber
534
593
  for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
@@ -561,13 +620,6 @@ export class BlockStore {
561
620
  }
562
621
  }
563
622
 
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
623
  async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
572
624
  const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
573
625
  if (latestCheckpointNumber === undefined) {
@@ -576,6 +628,84 @@ export class BlockStore {
576
628
  return CheckpointNumber(latestCheckpointNumber);
577
629
  }
578
630
 
631
+ async hasProposedCheckpoint(): Promise<boolean> {
632
+ const proposed = await this.#proposedCheckpoint.getAsync();
633
+ return proposed !== undefined;
634
+ }
635
+
636
+ /** Deletes the proposed checkpoint from storage. */
637
+ async deleteProposedCheckpoint(): Promise<void> {
638
+ await this.#proposedCheckpoint.delete();
639
+ }
640
+
641
+ /** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
642
+ async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
643
+ const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
644
+ if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
645
+ await this.#proposedCheckpoint.delete();
646
+ }
647
+ }
648
+
649
+ /** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
650
+ async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
651
+ const stored = await this.#proposedCheckpoint.getAsync();
652
+ if (!stored) {
653
+ return undefined;
654
+ }
655
+ return this.convertToProposedCheckpointData(stored);
656
+ }
657
+
658
+ /**
659
+ * Gets the checkpoint at the proposed tip
660
+ * - pending checkpoint if it exists
661
+ * - fallsback to latest confirmed checkpoint otherwise
662
+ * @returns CommonCheckpointData
663
+ */
664
+ async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
665
+ const stored = await this.#proposedCheckpoint.getAsync();
666
+ if (!stored) {
667
+ return this.getCheckpointData(await this.getLatestCheckpointNumber());
668
+ }
669
+ return this.convertToProposedCheckpointData(stored);
670
+ }
671
+
672
+ private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
673
+ return {
674
+ checkpointNumber: CheckpointNumber(stored.checkpointNumber),
675
+ header: CheckpointHeader.fromBuffer(stored.header),
676
+ archive: AppendOnlyTreeSnapshot.fromBuffer(stored.archive),
677
+ checkpointOutHash: Fr.fromBuffer(stored.checkpointOutHash),
678
+ startBlock: BlockNumber(stored.startBlock),
679
+ blockCount: stored.blockCount,
680
+ totalManaUsed: BigInt(stored.totalManaUsed),
681
+ feeAssetPriceModifier: BigInt(stored.feeAssetPriceModifier),
682
+ };
683
+ }
684
+
685
+ /**
686
+ * Attempts to get the proposedCheckpoint's number, if there is not one, then fallback to the latest confirmed checkpoint number.
687
+ * @returns CheckpointNumber
688
+ */
689
+ async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
690
+ const proposed = await this.getProposedCheckpoint();
691
+ if (!proposed) {
692
+ return await this.getLatestCheckpointNumber();
693
+ }
694
+ return CheckpointNumber(proposed.checkpointNumber);
695
+ }
696
+
697
+ /**
698
+ * Attempts to get the proposedCheckpoint's block number, if there is not one, then fallback to the checkpointed block number
699
+ * @returns BlockNumber
700
+ */
701
+ async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
702
+ const proposed = await this.getProposedCheckpoint();
703
+ if (!proposed) {
704
+ return await this.getCheckpointedL2BlockNumber();
705
+ }
706
+ return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
707
+ }
708
+
579
709
  async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
580
710
  const blockStorage = await this.#blocks.getAsync(number);
581
711
  if (!blockStorage) {
@@ -950,6 +1080,47 @@ export class BlockStore {
950
1080
  return this.#lastSynchedL1Block.set(l1BlockNumber);
951
1081
  }
952
1082
 
1083
+ /** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
1084
+ * Computes archive and checkpointOutHash from the stored blocks. */
1085
+ async setProposedCheckpoint(proposed: ProposedCheckpointInput) {
1086
+ return await this.db.transactionAsync(async () => {
1087
+ const current = await this.getProposedCheckpointNumber();
1088
+ if (proposed.checkpointNumber <= current) {
1089
+ throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
1090
+ }
1091
+ const confirmed = await this.getLatestCheckpointNumber();
1092
+ if (proposed.checkpointNumber !== confirmed + 1) {
1093
+ throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
1094
+ }
1095
+
1096
+ // Ensure the previous checkpoint + blocks exist
1097
+ const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
1098
+ const blocks: L2Block[] = [];
1099
+ for (let i = 0; i < proposed.blockCount; i++) {
1100
+ const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
1101
+ if (!block) {
1102
+ throw new BlockNotFoundError(proposed.startBlock + i);
1103
+ }
1104
+ blocks.push(block);
1105
+ }
1106
+ this.validateCheckpointBlocks(blocks, previousBlock);
1107
+
1108
+ const archive = blocks[blocks.length - 1].archive;
1109
+ const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
1110
+
1111
+ await this.#proposedCheckpoint.set({
1112
+ header: proposed.header.toBuffer(),
1113
+ archive: archive.toBuffer(),
1114
+ checkpointOutHash: checkpointOutHash.toBuffer(),
1115
+ checkpointNumber: proposed.checkpointNumber,
1116
+ startBlock: proposed.startBlock,
1117
+ blockCount: proposed.blockCount,
1118
+ totalManaUsed: proposed.totalManaUsed.toString(),
1119
+ feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
1120
+ });
1121
+ });
1122
+ }
1123
+
953
1124
  async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
954
1125
  const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
955
1126
  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.
@@ -26,9 +26,16 @@ export class L2TipsCache {
26
26
  }
27
27
 
28
28
  private async loadFromStore(): Promise<L2Tips> {
29
- const [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([
30
- this.blockStore.getLatestBlockNumber(),
29
+ const [
30
+ latestBlockNumber,
31
+ provenBlockNumber,
32
+ proposedCheckpointBlockNumber,
33
+ checkpointedBlockNumber,
34
+ finalizedBlockNumber,
35
+ ] = await Promise.all([
36
+ this.blockStore.getLatestL2BlockNumber(),
31
37
  this.blockStore.getProvenBlockNumber(),
38
+ this.blockStore.getProposedCheckpointL2BlockNumber(),
32
39
  this.blockStore.getCheckpointedL2BlockNumber(),
33
40
  this.blockStore.getFinalizedL2BlockNumber(),
34
41
  ]);
@@ -42,19 +49,34 @@ export class L2TipsCache {
42
49
  const getBlockData = (blockNumber: BlockNumber) =>
43
50
  blockNumber > beforeInitialBlockNumber ? this.blockStore.getBlockData(blockNumber) : genesisBlockHeader;
44
51
 
45
- const [latestBlockData, provenBlockData, checkpointedBlockData, finalizedBlockData] = await Promise.all(
46
- [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber].map(getBlockData),
47
- );
52
+ const [latestBlockData, provenBlockData, proposedCheckpointBlockData, checkpointedBlockData, finalizedBlockData] =
53
+ await Promise.all(
54
+ [
55
+ latestBlockNumber,
56
+ provenBlockNumber,
57
+ proposedCheckpointBlockNumber,
58
+ checkpointedBlockNumber,
59
+ finalizedBlockNumber,
60
+ ].map(getBlockData),
61
+ );
48
62
 
49
- if (!latestBlockData || !provenBlockData || !finalizedBlockData || !checkpointedBlockData) {
63
+ if (
64
+ !latestBlockData ||
65
+ !provenBlockData ||
66
+ !finalizedBlockData ||
67
+ !checkpointedBlockData ||
68
+ !proposedCheckpointBlockData
69
+ ) {
50
70
  throw new Error('Failed to load block data for L2 tips');
51
71
  }
52
72
 
53
- const [provenCheckpointId, finalizedCheckpointId, checkpointedCheckpointId] = await Promise.all([
54
- this.getCheckpointIdForBlock(provenBlockData),
55
- this.getCheckpointIdForBlock(finalizedBlockData),
56
- this.getCheckpointIdForBlock(checkpointedBlockData),
57
- ]);
73
+ const [provenCheckpointId, finalizedCheckpointId, proposedCheckpointId, checkpointedCheckpointId] =
74
+ await Promise.all([
75
+ this.getCheckpointIdForBlock(provenBlockData),
76
+ this.getCheckpointIdForBlock(finalizedBlockData),
77
+ this.getCheckpointIdForProposedCheckpoint(checkpointedBlockData),
78
+ this.getCheckpointIdForBlock(checkpointedBlockData),
79
+ ]);
58
80
 
59
81
  return {
60
82
  proposed: { number: latestBlockNumber, hash: latestBlockData.blockHash.toString() },
@@ -62,6 +84,10 @@ export class L2TipsCache {
62
84
  block: { number: provenBlockNumber, hash: provenBlockData.blockHash.toString() },
63
85
  checkpoint: provenCheckpointId,
64
86
  },
87
+ proposedCheckpoint: {
88
+ block: { number: proposedCheckpointBlockNumber, hash: proposedCheckpointBlockData.blockHash.toString() },
89
+ checkpoint: proposedCheckpointId,
90
+ },
65
91
  finalized: {
66
92
  block: { number: finalizedBlockNumber, hash: finalizedBlockData.blockHash.toString() },
67
93
  checkpoint: finalizedCheckpointId,
@@ -73,6 +99,19 @@ export class L2TipsCache {
73
99
  };
74
100
  }
75
101
 
102
+ private async getCheckpointIdForProposedCheckpoint(
103
+ checkpointedBlockData: Pick<BlockData, 'checkpointNumber'>,
104
+ ): Promise<CheckpointId> {
105
+ const checkpointData = await this.blockStore.getProposedCheckpointOnly();
106
+ if (!checkpointData) {
107
+ return this.getCheckpointIdForBlock(checkpointedBlockData);
108
+ }
109
+ return {
110
+ number: checkpointData.checkpointNumber,
111
+ hash: checkpointData.header.hash().toString(),
112
+ };
113
+ }
114
+
76
115
  private async getCheckpointIdForBlock(blockData: Pick<BlockData, 'checkpointNumber'>): Promise<CheckpointId> {
77
116
  const checkpointData = await this.blockStore.getCheckpointData(blockData.checkpointNumber);
78
117
  if (!checkpointData) {
@@ -161,15 +161,6 @@ export class MessageStore {
161
161
  lastMessage = message;
162
162
  }
163
163
 
164
- // Update the L1 sync point to that of the last message added.
165
- const currentSyncPoint = await this.getSynchedL1Block();
166
- if (!currentSyncPoint || currentSyncPoint.l1BlockNumber < lastMessage!.l1BlockNumber) {
167
- await this.setSynchedL1Block({
168
- l1BlockNumber: lastMessage!.l1BlockNumber,
169
- l1BlockHash: lastMessage!.l1BlockHash,
170
- });
171
- }
172
-
173
164
  // Update total message count with the number of inserted messages.
174
165
  await this.increaseTotalMessageCount(messageCount);
175
166
  });
@@ -194,9 +185,16 @@ export class MessageStore {
194
185
  return this.#inboxTreeInProgress.getAsync();
195
186
  }
196
187
 
197
- /** Persists the inbox tree-in-progress checkpoint number from L1 state. */
198
- public async setInboxTreeInProgress(value: bigint): Promise<void> {
199
- await this.#inboxTreeInProgress.set(value);
188
+ /** Atomically updates the message sync state: the L1 sync point and the inbox tree-in-progress marker. */
189
+ public setMessageSyncState(l1Block: L1BlockId, treeInProgress: bigint | undefined): Promise<void> {
190
+ return this.db.transactionAsync(async () => {
191
+ await this.setSynchedL1Block(l1Block);
192
+ if (treeInProgress !== undefined) {
193
+ await this.#inboxTreeInProgress.set(treeInProgress);
194
+ } else {
195
+ await this.#inboxTreeInProgress.delete();
196
+ }
197
+ });
200
198
  }
201
199
 
202
200
  public async getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise<Fr[]> {
@@ -8,7 +8,7 @@ export type InboxMessage = {
8
8
  index: bigint;
9
9
  leaf: Fr;
10
10
  checkpointNumber: CheckpointNumber;
11
- l1BlockNumber: bigint; // L1 block number - NOT Aztec L2
11
+ l1BlockNumber: bigint;
12
12
  l1BlockHash: Buffer32;
13
13
  rollingHash: Buffer16;
14
14
  };