@aztec/archiver 0.0.1-commit.e588bc7e5 → 0.0.1-commit.e5a3663dd

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 (105) hide show
  1. package/dest/archiver.d.ts +19 -11
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +96 -53
  4. package/dest/config.d.ts +3 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +14 -3
  7. package/dest/errors.d.ts +32 -5
  8. package/dest/errors.d.ts.map +1 -1
  9. package/dest/errors.js +51 -6
  10. package/dest/factory.d.ts +4 -4
  11. package/dest/factory.d.ts.map +1 -1
  12. package/dest/factory.js +13 -10
  13. package/dest/index.d.ts +10 -3
  14. package/dest/index.d.ts.map +1 -1
  15. package/dest/index.js +9 -2
  16. package/dest/l1/calldata_retriever.d.ts +2 -1
  17. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  18. package/dest/l1/calldata_retriever.js +9 -4
  19. package/dest/l1/data_retrieval.d.ts +18 -9
  20. package/dest/l1/data_retrieval.d.ts.map +1 -1
  21. package/dest/l1/data_retrieval.js +13 -19
  22. package/dest/l1/validate_historical_logs.d.ts +23 -0
  23. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  24. package/dest/l1/validate_historical_logs.js +108 -0
  25. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  26. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  27. package/dest/modules/contract_data_source_adapter.js +42 -0
  28. package/dest/modules/data_source_base.d.ts +16 -10
  29. package/dest/modules/data_source_base.d.ts.map +1 -1
  30. package/dest/modules/data_source_base.js +71 -60
  31. package/dest/modules/data_store_updater.d.ts +16 -9
  32. package/dest/modules/data_store_updater.d.ts.map +1 -1
  33. package/dest/modules/data_store_updater.js +52 -40
  34. package/dest/modules/instrumentation.d.ts +7 -2
  35. package/dest/modules/instrumentation.d.ts.map +1 -1
  36. package/dest/modules/instrumentation.js +22 -6
  37. package/dest/modules/l1_synchronizer.d.ts +8 -4
  38. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  39. package/dest/modules/l1_synchronizer.js +212 -79
  40. package/dest/modules/validation.d.ts +4 -3
  41. package/dest/modules/validation.d.ts.map +1 -1
  42. package/dest/modules/validation.js +4 -4
  43. package/dest/store/block_store.d.ts +60 -21
  44. package/dest/store/block_store.d.ts.map +1 -1
  45. package/dest/store/block_store.js +229 -70
  46. package/dest/store/contract_class_store.d.ts +17 -3
  47. package/dest/store/contract_class_store.d.ts.map +1 -1
  48. package/dest/store/contract_class_store.js +17 -1
  49. package/dest/store/contract_instance_store.d.ts +28 -1
  50. package/dest/store/contract_instance_store.d.ts.map +1 -1
  51. package/dest/store/contract_instance_store.js +31 -0
  52. package/dest/store/data_stores.d.ts +68 -0
  53. package/dest/store/data_stores.d.ts.map +1 -0
  54. package/dest/store/data_stores.js +50 -0
  55. package/dest/store/function_names_cache.d.ts +17 -0
  56. package/dest/store/function_names_cache.d.ts.map +1 -0
  57. package/dest/store/function_names_cache.js +30 -0
  58. package/dest/store/l2_tips_cache.d.ts +1 -1
  59. package/dest/store/l2_tips_cache.d.ts.map +1 -1
  60. package/dest/store/l2_tips_cache.js +3 -3
  61. package/dest/store/log_store.d.ts +1 -1
  62. package/dest/store/log_store.d.ts.map +1 -1
  63. package/dest/store/log_store.js +2 -4
  64. package/dest/store/message_store.d.ts +9 -3
  65. package/dest/store/message_store.d.ts.map +1 -1
  66. package/dest/store/message_store.js +31 -1
  67. package/dest/test/fake_l1_state.d.ts +14 -3
  68. package/dest/test/fake_l1_state.d.ts.map +1 -1
  69. package/dest/test/fake_l1_state.js +55 -15
  70. package/dest/test/mock_l2_block_source.d.ts +12 -3
  71. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  72. package/dest/test/mock_l2_block_source.js +24 -2
  73. package/dest/test/noop_l1_archiver.d.ts +4 -4
  74. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  75. package/dest/test/noop_l1_archiver.js +9 -7
  76. package/package.json +13 -13
  77. package/src/archiver.ts +113 -52
  78. package/src/config.ts +15 -1
  79. package/src/errors.ts +75 -8
  80. package/src/factory.ts +11 -10
  81. package/src/index.ts +17 -2
  82. package/src/l1/calldata_retriever.ts +15 -4
  83. package/src/l1/data_retrieval.ts +30 -35
  84. package/src/l1/validate_historical_logs.ts +140 -0
  85. package/src/modules/contract_data_source_adapter.ts +59 -0
  86. package/src/modules/data_source_base.ts +75 -57
  87. package/src/modules/data_store_updater.ts +71 -39
  88. package/src/modules/instrumentation.ts +27 -7
  89. package/src/modules/l1_synchronizer.ts +301 -83
  90. package/src/modules/validation.ts +8 -7
  91. package/src/store/block_store.ts +264 -77
  92. package/src/store/contract_class_store.ts +28 -2
  93. package/src/store/contract_instance_store.ts +43 -0
  94. package/src/store/data_stores.ts +108 -0
  95. package/src/store/function_names_cache.ts +37 -0
  96. package/src/store/l2_tips_cache.ts +9 -3
  97. package/src/store/log_store.ts +2 -5
  98. package/src/store/message_store.ts +35 -2
  99. package/src/test/fake_l1_state.ts +62 -24
  100. package/src/test/mock_l2_block_source.ts +23 -2
  101. package/src/test/noop_l1_archiver.ts +9 -7
  102. package/dest/store/kv_archiver_store.d.ts +0 -377
  103. package/dest/store/kv_archiver_store.d.ts.map +0 -1
  104. package/dest/store/kv_archiver_store.js +0 -494
  105. package/src/store/kv_archiver_store.ts +0 -713
@@ -2,32 +2,37 @@ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { EpochCache } from '@aztec/epoch-cache';
3
3
  import { InboxContract, type InboxContractState, RollupContract } from '@aztec/ethereum/contracts';
4
4
  import type { L1BlockId } from '@aztec/ethereum/l1-types';
5
+ import { getFinalizedL1Block } from '@aztec/ethereum/queries';
5
6
  import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
6
7
  import { asyncPool } from '@aztec/foundation/async-pool';
7
8
  import { maxBigint } from '@aztec/foundation/bigint';
8
9
  import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
9
10
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
10
- import { pick } from '@aztec/foundation/collection';
11
+ import { compactArray, partition, pick } from '@aztec/foundation/collection';
11
12
  import { Fr } from '@aztec/foundation/curves/bn254';
13
+ import { EthAddress } from '@aztec/foundation/eth-address';
12
14
  import { type Logger, createLogger } from '@aztec/foundation/log';
13
15
  import { retryTimes } from '@aztec/foundation/retry';
14
16
  import { count } from '@aztec/foundation/string';
15
17
  import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
16
18
  import { isDefined, isErrorClass } from '@aztec/foundation/types';
17
19
  import { type ArchiverEmitter, L2BlockSourceEvents, type ValidateCheckpointResult } from '@aztec/stdlib/block';
18
- import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
20
+ import { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
19
21
  import { type L1RollupConstants, getEpochAtSlot, getSlotAtNextL1Block } from '@aztec/stdlib/epoch-helpers';
20
22
  import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
23
+ import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
21
24
  import { type Traceable, type Tracer, execInSpan, trackSpan } from '@aztec/telemetry-client';
22
25
 
23
26
  import { InitialCheckpointNumberNotSequentialError } from '../errors.js';
24
27
  import {
25
- retrieveCheckpointsFromRollup,
28
+ type RetrievedCheckpointFromCalldata,
29
+ getCheckpointBlobDataFromBlobs,
30
+ retrieveCheckpointCalldataFromRollup,
26
31
  retrieveL1ToL2Message,
27
32
  retrieveL1ToL2Messages,
28
33
  retrievedToPublishedCheckpoint,
29
34
  } from '../l1/data_retrieval.js';
30
- import type { KVArchiverDataStore } from '../store/kv_archiver_store.js';
35
+ import { type ArchiverDataStores, getArchiverSynchPoint } from '../store/data_stores.js';
31
36
  import type { L2TipsCache } from '../store/l2_tips_cache.js';
32
37
  import { MessageStoreError } from '../store/message_store.js';
33
38
  import type { InboxMessage } from '../structs/inbox_message.js';
@@ -62,10 +67,11 @@ export class ArchiverL1Synchronizer implements Traceable {
62
67
  private readonly debugClient: ViemPublicDebugClient,
63
68
  private readonly rollup: RollupContract,
64
69
  private readonly inbox: InboxContract,
65
- private readonly store: KVArchiverDataStore,
70
+ private readonly stores: ArchiverDataStores,
66
71
  private config: {
67
72
  batchSize: number;
68
73
  skipValidateCheckpointAttestations?: boolean;
74
+ skipPromoteProposedCheckpointDuringL1Sync?: boolean;
69
75
  maxAllowedEthClientDriftSeconds: number;
70
76
  },
71
77
  private readonly blobClient: BlobClientInterface,
@@ -81,7 +87,7 @@ export class ArchiverL1Synchronizer implements Traceable {
81
87
  l2TipsCache?: L2TipsCache,
82
88
  private readonly log: Logger = createLogger('archiver:l1-sync'),
83
89
  ) {
84
- this.updater = new ArchiverDataStoreUpdater(this.store, l2TipsCache, {
90
+ this.updater = new ArchiverDataStoreUpdater(this.stores, l2TipsCache, {
85
91
  rollupManaLimit: l1Constants.rollupManaLimit,
86
92
  });
87
93
  this.tracer = tracer;
@@ -91,6 +97,7 @@ export class ArchiverL1Synchronizer implements Traceable {
91
97
  public setConfig(newConfig: {
92
98
  batchSize: number;
93
99
  skipValidateCheckpointAttestations?: boolean;
100
+ skipPromoteProposedCheckpointDuringL1Sync?: boolean;
94
101
  maxAllowedEthClientDriftSeconds: number;
95
102
  }) {
96
103
  this.config = newConfig;
@@ -106,6 +113,13 @@ export class ArchiverL1Synchronizer implements Traceable {
106
113
  return this.l1Timestamp;
107
114
  }
108
115
 
116
+ private getSignatureContext(): CoordinationSignatureContext {
117
+ return {
118
+ chainId: this.publicClient.chain.id,
119
+ rollupAddress: EthAddress.fromString(this.rollup.address),
120
+ };
121
+ }
122
+
109
123
  /** Checks that the ethereum node we are connected to has a latest timestamp no more than the allowed drift. Throw if not. */
110
124
  public async testEthereumNodeSynced(): Promise<void> {
111
125
  const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
@@ -148,14 +162,26 @@ export class ArchiverL1Synchronizer implements Traceable {
148
162
  );
149
163
  }
150
164
 
165
+ // Query finalized block on L1
166
+ const rawFinalizedL1Block = await getFinalizedL1Block(this.publicClient);
167
+ const finalizedL1Block: L1BlockId | undefined = rawFinalizedL1Block && {
168
+ l1BlockNumber: rawFinalizedL1Block.number,
169
+ l1BlockHash: Buffer32.fromString(rawFinalizedL1Block.hash),
170
+ };
171
+
151
172
  // Load sync point for blocks defaulting to start block
152
- const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await this.store.getSynchPoint();
153
- this.log.debug(`Starting new archiver sync iteration`, { blocksSynchedTo, currentL1BlockData });
173
+ const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await getArchiverSynchPoint(this.stores);
174
+ this.log.debug(`Starting new archiver sync iteration`, { blocksSynchedTo, currentL1BlockData, finalizedL1Block });
154
175
 
155
176
  // Sync L1 to L2 messages. We retry this a few times since there are error conditions that reset the sync point, requiring a new iteration.
156
177
  // Note that we cannot just wait for the l1 synchronizer to loop again, since the synchronizer would report as synced up to the current L1
157
178
  // block, when that wouldn't be the case, since L1 to L2 messages would need another iteration.
158
- await retryTimes(() => this.handleL1ToL2Messages(currentL1BlockData), 'Handling L1 to L2 messages', 3, 0.1);
179
+ await retryTimes(
180
+ () => this.handleL1ToL2Messages(currentL1BlockData, finalizedL1Block),
181
+ 'Handling L1 to L2 messages',
182
+ 3,
183
+ 0.1,
184
+ );
159
185
 
160
186
  if (currentL1BlockNumber > blocksSynchedTo) {
161
187
  // First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
@@ -180,7 +206,7 @@ export class ArchiverL1Synchronizer implements Traceable {
180
206
  // past it, since otherwise we'll keep downloading it and reprocessing it on every iteration until
181
207
  // we get a valid checkpoint to advance the syncpoint.
182
208
  if (!rollupStatus.validationResult?.valid && rollupStatus.lastL1BlockWithCheckpoint !== undefined) {
183
- await this.store.setCheckpointSynchedL1BlockNumber(rollupStatus.lastL1BlockWithCheckpoint);
209
+ await this.stores.blocks.setSynchedL1BlockNumber(rollupStatus.lastL1BlockWithCheckpoint);
184
210
  }
185
211
 
186
212
  // And lastly we check if we are missing any checkpoints behind us due to a possible L1 reorg.
@@ -195,7 +221,7 @@ export class ArchiverL1Synchronizer implements Traceable {
195
221
  }
196
222
 
197
223
  // Update the finalized L2 checkpoint based on L1 finality.
198
- await this.updateFinalizedCheckpoint();
224
+ await this.updateFinalizedCheckpoint(finalizedL1Block);
199
225
 
200
226
  // After syncing has completed, update the current l1 block number and timestamp,
201
227
  // otherwise we risk announcing to the world that we've synced to a given point,
@@ -212,15 +238,18 @@ export class ArchiverL1Synchronizer implements Traceable {
212
238
  });
213
239
  }
214
240
 
215
- /** Query L1 for its finalized block and update the finalized checkpoint accordingly. */
216
- private async updateFinalizedCheckpoint(): Promise<void> {
241
+ /** Updates the finalized checkpoint using the pre-fetched finalized L1 block from the current sync iteration. */
242
+ private async updateFinalizedCheckpoint(finalizedL1Block: L1BlockId | undefined): Promise<void> {
217
243
  try {
218
- const finalizedL1Block = await this.publicClient.getBlock({ blockTag: 'finalized', includeTransactions: false });
219
- const finalizedL1BlockNumber = finalizedL1Block.number;
244
+ if (!finalizedL1Block) {
245
+ this.log.trace(`Skipping finalized checkpoint update: L1 has no finalized block yet.`);
246
+ return;
247
+ }
248
+ const finalizedL1BlockNumber = finalizedL1Block.l1BlockNumber;
220
249
  const finalizedCheckpointNumber = await this.rollup.getProvenCheckpointNumber({
221
250
  blockNumber: finalizedL1BlockNumber,
222
251
  });
223
- const localFinalizedCheckpointNumber = await this.store.getFinalizedCheckpointNumber();
252
+ const localFinalizedCheckpointNumber = await this.stores.blocks.getFinalizedCheckpointNumber();
224
253
  if (localFinalizedCheckpointNumber !== finalizedCheckpointNumber) {
225
254
  await this.updater.setFinalizedCheckpointNumber(finalizedCheckpointNumber);
226
255
  this.log.info(`Updated finalized chain to checkpoint ${finalizedCheckpointNumber}`, {
@@ -239,8 +268,8 @@ export class ArchiverL1Synchronizer implements Traceable {
239
268
  /** Prune all proposed local blocks that should have been checkpointed by now. */
240
269
  private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) {
241
270
  const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
242
- this.store.getCheckpointedL2BlockNumber(),
243
- this.store.getLatestBlockNumber(),
271
+ this.stores.blocks.getCheckpointedL2BlockNumber(),
272
+ this.stores.blocks.getLatestL2BlockNumber(),
244
273
  ]);
245
274
 
246
275
  // If there are no uncheckpointed blocks, we got nothing to do
@@ -254,7 +283,10 @@ export class ArchiverL1Synchronizer implements Traceable {
254
283
  const firstUncheckpointedBlockNumber = BlockNumber(lastCheckpointedBlockNumber + 1);
255
284
 
256
285
  // What's the slot of the first uncheckpointed block?
257
- const [firstUncheckpointedBlockHeader] = await this.store.getBlockHeaders(firstUncheckpointedBlockNumber, 1);
286
+ const [firstUncheckpointedBlockHeader] = await this.stores.blocks.getBlockHeaders(
287
+ firstUncheckpointedBlockNumber,
288
+ 1,
289
+ );
258
290
  const firstUncheckpointedBlockSlot = firstUncheckpointedBlockHeader?.getSlot();
259
291
 
260
292
  if (firstUncheckpointedBlockSlot === undefined || firstUncheckpointedBlockSlot >= slotAtNextL1Block) {
@@ -267,6 +299,7 @@ export class ArchiverL1Synchronizer implements Traceable {
267
299
  `Pruning blocks after block ${lastCheckpointedBlockNumber} due to slot ${firstUncheckpointedBlockSlot} not being checkpointed`,
268
300
  { firstUncheckpointedBlockHeader: firstUncheckpointedBlockHeader.toInspect(), slotAtNextL1Block },
269
301
  );
302
+
270
303
  const prunedBlocks = await this.updater.removeUncheckpointedBlocksAfter(lastCheckpointedBlockNumber);
271
304
 
272
305
  if (prunedBlocks.length > 0) {
@@ -300,7 +333,7 @@ export class ArchiverL1Synchronizer implements Traceable {
300
333
  currentL1Timestamp: bigint,
301
334
  ): Promise<{ rollupCanPrune: boolean }> {
302
335
  const rollupCanPrune = await this.canPrune(currentL1BlockNumber, currentL1Timestamp);
303
- const localPendingCheckpointNumber = await this.store.getSynchedCheckpointNumber();
336
+ const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
304
337
  const canPrune = localPendingCheckpointNumber > provenCheckpointNumber && rollupCanPrune;
305
338
 
306
339
  if (canPrune) {
@@ -321,12 +354,12 @@ export class ArchiverL1Synchronizer implements Traceable {
321
354
  // promises when the gap between local pending and proven checkpoint numbers is large.
322
355
  const BATCH_SIZE = 10;
323
356
  const indices = Array.from({ length: checkpointsToUnwind }, (_, i) => CheckpointNumber(i + pruneFrom));
324
- const checkpoints = (await asyncPool(BATCH_SIZE, indices, idx => this.store.getCheckpointData(idx))).filter(
325
- isDefined,
326
- );
357
+ const checkpoints = (
358
+ await asyncPool(BATCH_SIZE, indices, idx => this.stores.blocks.getCheckpointData(idx))
359
+ ).filter(isDefined);
327
360
  const newBlocks = (
328
361
  await asyncPool(BATCH_SIZE, checkpoints, cp =>
329
- this.store.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)),
362
+ this.stores.blocks.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)),
330
363
  )
331
364
  )
332
365
  .filter(isDefined)
@@ -346,12 +379,12 @@ export class ArchiverL1Synchronizer implements Traceable {
346
379
  this.log.warn(
347
380
  `Removed ${count(checkpointsToUnwind, 'checkpoint')} after checkpoint ${provenCheckpointNumber} ` +
348
381
  `due to predicted reorg at L1 block ${currentL1BlockNumber}. ` +
349
- `Updated latest checkpoint is ${await this.store.getSynchedCheckpointNumber()}.`,
382
+ `Updated latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`,
350
383
  );
351
384
  this.instrumentation.processPrune(timer.ms());
352
385
  // TODO(palla/reorg): Do we need to set the block synched L1 block number here?
353
386
  // Seems like the next iteration should handle this.
354
- // await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
387
+ // await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
355
388
  }
356
389
 
357
390
  return { rollupCanPrune };
@@ -368,14 +401,17 @@ export class ArchiverL1Synchronizer implements Traceable {
368
401
  }
369
402
 
370
403
  @trackSpan('Archiver.handleL1ToL2Messages')
371
- private async handleL1ToL2Messages(currentL1Block: L1BlockId): Promise<boolean> {
404
+ private async handleL1ToL2Messages(
405
+ currentL1Block: L1BlockId,
406
+ finalizedL1Block: L1BlockId | undefined,
407
+ ): Promise<boolean> {
372
408
  // Load the syncpoint, which may have been updated in a previous iteration
373
409
  const {
374
410
  messagesSynchedTo = {
375
411
  l1BlockNumber: this.l1Constants.l1StartBlock,
376
412
  l1BlockHash: this.l1Constants.l1StartBlockHash,
377
413
  },
378
- } = await this.store.getSynchPoint();
414
+ } = await getArchiverSynchPoint(this.stores);
379
415
 
380
416
  // Nothing to do if L1 block number has not moved forward
381
417
  const currentL1BlockNumber = currentL1Block.l1BlockNumber;
@@ -385,10 +421,14 @@ export class ArchiverL1Synchronizer implements Traceable {
385
421
 
386
422
  // Compare local message store state with the remote. If they match, we just advance the match pointer.
387
423
  const remoteMessagesState = await this.inbox.getState({ blockNumber: currentL1BlockNumber });
388
- const localLastMessage = await this.store.getLastL1ToL2Message();
424
+ const localLastMessage = await this.stores.messages.getLastMessage();
389
425
  if (await this.localStateMatches(localLastMessage, remoteMessagesState)) {
390
426
  this.log.trace(`Local L1 to L2 messages are already in sync with remote at L1 block ${currentL1BlockNumber}`);
391
- await this.store.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress);
427
+ await this.stores.messages.setMessageSyncState(
428
+ currentL1Block,
429
+ remoteMessagesState.treeInProgress,
430
+ finalizedL1Block,
431
+ );
392
432
  return true;
393
433
  }
394
434
 
@@ -404,7 +444,7 @@ export class ArchiverL1Synchronizer implements Traceable {
404
444
  `Failed to store L1 to L2 messages retrieved from L1: ${error.message}. Rolling back syncpoint to retry.`,
405
445
  { inboxMessage: error.inboxMessage },
406
446
  );
407
- await this.rollbackL1ToL2Messages(remoteMessagesState.treeInProgress);
447
+ await this.rollbackL1ToL2Messages(remoteMessagesState);
408
448
  return false;
409
449
  }
410
450
  throw error;
@@ -413,24 +453,28 @@ export class ArchiverL1Synchronizer implements Traceable {
413
453
  // Note that, if there are no new messages to insert, but there was an L1 reorg that pruned out last messages,
414
454
  // we'd notice by comparing our local state with the remote one again, and seeing they don't match even after
415
455
  // our sync attempt. In this case, we also rollback our syncpoint, and trigger a retry.
416
- const localLastMessageAfterSync = await this.store.getLastL1ToL2Message();
456
+ const localLastMessageAfterSync = await this.stores.messages.getLastMessage();
417
457
  if (!(await this.localStateMatches(localLastMessageAfterSync, remoteMessagesState))) {
418
458
  this.log.warn(
419
459
  `Local L1 to L2 messages state does not match remote after sync attempt. Rolling back syncpoint to retry.`,
420
460
  { localLastMessageAfterSync, remoteMessagesState },
421
461
  );
422
- await this.rollbackL1ToL2Messages(remoteMessagesState.treeInProgress);
462
+ await this.rollbackL1ToL2Messages(remoteMessagesState);
423
463
  return false;
424
464
  }
425
465
 
426
466
  // Advance the syncpoint after a successful sync
427
- await this.store.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress);
467
+ await this.stores.messages.setMessageSyncState(
468
+ currentL1Block,
469
+ remoteMessagesState.treeInProgress,
470
+ finalizedL1Block,
471
+ );
428
472
  return true;
429
473
  }
430
474
 
431
475
  /** Checks if the local rolling hash and message count matches the remote state */
432
476
  private async localStateMatches(localLastMessage: InboxMessage | undefined, remoteState: InboxContractState) {
433
- const localMessageCount = await this.store.getTotalL1ToL2MessageCount();
477
+ const localMessageCount = await this.stores.messages.getTotalL1ToL2MessageCount();
434
478
  this.log.trace(`Comparing local and remote inbox state`, { localMessageCount, localLastMessage, remoteState });
435
479
 
436
480
  return (
@@ -452,7 +496,7 @@ export class ArchiverL1Synchronizer implements Traceable {
452
496
  this.log.trace(`Retrieving L1 to L2 messages in L1 blocks ${searchStartBlock}-${searchEndBlock}`);
453
497
  const messages = await retrieveL1ToL2Messages(this.inbox, searchStartBlock, searchEndBlock);
454
498
  const timer = new Timer();
455
- await this.store.addL1ToL2Messages(messages);
499
+ await this.stores.messages.addL1ToL2Messages(messages);
456
500
  const perMsg = timer.ms() / messages.length;
457
501
  this.instrumentation.processNewMessages(messages.length, perMsg);
458
502
  for (const msg of messages) {
@@ -474,18 +518,49 @@ export class ArchiverL1Synchronizer implements Traceable {
474
518
  * Rolls back local L1 to L2 messages to the last common message with L1, and updates the syncpoint to the L1 block of that message.
475
519
  * If no common message is found, rolls back all messages and sets the syncpoint to the start block.
476
520
  */
477
- private async rollbackL1ToL2Messages(remoteTreeInProgress: bigint): Promise<L1BlockId> {
478
- // Slowly go back through our messages until we find the last common message.
479
- // We could query the logs in batch as an optimization, but the depth of the reorg should not be deep, and this
480
- // is a very rare case, so it's fine to query one log at a time.
521
+ private async rollbackL1ToL2Messages(remoteMessagesState: InboxContractState): Promise<L1BlockId> {
522
+ const { treeInProgress: remoteTreeInProgress, messagesRollingHash: remoteRollingHash } = remoteMessagesState;
523
+
524
+ const messagesFinalizedL1Block = await this.stores.messages.getMessagesFinalizedL1Block();
525
+ const finalizedL1BlockNumber = messagesFinalizedL1Block?.l1BlockNumber;
526
+
527
+ // Slowly go back through our messages until we find the last common message. We could query the logs in
528
+ // batch as an optimization, but the depth of the reorg should not be deep, and this is a very rare case,
529
+ // so it's fine to query one log at a time.
481
530
  let commonMsg: undefined | InboxMessage;
482
531
  let messagesToDelete = 0;
483
532
  this.log.verbose(`Searching most recent common L1 to L2 message`);
484
- for await (const localMsg of this.store.iterateL1ToL2Messages({ reverse: true })) {
533
+ for await (const localMsg of this.stores.messages.iterateL1ToL2Messages({ reverse: true })) {
534
+ const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteMessagesState };
535
+
536
+ // First check if the local message rolling hash matches the current rolling hash of the inbox contract,
537
+ // which means we just need to rollback some local messages and we should be back in sync. This means there
538
+ // was an L1 reorg that removed some of the messages we had, but no new messages were added compared.
539
+ if (localMsg.rollingHash.equals(remoteRollingHash)) {
540
+ this.log.info(
541
+ `Found common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber} matching current remote state`,
542
+ logCtx,
543
+ );
544
+ commonMsg = localMsg;
545
+ break;
546
+ }
547
+
548
+ // Messages at or below the finalized L1 block cannot have been reorged — accept as common without querying L1.
549
+ if (finalizedL1BlockNumber !== undefined && localMsg.l1BlockNumber <= finalizedL1BlockNumber) {
550
+ this.log.info(`Found common L1 to L2 message at finalized L1 block ${localMsg.l1BlockNumber}`, logCtx);
551
+ commonMsg = localMsg;
552
+ break;
553
+ }
554
+
555
+ // If there's no match with the current remote state, check if the message exists on the inbox contract at all
556
+ // by looking at the inbox events. If the L1 reorg *added* new messages in addition to deleting existing ones,
557
+ // then the current remote state's rolling hash will not match anything we have locally, so we need to check existence
558
+ // of individual messages via logs. Note we use logs and not historical queries so we don't have to depend on
559
+ // an archival rpc node, since the message could be from a long time ago if we're catching up with syncing.
485
560
  const remoteMsg = await retrieveL1ToL2Message(this.inbox, localMsg);
486
- const logCtx = { remoteMsg, localMsg: localMsg };
561
+ logCtx.remoteMsg = remoteMsg;
487
562
  if (remoteMsg && remoteMsg.rollingHash.equals(localMsg.rollingHash)) {
488
- this.log.verbose(
563
+ this.log.info(
489
564
  `Found most recent common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber}`,
490
565
  logCtx,
491
566
  );
@@ -505,17 +580,29 @@ export class ArchiverL1Synchronizer implements Traceable {
505
580
  if (messagesToDelete > 0) {
506
581
  const lastGoodIndex = commonMsg?.index;
507
582
  this.log.warn(`Rolling back all local L1 to L2 messages after index ${lastGoodIndex ?? 'initial'}`);
508
- await this.store.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
583
+ await this.stores.messages.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
509
584
  }
510
585
 
511
586
  // Update the syncpoint so the loop below reprocesses the changed messages. We go to the block before
512
587
  // the last common one, so we force reprocessing it, in case new messages were added on that same L1 block
513
- // after the last common message.
514
- const syncPointL1BlockNumber = commonMsg ? commonMsg.l1BlockNumber - 1n : this.l1Constants.l1StartBlock;
515
- const syncPointL1BlockHash = await this.getL1BlockHash(syncPointL1BlockNumber);
588
+ // after the last common message. Cap at the finalized L1 block: messages at or below finalized cannot
589
+ // have been reorged, so there is no need to walk back any further than that.
590
+ const syncPointL1BlockNumber = maxBigint(
591
+ ...compactArray([
592
+ commonMsg ? commonMsg.l1BlockNumber - 1n : undefined,
593
+ finalizedL1BlockNumber,
594
+ this.l1Constants.l1StartBlock,
595
+ ]),
596
+ );
597
+
598
+ const syncPointL1BlockHash =
599
+ syncPointL1BlockNumber === finalizedL1BlockNumber
600
+ ? messagesFinalizedL1Block!.l1BlockHash
601
+ : await this.getL1BlockHash(syncPointL1BlockNumber);
602
+
516
603
  const messagesSyncPoint = { l1BlockNumber: syncPointL1BlockNumber, l1BlockHash: syncPointL1BlockHash };
517
- await this.store.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
518
- this.log.verbose(`Updated messages syncpoint to L1 block ${syncPointL1BlockNumber}`, {
604
+ await this.stores.messages.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
605
+ this.log.verbose(`Updated messages syncpoint to L1 block ${messagesSyncPoint.l1BlockNumber}`, {
519
606
  ...messagesSyncPoint,
520
607
  remoteTreeInProgress,
521
608
  });
@@ -536,9 +623,9 @@ export class ArchiverL1Synchronizer implements Traceable {
536
623
  currentL1BlockNumber: bigint,
537
624
  initialSyncComplete: boolean,
538
625
  ): Promise<RollupStatus> {
539
- const localPendingCheckpointNumber = await this.store.getSynchedCheckpointNumber();
626
+ const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
540
627
  const initialValidationResult: ValidateCheckpointResult | undefined =
541
- await this.store.getPendingChainValidationStatus();
628
+ await this.stores.blocks.getPendingChainValidationStatus();
542
629
  const {
543
630
  provenCheckpointNumber,
544
631
  provenArchive,
@@ -568,7 +655,7 @@ export class ArchiverL1Synchronizer implements Traceable {
568
655
  // we need to set it to zero. This is an edge case because we dont have a checkpoint zero (initial checkpoint is one),
569
656
  // so localCheckpointForDestinationProvenCheckpointNumber would not be found below.
570
657
  if (provenCheckpointNumber === 0) {
571
- const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber();
658
+ const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
572
659
  if (localProvenCheckpointNumber !== provenCheckpointNumber) {
573
660
  await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
574
661
  this.log.info(`Rolled back proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
@@ -576,11 +663,11 @@ export class ArchiverL1Synchronizer implements Traceable {
576
663
  }
577
664
 
578
665
  const localCheckpointForDestinationProvenCheckpointNumber =
579
- await this.store.getCheckpointData(provenCheckpointNumber);
666
+ await this.stores.blocks.getCheckpointData(provenCheckpointNumber);
580
667
 
581
668
  // Sanity check. I've hit what seems to be a state where the proven checkpoint is set to a value greater than the latest
582
669
  // synched checkpoint when requesting L2Tips from the archiver. This is the only place where the proven checkpoint is set.
583
- const synched = await this.store.getSynchedCheckpointNumber();
670
+ const synched = await this.stores.blocks.getLatestCheckpointNumber();
584
671
  if (
585
672
  localCheckpointForDestinationProvenCheckpointNumber &&
586
673
  synched < localCheckpointForDestinationProvenCheckpointNumber.checkpointNumber
@@ -600,7 +687,7 @@ export class ArchiverL1Synchronizer implements Traceable {
600
687
  localCheckpointForDestinationProvenCheckpointNumber &&
601
688
  provenArchive.equals(localCheckpointForDestinationProvenCheckpointNumber.archive.root)
602
689
  ) {
603
- const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber();
690
+ const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
604
691
  if (localProvenCheckpointNumber !== provenCheckpointNumber) {
605
692
  await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
606
693
  this.log.info(`Updated proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
@@ -628,7 +715,7 @@ export class ArchiverL1Synchronizer implements Traceable {
628
715
  // If we have 0 checkpoints locally and there are no checkpoints onchain there is nothing to do.
629
716
  const noCheckpoints = localPendingCheckpointNumber === 0 && pendingCheckpointNumber === 0;
630
717
  if (noCheckpoints) {
631
- await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
718
+ await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
632
719
  this.log.debug(
633
720
  `No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}, no checkpoints on chain`,
634
721
  );
@@ -640,7 +727,7 @@ export class ArchiverL1Synchronizer implements Traceable {
640
727
  // Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there
641
728
  // are any state that could be impacted by it. If we have no checkpoints, there is no impact.
642
729
  if (localPendingCheckpointNumber > 0) {
643
- const localPendingCheckpoint = await this.store.getCheckpointData(localPendingCheckpointNumber);
730
+ const localPendingCheckpoint = await this.stores.blocks.getCheckpointData(localPendingCheckpointNumber);
644
731
  if (localPendingCheckpoint === undefined) {
645
732
  throw new Error(`Missing checkpoint ${localPendingCheckpointNumber}`);
646
733
  }
@@ -655,7 +742,7 @@ export class ArchiverL1Synchronizer implements Traceable {
655
742
  // However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing checkpoints.
656
743
  // We must only set this block number based on actually retrieved logs.
657
744
  // TODO(#8621): Tackle this properly when we handle L1 Re-orgs.
658
- // await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
745
+ // await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
659
746
  this.log.debug(`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
660
747
  return rollupStatus;
661
748
  }
@@ -675,7 +762,7 @@ export class ArchiverL1Synchronizer implements Traceable {
675
762
 
676
763
  let tipAfterUnwind = localPendingCheckpointNumber;
677
764
  while (true) {
678
- const candidateCheckpoint = await this.store.getCheckpointData(tipAfterUnwind);
765
+ const candidateCheckpoint = await this.stores.blocks.getCheckpointData(tipAfterUnwind);
679
766
  if (candidateCheckpoint === undefined) {
680
767
  break;
681
768
  }
@@ -700,7 +787,7 @@ export class ArchiverL1Synchronizer implements Traceable {
700
787
  this.log.warn(
701
788
  `Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` +
702
789
  `due to mismatched checkpoint hashes at L1 block ${currentL1BlockNumber}. ` +
703
- `Updated L2 latest checkpoint is ${await this.store.getSynchedCheckpointNumber()}.`,
790
+ `Updated L2 latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`,
704
791
  );
705
792
  }
706
793
  }
@@ -717,22 +804,20 @@ export class ArchiverL1Synchronizer implements Traceable {
717
804
 
718
805
  this.log.trace(`Retrieving checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
719
806
 
720
- // TODO(md): Retrieve from blob client then from consensus client, then from peers
721
- const retrievedCheckpoints = await execInSpan(this.tracer, 'Archiver.retrieveCheckpointsFromRollup', () =>
722
- retrieveCheckpointsFromRollup(
807
+ // First fetch calldata only, no blobs yet, since we may be able to just get that data out of the proposed chain
808
+ const calldataCheckpoints = await execInSpan(this.tracer, 'Archiver.retrieveCheckpointCalldataFromRollup', () =>
809
+ retrieveCheckpointCalldataFromRollup(
723
810
  this.rollup,
724
811
  this.publicClient,
725
812
  this.debugClient,
726
- this.blobClient,
727
813
  searchStartBlock, // TODO(palla/reorg): If the L2 reorg was due to an L1 reorg, we need to start search earlier
728
814
  searchEndBlock,
729
815
  this.instrumentation,
730
816
  this.log,
731
- !initialSyncComplete, // isHistoricalSync
732
817
  ),
733
818
  );
734
819
 
735
- if (retrievedCheckpoints.length === 0) {
820
+ if (calldataCheckpoints.length === 0) {
736
821
  // We are not calling `setBlockSynchedL1BlockNumber` because it may cause sync issues if based off infura.
737
822
  // See further details in earlier comments.
738
823
  this.log.trace(`Retrieved no new checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
@@ -740,21 +825,56 @@ export class ArchiverL1Synchronizer implements Traceable {
740
825
  }
741
826
 
742
827
  this.log.debug(
743
- `Retrieved ${retrievedCheckpoints.length} new checkpoints between L1 blocks ${searchStartBlock} and ${searchEndBlock}`,
828
+ `Retrieved ${calldataCheckpoints.length} new checkpoint calldata between L1 blocks ${searchStartBlock} and ${searchEndBlock}`,
744
829
  {
745
- lastProcessedCheckpoint: retrievedCheckpoints[retrievedCheckpoints.length - 1].l1,
830
+ lastProcessedCheckpoint: calldataCheckpoints[calldataCheckpoints.length - 1].l1,
746
831
  searchStartBlock,
747
832
  searchEndBlock,
748
833
  },
749
834
  );
750
835
 
751
- const publishedCheckpoints = await Promise.all(retrievedCheckpoints.map(b => retrievedToPublishedCheckpoint(b)));
836
+ // Check if the last checkpoint matches a local pending entry (so we can skip blob fetch).
837
+ // We only check the last one; if it matches, the blob fetch is skipped for that entry.
838
+ // TODO(palla/pipelining): We may have more than a single checkpoint to promote
839
+ const lastCalldataCheckpoint = calldataCheckpoints[calldataCheckpoints.length - 1];
840
+ const promoteResult = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
841
+ const checkpointToPromote = promoteResult && !('diverged' in promoteResult) ? promoteResult : undefined;
842
+ const evictProposedFrom =
843
+ promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
844
+
845
+ // Then fetch blobs in parallel and build the full published checkpoints
846
+ const toFetchBlobs = checkpointToPromote ? calldataCheckpoints.slice(0, -1) : calldataCheckpoints;
847
+ const blobFetched = await asyncPool(10, toFetchBlobs, async checkpoint =>
848
+ retrievedToPublishedCheckpoint({
849
+ ...checkpoint,
850
+ checkpointBlobData: await getCheckpointBlobDataFromBlobs(
851
+ this.blobClient,
852
+ checkpoint.l1.blockHash,
853
+ checkpoint.blobHashes,
854
+ checkpoint.checkpointNumber,
855
+ this.log,
856
+ !initialSyncComplete,
857
+ checkpoint.parentBeaconBlockRoot,
858
+ checkpoint.l1.timestamp,
859
+ ),
860
+ }),
861
+ );
862
+
863
+ // And add the promoted checkpoint to the list of all checkpoints
864
+ const publishedCheckpoints = checkpointToPromote ? [...blobFetched, checkpointToPromote] : blobFetched;
752
865
  const validCheckpoints: PublishedCheckpoint[] = [];
753
866
 
867
+ // Now loop through all checkpoints and validate their attestations
754
868
  for (const published of publishedCheckpoints) {
755
869
  const validationResult = this.config.skipValidateCheckpointAttestations
756
870
  ? { valid: true as const }
757
- : await validateCheckpointAttestations(published, this.epochCache, this.l1Constants, this.log);
871
+ : await validateCheckpointAttestations(
872
+ published,
873
+ this.epochCache,
874
+ this.l1Constants,
875
+ this.getSignatureContext(),
876
+ this.log,
877
+ );
758
878
 
759
879
  // Only update the validation result if it has changed, so we can keep track of the first invalid checkpoint
760
880
  // in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
@@ -792,7 +912,7 @@ export class ArchiverL1Synchronizer implements Traceable {
792
912
  // Check the inHash of the checkpoint against the l1->l2 messages.
793
913
  // The messages should've been synced up to the currentL1BlockNumber and must be available for the published
794
914
  // checkpoints we just retrieved.
795
- const l1ToL2Messages = await this.store.getL1ToL2Messages(published.checkpoint.number);
915
+ const l1ToL2Messages = await this.stores.messages.getL1ToL2Messages(published.checkpoint.number);
796
916
  const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
797
917
  const publishedInHash = published.checkpoint.header.inHash;
798
918
  if (!computedInHash.equals(publishedInHash)) {
@@ -831,15 +951,35 @@ export class ArchiverL1Synchronizer implements Traceable {
831
951
  try {
832
952
  const updatedValidationResult =
833
953
  rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
954
+
955
+ // Split valid checkpoints: the promoted one (if any) is persisted via the proposed-promotion path,
956
+ // the rest via addCheckpoints. Both paths run within the same store transaction for atomicity.
957
+ const [[maybeValidCheckpointToPromote], checkpointsToAdd] = partition(
958
+ validCheckpoints,
959
+ c => c.checkpoint.number === checkpointToPromote?.checkpoint.number,
960
+ );
961
+
834
962
  const [processDuration, result] = await elapsed(() =>
835
963
  execInSpan(this.tracer, 'Archiver.addCheckpoints', () =>
836
- this.updater.addCheckpoints(validCheckpoints, updatedValidationResult),
964
+ this.updater.addCheckpoints(
965
+ checkpointsToAdd,
966
+ updatedValidationResult,
967
+ maybeValidCheckpointToPromote && {
968
+ l1: lastCalldataCheckpoint.l1,
969
+ attestations: lastCalldataCheckpoint.attestations,
970
+ checkpoint: maybeValidCheckpointToPromote,
971
+ },
972
+ evictProposedFrom,
973
+ ),
837
974
  ),
838
975
  );
839
- this.instrumentation.processNewBlocks(
840
- processDuration / validCheckpoints.length,
841
- validCheckpoints.flatMap(c => c.checkpoint.blocks),
842
- );
976
+
977
+ if (checkpointsToAdd.length > 0) {
978
+ this.instrumentation.processNewCheckpointedBlocks(
979
+ processDuration / checkpointsToAdd.length,
980
+ checkpointsToAdd.flatMap(c => c.checkpoint.blocks),
981
+ );
982
+ }
843
983
 
844
984
  // If blocks were pruned due to conflict with L1 checkpoints, emit event
845
985
  if (result.prunedBlocks && result.prunedBlocks.length > 0) {
@@ -864,10 +1004,10 @@ export class ArchiverL1Synchronizer implements Traceable {
864
1004
  if (err instanceof InitialCheckpointNumberNotSequentialError) {
865
1005
  const { previousCheckpointNumber, newCheckpointNumber } = err;
866
1006
  const previousCheckpoint = previousCheckpointNumber
867
- ? await this.store.getCheckpointData(CheckpointNumber(previousCheckpointNumber))
1007
+ ? await this.stores.blocks.getCheckpointData(CheckpointNumber(previousCheckpointNumber))
868
1008
  : undefined;
869
1009
  const updatedL1SyncPoint = previousCheckpoint?.l1.blockNumber ?? this.l1Constants.l1StartBlock;
870
- await this.store.setCheckpointSynchedL1BlockNumber(updatedL1SyncPoint);
1010
+ await this.stores.blocks.setSynchedL1BlockNumber(updatedL1SyncPoint);
871
1011
  this.log.warn(
872
1012
  `Attempting to insert checkpoint ${newCheckpointNumber} with previous block ${previousCheckpointNumber}. Rolling back L1 sync point to ${updatedL1SyncPoint} to try and fetch the missing blocks.`,
873
1013
  {
@@ -892,7 +1032,7 @@ export class ArchiverL1Synchronizer implements Traceable {
892
1032
  });
893
1033
  }
894
1034
  lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
895
- lastL1BlockWithCheckpoint = retrievedCheckpoints.at(-1)?.l1.blockNumber ?? lastL1BlockWithCheckpoint;
1035
+ lastL1BlockWithCheckpoint = calldataCheckpoints.at(-1)?.l1.blockNumber ?? lastL1BlockWithCheckpoint;
896
1036
  } while (searchEndBlock < currentL1BlockNumber);
897
1037
 
898
1038
  // Important that we update AFTER inserting the blocks.
@@ -901,6 +1041,82 @@ export class ArchiverL1Synchronizer implements Traceable {
901
1041
  return { ...rollupStatus, lastRetrievedCheckpoint, lastL1BlockWithCheckpoint };
902
1042
  }
903
1043
 
1044
+ /**
1045
+ * Checks if a specific checkpoint matches a local pending entry, and if so, loads local data to build
1046
+ * a synthetic published checkpoint (skipping blob fetch).
1047
+ *
1048
+ * Returns { diverged: true, fromCheckpointNumber } when the L1 checkpoint does NOT match local pending
1049
+ * data for that number, so the caller can evict the entire pending suffix >= fromCheckpointNumber
1050
+ * (those entries chain off the now-invalid local state) within the same addCheckpoints transaction.
1051
+ */
1052
+ private async tryBuildPublishedCheckpointFromProposed(
1053
+ calldataCheckpoint: RetrievedCheckpointFromCalldata | undefined,
1054
+ ): Promise<PublishedCheckpoint | { diverged: true; fromCheckpointNumber: CheckpointNumber } | undefined> {
1055
+ if (this.config.skipPromoteProposedCheckpointDuringL1Sync || !calldataCheckpoint) {
1056
+ return undefined;
1057
+ }
1058
+
1059
+ // Look up the specific pending entry for the checkpoint being mined, not just the tip
1060
+ const proposed = await this.stores.blocks.getProposedCheckpointByNumber(calldataCheckpoint.checkpointNumber);
1061
+ if (!proposed) {
1062
+ return undefined;
1063
+ }
1064
+
1065
+ if (
1066
+ !proposed.header.equals(calldataCheckpoint.header) ||
1067
+ !proposed.archive.root.equals(calldataCheckpoint.archiveRoot)
1068
+ ) {
1069
+ this.log.warn(
1070
+ `Local proposed checkpoint ${proposed.checkpointNumber} does not match checkpoint retrieved from L1, overriding with L1 data`,
1071
+ {
1072
+ proposedCheckpointNumber: proposed.checkpointNumber,
1073
+ proposedHeader: proposed.header.toInspect(),
1074
+ proposedArchiveRoot: proposed.archive.root.toString(),
1075
+ calldataCheckpointNumber: calldataCheckpoint.checkpointNumber,
1076
+ calldataHeader: calldataCheckpoint.header.toInspect(),
1077
+ calldataArchiveRoot: calldataCheckpoint.archiveRoot.toString(),
1078
+ },
1079
+ );
1080
+ // Return a divergence signal so the caller can evict pending >= this number
1081
+ return { diverged: true, fromCheckpointNumber: proposed.checkpointNumber };
1082
+ }
1083
+
1084
+ this.log.debug(
1085
+ `Building published checkpoint from proposed ${calldataCheckpoint.checkpointNumber} (skipping blob fetch)`,
1086
+ { proposedHeader: proposed.header.toInspect(), proposedArchiveRoot: proposed.archive.root.toString() },
1087
+ );
1088
+
1089
+ const blocks = await this.stores.blocks.getBlocks(BlockNumber(proposed.startBlock), proposed.blockCount);
1090
+ if (blocks.length !== proposed.blockCount) {
1091
+ this.log.warn(
1092
+ `Local proposed checkpoint ${proposed.checkpointNumber} has wrong block count (expected ${proposed.blockCount} blocks starting at ${proposed.startBlock} but got ${blocks.length})`,
1093
+ {
1094
+ proposedCheckpointNumber: proposed.checkpointNumber,
1095
+ proposedStartBlock: proposed.startBlock,
1096
+ proposedBlockCount: proposed.blockCount,
1097
+ retrievedBlocks: blocks.map(b => b.number),
1098
+ },
1099
+ );
1100
+ return undefined;
1101
+ }
1102
+
1103
+ const checkpoint = Checkpoint.from({
1104
+ archive: proposed.archive,
1105
+ header: proposed.header,
1106
+ blocks,
1107
+ number: proposed.checkpointNumber,
1108
+ feeAssetPriceModifier: proposed.feeAssetPriceModifier,
1109
+ });
1110
+ const promotedCheckpoint = PublishedCheckpoint.from({
1111
+ checkpoint,
1112
+ l1: calldataCheckpoint.l1,
1113
+ attestations: calldataCheckpoint.attestations,
1114
+ });
1115
+ this.instrumentation.processCheckpointPromoted();
1116
+
1117
+ return promotedCheckpoint;
1118
+ }
1119
+
904
1120
  private async checkForNewCheckpointsBeforeL1SyncPoint(
905
1121
  status: RollupStatus,
906
1122
  blocksSynchedTo: bigint,
@@ -910,7 +1126,7 @@ export class ArchiverL1Synchronizer implements Traceable {
910
1126
  // Compare the last checkpoint we have (either retrieved in this round or loaded from store) with what the
911
1127
  // rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
912
1128
  const latestLocalCheckpointNumber =
913
- lastRetrievedCheckpoint?.checkpoint.number ?? (await this.store.getSynchedCheckpointNumber());
1129
+ lastRetrievedCheckpoint?.checkpoint.number ?? (await this.stores.blocks.getLatestCheckpointNumber());
914
1130
  if (latestLocalCheckpointNumber < pendingCheckpointNumber) {
915
1131
  // Here we have consumed all logs until the `currentL1Block` we pinned at the beginning of the archiver loop,
916
1132
  // but still haven't reached the pending checkpoint according to the call to the rollup contract.
@@ -923,7 +1139,9 @@ export class ArchiverL1Synchronizer implements Traceable {
923
1139
  latestLocalCheckpointArchive = lastRetrievedCheckpoint.checkpoint.archive.root.toString();
924
1140
  targetL1BlockNumber = lastRetrievedCheckpoint.l1.blockNumber;
925
1141
  } else if (latestLocalCheckpointNumber > 0) {
926
- const checkpoint = await this.store.getRangeOfCheckpoints(latestLocalCheckpointNumber, 1).then(([c]) => c);
1142
+ const checkpoint = await this.stores.blocks
1143
+ .getRangeOfCheckpoints(latestLocalCheckpointNumber, 1)
1144
+ .then(([c]) => c);
927
1145
  latestLocalCheckpointArchive = checkpoint.archive.root.toString();
928
1146
  targetL1BlockNumber = checkpoint.l1.blockNumber;
929
1147
  }
@@ -938,7 +1156,7 @@ export class ArchiverL1Synchronizer implements Traceable {
938
1156
  ...status,
939
1157
  },
940
1158
  );
941
- await this.store.setCheckpointSynchedL1BlockNumber(targetL1BlockNumber);
1159
+ await this.stores.blocks.setSynchedL1BlockNumber(targetL1BlockNumber);
942
1160
  } else {
943
1161
  this.log.trace(`No new checkpoints behind L1 sync point to retrieve.`, {
944
1162
  latestLocalCheckpointNumber,
@@ -948,7 +1166,7 @@ export class ArchiverL1Synchronizer implements Traceable {
948
1166
  }
949
1167
 
950
1168
  private async getCheckpointHeader(number: CheckpointNumber) {
951
- const checkpoint = await this.store.getCheckpointData(number);
1169
+ const checkpoint = await this.stores.blocks.getCheckpointData(number);
952
1170
  if (!checkpoint) {
953
1171
  return undefined;
954
1172
  }