@aztec/archiver 0.0.1-commit.9ef841308 → 0.0.1-commit.a89ec08

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 (65) hide show
  1. package/dest/archiver.d.ts +2 -1
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +1 -0
  4. package/dest/config.d.ts +3 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +1 -2
  7. package/dest/errors.d.ts +1 -15
  8. package/dest/errors.d.ts.map +1 -1
  9. package/dest/errors.js +0 -18
  10. package/dest/factory.d.ts +2 -2
  11. package/dest/factory.d.ts.map +1 -1
  12. package/dest/factory.js +3 -1
  13. package/dest/l1/calldata_retriever.d.ts +1 -1
  14. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  15. package/dest/l1/calldata_retriever.js +1 -2
  16. package/dest/l1/data_retrieval.d.ts +3 -6
  17. package/dest/l1/data_retrieval.d.ts.map +1 -1
  18. package/dest/l1/data_retrieval.js +6 -12
  19. package/dest/modules/data_source_base.d.ts +3 -3
  20. package/dest/modules/data_source_base.d.ts.map +1 -1
  21. package/dest/modules/data_source_base.js +4 -4
  22. package/dest/modules/data_store_updater.d.ts +6 -3
  23. package/dest/modules/data_store_updater.d.ts.map +1 -1
  24. package/dest/modules/data_store_updater.js +56 -5
  25. package/dest/modules/instrumentation.d.ts +1 -12
  26. package/dest/modules/instrumentation.d.ts.map +1 -1
  27. package/dest/modules/instrumentation.js +0 -10
  28. package/dest/modules/l1_synchronizer.d.ts +1 -1
  29. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  30. package/dest/modules/l1_synchronizer.js +13 -18
  31. package/dest/store/block_store.d.ts +2 -1
  32. package/dest/store/block_store.d.ts.map +1 -1
  33. package/dest/store/block_store.js +50 -5
  34. package/dest/store/contract_class_store.d.ts +3 -2
  35. package/dest/store/contract_class_store.d.ts.map +1 -1
  36. package/dest/store/contract_class_store.js +65 -1
  37. package/dest/store/kv_archiver_store.d.ts +8 -10
  38. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  39. package/dest/store/kv_archiver_store.js +8 -10
  40. package/dest/store/log_store.d.ts +3 -6
  41. package/dest/store/log_store.d.ts.map +1 -1
  42. package/dest/store/log_store.js +6 -45
  43. package/dest/store/message_store.d.ts +1 -5
  44. package/dest/store/message_store.d.ts.map +1 -1
  45. package/dest/store/message_store.js +0 -13
  46. package/dest/test/fake_l1_state.d.ts +8 -1
  47. package/dest/test/fake_l1_state.d.ts.map +1 -1
  48. package/dest/test/fake_l1_state.js +16 -11
  49. package/package.json +13 -13
  50. package/src/archiver.ts +1 -0
  51. package/src/config.ts +1 -8
  52. package/src/errors.ts +0 -30
  53. package/src/factory.ts +3 -1
  54. package/src/l1/calldata_retriever.ts +1 -2
  55. package/src/l1/data_retrieval.ts +9 -17
  56. package/src/modules/data_source_base.ts +3 -8
  57. package/src/modules/data_store_updater.ts +82 -3
  58. package/src/modules/instrumentation.ts +0 -20
  59. package/src/modules/l1_synchronizer.ts +25 -29
  60. package/src/store/block_store.ts +62 -5
  61. package/src/store/contract_class_store.ts +103 -1
  62. package/src/store/kv_archiver_store.ts +15 -12
  63. package/src/store/log_store.ts +15 -60
  64. package/src/store/message_store.ts +0 -19
  65. package/src/test/fake_l1_state.ts +20 -15
package/src/config.ts CHANGED
@@ -8,12 +8,7 @@ import {
8
8
  getConfigFromMappings,
9
9
  numberConfigHelper,
10
10
  } from '@aztec/foundation/config';
11
- import {
12
- type ChainConfig,
13
- type PipelineConfig,
14
- chainConfigMappings,
15
- pipelineConfigMappings,
16
- } from '@aztec/stdlib/config';
11
+ import { type ChainConfig, chainConfigMappings } from '@aztec/stdlib/config';
17
12
  import type { ArchiverSpecificConfig } from '@aztec/stdlib/interfaces/server';
18
13
 
19
14
  /**
@@ -26,13 +21,11 @@ import type { ArchiverSpecificConfig } from '@aztec/stdlib/interfaces/server';
26
21
  export type ArchiverConfig = ArchiverSpecificConfig &
27
22
  L1ReaderConfig &
28
23
  L1ContractsConfig &
29
- PipelineConfig & // required to pass through to epoch cache
30
24
  BlobClientConfig &
31
25
  ChainConfig;
32
26
 
33
27
  export const archiverConfigMappings: ConfigMappingsType<ArchiverConfig> = {
34
28
  ...blobClientConfigMapping,
35
- ...pipelineConfigMappings,
36
29
  archiverPollingIntervalMS: {
37
30
  env: 'ARCHIVER_POLLING_INTERVAL_MS',
38
31
  description: 'The polling interval in ms for retrieving new L2 blocks and encrypted logs.',
package/src/errors.ts CHANGED
@@ -74,36 +74,6 @@ export class BlockAlreadyCheckpointedError extends Error {
74
74
  }
75
75
  }
76
76
 
77
- /** Thrown when logs are added for a tag whose last stored log has a higher block number than the new log. */
78
- export class OutOfOrderLogInsertionError extends Error {
79
- constructor(
80
- public readonly logType: 'private' | 'public',
81
- public readonly tag: string,
82
- public readonly lastBlockNumber: number,
83
- public readonly newBlockNumber: number,
84
- ) {
85
- super(
86
- `Out-of-order ${logType} log insertion for tag ${tag}: ` +
87
- `last existing log is from block ${lastBlockNumber} but new log is from block ${newBlockNumber}`,
88
- );
89
- this.name = 'OutOfOrderLogInsertionError';
90
- }
91
- }
92
-
93
- /** Thrown when L1 to L2 messages are requested for a checkpoint whose message tree hasn't been sealed yet. */
94
- export class L1ToL2MessagesNotReadyError extends Error {
95
- constructor(
96
- public readonly checkpointNumber: number,
97
- public readonly inboxTreeInProgress: bigint,
98
- ) {
99
- super(
100
- `Cannot get L1 to L2 messages for checkpoint ${checkpointNumber}: ` +
101
- `inbox tree in progress is ${inboxTreeInProgress}, messages not yet sealed`,
102
- );
103
- this.name = 'L1ToL2MessagesNotReadyError';
104
- }
105
- }
106
-
107
77
  /** Thrown when a proposed block conflicts with an already checkpointed block (different content). */
108
78
  export class CannotOverwriteCheckpointedBlockError extends Error {
109
79
  constructor(
package/src/factory.ts CHANGED
@@ -8,13 +8,13 @@ import { Buffer32 } from '@aztec/foundation/buffer';
8
8
  import { merge } from '@aztec/foundation/collection';
9
9
  import { Fr } from '@aztec/foundation/curves/bn254';
10
10
  import { DateProvider } from '@aztec/foundation/timer';
11
+ import type { DataStoreConfig } from '@aztec/kv-store/config';
11
12
  import { createStore } from '@aztec/kv-store/lmdb-v2';
12
13
  import { protocolContractNames } from '@aztec/protocol-contracts';
13
14
  import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle';
14
15
  import { FunctionType, decodeFunctionSignature } from '@aztec/stdlib/abi';
15
16
  import type { ArchiverEmitter } from '@aztec/stdlib/block';
16
17
  import { type ContractClassPublicWithCommitment, computePublicBytecodeCommitment } from '@aztec/stdlib/contract';
17
- import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
18
18
  import { getTelemetryClient } from '@aztec/telemetry-client';
19
19
 
20
20
  import { EventEmitter } from 'events';
@@ -189,6 +189,8 @@ export async function registerProtocolContracts(store: KVArchiverDataStore) {
189
189
  const contractClassPublic: ContractClassPublicWithCommitment = {
190
190
  ...contract.contractClass,
191
191
  publicBytecodeCommitment,
192
+ privateFunctions: [],
193
+ utilityFunctions: [],
192
194
  };
193
195
 
194
196
  const publicFunctionSignatures = contract.artifact.functions
@@ -1,7 +1,6 @@
1
1
  import { MULTI_CALL_3_ADDRESS, type ViemCommitteeAttestations, type ViemHeader } from '@aztec/ethereum/contracts';
2
2
  import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
3
3
  import { CheckpointNumber } from '@aztec/foundation/branded-types';
4
- import { LruSet } from '@aztec/foundation/collection';
5
4
  import { Fr } from '@aztec/foundation/curves/bn254';
6
5
  import { EthAddress } from '@aztec/foundation/eth-address';
7
6
  import type { Logger } from '@aztec/foundation/log';
@@ -45,7 +44,7 @@ type CheckpointData = {
45
44
  */
46
45
  export class CalldataRetriever {
47
46
  /** Tx hashes we've already logged for trace+debug failure (log once per tx per process). */
48
- private static readonly traceFailureWarnedTxHashes = new LruSet<string>(1000);
47
+ private static readonly traceFailureWarnedTxHashes = new Set<string>();
49
48
 
50
49
  /** Clears the trace-failure warned set. For testing only. */
51
50
  static resetTraceFailureWarnedForTesting(): void {
@@ -265,9 +265,6 @@ async function processCheckpointProposedLogs(
265
265
  checkpointNumber,
266
266
  expectedHashes,
267
267
  );
268
- const { timestamp, parentBeaconBlockRoot } = await getL1Block(publicClient, log.l1BlockNumber);
269
- const l1 = new L1PublishedData(log.l1BlockNumber, timestamp, log.l1BlockHash.toString());
270
-
271
268
  const checkpointBlobData = await getCheckpointBlobDataFromBlobs(
272
269
  blobClient,
273
270
  checkpoint.blockHash,
@@ -275,8 +272,12 @@ async function processCheckpointProposedLogs(
275
272
  checkpointNumber,
276
273
  logger,
277
274
  isHistoricalSync,
278
- parentBeaconBlockRoot,
279
- timestamp,
275
+ );
276
+
277
+ const l1 = new L1PublishedData(
278
+ log.l1BlockNumber,
279
+ await getL1BlockTime(publicClient, log.l1BlockNumber),
280
+ log.l1BlockHash.toString(),
280
281
  );
281
282
 
282
283
  retrievedCheckpoints.push({ ...checkpoint, checkpointBlobData, l1, chainId, version });
@@ -297,12 +298,9 @@ async function processCheckpointProposedLogs(
297
298
  return retrievedCheckpoints;
298
299
  }
299
300
 
300
- export async function getL1Block(
301
- publicClient: ViemPublicClient,
302
- blockNumber: bigint,
303
- ): Promise<{ timestamp: bigint; parentBeaconBlockRoot: string | undefined }> {
301
+ export async function getL1BlockTime(publicClient: ViemPublicClient, blockNumber: bigint): Promise<bigint> {
304
302
  const block = await publicClient.getBlock({ blockNumber, includeTransactions: false });
305
- return { timestamp: block.timestamp, parentBeaconBlockRoot: block.parentBeaconBlockRoot };
303
+ return block.timestamp;
306
304
  }
307
305
 
308
306
  export async function getCheckpointBlobDataFromBlobs(
@@ -312,14 +310,8 @@ export async function getCheckpointBlobDataFromBlobs(
312
310
  checkpointNumber: CheckpointNumber,
313
311
  logger: Logger,
314
312
  isHistoricalSync: boolean,
315
- parentBeaconBlockRoot?: string,
316
- l1BlockTimestamp?: bigint,
317
313
  ): Promise<CheckpointBlobData> {
318
- const blobBodies = await blobClient.getBlobSidecar(blockHash, blobHashes, {
319
- isHistoricalSync,
320
- parentBeaconBlockRoot,
321
- l1BlockTimestamp,
322
- });
314
+ const blobBodies = await blobClient.getBlobSidecar(blockHash, blobHashes, { isHistoricalSync });
323
315
  if (blobBodies.length === 0) {
324
316
  throw new NoBlobBodiesFoundError(checkpointNumber);
325
317
  }
@@ -165,21 +165,16 @@ export abstract class ArchiverDataSourceBase
165
165
  return (await this.store.getPendingChainValidationStatus()) ?? { valid: true };
166
166
  }
167
167
 
168
- public getPrivateLogsByTags(
169
- tags: SiloedTag[],
170
- page?: number,
171
- upToBlockNumber?: BlockNumber,
172
- ): Promise<TxScopedL2Log[][]> {
173
- return this.store.getPrivateLogsByTags(tags, page, upToBlockNumber);
168
+ public getPrivateLogsByTags(tags: SiloedTag[], page?: number): Promise<TxScopedL2Log[][]> {
169
+ return this.store.getPrivateLogsByTags(tags, page);
174
170
  }
175
171
 
176
172
  public getPublicLogsByTagsFromContract(
177
173
  contractAddress: AztecAddress,
178
174
  tags: Tag[],
179
175
  page?: number,
180
- upToBlockNumber?: BlockNumber,
181
176
  ): Promise<TxScopedL2Log[][]> {
182
- return this.store.getPublicLogsByTagsFromContract(contractAddress, tags, page, upToBlockNumber);
177
+ return this.store.getPublicLogsByTagsFromContract(contractAddress, tags, page);
183
178
  }
184
179
 
185
180
  public getPublicLogs(filter: LogFilter): Promise<GetPublicLogsResponse> {
@@ -1,7 +1,12 @@
1
1
  import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
2
  import { filterAsync } from '@aztec/foundation/collection';
3
+ import { Fr } from '@aztec/foundation/curves/bn254';
3
4
  import { createLogger } from '@aztec/foundation/log';
4
- import { ContractClassPublishedEvent } from '@aztec/protocol-contracts/class-registry';
5
+ import {
6
+ ContractClassPublishedEvent,
7
+ PrivateFunctionBroadcastedEvent,
8
+ UtilityFunctionBroadcastedEvent,
9
+ } from '@aztec/protocol-contracts/class-registry';
5
10
  import {
6
11
  ContractInstancePublishedEvent,
7
12
  ContractInstanceUpdatedEvent,
@@ -10,12 +15,18 @@ import type { L2Block, ValidateCheckpointResult } from '@aztec/stdlib/block';
10
15
  import { type PublishedCheckpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
11
16
  import {
12
17
  type ContractClassPublicWithCommitment,
18
+ type ExecutablePrivateFunctionWithMembershipProof,
19
+ type UtilityFunctionWithMembershipProof,
13
20
  computeContractAddressFromInstance,
14
21
  computeContractClassId,
22
+ isValidPrivateFunctionMembershipProof,
23
+ isValidUtilityFunctionMembershipProof,
15
24
  } from '@aztec/stdlib/contract';
16
25
  import type { ContractClassLog, PrivateLog, PublicLog } from '@aztec/stdlib/logs';
17
26
  import type { UInt64 } from '@aztec/stdlib/types';
18
27
 
28
+ import groupBy from 'lodash.groupby';
29
+
19
30
  import type { KVArchiverDataStore } from '../store/kv_archiver_store.js';
20
31
  import type { L2TipsCache } from '../store/l2_tips_cache.js';
21
32
 
@@ -46,7 +57,8 @@ export class ArchiverDataStoreUpdater {
46
57
  /**
47
58
  * Adds a proposed block to the store with contract class/instance extraction from logs.
48
59
  * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
49
- * Extracts ContractClassPublished, ContractInstancePublished, ContractInstanceUpdated events from the block logs.
60
+ * Extracts ContractClassPublished, ContractInstancePublished, ContractInstanceUpdated events,
61
+ * and individually broadcasted functions from the block logs.
50
62
  *
51
63
  * @param block - The proposed L2 block to add.
52
64
  * @param pendingChainValidationStatus - Optional validation status to set.
@@ -78,7 +90,8 @@ export class ArchiverDataStoreUpdater {
78
90
  * Reconciles local blocks with incoming checkpoints from L1.
79
91
  * Adds new checkpoints to the store with contract class/instance extraction from logs.
80
92
  * Prunes any local blocks that conflict with checkpoint data (by comparing archive roots).
81
- * Extracts ContractClassPublished, ContractInstancePublished, ContractInstanceUpdated events from the checkpoint block logs.
93
+ * Extracts ContractClassPublished, ContractInstancePublished, ContractInstanceUpdated events,
94
+ * and individually broadcasted functions from the checkpoint block logs.
82
95
  *
83
96
  * @param checkpoints - The published checkpoints to add.
84
97
  * @param pendingChainValidationStatus - Optional validation status to set.
@@ -303,6 +316,9 @@ export class ArchiverDataStoreUpdater {
303
316
  this.updatePublishedContractClasses(contractClassLogs, block.number, operation),
304
317
  this.updateDeployedContractInstances(privateLogs, block.number, operation),
305
318
  this.updateUpdatedContractInstances(publicLogs, block.header.globalVariables.timestamp, operation),
319
+ operation === Operation.Store
320
+ ? this.storeBroadcastedIndividualFunctions(contractClassLogs, block.number)
321
+ : Promise.resolve(true),
306
322
  ])
307
323
  ).every(Boolean);
308
324
  }
@@ -421,4 +437,67 @@ export class ArchiverDataStoreUpdater {
421
437
  }
422
438
  return true;
423
439
  }
440
+
441
+ /**
442
+ * Stores the functions that were broadcasted individually.
443
+ *
444
+ * @dev Beware that there is not a delete variant of this, since they are added to contract classes
445
+ * and will be deleted as part of the class if needed.
446
+ */
447
+ private async storeBroadcastedIndividualFunctions(
448
+ allLogs: ContractClassLog[],
449
+ _blockNum: BlockNumber,
450
+ ): Promise<boolean> {
451
+ // Filter out private and utility function broadcast events
452
+ const privateFnEvents = allLogs
453
+ .filter(log => PrivateFunctionBroadcastedEvent.isPrivateFunctionBroadcastedEvent(log))
454
+ .map(log => PrivateFunctionBroadcastedEvent.fromLog(log));
455
+ const utilityFnEvents = allLogs
456
+ .filter(log => UtilityFunctionBroadcastedEvent.isUtilityFunctionBroadcastedEvent(log))
457
+ .map(log => UtilityFunctionBroadcastedEvent.fromLog(log));
458
+
459
+ // Group all events by contract class id
460
+ for (const [classIdString, classEvents] of Object.entries(
461
+ groupBy([...privateFnEvents, ...utilityFnEvents], e => e.contractClassId.toString()),
462
+ )) {
463
+ const contractClassId = Fr.fromHexString(classIdString);
464
+ const contractClass = await this.store.getContractClass(contractClassId);
465
+ if (!contractClass) {
466
+ this.log.warn(`Skipping broadcasted functions as contract class ${contractClassId.toString()} was not found`);
467
+ continue;
468
+ }
469
+
470
+ // Split private and utility functions, and filter out invalid ones
471
+ const allFns = classEvents.map(e => e.toFunctionWithMembershipProof());
472
+ const privateFns = allFns.filter(
473
+ (fn): fn is ExecutablePrivateFunctionWithMembershipProof => 'utilityFunctionsTreeRoot' in fn,
474
+ );
475
+ const utilityFns = allFns.filter(
476
+ (fn): fn is UtilityFunctionWithMembershipProof => 'privateFunctionsArtifactTreeRoot' in fn,
477
+ );
478
+
479
+ const privateFunctionsWithValidity = await Promise.all(
480
+ privateFns.map(async fn => ({ fn, valid: await isValidPrivateFunctionMembershipProof(fn, contractClass) })),
481
+ );
482
+ const validPrivateFns = privateFunctionsWithValidity.filter(({ valid }) => valid).map(({ fn }) => fn);
483
+ const utilityFunctionsWithValidity = await Promise.all(
484
+ utilityFns.map(async fn => ({
485
+ fn,
486
+ valid: await isValidUtilityFunctionMembershipProof(fn, contractClass),
487
+ })),
488
+ );
489
+ const validUtilityFns = utilityFunctionsWithValidity.filter(({ valid }) => valid).map(({ fn }) => fn);
490
+ const validFnCount = validPrivateFns.length + validUtilityFns.length;
491
+ if (validFnCount !== allFns.length) {
492
+ this.log.warn(`Skipping ${allFns.length - validFnCount} invalid functions`);
493
+ }
494
+
495
+ // Store the functions in the contract class in a single operation
496
+ if (validFnCount > 0) {
497
+ this.log.verbose(`Storing ${validFnCount} functions for contract class ${contractClassId.toString()}`);
498
+ }
499
+ await this.store.addFunctions(contractClassId, validPrivateFns, validUtilityFns);
500
+ }
501
+ return true;
502
+ }
424
503
  }
@@ -1,9 +1,6 @@
1
- import type { SlotNumber } from '@aztec/foundation/branded-types';
2
1
  import { createLogger } from '@aztec/foundation/log';
3
2
  import type { L2Block } from '@aztec/stdlib/block';
4
3
  import type { CheckpointData } from '@aztec/stdlib/checkpoint';
5
- import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
6
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
7
4
  import {
8
5
  Attributes,
9
6
  type Gauge,
@@ -41,8 +38,6 @@ export class ArchiverInstrumentation {
41
38
 
42
39
  private blockProposalTxTargetCount: UpDownCounter;
43
40
 
44
- private checkpointL1InclusionDelay: Histogram;
45
-
46
41
  private log = createLogger('archiver:instrumentation');
47
42
 
48
43
  private constructor(
@@ -90,8 +85,6 @@ export class ArchiverInstrumentation {
90
85
  },
91
86
  );
92
87
 
93
- this.checkpointL1InclusionDelay = meter.createHistogram(Metrics.ARCHIVER_CHECKPOINT_L1_INCLUSION_DELAY);
94
-
95
88
  this.dbMetrics = new LmdbMetrics(
96
89
  meter,
97
90
  {
@@ -168,17 +161,4 @@ export class ArchiverInstrumentation {
168
161
  [Attributes.L1_BLOCK_PROPOSAL_USED_TRACE]: usedTrace,
169
162
  });
170
163
  }
171
-
172
- /**
173
- * Records L1 inclusion timing for a checkpoint observed on L1 (seconds into the L2 slot).
174
- */
175
- public processCheckpointL1Timing(data: {
176
- slotNumber: SlotNumber;
177
- l1Timestamp: bigint;
178
- l1Constants: Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>;
179
- }): void {
180
- const slotStartTs = getTimestampForSlot(data.slotNumber, data.l1Constants);
181
- const inclusionDelaySeconds = Number(data.l1Timestamp - slotStartTs);
182
- this.checkpointL1InclusionDelay.record(inclusionDelaySeconds);
183
- }
184
164
  }
@@ -3,7 +3,6 @@ import { EpochCache } from '@aztec/epoch-cache';
3
3
  import { InboxContract, RollupContract } from '@aztec/ethereum/contracts';
4
4
  import type { L1BlockId } from '@aztec/ethereum/l1-types';
5
5
  import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
6
- import { asyncPool } from '@aztec/foundation/async-pool';
7
6
  import { maxBigint } from '@aztec/foundation/bigint';
8
7
  import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
9
8
  import { Buffer32 } from '@aztec/foundation/buffer';
@@ -246,13 +245,22 @@ export class ArchiverL1Synchronizer implements Traceable {
246
245
  const localFinalizedCheckpointNumber = await this.store.getFinalizedCheckpointNumber();
247
246
  if (localFinalizedCheckpointNumber !== finalizedCheckpointNumber) {
248
247
  await this.updater.setFinalizedCheckpointNumber(finalizedCheckpointNumber);
249
- this.log.info(`Updated finalized chain to checkpoint ${finalizedCheckpointNumber}`, {
250
- finalizedCheckpointNumber,
251
- finalizedL1BlockNumber,
252
- });
248
+ const finalizedL2BlockNumber = await this.store.getFinalizedL2BlockNumber();
249
+ this.log.info(
250
+ `Updated finalized chain to checkpoint ${finalizedCheckpointNumber} (L2 block ${finalizedL2BlockNumber})`,
251
+ {
252
+ finalizedCheckpointNumber,
253
+ previousFinalizedCheckpointNumber: localFinalizedCheckpointNumber,
254
+ finalizedL2BlockNumber,
255
+ finalizedL1BlockNumber,
256
+ },
257
+ );
258
+ }
259
+ } catch (err: any) {
260
+ // The rollup contract may not exist at the finalized L1 block right after deployment.
261
+ if (!err?.message?.includes('returned no data')) {
262
+ this.log.warn(`Failed to update finalized checkpoint: ${err}`);
253
263
  }
254
- } catch (err) {
255
- this.log.warn(`Failed to update finalized checkpoint: ${err}`);
256
264
  }
257
265
  }
258
266
 
@@ -334,20 +342,17 @@ export class ArchiverL1Synchronizer implements Traceable {
334
342
 
335
343
  const checkpointsToUnwind = localPendingCheckpointNumber - provenCheckpointNumber;
336
344
 
337
- // Fetch checkpoints and blocks in bounded batches to avoid unbounded concurrent
338
- // promises when the gap between local pending and proven checkpoint numbers is large.
339
- const BATCH_SIZE = 10;
340
- const indices = Array.from({ length: checkpointsToUnwind }, (_, i) => CheckpointNumber(i + pruneFrom));
341
- const checkpoints = (await asyncPool(BATCH_SIZE, indices, idx => this.store.getCheckpointData(idx))).filter(
342
- isDefined,
345
+ const checkpointPromises = Array.from({ length: checkpointsToUnwind })
346
+ .fill(0)
347
+ .map((_, i) => this.store.getCheckpointData(CheckpointNumber(i + pruneFrom)));
348
+ const checkpoints = await Promise.all(checkpointPromises);
349
+
350
+ const blockPromises = await Promise.all(
351
+ checkpoints
352
+ .filter(isDefined)
353
+ .map(cp => this.store.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber))),
343
354
  );
344
- const newBlocks = (
345
- await asyncPool(BATCH_SIZE, checkpoints, cp =>
346
- this.store.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)),
347
- )
348
- )
349
- .filter(isDefined)
350
- .flat();
355
+ const newBlocks = blockPromises.filter(isDefined).flat();
351
356
 
352
357
  // Emit an event for listening services to react to the chain prune
353
358
  this.events.emit(L2BlockSourceEvents.L2PruneUnproven, {
@@ -395,7 +400,6 @@ export class ArchiverL1Synchronizer implements Traceable {
395
400
  const localMessagesInserted = await this.store.getTotalL1ToL2MessageCount();
396
401
  const localLastMessage = await this.store.getLastL1ToL2Message();
397
402
  const remoteMessagesState = await this.inbox.getState({ blockNumber: currentL1BlockNumber });
398
- await this.store.setInboxTreeInProgress(remoteMessagesState.treeInProgress);
399
403
 
400
404
  this.log.trace(`Retrieved remote inbox state at L1 block ${currentL1BlockNumber}.`, {
401
405
  localMessagesInserted,
@@ -830,14 +834,6 @@ export class ArchiverL1Synchronizer implements Traceable {
830
834
  );
831
835
  }
832
836
 
833
- for (const published of validCheckpoints) {
834
- this.instrumentation.processCheckpointL1Timing({
835
- slotNumber: published.checkpoint.header.slotNumber,
836
- l1Timestamp: published.l1.timestamp,
837
- l1Constants: this.l1Constants,
838
- });
839
- }
840
-
841
837
  try {
842
838
  const updatedValidationResult =
843
839
  rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
@@ -227,21 +227,34 @@ export class BlockStore {
227
227
  }
228
228
 
229
229
  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
230
  const firstCheckpointNumber = checkpoints[0].checkpoint.number;
232
231
  const previousCheckpointNumber = await this.getLatestCheckpointNumber();
233
232
 
234
- if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
233
+ // Handle already-stored checkpoints at the start of the batch.
234
+ // This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
235
+ // We accept them if archives match (same content) and update their L1 metadata.
236
+ if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
237
+ checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
238
+ if (checkpoints.length === 0) {
239
+ return true;
240
+ }
241
+ // Re-check sequentiality after skipping
242
+ const newFirstNumber = checkpoints[0].checkpoint.number;
243
+ if (previousCheckpointNumber !== newFirstNumber - 1) {
244
+ throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
245
+ }
246
+ } else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
235
247
  throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
236
248
  }
237
249
 
238
250
  // Extract the previous checkpoint if there is one
251
+ const currentFirstCheckpointNumber = checkpoints[0].checkpoint.number;
239
252
  let previousCheckpointData: CheckpointData | undefined = undefined;
240
- if (previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1) {
253
+ if (currentFirstCheckpointNumber - 1 !== INITIAL_CHECKPOINT_NUMBER - 1) {
241
254
  // There should be a previous checkpoint
242
- previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
255
+ previousCheckpointData = await this.getCheckpointData(CheckpointNumber(currentFirstCheckpointNumber - 1));
243
256
  if (previousCheckpointData === undefined) {
244
- throw new CheckpointNotFoundError(previousCheckpointNumber);
257
+ throw new CheckpointNotFoundError(CheckpointNumber(currentFirstCheckpointNumber - 1));
245
258
  }
246
259
  }
247
260
 
@@ -331,6 +344,50 @@ export class BlockStore {
331
344
  });
332
345
  }
333
346
 
347
+ /**
348
+ * Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
349
+ * Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
350
+ */
351
+ private async skipOrUpdateAlreadyStoredCheckpoints(
352
+ checkpoints: PublishedCheckpoint[],
353
+ latestStored: CheckpointNumber,
354
+ ): Promise<PublishedCheckpoint[]> {
355
+ let i = 0;
356
+ for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
357
+ const incoming = checkpoints[i];
358
+ const stored = await this.getCheckpointData(incoming.checkpoint.number);
359
+ if (!stored) {
360
+ // Should not happen if latestStored is correct, but be safe
361
+ break;
362
+ }
363
+ // Verify the checkpoint content matches (archive root)
364
+ if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
365
+ throw new Error(
366
+ `Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
367
+ `Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
368
+ );
369
+ }
370
+ // Update L1 metadata and attestations for the already-stored checkpoint
371
+ this.#log.warn(
372
+ `Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
373
+ `(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
374
+ );
375
+ await this.#checkpoints.set(incoming.checkpoint.number, {
376
+ header: incoming.checkpoint.header.toBuffer(),
377
+ archive: incoming.checkpoint.archive.toBuffer(),
378
+ checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
379
+ l1: incoming.l1.toBuffer(),
380
+ attestations: incoming.attestations.map(a => a.toBuffer()),
381
+ checkpointNumber: incoming.checkpoint.number,
382
+ startBlock: incoming.checkpoint.blocks[0].number,
383
+ blockCount: incoming.checkpoint.blocks.length,
384
+ });
385
+ // Update the sync point to reflect the new L1 block
386
+ await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
387
+ }
388
+ return checkpoints.slice(i);
389
+ }
390
+
334
391
  private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
335
392
  const blockHash = await block.hash();
336
393