@aztec/archiver 0.0.1-commit.181e2d196 → 0.0.1-commit.1a421b1a1

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 (50) hide show
  1. package/dest/archiver.d.ts +3 -3
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +43 -18
  4. package/dest/errors.d.ts +7 -9
  5. package/dest/errors.d.ts.map +1 -1
  6. package/dest/errors.js +9 -14
  7. package/dest/factory.d.ts +3 -4
  8. package/dest/factory.d.ts.map +1 -1
  9. package/dest/factory.js +8 -8
  10. package/dest/modules/data_source_base.d.ts +3 -3
  11. package/dest/modules/data_source_base.d.ts.map +1 -1
  12. package/dest/modules/data_store_updater.d.ts +15 -7
  13. package/dest/modules/data_store_updater.d.ts.map +1 -1
  14. package/dest/modules/data_store_updater.js +31 -12
  15. package/dest/modules/l1_synchronizer.d.ts +2 -1
  16. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  17. package/dest/modules/l1_synchronizer.js +28 -2
  18. package/dest/store/block_store.d.ts +10 -12
  19. package/dest/store/block_store.d.ts.map +1 -1
  20. package/dest/store/block_store.js +54 -57
  21. package/dest/store/kv_archiver_store.d.ts +16 -8
  22. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  23. package/dest/store/kv_archiver_store.js +19 -7
  24. package/dest/store/message_store.js +1 -1
  25. package/dest/test/fake_l1_state.d.ts +8 -1
  26. package/dest/test/fake_l1_state.d.ts.map +1 -1
  27. package/dest/test/fake_l1_state.js +28 -2
  28. package/dest/test/mock_l2_block_source.d.ts +4 -3
  29. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  30. package/dest/test/mock_l2_block_source.js +5 -2
  31. package/dest/test/mock_structs.d.ts +4 -1
  32. package/dest/test/mock_structs.d.ts.map +1 -1
  33. package/dest/test/mock_structs.js +13 -1
  34. package/dest/test/noop_l1_archiver.d.ts +4 -1
  35. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  36. package/dest/test/noop_l1_archiver.js +5 -1
  37. package/package.json +13 -13
  38. package/src/archiver.ts +46 -19
  39. package/src/errors.ts +10 -24
  40. package/src/factory.ts +6 -5
  41. package/src/modules/data_source_base.ts +2 -2
  42. package/src/modules/data_store_updater.ts +29 -13
  43. package/src/modules/l1_synchronizer.ts +32 -3
  44. package/src/store/block_store.ts +61 -67
  45. package/src/store/kv_archiver_store.ts +22 -8
  46. package/src/store/message_store.ts +1 -1
  47. package/src/test/fake_l1_state.ts +35 -4
  48. package/src/test/mock_l2_block_source.ts +8 -2
  49. package/src/test/mock_structs.ts +20 -6
  50. package/src/test/noop_l1_archiver.ts +7 -1
package/src/factory.ts CHANGED
@@ -7,14 +7,13 @@ import { Buffer32 } from '@aztec/foundation/buffer';
7
7
  import { merge } from '@aztec/foundation/collection';
8
8
  import { Fr } from '@aztec/foundation/curves/bn254';
9
9
  import { DateProvider } from '@aztec/foundation/timer';
10
- import type { DataStoreConfig } from '@aztec/kv-store/config';
11
10
  import { createStore } from '@aztec/kv-store/lmdb-v2';
12
11
  import { protocolContractNames } from '@aztec/protocol-contracts';
13
12
  import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle';
14
13
  import { FunctionType, decodeFunctionSignature } from '@aztec/stdlib/abi';
15
14
  import type { ArchiverEmitter } from '@aztec/stdlib/block';
16
15
  import { type ContractClassPublic, computePublicBytecodeCommitment } from '@aztec/stdlib/contract';
17
- import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
16
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
18
17
  import { getTelemetryClient } from '@aztec/telemetry-client';
19
18
 
20
19
  import { EventEmitter } from 'events';
@@ -32,14 +31,13 @@ export const ARCHIVER_STORE_NAME = 'archiver';
32
31
  /** Creates an archiver store. */
33
32
  export async function createArchiverStore(
34
33
  userConfig: Pick<ArchiverConfig, 'archiverStoreMapSizeKb' | 'maxLogs'> & DataStoreConfig,
35
- l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
36
34
  ) {
37
35
  const config = {
38
36
  ...userConfig,
39
37
  dataStoreMapSizeKb: userConfig.archiverStoreMapSizeKb ?? userConfig.dataStoreMapSizeKb,
40
38
  };
41
39
  const store = await createStore(ARCHIVER_STORE_NAME, ARCHIVER_DB_VERSION, config);
42
- return new KVArchiverDataStore(store, config.maxLogs, l1Constants);
40
+ return new KVArchiverDataStore(store, config.maxLogs);
43
41
  }
44
42
 
45
43
  /**
@@ -54,7 +52,7 @@ export async function createArchiver(
54
52
  deps: ArchiverDeps,
55
53
  opts: { blockUntilSync: boolean } = { blockUntilSync: true },
56
54
  ): Promise<Archiver> {
57
- const archiverStore = await createArchiverStore(config, { epochDuration: config.aztecEpochDuration });
55
+ const archiverStore = await createArchiverStore(config);
58
56
  await registerProtocolContracts(archiverStore);
59
57
 
60
58
  // Create Ethereum clients
@@ -85,6 +83,7 @@ export async function createArchiver(
85
83
  genesisArchiveRoot,
86
84
  slashingProposerAddress,
87
85
  targetCommitteeSize,
86
+ rollupManaLimit,
88
87
  ] = await Promise.all([
89
88
  rollup.getL1StartBlock(),
90
89
  rollup.getL1GenesisTime(),
@@ -92,6 +91,7 @@ export async function createArchiver(
92
91
  rollup.getGenesisArchiveTreeRoot(),
93
92
  rollup.getSlashingProposerAddress(),
94
93
  rollup.getTargetCommitteeSize(),
94
+ rollup.getManaLimit(),
95
95
  ] as const);
96
96
 
97
97
  const l1StartBlockHash = await publicClient
@@ -110,6 +110,7 @@ export async function createArchiver(
110
110
  proofSubmissionEpochs: Number(proofSubmissionEpochs),
111
111
  targetCommitteeSize,
112
112
  genesisArchiveRoot: Fr.fromString(genesisArchiveRoot.toString()),
113
+ rollupManaLimit: Number(rollupManaLimit),
113
114
  };
114
115
 
115
116
  const archiverConfig = merge(
@@ -46,9 +46,9 @@ export abstract class ArchiverDataSourceBase
46
46
 
47
47
  abstract getL2Tips(): Promise<L2Tips>;
48
48
 
49
- abstract getL2SlotNumber(): Promise<SlotNumber | undefined>;
49
+ abstract getSyncedL2SlotNumber(): Promise<SlotNumber | undefined>;
50
50
 
51
- abstract getL2EpochNumber(): Promise<EpochNumber | undefined>;
51
+ abstract getSyncedL2EpochNumber(): Promise<EpochNumber | undefined>;
52
52
 
53
53
  abstract isEpochComplete(epochNumber: EpochNumber): Promise<boolean>;
54
54
 
@@ -11,7 +11,7 @@ import {
11
11
  ContractInstanceUpdatedEvent,
12
12
  } from '@aztec/protocol-contracts/instance-registry';
13
13
  import type { L2Block, ValidateCheckpointResult } from '@aztec/stdlib/block';
14
- import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
14
+ import { type PublishedCheckpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
15
15
  import {
16
16
  type ExecutablePrivateFunctionWithMembershipProof,
17
17
  type UtilityFunctionWithMembershipProof,
@@ -48,32 +48,33 @@ export class ArchiverDataStoreUpdater {
48
48
  constructor(
49
49
  private store: KVArchiverDataStore,
50
50
  private l2TipsCache?: L2TipsCache,
51
+ private opts: { rollupManaLimit?: number } = {},
51
52
  ) {}
52
53
 
53
54
  /**
54
- * Adds proposed blocks to the store with contract class/instance extraction from logs.
55
- * These are uncheckpointed blocks that have been proposed by the sequencer but not yet included in a checkpoint on L1.
55
+ * Adds a proposed block to the store with contract class/instance extraction from logs.
56
+ * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
56
57
  * Extracts ContractClassPublished, ContractInstancePublished, ContractInstanceUpdated events,
57
58
  * and individually broadcasted functions from the block logs.
58
59
  *
59
- * @param blocks - The proposed L2 blocks to add.
60
+ * @param block - The proposed L2 block to add.
60
61
  * @param pendingChainValidationStatus - Optional validation status to set.
61
62
  * @returns True if the operation is successful.
62
63
  */
63
- public async addProposedBlocks(
64
- blocks: L2Block[],
64
+ public async addProposedBlock(
65
+ block: L2Block,
65
66
  pendingChainValidationStatus?: ValidateCheckpointResult,
66
67
  ): Promise<boolean> {
67
68
  const result = await this.store.transactionAsync(async () => {
68
- await this.store.addProposedBlocks(blocks);
69
+ await this.store.addProposedBlock(block);
69
70
 
70
71
  const opResults = await Promise.all([
71
72
  // Update the pending chain validation status if provided
72
73
  pendingChainValidationStatus && this.store.setPendingChainValidationStatus(pendingChainValidationStatus),
73
- // Add any logs emitted during the retrieved blocks
74
- this.store.addLogs(blocks),
75
- // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
76
- ...blocks.map(block => this.addContractDataToDb(block)),
74
+ // Add any logs emitted during the retrieved block
75
+ this.store.addLogs([block]),
76
+ // Unroll all logs emitted during the retrieved block and extract any contract classes and instances from it
77
+ this.addContractDataToDb(block),
77
78
  ]);
78
79
 
79
80
  await this.l2TipsCache?.refresh();
@@ -97,13 +98,17 @@ export class ArchiverDataStoreUpdater {
97
98
  checkpoints: PublishedCheckpoint[],
98
99
  pendingChainValidationStatus?: ValidateCheckpointResult,
99
100
  ): Promise<ReconcileCheckpointsResult> {
101
+ for (const checkpoint of checkpoints) {
102
+ validateCheckpoint(checkpoint.checkpoint, { rollupManaLimit: this.opts?.rollupManaLimit });
103
+ }
104
+
100
105
  const result = await this.store.transactionAsync(async () => {
101
106
  // Before adding checkpoints, check for conflicts with local blocks if any
102
107
  const { prunedBlocks, lastAlreadyInsertedBlockNumber } = await this.pruneMismatchingLocalBlocks(checkpoints);
103
108
 
104
109
  await this.store.addCheckpoints(checkpoints);
105
110
 
106
- // Filter out blocks that were already inserted via addProposedBlocks() to avoid duplicating logs/contract data
111
+ // Filter out blocks that were already inserted via addProposedBlock() to avoid duplicating logs/contract data
107
112
  const newBlocks = checkpoints
108
113
  .flatMap((ch: PublishedCheckpoint) => ch.checkpoint.blocks)
109
114
  .filter(b => lastAlreadyInsertedBlockNumber === undefined || b.number > lastAlreadyInsertedBlockNumber);
@@ -173,7 +178,7 @@ export class ArchiverDataStoreUpdater {
173
178
  this.log.verbose(`Block number ${blockNumber} already inserted and matches checkpoint`, blockInfos);
174
179
  lastAlreadyInsertedBlockNumber = blockNumber;
175
180
  } else {
176
- this.log.warn(`Conflict detected at block ${blockNumber} between checkpointed and local block`, blockInfos);
181
+ this.log.info(`Conflict detected at block ${blockNumber} between checkpointed and local block`, blockInfos);
177
182
  const prunedBlocks = await this.removeBlocksAfter(BlockNumber(blockNumber - 1));
178
183
  return { prunedBlocks, lastAlreadyInsertedBlockNumber };
179
184
  }
@@ -276,6 +281,17 @@ export class ArchiverDataStoreUpdater {
276
281
  });
277
282
  }
278
283
 
284
+ /**
285
+ * Updates the finalized checkpoint number and refreshes the L2 tips cache.
286
+ * @param checkpointNumber - The checkpoint number to set as finalized.
287
+ */
288
+ public async setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber): Promise<void> {
289
+ await this.store.transactionAsync(async () => {
290
+ await this.store.setFinalizedCheckpointNumber(checkpointNumber);
291
+ await this.l2TipsCache?.refresh();
292
+ });
293
+ }
294
+
279
295
  /** Extracts and stores contract data from a single block. */
280
296
  private addContractDataToDb(block: L2Block): Promise<boolean> {
281
297
  return this.updateContractDataOnDb(block, Operation.Store);
@@ -69,13 +69,18 @@ export class ArchiverL1Synchronizer implements Traceable {
69
69
  private readonly epochCache: EpochCache,
70
70
  private readonly dateProvider: DateProvider,
71
71
  private readonly instrumentation: ArchiverInstrumentation,
72
- private readonly l1Constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr },
72
+ private readonly l1Constants: L1RollupConstants & {
73
+ l1StartBlockHash: Buffer32;
74
+ genesisArchiveRoot: Fr;
75
+ },
73
76
  private readonly events: ArchiverEmitter,
74
77
  tracer: Tracer,
75
78
  l2TipsCache?: L2TipsCache,
76
79
  private readonly log: Logger = createLogger('archiver:l1-sync'),
77
80
  ) {
78
- this.updater = new ArchiverDataStoreUpdater(this.store, l2TipsCache);
81
+ this.updater = new ArchiverDataStoreUpdater(this.store, l2TipsCache, {
82
+ rollupManaLimit: l1Constants.rollupManaLimit,
83
+ });
79
84
  this.tracer = tracer;
80
85
  }
81
86
 
@@ -211,6 +216,9 @@ export class ArchiverL1Synchronizer implements Traceable {
211
216
  this.instrumentation.updateL1BlockHeight(currentL1BlockNumber);
212
217
  }
213
218
 
219
+ // Update the finalized L2 checkpoint based on L1 finality.
220
+ await this.updateFinalizedCheckpoint();
221
+
214
222
  // After syncing has completed, update the current l1 block number and timestamp,
215
223
  // otherwise we risk announcing to the world that we've synced to a given point,
216
224
  // but the corresponding blocks have not been processed (see #12631).
@@ -226,6 +234,27 @@ export class ArchiverL1Synchronizer implements Traceable {
226
234
  });
227
235
  }
228
236
 
237
+ /** Query L1 for its finalized block and update the finalized checkpoint accordingly. */
238
+ private async updateFinalizedCheckpoint(): Promise<void> {
239
+ try {
240
+ const finalizedL1Block = await this.publicClient.getBlock({ blockTag: 'finalized', includeTransactions: false });
241
+ const finalizedL1BlockNumber = finalizedL1Block.number;
242
+ const finalizedCheckpointNumber = await this.rollup.getProvenCheckpointNumber({
243
+ blockNumber: finalizedL1BlockNumber,
244
+ });
245
+ const localFinalizedCheckpointNumber = await this.store.getFinalizedCheckpointNumber();
246
+ if (localFinalizedCheckpointNumber !== finalizedCheckpointNumber) {
247
+ await this.updater.setFinalizedCheckpointNumber(finalizedCheckpointNumber);
248
+ this.log.info(`Updated finalized chain to checkpoint ${finalizedCheckpointNumber}`, {
249
+ finalizedCheckpointNumber,
250
+ finalizedL1BlockNumber,
251
+ });
252
+ }
253
+ } catch (err) {
254
+ this.log.warn(`Failed to update finalized checkpoint: ${err}`);
255
+ }
256
+ }
257
+
229
258
  /** Prune all proposed local blocks that should have been checkpointed by now. */
230
259
  private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) {
231
260
  const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
@@ -822,7 +851,7 @@ export class ArchiverL1Synchronizer implements Traceable {
822
851
  const prunedCheckpointNumber = result.prunedBlocks[0].checkpointNumber;
823
852
  const prunedSlotNumber = result.prunedBlocks[0].header.globalVariables.slotNumber;
824
853
 
825
- this.log.warn(
854
+ this.log.info(
826
855
  `Pruned ${result.prunedBlocks.length} mismatching blocks for checkpoint ${prunedCheckpointNumber}`,
827
856
  { prunedBlocks: result.prunedBlocks.map(b => b.toBlockInfo()), prunedSlotNumber, prunedCheckpointNumber },
828
857
  );
@@ -20,7 +20,6 @@ import {
20
20
  serializeValidateCheckpointResult,
21
21
  } from '@aztec/stdlib/block';
22
22
  import { type CheckpointData, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
23
- import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
24
23
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
25
24
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
26
25
  import {
@@ -35,15 +34,14 @@ import {
35
34
  } from '@aztec/stdlib/tx';
36
35
 
37
36
  import {
37
+ BlockAlreadyCheckpointedError,
38
38
  BlockArchiveNotConsistentError,
39
39
  BlockIndexNotSequentialError,
40
40
  BlockNotFoundError,
41
41
  BlockNumberNotSequentialError,
42
42
  CannotOverwriteCheckpointedBlockError,
43
43
  CheckpointNotFoundError,
44
- CheckpointNumberNotConsistentError,
45
44
  CheckpointNumberNotSequentialError,
46
- InitialBlockNumberNotSequentialError,
47
45
  InitialCheckpointNumberNotSequentialError,
48
46
  } from '../errors.js';
49
47
 
@@ -97,6 +95,9 @@ export class BlockStore {
97
95
  /** Stores last proven checkpoint */
98
96
  #lastProvenCheckpoint: AztecAsyncSingleton<number>;
99
97
 
98
+ /** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
99
+ #lastFinalizedCheckpoint: AztecAsyncSingleton<number>;
100
+
100
101
  /** Stores the pending chain validation status */
101
102
  #pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;
102
103
 
@@ -111,10 +112,7 @@ export class BlockStore {
111
112
 
112
113
  #log = createLogger('archiver:block_store');
113
114
 
114
- constructor(
115
- private db: AztecAsyncKVStore,
116
- private l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
117
- ) {
115
+ constructor(private db: AztecAsyncKVStore) {
118
116
  this.#blocks = db.openMap('archiver_blocks');
119
117
  this.#blockTxs = db.openMap('archiver_block_txs');
120
118
  this.#txEffects = db.openMap('archiver_tx_effects');
@@ -123,41 +121,42 @@ export class BlockStore {
123
121
  this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
124
122
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
125
123
  this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint');
124
+ this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint');
126
125
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
127
126
  this.#checkpoints = db.openMap('archiver_checkpoints');
128
127
  this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
129
128
  }
130
129
 
131
130
  /**
132
- * Computes the finalized block number based on the proven block number.
133
- * A block is considered finalized when it's 2 epochs behind the proven block.
134
- * TODO(#13569): Compute proper finalized block number based on L1 finalized block.
135
- * TODO(palla/mbps): Even the provisional computation is wrong, since it should subtract checkpoints, not blocks
131
+ * Returns the finalized L2 block number. An L2 block is finalized when it was proven
132
+ * in an L1 block that has itself been finalized on Ethereum.
136
133
  * @returns The finalized block number.
137
134
  */
138
135
  async getFinalizedL2BlockNumber(): Promise<BlockNumber> {
139
- const provenBlockNumber = await this.getProvenBlockNumber();
140
- return BlockNumber(Math.max(provenBlockNumber - this.l1Constants.epochDuration * 2, 0));
136
+ const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber();
137
+ if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
138
+ return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
139
+ }
140
+ const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber);
141
+ if (!checkpointStorage) {
142
+ throw new CheckpointNotFoundError(finalizedCheckpointNumber);
143
+ }
144
+ return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
141
145
  }
142
146
 
143
147
  /**
144
- * Append new proposed blocks to the store's list. All blocks must be for the 'current' checkpoint.
145
- * These are uncheckpointed blocks that have been proposed by the sequencer but not yet included in a checkpoint on L1.
148
+ * Append a new proposed block to the store.
149
+ * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
146
150
  * For checkpointed blocks (already published to L1), use addCheckpoints() instead.
147
- * @param blocks - The proposed L2 blocks to be added to the store.
151
+ * @param block - The proposed L2 block to be added to the store.
148
152
  * @returns True if the operation is successful.
149
153
  */
150
- async addProposedBlocks(blocks: L2Block[], opts: { force?: boolean } = {}): Promise<boolean> {
151
- if (blocks.length === 0) {
152
- return true;
153
- }
154
-
154
+ async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
155
155
  return await this.db.transactionAsync(async () => {
156
- // Check that the block immediately before the first block to be added is present in the store.
157
- const firstBlockNumber = blocks[0].number;
158
- const firstBlockCheckpointNumber = blocks[0].checkpointNumber;
159
- const firstBlockIndex = blocks[0].indexWithinCheckpoint;
160
- const firstBlockLastArchive = blocks[0].header.lastArchive.root;
156
+ const blockNumber = block.number;
157
+ const blockCheckpointNumber = block.checkpointNumber;
158
+ const blockIndex = block.indexWithinCheckpoint;
159
+ const blockLastArchive = block.header.lastArchive.root;
161
160
 
162
161
  // Extract the latest block and checkpoint numbers
163
162
  const previousBlockNumber = await this.getLatestBlockNumber();
@@ -165,71 +164,52 @@ export class BlockStore {
165
164
 
166
165
  // Verify we're not overwriting checkpointed blocks
167
166
  const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
168
- if (!opts.force && firstBlockNumber <= lastCheckpointedBlockNumber) {
169
- throw new CannotOverwriteCheckpointedBlockError(firstBlockNumber, lastCheckpointedBlockNumber);
167
+ if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
168
+ // Check if the proposed block matches the already-checkpointed one
169
+ const existingBlock = await this.getBlock(BlockNumber(blockNumber));
170
+ if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
171
+ throw new BlockAlreadyCheckpointedError(blockNumber);
172
+ }
173
+ throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber);
170
174
  }
171
175
 
172
- // Check that the first block number is the expected one
173
- if (!opts.force && previousBlockNumber !== firstBlockNumber - 1) {
174
- throw new InitialBlockNumberNotSequentialError(firstBlockNumber, previousBlockNumber);
176
+ // Check that the block number is the expected one
177
+ if (!opts.force && previousBlockNumber !== blockNumber - 1) {
178
+ throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
175
179
  }
176
180
 
177
181
  // The same check as above but for checkpoints
178
- if (!opts.force && previousCheckpointNumber !== firstBlockCheckpointNumber - 1) {
179
- throw new InitialCheckpointNumberNotSequentialError(firstBlockCheckpointNumber, previousCheckpointNumber);
182
+ if (!opts.force && previousCheckpointNumber !== blockCheckpointNumber - 1) {
183
+ throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, previousCheckpointNumber);
180
184
  }
181
185
 
182
186
  // Extract the previous block if there is one and see if it is for the same checkpoint or not
183
187
  const previousBlockResult = await this.getBlock(previousBlockNumber);
184
188
 
185
- let expectedFirstblockIndex = 0;
189
+ let expectedBlockIndex = 0;
186
190
  let previousBlockIndex: number | undefined = undefined;
187
191
  if (previousBlockResult !== undefined) {
188
- if (previousBlockResult.checkpointNumber === firstBlockCheckpointNumber) {
192
+ if (previousBlockResult.checkpointNumber === blockCheckpointNumber) {
189
193
  // The previous block is for the same checkpoint, therefore our index should follow it
190
194
  previousBlockIndex = previousBlockResult.indexWithinCheckpoint;
191
- expectedFirstblockIndex = previousBlockIndex + 1;
195
+ expectedBlockIndex = previousBlockIndex + 1;
192
196
  }
193
- if (!previousBlockResult.archive.root.equals(firstBlockLastArchive)) {
197
+ if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
194
198
  throw new BlockArchiveNotConsistentError(
195
- firstBlockNumber,
199
+ blockNumber,
196
200
  previousBlockResult.number,
197
- firstBlockLastArchive,
201
+ blockLastArchive,
198
202
  previousBlockResult.archive.root,
199
203
  );
200
204
  }
201
205
  }
202
206
 
203
- // Now check that the first block has the expected index value
204
- if (!opts.force && expectedFirstblockIndex !== firstBlockIndex) {
205
- throw new BlockIndexNotSequentialError(firstBlockIndex, previousBlockIndex);
207
+ // Now check that the block has the expected index value
208
+ if (!opts.force && expectedBlockIndex !== blockIndex) {
209
+ throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex);
206
210
  }
207
211
 
208
- // Iterate over blocks array and insert them, checking that the block numbers and indexes are sequential. Also check they are for the correct checkpoint.
209
- let previousBlock: L2Block | undefined = undefined;
210
- for (const block of blocks) {
211
- if (!opts.force && previousBlock) {
212
- if (previousBlock.number + 1 !== block.number) {
213
- throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
214
- }
215
- if (previousBlock.indexWithinCheckpoint + 1 !== block.indexWithinCheckpoint) {
216
- throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
217
- }
218
- if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
219
- throw new BlockArchiveNotConsistentError(
220
- block.number,
221
- previousBlock.number,
222
- block.header.lastArchive.root,
223
- previousBlock.archive.root,
224
- );
225
- }
226
- }
227
- if (!opts.force && firstBlockCheckpointNumber !== block.checkpointNumber) {
228
- throw new CheckpointNumberNotConsistentError(block.checkpointNumber, firstBlockCheckpointNumber);
229
- }
230
- previousBlock = block;
231
- await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
232
- }
212
+ await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
233
213
 
234
214
  return true;
235
215
  });
@@ -976,6 +956,20 @@ export class BlockStore {
976
956
  return result;
977
957
  }
978
958
 
959
+ async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
960
+ const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
961
+ this.getLatestCheckpointNumber(),
962
+ this.#lastFinalizedCheckpoint.getAsync(),
963
+ ]);
964
+ return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber
965
+ ? latestCheckpointNumber
966
+ : CheckpointNumber(finalizedCheckpointNumber ?? 0);
967
+ }
968
+
969
+ setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
970
+ return this.#lastFinalizedCheckpoint.set(checkpointNumber);
971
+ }
972
+
979
973
  #computeBlockRange(start: BlockNumber, limit: number): Required<Pick<Range<number>, 'start' | 'limit'>> {
980
974
  if (limit < 1) {
981
975
  throw new Error(`Invalid limit: ${limit}`);
@@ -22,7 +22,6 @@ import type {
22
22
  ExecutablePrivateFunctionWithMembershipProof,
23
23
  UtilityFunctionWithMembershipProof,
24
24
  } from '@aztec/stdlib/contract';
25
- import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
26
25
  import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client';
27
26
  import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
28
27
  import type { BlockHeader, TxHash, TxReceipt } from '@aztec/stdlib/tx';
@@ -71,9 +70,8 @@ export class KVArchiverDataStore implements ContractDataSource {
71
70
  constructor(
72
71
  private db: AztecAsyncKVStore,
73
72
  logsMaxPageSize: number = 1000,
74
- l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
75
73
  ) {
76
- this.#blockStore = new BlockStore(db, l1Constants);
74
+ this.#blockStore = new BlockStore(db);
77
75
  this.#logStore = new LogStore(db, this.#blockStore, logsMaxPageSize);
78
76
  this.#messageStore = new MessageStore(db);
79
77
  this.#contractClassStore = new ContractClassStore(db);
@@ -246,14 +244,14 @@ export class KVArchiverDataStore implements ContractDataSource {
246
244
  }
247
245
 
248
246
  /**
249
- * Append new proposed blocks to the store's list.
250
- * These are uncheckpointed blocks that have been proposed by the sequencer but not yet included in a checkpoint on L1.
247
+ * Append a new proposed block to the store.
248
+ * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
251
249
  * For checkpointed blocks (already published to L1), use addCheckpoints() instead.
252
- * @param blocks - The proposed L2 blocks to be added to the store.
250
+ * @param block - The proposed L2 block to be added to the store.
253
251
  * @returns True if the operation is successful.
254
252
  */
255
- addProposedBlocks(blocks: L2Block[], opts: { force?: boolean; checkpointNumber?: number } = {}): Promise<boolean> {
256
- return this.#blockStore.addProposedBlocks(blocks, opts);
253
+ addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
254
+ return this.#blockStore.addProposedBlock(block, opts);
257
255
  }
258
256
 
259
257
  /**
@@ -542,6 +540,22 @@ export class KVArchiverDataStore implements ContractDataSource {
542
540
  await this.#blockStore.setProvenCheckpointNumber(checkpointNumber);
543
541
  }
544
542
 
543
+ /**
544
+ * Gets the number of the latest finalized checkpoint processed.
545
+ * @returns The number of the latest finalized checkpoint processed.
546
+ */
547
+ getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
548
+ return this.#blockStore.getFinalizedCheckpointNumber();
549
+ }
550
+
551
+ /**
552
+ * Stores the number of the latest finalized checkpoint processed.
553
+ * @param checkpointNumber - The number of the latest finalized checkpoint processed.
554
+ */
555
+ async setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
556
+ await this.#blockStore.setFinalizedCheckpointNumber(checkpointNumber);
557
+ }
558
+
545
559
  async setBlockSynchedL1BlockNumber(l1BlockNumber: bigint) {
546
560
  await this.#blockStore.setSynchedL1BlockNumber(l1BlockNumber);
547
561
  }
@@ -137,7 +137,7 @@ export class MessageStore {
137
137
  );
138
138
  }
139
139
 
140
- // Check the first message in a block has the correct index.
140
+ // Check the first message in a checkpoint has the correct index.
141
141
  if (
142
142
  (!lastMessage || message.checkpointNumber > lastMessage.checkpointNumber) &&
143
143
  message.index !== expectedStart
@@ -150,8 +150,12 @@ export class FakeL1State {
150
150
  // Computed from checkpoints based on L1 block visibility
151
151
  private pendingCheckpointNumber: CheckpointNumber = CheckpointNumber(0);
152
152
 
153
+ // The L1 block number reported as "finalized" (defaults to the start block)
154
+ private finalizedL1BlockNumber: bigint;
155
+
153
156
  constructor(private readonly config: FakeL1StateConfig) {
154
157
  this.l1BlockNumber = config.l1StartBlock;
158
+ this.finalizedL1BlockNumber = config.l1StartBlock;
155
159
  this.lastArchive = new AppendOnlyTreeSnapshot(config.genesisArchiveRoot, 1);
156
160
  }
157
161
 
@@ -283,11 +287,30 @@ export class FakeL1State {
283
287
  this.updatePendingCheckpointNumber();
284
288
  }
285
289
 
290
+ /** Sets the L1 block number that will be reported as "finalized". */
291
+ setFinalizedL1BlockNumber(blockNumber: bigint): void {
292
+ this.finalizedL1BlockNumber = blockNumber;
293
+ }
294
+
286
295
  /** Marks a checkpoint as proven. Updates provenCheckpointNumber. */
287
296
  markCheckpointAsProven(checkpointNumber: CheckpointNumber): void {
288
297
  this.provenCheckpointNumber = checkpointNumber;
289
298
  }
290
299
 
300
+ /**
301
+ * Simulates what `rollup.getProvenCheckpointNumber({ blockNumber: atL1Block })` would return.
302
+ */
303
+ getProvenCheckpointNumberAtL1Block(atL1Block: bigint): CheckpointNumber {
304
+ if (this.provenCheckpointNumber === 0) {
305
+ return CheckpointNumber(0);
306
+ }
307
+ const checkpoint = this.checkpoints.find(cp => cp.checkpointNumber === this.provenCheckpointNumber);
308
+ if (checkpoint && checkpoint.l1BlockNumber <= atL1Block) {
309
+ return this.provenCheckpointNumber;
310
+ }
311
+ return CheckpointNumber(0);
312
+ }
313
+
291
314
  /** Sets the target committee size for attestation validation. */
292
315
  setTargetCommitteeSize(size: number): void {
293
316
  this.targetCommitteeSize = size;
@@ -406,6 +429,11 @@ export class FakeL1State {
406
429
  });
407
430
  });
408
431
 
432
+ mockRollup.getProvenCheckpointNumber.mockImplementation((options?: { blockNumber?: bigint }) => {
433
+ const atBlock = options?.blockNumber ?? this.l1BlockNumber;
434
+ return Promise.resolve(this.getProvenCheckpointNumberAtL1Block(atBlock));
435
+ });
436
+
409
437
  mockRollup.canPruneAtTime.mockImplementation(() => Promise.resolve(this.canPruneResult));
410
438
 
411
439
  // Mock the wrapper method for fetching checkpoint events
@@ -449,10 +477,13 @@ export class FakeL1State {
449
477
  publicClient.getChainId.mockResolvedValue(1);
450
478
  publicClient.getBlockNumber.mockImplementation(() => Promise.resolve(this.l1BlockNumber));
451
479
 
452
- // Use async function pattern that existing test uses for getBlock
453
-
454
- publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint } = {}) => {
455
- const blockNum = args.blockNumber ?? (await publicClient.getBlockNumber());
480
+ publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint; blockTag?: string } = {}) => {
481
+ let blockNum: bigint;
482
+ if (args.blockTag === 'finalized') {
483
+ blockNum = this.finalizedL1BlockNumber;
484
+ } else {
485
+ blockNum = args.blockNumber ?? (await publicClient.getBlockNumber());
486
+ }
456
487
  return {
457
488
  number: blockNum,
458
489
  timestamp: BigInt(blockNum) * BigInt(this.config.ethereumSlotDuration) + this.config.l1GenesisTime,
@@ -42,6 +42,12 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource {
42
42
  await this.createCheckpoints(numBlocks, 1);
43
43
  }
44
44
 
45
+ public getCheckpointNumber(): Promise<CheckpointNumber> {
46
+ return Promise.resolve(
47
+ this.checkpointList.length === 0 ? CheckpointNumber.ZERO : CheckpointNumber(this.checkpointList.length),
48
+ );
49
+ }
50
+
45
51
  /** Creates checkpoints, each containing `blocksPerCheckpoint` blocks. */
46
52
  public async createCheckpoints(numCheckpoints: number, blocksPerCheckpoint: number = 1) {
47
53
  for (let c = 0; c < numCheckpoints; c++) {
@@ -441,11 +447,11 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource {
441
447
  };
442
448
  }
443
449
 
444
- getL2EpochNumber(): Promise<EpochNumber> {
450
+ getSyncedL2EpochNumber(): Promise<EpochNumber> {
445
451
  throw new Error('Method not implemented.');
446
452
  }
447
453
 
448
- getL2SlotNumber(): Promise<SlotNumber> {
454
+ getSyncedL2SlotNumber(): Promise<SlotNumber> {
449
455
  throw new Error('Method not implemented.');
450
456
  }
451
457