@aztec/archiver 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a

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 (106) hide show
  1. package/README.md +7 -5
  2. package/dest/archiver.d.ts +49 -14
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +216 -68
  5. package/dest/config.d.ts +4 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +11 -16
  8. package/dest/errors.d.ts +19 -13
  9. package/dest/errors.d.ts.map +1 -1
  10. package/dest/errors.js +32 -16
  11. package/dest/factory.d.ts +12 -6
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +31 -21
  14. package/dest/index.d.ts +9 -2
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +8 -1
  17. package/dest/l1/calldata_retriever.d.ts +2 -1
  18. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  19. package/dest/l1/calldata_retriever.js +13 -4
  20. package/dest/l1/trace_tx.d.ts +12 -66
  21. package/dest/l1/trace_tx.d.ts.map +1 -1
  22. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  23. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  24. package/dest/modules/contract_data_source_adapter.js +40 -0
  25. package/dest/modules/data_source_base.d.ts +71 -49
  26. package/dest/modules/data_source_base.d.ts.map +1 -1
  27. package/dest/modules/data_source_base.js +269 -143
  28. package/dest/modules/data_store_updater.d.ts +18 -6
  29. package/dest/modules/data_store_updater.d.ts.map +1 -1
  30. package/dest/modules/data_store_updater.js +108 -59
  31. package/dest/modules/instrumentation.js +3 -1
  32. package/dest/modules/l1_synchronizer.d.ts +6 -5
  33. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  34. package/dest/modules/l1_synchronizer.js +203 -100
  35. package/dest/modules/outbox_trees_resolver.d.ts +62 -0
  36. package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
  37. package/dest/modules/outbox_trees_resolver.js +162 -0
  38. package/dest/modules/validation.d.ts +4 -3
  39. package/dest/modules/validation.d.ts.map +1 -1
  40. package/dest/modules/validation.js +4 -4
  41. package/dest/store/block_store.d.ts +155 -98
  42. package/dest/store/block_store.d.ts.map +1 -1
  43. package/dest/store/block_store.js +473 -265
  44. package/dest/store/contract_class_store.d.ts +17 -3
  45. package/dest/store/contract_class_store.d.ts.map +1 -1
  46. package/dest/store/contract_class_store.js +17 -1
  47. package/dest/store/contract_instance_store.d.ts +28 -1
  48. package/dest/store/contract_instance_store.d.ts.map +1 -1
  49. package/dest/store/contract_instance_store.js +31 -0
  50. package/dest/store/data_stores.d.ts +68 -0
  51. package/dest/store/data_stores.d.ts.map +1 -0
  52. package/dest/store/data_stores.js +54 -0
  53. package/dest/store/function_names_cache.d.ts +17 -0
  54. package/dest/store/function_names_cache.d.ts.map +1 -0
  55. package/dest/store/function_names_cache.js +30 -0
  56. package/dest/store/l2_tips_cache.d.ts +13 -8
  57. package/dest/store/l2_tips_cache.d.ts.map +1 -1
  58. package/dest/store/l2_tips_cache.js +13 -96
  59. package/dest/store/log_store.d.ts +42 -40
  60. package/dest/store/log_store.d.ts.map +1 -1
  61. package/dest/store/log_store.js +257 -478
  62. package/dest/store/log_store_codec.d.ts +70 -0
  63. package/dest/store/log_store_codec.d.ts.map +1 -0
  64. package/dest/store/log_store_codec.js +101 -0
  65. package/dest/store/message_store.d.ts +9 -3
  66. package/dest/store/message_store.d.ts.map +1 -1
  67. package/dest/store/message_store.js +31 -1
  68. package/dest/test/fake_l1_state.d.ts +2 -1
  69. package/dest/test/fake_l1_state.d.ts.map +1 -1
  70. package/dest/test/fake_l1_state.js +25 -8
  71. package/dest/test/mock_l2_block_source.d.ts +47 -51
  72. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  73. package/dest/test/mock_l2_block_source.js +223 -185
  74. package/dest/test/noop_l1_archiver.d.ts +10 -6
  75. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  76. package/dest/test/noop_l1_archiver.js +19 -8
  77. package/package.json +14 -14
  78. package/src/archiver.ts +265 -72
  79. package/src/config.ts +12 -19
  80. package/src/errors.ts +46 -23
  81. package/src/factory.ts +38 -20
  82. package/src/index.ts +16 -1
  83. package/src/l1/calldata_retriever.ts +14 -4
  84. package/src/modules/contract_data_source_adapter.ts +55 -0
  85. package/src/modules/data_source_base.ts +335 -187
  86. package/src/modules/data_store_updater.ts +118 -61
  87. package/src/modules/instrumentation.ts +1 -1
  88. package/src/modules/l1_synchronizer.ts +278 -120
  89. package/src/modules/outbox_trees_resolver.ts +199 -0
  90. package/src/modules/validation.ts +8 -7
  91. package/src/store/block_store.ts +566 -323
  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 +104 -0
  95. package/src/store/function_names_cache.ts +37 -0
  96. package/src/store/l2_tips_cache.ts +16 -115
  97. package/src/store/log_store.ts +296 -650
  98. package/src/store/log_store_codec.ts +132 -0
  99. package/src/store/message_store.ts +35 -2
  100. package/src/test/fake_l1_state.ts +24 -14
  101. package/src/test/mock_l2_block_source.ts +271 -239
  102. package/src/test/noop_l1_archiver.ts +39 -8
  103. package/dest/store/kv_archiver_store.d.ts +0 -383
  104. package/dest/store/kv_archiver_store.d.ts.map +0 -1
  105. package/dest/store/kv_archiver_store.js +0 -501
  106. package/src/store/kv_archiver_store.ts +0 -728
@@ -8,17 +8,19 @@ import { asyncPool } from '@aztec/foundation/async-pool';
8
8
  import { maxBigint } from '@aztec/foundation/bigint';
9
9
  import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
10
10
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
11
- import { partition, pick } from '@aztec/foundation/collection';
11
+ import { compactArray, partition, pick } from '@aztec/foundation/collection';
12
12
  import { Fr } from '@aztec/foundation/curves/bn254';
13
+ import { EthAddress } from '@aztec/foundation/eth-address';
13
14
  import { type Logger, createLogger } from '@aztec/foundation/log';
14
15
  import { retryTimes } from '@aztec/foundation/retry';
15
16
  import { count } from '@aztec/foundation/string';
16
17
  import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
17
18
  import { isDefined, isErrorClass } from '@aztec/foundation/types';
18
19
  import { type ArchiverEmitter, L2BlockSourceEvents, type ValidateCheckpointResult } from '@aztec/stdlib/block';
19
- import { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
20
+ import { Checkpoint, type CheckpointData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
20
21
  import { type L1RollupConstants, getEpochAtSlot, getSlotAtNextL1Block } from '@aztec/stdlib/epoch-helpers';
21
22
  import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
23
+ import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
22
24
  import { type Traceable, type Tracer, execInSpan, trackSpan } from '@aztec/telemetry-client';
23
25
 
24
26
  import { InitialCheckpointNumberNotSequentialError } from '../errors.js';
@@ -30,7 +32,8 @@ import {
30
32
  retrieveL1ToL2Messages,
31
33
  retrievedToPublishedCheckpoint,
32
34
  } from '../l1/data_retrieval.js';
33
- import type { KVArchiverDataStore } from '../store/kv_archiver_store.js';
35
+ import type { RejectedCheckpoint } from '../store/block_store.js';
36
+ import { type ArchiverDataStores, getArchiverSynchPoint } from '../store/data_stores.js';
34
37
  import type { L2TipsCache } from '../store/l2_tips_cache.js';
35
38
  import { MessageStoreError } from '../store/message_store.js';
36
39
  import type { InboxMessage } from '../structs/inbox_message.js';
@@ -44,13 +47,15 @@ type RollupStatus = {
44
47
  pendingCheckpointNumber: CheckpointNumber;
45
48
  pendingArchive: string;
46
49
  validationResult: ValidateCheckpointResult | undefined;
50
+ /** Last valid checkpoint observed on L1 and synced on this iteration */
47
51
  lastRetrievedCheckpoint?: PublishedCheckpoint;
48
- lastL1BlockWithCheckpoint?: bigint;
52
+ /** Last checkpoint observed on L1 across both valid and rejected entries on this iteration */
53
+ lastSeenCheckpoint?: PublishedCheckpoint;
49
54
  };
50
55
 
51
56
  /**
52
57
  * Handles L1 synchronization for the archiver.
53
- * Responsible for fetching checkpoints, L1L2 messages, and handling L1 reorgs.
58
+ * Responsible for fetching checkpoints, L1 to L2 messages, and handling L1 reorgs.
54
59
  */
55
60
  export class ArchiverL1Synchronizer implements Traceable {
56
61
  private l1BlockNumber: bigint | undefined;
@@ -65,7 +70,7 @@ export class ArchiverL1Synchronizer implements Traceable {
65
70
  private readonly debugClient: ViemPublicDebugClient,
66
71
  private readonly rollup: RollupContract,
67
72
  private readonly inbox: InboxContract,
68
- private readonly store: KVArchiverDataStore,
73
+ private readonly stores: ArchiverDataStores,
69
74
  private config: {
70
75
  batchSize: number;
71
76
  skipValidateCheckpointAttestations?: boolean;
@@ -85,7 +90,7 @@ export class ArchiverL1Synchronizer implements Traceable {
85
90
  l2TipsCache?: L2TipsCache,
86
91
  private readonly log: Logger = createLogger('archiver:l1-sync'),
87
92
  ) {
88
- this.updater = new ArchiverDataStoreUpdater(this.store, l2TipsCache, {
93
+ this.updater = new ArchiverDataStoreUpdater(this.stores, l2TipsCache, {
89
94
  rollupManaLimit: l1Constants.rollupManaLimit,
90
95
  });
91
96
  this.tracer = tracer;
@@ -111,6 +116,13 @@ export class ArchiverL1Synchronizer implements Traceable {
111
116
  return this.l1Timestamp;
112
117
  }
113
118
 
119
+ private getSignatureContext(): CoordinationSignatureContext {
120
+ return {
121
+ chainId: this.publicClient.chain.id,
122
+ rollupAddress: EthAddress.fromString(this.rollup.address),
123
+ };
124
+ }
125
+
114
126
  /** Checks that the ethereum node we are connected to has a latest timestamp no more than the allowed drift. Throw if not. */
115
127
  public async testEthereumNodeSynced(): Promise<void> {
116
128
  const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
@@ -143,24 +155,36 @@ export class ArchiverL1Synchronizer implements Traceable {
143
155
  return;
144
156
  }
145
157
 
146
- // Warn if the latest L1 block timestamp is too old
158
+ // Log at error if the latest L1 block timestamp is too old
147
159
  const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
148
160
  const now = this.dateProvider.nowInSeconds();
149
161
  if (maxAllowedDelay > 0 && Number(currentL1Timestamp) <= now - maxAllowedDelay) {
150
- this.log.warn(
162
+ this.log.error(
151
163
  `Latest L1 block ${currentL1BlockNumber} timestamp ${currentL1Timestamp} is too old. Make sure your Ethereum node is synced.`,
152
164
  { currentL1BlockNumber, currentL1Timestamp, now, maxAllowedDelay },
153
165
  );
154
166
  }
155
167
 
168
+ // Query finalized block on L1
169
+ const rawFinalizedL1Block = await getFinalizedL1Block(this.publicClient);
170
+ const finalizedL1Block: L1BlockId | undefined = rawFinalizedL1Block && {
171
+ l1BlockNumber: rawFinalizedL1Block.number,
172
+ l1BlockHash: Buffer32.fromString(rawFinalizedL1Block.hash),
173
+ };
174
+
156
175
  // Load sync point for blocks defaulting to start block
157
- const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await this.store.getSynchPoint();
158
- this.log.debug(`Starting new archiver sync iteration`, { blocksSynchedTo, currentL1BlockData });
176
+ const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await getArchiverSynchPoint(this.stores);
177
+ this.log.debug(`Starting new archiver sync iteration`, { blocksSynchedTo, currentL1BlockData, finalizedL1Block });
159
178
 
160
179
  // 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.
161
180
  // 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
162
181
  // block, when that wouldn't be the case, since L1 to L2 messages would need another iteration.
163
- await retryTimes(() => this.handleL1ToL2Messages(currentL1BlockData), 'Handling L1 to L2 messages', 3, 0.1);
182
+ await retryTimes(
183
+ () => this.handleL1ToL2Messages(currentL1BlockData, finalizedL1Block),
184
+ 'Handling L1 to L2 messages',
185
+ 3,
186
+ 0.1,
187
+ );
164
188
 
165
189
  if (currentL1BlockNumber > blocksSynchedTo) {
166
190
  // First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
@@ -181,18 +205,10 @@ export class ArchiverL1Synchronizer implements Traceable {
181
205
  currentL1Timestamp,
182
206
  );
183
207
 
184
- // If the last checkpoint we processed had an invalid attestation, we manually advance the L1 syncpoint
185
- // past it, since otherwise we'll keep downloading it and reprocessing it on every iteration until
186
- // we get a valid checkpoint to advance the syncpoint.
187
- if (!rollupStatus.validationResult?.valid && rollupStatus.lastL1BlockWithCheckpoint !== undefined) {
188
- await this.store.setCheckpointSynchedL1BlockNumber(rollupStatus.lastL1BlockWithCheckpoint);
189
- }
190
-
191
208
  // And lastly we check if we are missing any checkpoints behind us due to a possible L1 reorg.
192
209
  // We only do this if rollup cant prune on the next submission. Otherwise we will end up
193
- // re-syncing the checkpoints we have just unwound above. We also dont do this if the last checkpoint is invalid,
194
- // since the archiver will rightfully refuse to sync up to it.
195
- if (!rollupCanPrune && rollupStatus.validationResult?.valid) {
210
+ // re-syncing the checkpoints we have just unwound above.
211
+ if (!rollupCanPrune) {
196
212
  await this.checkForNewCheckpointsBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
197
213
  }
198
214
 
@@ -200,7 +216,7 @@ export class ArchiverL1Synchronizer implements Traceable {
200
216
  }
201
217
 
202
218
  // Update the finalized L2 checkpoint based on L1 finality.
203
- await this.updateFinalizedCheckpoint();
219
+ await this.updateFinalizedCheckpoint(finalizedL1Block);
204
220
 
205
221
  // After syncing has completed, update the current l1 block number and timestamp,
206
222
  // otherwise we risk announcing to the world that we've synced to a given point,
@@ -217,19 +233,18 @@ export class ArchiverL1Synchronizer implements Traceable {
217
233
  });
218
234
  }
219
235
 
220
- /** Query L1 for its finalized block and update the finalized checkpoint accordingly. */
221
- private async updateFinalizedCheckpoint(): Promise<void> {
236
+ /** Updates the finalized checkpoint using the pre-fetched finalized L1 block from the current sync iteration. */
237
+ private async updateFinalizedCheckpoint(finalizedL1Block: L1BlockId | undefined): Promise<void> {
222
238
  try {
223
- const finalizedL1Block = await getFinalizedL1Block(this.publicClient);
224
239
  if (!finalizedL1Block) {
225
240
  this.log.trace(`Skipping finalized checkpoint update: L1 has no finalized block yet.`);
226
241
  return;
227
242
  }
228
- const finalizedL1BlockNumber = finalizedL1Block.number;
243
+ const finalizedL1BlockNumber = finalizedL1Block.l1BlockNumber;
229
244
  const finalizedCheckpointNumber = await this.rollup.getProvenCheckpointNumber({
230
245
  blockNumber: finalizedL1BlockNumber,
231
246
  });
232
- const localFinalizedCheckpointNumber = await this.store.getFinalizedCheckpointNumber();
247
+ const localFinalizedCheckpointNumber = await this.stores.blocks.getFinalizedCheckpointNumber();
233
248
  if (localFinalizedCheckpointNumber !== finalizedCheckpointNumber) {
234
249
  await this.updater.setFinalizedCheckpointNumber(finalizedCheckpointNumber);
235
250
  this.log.info(`Updated finalized chain to checkpoint ${finalizedCheckpointNumber}`, {
@@ -248,8 +263,8 @@ export class ArchiverL1Synchronizer implements Traceable {
248
263
  /** Prune all proposed local blocks that should have been checkpointed by now. */
249
264
  private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) {
250
265
  const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
251
- this.store.getCheckpointedL2BlockNumber(),
252
- this.store.getLatestBlockNumber(),
266
+ this.stores.blocks.getCheckpointedL2BlockNumber(),
267
+ this.stores.blocks.getLatestL2BlockNumber(),
253
268
  ]);
254
269
 
255
270
  // If there are no uncheckpointed blocks, we got nothing to do
@@ -263,8 +278,10 @@ export class ArchiverL1Synchronizer implements Traceable {
263
278
  const firstUncheckpointedBlockNumber = BlockNumber(lastCheckpointedBlockNumber + 1);
264
279
 
265
280
  // What's the slot of the first uncheckpointed block?
266
- const [firstUncheckpointedBlockHeader] = await this.store.getBlockHeaders(firstUncheckpointedBlockNumber, 1);
267
- const firstUncheckpointedBlockSlot = firstUncheckpointedBlockHeader?.getSlot();
281
+ const firstUncheckpointedBlockData = await this.stores.blocks.getBlockData({
282
+ number: firstUncheckpointedBlockNumber,
283
+ });
284
+ const firstUncheckpointedBlockSlot = firstUncheckpointedBlockData?.header.getSlot();
268
285
 
269
286
  if (firstUncheckpointedBlockSlot === undefined || firstUncheckpointedBlockSlot >= slotAtNextL1Block) {
270
287
  return;
@@ -274,11 +291,10 @@ export class ArchiverL1Synchronizer implements Traceable {
274
291
  // This also clears any proposed checkpoint whose blocks are being pruned.
275
292
  this.log.warn(
276
293
  `Pruning blocks after block ${lastCheckpointedBlockNumber} due to slot ${firstUncheckpointedBlockSlot} not being checkpointed`,
277
- { firstUncheckpointedBlockHeader: firstUncheckpointedBlockHeader.toInspect(), slotAtNextL1Block },
294
+ { firstUncheckpointedBlockHeader: firstUncheckpointedBlockData?.header.toInspect(), slotAtNextL1Block },
278
295
  );
279
296
 
280
297
  const prunedBlocks = await this.updater.removeUncheckpointedBlocksAfter(lastCheckpointedBlockNumber);
281
-
282
298
  if (prunedBlocks.length > 0) {
283
299
  this.events.emit(L2BlockSourceEvents.L2PruneUncheckpointed, {
284
300
  type: L2BlockSourceEvents.L2PruneUncheckpointed,
@@ -310,7 +326,7 @@ export class ArchiverL1Synchronizer implements Traceable {
310
326
  currentL1Timestamp: bigint,
311
327
  ): Promise<{ rollupCanPrune: boolean }> {
312
328
  const rollupCanPrune = await this.canPrune(currentL1BlockNumber, currentL1Timestamp);
313
- const localPendingCheckpointNumber = await this.store.getSynchedCheckpointNumber();
329
+ const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
314
330
  const canPrune = localPendingCheckpointNumber > provenCheckpointNumber && rollupCanPrune;
315
331
 
316
332
  if (canPrune) {
@@ -331,12 +347,12 @@ export class ArchiverL1Synchronizer implements Traceable {
331
347
  // promises when the gap between local pending and proven checkpoint numbers is large.
332
348
  const BATCH_SIZE = 10;
333
349
  const indices = Array.from({ length: checkpointsToUnwind }, (_, i) => CheckpointNumber(i + pruneFrom));
334
- const checkpoints = (await asyncPool(BATCH_SIZE, indices, idx => this.store.getCheckpointData(idx))).filter(
335
- isDefined,
336
- );
350
+ const checkpoints = (
351
+ await asyncPool(BATCH_SIZE, indices, idx => this.stores.blocks.getCheckpointData(idx))
352
+ ).filter(isDefined);
337
353
  const newBlocks = (
338
354
  await asyncPool(BATCH_SIZE, checkpoints, cp =>
339
- this.store.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)),
355
+ this.stores.blocks.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)),
340
356
  )
341
357
  )
342
358
  .filter(isDefined)
@@ -356,12 +372,12 @@ export class ArchiverL1Synchronizer implements Traceable {
356
372
  this.log.warn(
357
373
  `Removed ${count(checkpointsToUnwind, 'checkpoint')} after checkpoint ${provenCheckpointNumber} ` +
358
374
  `due to predicted reorg at L1 block ${currentL1BlockNumber}. ` +
359
- `Updated latest checkpoint is ${await this.store.getSynchedCheckpointNumber()}.`,
375
+ `Updated latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`,
360
376
  );
361
377
  this.instrumentation.processPrune(timer.ms());
362
378
  // TODO(palla/reorg): Do we need to set the block synched L1 block number here?
363
379
  // Seems like the next iteration should handle this.
364
- // await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
380
+ // await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
365
381
  }
366
382
 
367
383
  return { rollupCanPrune };
@@ -378,14 +394,17 @@ export class ArchiverL1Synchronizer implements Traceable {
378
394
  }
379
395
 
380
396
  @trackSpan('Archiver.handleL1ToL2Messages')
381
- private async handleL1ToL2Messages(currentL1Block: L1BlockId): Promise<boolean> {
397
+ private async handleL1ToL2Messages(
398
+ currentL1Block: L1BlockId,
399
+ finalizedL1Block: L1BlockId | undefined,
400
+ ): Promise<boolean> {
382
401
  // Load the syncpoint, which may have been updated in a previous iteration
383
402
  const {
384
403
  messagesSynchedTo = {
385
404
  l1BlockNumber: this.l1Constants.l1StartBlock,
386
405
  l1BlockHash: this.l1Constants.l1StartBlockHash,
387
406
  },
388
- } = await this.store.getSynchPoint();
407
+ } = await getArchiverSynchPoint(this.stores);
389
408
 
390
409
  // Nothing to do if L1 block number has not moved forward
391
410
  const currentL1BlockNumber = currentL1Block.l1BlockNumber;
@@ -395,10 +414,14 @@ export class ArchiverL1Synchronizer implements Traceable {
395
414
 
396
415
  // Compare local message store state with the remote. If they match, we just advance the match pointer.
397
416
  const remoteMessagesState = await this.inbox.getState({ blockNumber: currentL1BlockNumber });
398
- const localLastMessage = await this.store.getLastL1ToL2Message();
417
+ const localLastMessage = await this.stores.messages.getLastMessage();
399
418
  if (await this.localStateMatches(localLastMessage, remoteMessagesState)) {
400
419
  this.log.trace(`Local L1 to L2 messages are already in sync with remote at L1 block ${currentL1BlockNumber}`);
401
- await this.store.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress);
420
+ await this.stores.messages.setMessageSyncState(
421
+ currentL1Block,
422
+ remoteMessagesState.treeInProgress,
423
+ finalizedL1Block,
424
+ );
402
425
  return true;
403
426
  }
404
427
 
@@ -423,7 +446,7 @@ export class ArchiverL1Synchronizer implements Traceable {
423
446
  // Note that, if there are no new messages to insert, but there was an L1 reorg that pruned out last messages,
424
447
  // we'd notice by comparing our local state with the remote one again, and seeing they don't match even after
425
448
  // our sync attempt. In this case, we also rollback our syncpoint, and trigger a retry.
426
- const localLastMessageAfterSync = await this.store.getLastL1ToL2Message();
449
+ const localLastMessageAfterSync = await this.stores.messages.getLastMessage();
427
450
  if (!(await this.localStateMatches(localLastMessageAfterSync, remoteMessagesState))) {
428
451
  this.log.warn(
429
452
  `Local L1 to L2 messages state does not match remote after sync attempt. Rolling back syncpoint to retry.`,
@@ -434,13 +457,17 @@ export class ArchiverL1Synchronizer implements Traceable {
434
457
  }
435
458
 
436
459
  // Advance the syncpoint after a successful sync
437
- await this.store.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress);
460
+ await this.stores.messages.setMessageSyncState(
461
+ currentL1Block,
462
+ remoteMessagesState.treeInProgress,
463
+ finalizedL1Block,
464
+ );
438
465
  return true;
439
466
  }
440
467
 
441
468
  /** Checks if the local rolling hash and message count matches the remote state */
442
469
  private async localStateMatches(localLastMessage: InboxMessage | undefined, remoteState: InboxContractState) {
443
- const localMessageCount = await this.store.getTotalL1ToL2MessageCount();
470
+ const localMessageCount = await this.stores.messages.getTotalL1ToL2MessageCount();
444
471
  this.log.trace(`Comparing local and remote inbox state`, { localMessageCount, localLastMessage, remoteState });
445
472
 
446
473
  return (
@@ -462,7 +489,7 @@ export class ArchiverL1Synchronizer implements Traceable {
462
489
  this.log.trace(`Retrieving L1 to L2 messages in L1 blocks ${searchStartBlock}-${searchEndBlock}`);
463
490
  const messages = await retrieveL1ToL2Messages(this.inbox, searchStartBlock, searchEndBlock);
464
491
  const timer = new Timer();
465
- await this.store.addL1ToL2Messages(messages);
492
+ await this.stores.messages.addL1ToL2Messages(messages);
466
493
  const perMsg = timer.ms() / messages.length;
467
494
  this.instrumentation.processNewMessages(messages.length, perMsg);
468
495
  for (const msg of messages) {
@@ -487,13 +514,16 @@ export class ArchiverL1Synchronizer implements Traceable {
487
514
  private async rollbackL1ToL2Messages(remoteMessagesState: InboxContractState): Promise<L1BlockId> {
488
515
  const { treeInProgress: remoteTreeInProgress, messagesRollingHash: remoteRollingHash } = remoteMessagesState;
489
516
 
517
+ const messagesFinalizedL1Block = await this.stores.messages.getMessagesFinalizedL1Block();
518
+ const finalizedL1BlockNumber = messagesFinalizedL1Block?.l1BlockNumber;
519
+
490
520
  // Slowly go back through our messages until we find the last common message. We could query the logs in
491
521
  // batch as an optimization, but the depth of the reorg should not be deep, and this is a very rare case,
492
522
  // so it's fine to query one log at a time.
493
523
  let commonMsg: undefined | InboxMessage;
494
524
  let messagesToDelete = 0;
495
525
  this.log.verbose(`Searching most recent common L1 to L2 message`);
496
- for await (const localMsg of this.store.iterateL1ToL2Messages({ reverse: true })) {
526
+ for await (const localMsg of this.stores.messages.iterateL1ToL2Messages({ reverse: true })) {
497
527
  const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteMessagesState };
498
528
 
499
529
  // First check if the local message rolling hash matches the current rolling hash of the inbox contract,
@@ -508,6 +538,13 @@ export class ArchiverL1Synchronizer implements Traceable {
508
538
  break;
509
539
  }
510
540
 
541
+ // Messages at or below the finalized L1 block cannot have been reorged — accept as common without querying L1.
542
+ if (finalizedL1BlockNumber !== undefined && localMsg.l1BlockNumber <= finalizedL1BlockNumber) {
543
+ this.log.info(`Found common L1 to L2 message at finalized L1 block ${localMsg.l1BlockNumber}`, logCtx);
544
+ commonMsg = localMsg;
545
+ break;
546
+ }
547
+
511
548
  // If there's no match with the current remote state, check if the message exists on the inbox contract at all
512
549
  // by looking at the inbox events. If the L1 reorg *added* new messages in addition to deleting existing ones,
513
550
  // then the current remote state's rolling hash will not match anything we have locally, so we need to check existence
@@ -536,17 +573,29 @@ export class ArchiverL1Synchronizer implements Traceable {
536
573
  if (messagesToDelete > 0) {
537
574
  const lastGoodIndex = commonMsg?.index;
538
575
  this.log.warn(`Rolling back all local L1 to L2 messages after index ${lastGoodIndex ?? 'initial'}`);
539
- await this.store.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
576
+ await this.stores.messages.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
540
577
  }
541
578
 
542
579
  // Update the syncpoint so the loop below reprocesses the changed messages. We go to the block before
543
580
  // the last common one, so we force reprocessing it, in case new messages were added on that same L1 block
544
- // after the last common message.
545
- const syncPointL1BlockNumber = commonMsg ? commonMsg.l1BlockNumber - 1n : this.l1Constants.l1StartBlock;
546
- const syncPointL1BlockHash = await this.getL1BlockHash(syncPointL1BlockNumber);
581
+ // after the last common message. Cap at the finalized L1 block: messages at or below finalized cannot
582
+ // have been reorged, so there is no need to walk back any further than that.
583
+ const syncPointL1BlockNumber = maxBigint(
584
+ ...compactArray([
585
+ commonMsg ? commonMsg.l1BlockNumber - 1n : undefined,
586
+ finalizedL1BlockNumber,
587
+ this.l1Constants.l1StartBlock,
588
+ ]),
589
+ );
590
+
591
+ const syncPointL1BlockHash =
592
+ syncPointL1BlockNumber === finalizedL1BlockNumber
593
+ ? messagesFinalizedL1Block!.l1BlockHash
594
+ : await this.getL1BlockHash(syncPointL1BlockNumber);
595
+
547
596
  const messagesSyncPoint = { l1BlockNumber: syncPointL1BlockNumber, l1BlockHash: syncPointL1BlockHash };
548
- await this.store.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
549
- this.log.verbose(`Updated messages syncpoint to L1 block ${syncPointL1BlockNumber}`, {
597
+ await this.stores.messages.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
598
+ this.log.verbose(`Updated messages syncpoint to L1 block ${messagesSyncPoint.l1BlockNumber}`, {
550
599
  ...messagesSyncPoint,
551
600
  remoteTreeInProgress,
552
601
  });
@@ -567,9 +616,9 @@ export class ArchiverL1Synchronizer implements Traceable {
567
616
  currentL1BlockNumber: bigint,
568
617
  initialSyncComplete: boolean,
569
618
  ): Promise<RollupStatus> {
570
- const localPendingCheckpointNumber = await this.store.getSynchedCheckpointNumber();
619
+ const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
571
620
  const initialValidationResult: ValidateCheckpointResult | undefined =
572
- await this.store.getPendingChainValidationStatus();
621
+ await this.stores.blocks.getPendingChainValidationStatus();
573
622
  const {
574
623
  provenCheckpointNumber,
575
624
  provenArchive,
@@ -599,7 +648,7 @@ export class ArchiverL1Synchronizer implements Traceable {
599
648
  // we need to set it to zero. This is an edge case because we dont have a checkpoint zero (initial checkpoint is one),
600
649
  // so localCheckpointForDestinationProvenCheckpointNumber would not be found below.
601
650
  if (provenCheckpointNumber === 0) {
602
- const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber();
651
+ const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
603
652
  if (localProvenCheckpointNumber !== provenCheckpointNumber) {
604
653
  await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
605
654
  this.log.info(`Rolled back proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
@@ -607,11 +656,11 @@ export class ArchiverL1Synchronizer implements Traceable {
607
656
  }
608
657
 
609
658
  const localCheckpointForDestinationProvenCheckpointNumber =
610
- await this.store.getCheckpointData(provenCheckpointNumber);
659
+ await this.stores.blocks.getCheckpointData(provenCheckpointNumber);
611
660
 
612
661
  // Sanity check. I've hit what seems to be a state where the proven checkpoint is set to a value greater than the latest
613
662
  // synched checkpoint when requesting L2Tips from the archiver. This is the only place where the proven checkpoint is set.
614
- const synched = await this.store.getSynchedCheckpointNumber();
663
+ const synched = await this.stores.blocks.getLatestCheckpointNumber();
615
664
  if (
616
665
  localCheckpointForDestinationProvenCheckpointNumber &&
617
666
  synched < localCheckpointForDestinationProvenCheckpointNumber.checkpointNumber
@@ -631,7 +680,7 @@ export class ArchiverL1Synchronizer implements Traceable {
631
680
  localCheckpointForDestinationProvenCheckpointNumber &&
632
681
  provenArchive.equals(localCheckpointForDestinationProvenCheckpointNumber.archive.root)
633
682
  ) {
634
- const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber();
683
+ const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
635
684
  if (localProvenCheckpointNumber !== provenCheckpointNumber) {
636
685
  await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
637
686
  this.log.info(`Updated proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
@@ -659,7 +708,7 @@ export class ArchiverL1Synchronizer implements Traceable {
659
708
  // If we have 0 checkpoints locally and there are no checkpoints onchain there is nothing to do.
660
709
  const noCheckpoints = localPendingCheckpointNumber === 0 && pendingCheckpointNumber === 0;
661
710
  if (noCheckpoints) {
662
- await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
711
+ await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
663
712
  this.log.debug(
664
713
  `No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}, no checkpoints on chain`,
665
714
  );
@@ -671,7 +720,7 @@ export class ArchiverL1Synchronizer implements Traceable {
671
720
  // Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there
672
721
  // are any state that could be impacted by it. If we have no checkpoints, there is no impact.
673
722
  if (localPendingCheckpointNumber > 0) {
674
- const localPendingCheckpoint = await this.store.getCheckpointData(localPendingCheckpointNumber);
723
+ const localPendingCheckpoint = await this.stores.blocks.getCheckpointData(localPendingCheckpointNumber);
675
724
  if (localPendingCheckpoint === undefined) {
676
725
  throw new Error(`Missing checkpoint ${localPendingCheckpointNumber}`);
677
726
  }
@@ -686,7 +735,7 @@ export class ArchiverL1Synchronizer implements Traceable {
686
735
  // However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing checkpoints.
687
736
  // We must only set this block number based on actually retrieved logs.
688
737
  // TODO(#8621): Tackle this properly when we handle L1 Re-orgs.
689
- // await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
738
+ // await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
690
739
  this.log.debug(`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
691
740
  return rollupStatus;
692
741
  }
@@ -706,7 +755,7 @@ export class ArchiverL1Synchronizer implements Traceable {
706
755
 
707
756
  let tipAfterUnwind = localPendingCheckpointNumber;
708
757
  while (true) {
709
- const candidateCheckpoint = await this.store.getCheckpointData(tipAfterUnwind);
758
+ const candidateCheckpoint = await this.stores.blocks.getCheckpointData(tipAfterUnwind);
710
759
  if (candidateCheckpoint === undefined) {
711
760
  break;
712
761
  }
@@ -731,7 +780,7 @@ export class ArchiverL1Synchronizer implements Traceable {
731
780
  this.log.warn(
732
781
  `Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` +
733
782
  `due to mismatched checkpoint hashes at L1 block ${currentL1BlockNumber}. ` +
734
- `Updated L2 latest checkpoint is ${await this.store.getSynchedCheckpointNumber()}.`,
783
+ `Updated L2 latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`,
735
784
  );
736
785
  }
737
786
  }
@@ -741,7 +790,7 @@ export class ArchiverL1Synchronizer implements Traceable {
741
790
  let searchStartBlock: bigint = blocksSynchedTo;
742
791
  let searchEndBlock: bigint = blocksSynchedTo;
743
792
  let lastRetrievedCheckpoint: PublishedCheckpoint | undefined;
744
- let lastL1BlockWithCheckpoint: bigint | undefined = undefined;
793
+ let lastSeenCheckpoint: PublishedCheckpoint | undefined;
745
794
 
746
795
  do {
747
796
  [searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
@@ -777,11 +826,14 @@ export class ArchiverL1Synchronizer implements Traceable {
777
826
  },
778
827
  );
779
828
 
780
- // Check if the last checkpoint matches the proposed one (so we can skip blob fetch).
781
- // We only check the last one because the proposed checkpoint is always the most recent one,
782
- // and if it's in a multi-checkpoint batch it will always be last (sorted by L1 block number).
829
+ // Check if the last checkpoint matches a local pending entry (so we can skip blob fetch).
830
+ // We only check the last one; if it matches, the blob fetch is skipped for that entry.
831
+ // TODO(palla/pipelining): We may have more than a single checkpoint to promote
783
832
  const lastCalldataCheckpoint = calldataCheckpoints[calldataCheckpoints.length - 1];
784
- const checkpointToPromote = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
833
+ const promoteResult = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
834
+ const checkpointToPromote = promoteResult && !('diverged' in promoteResult) ? promoteResult : undefined;
835
+ const evictProposedFrom =
836
+ promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
785
837
 
786
838
  // Then fetch blobs in parallel and build the full published checkpoints
787
839
  const toFetchBlobs = checkpointToPromote ? calldataCheckpoints.slice(0, -1) : calldataCheckpoints;
@@ -807,21 +859,42 @@ export class ArchiverL1Synchronizer implements Traceable {
807
859
 
808
860
  // Now loop through all checkpoints and validate their attestations
809
861
  for (const published of publishedCheckpoints) {
862
+ // Check the attestations uploaded by the publisher to L1 are correct
863
+ // Rollup contract does not validate attestations to save on gas, so this
864
+ // falls on the nodes to verify offchain and skip those checkpoints.
810
865
  const validationResult = this.config.skipValidateCheckpointAttestations
811
866
  ? { valid: true as const }
812
- : await validateCheckpointAttestations(published, this.epochCache, this.l1Constants, this.log);
867
+ : await validateCheckpointAttestations(
868
+ published,
869
+ this.epochCache,
870
+ this.l1Constants,
871
+ this.getSignatureContext(),
872
+ this.log,
873
+ );
874
+
875
+ // Also skip the checkpoint if it builds on a previously-rejected ancestor. Without
876
+ // this, addCheckpoints would throw InitialCheckpointNumberNotSequentialError when the
877
+ // ancestor was skipped earlier (e.g. due to invalid attestations), the catch handler
878
+ // would roll back the L1 sync point, and the next iteration would re-fetch and re-throw.
879
+ const rejectedAncestor = await this.stores.blocks.getRejectedCheckpointByArchiveRoot(
880
+ published.checkpoint.header.lastArchiveRoot,
881
+ );
813
882
 
814
- // Only update the validation result if it has changed, so we can keep track of the first invalid checkpoint
883
+ // Update the validation result if it has changed, so we can keep track of the first invalid checkpoint
815
884
  // in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
816
885
  // There is an exception though: if a checkpoint is invalidated and replaced with another invalid checkpoint,
817
886
  // we need to update the validation result, since we need to be able to invalidate the new one.
818
887
  // See test 'chain progresses if an invalid checkpoint is invalidated with an invalid one' for more info.
819
- if (
820
- rollupStatus.validationResult?.valid !== validationResult.valid ||
821
- (!rollupStatus.validationResult.valid &&
822
- !validationResult.valid &&
823
- rollupStatus.validationResult.checkpoint.checkpointNumber === validationResult.checkpoint.checkpointNumber)
824
- ) {
888
+ // Do not update the validation result if there is a rejected ancestor, since in that case we want to keep the
889
+ // original invalidation, as the new checkpoint is extending from a previous invalid one.
890
+ const validStatusChanged = rollupStatus.validationResult?.valid !== validationResult.valid;
891
+ const invalidStatusWithSameCheckpointNumber =
892
+ !validationResult.valid &&
893
+ rollupStatus.validationResult &&
894
+ !rollupStatus.validationResult.valid &&
895
+ rollupStatus.validationResult.checkpoint.checkpointNumber === validationResult.checkpoint.checkpointNumber;
896
+
897
+ if (!rejectedAncestor && (validStatusChanged || invalidStatusWithSameCheckpointNumber)) {
825
898
  rollupStatus.validationResult = validationResult;
826
899
  }
827
900
 
@@ -838,16 +911,62 @@ export class ArchiverL1Synchronizer implements Traceable {
838
911
  validationResult,
839
912
  });
840
913
 
841
- // We keep consuming checkpoints if we find an invalid one, since we do not listen for CheckpointInvalidated events
842
- // We just pretend the invalid ones are not there and keep consuming the next checkpoints
843
- // Note that this breaks if the committee ever attests to a descendant of an invalid checkpoint
914
+ // Persist a rejected-ancestor entry so any later checkpoint that builds on this one
915
+ // is detected and skipped (rather than tripping the addCheckpoints consecutive-number
916
+ // check and causing the sync point to roll back in a loop).
917
+ await this.stores.blocks.addRejectedCheckpoint({
918
+ checkpointNumber: published.checkpoint.number,
919
+ archiveRoot: published.checkpoint.archive.root,
920
+ parentArchiveRoot: published.checkpoint.header.lastArchiveRoot,
921
+ slotNumber: published.checkpoint.header.slotNumber,
922
+ l1: published.l1,
923
+ reason: 'invalid-attestations' as const,
924
+ });
925
+
926
+ continue;
927
+ }
928
+
929
+ if (rejectedAncestor) {
930
+ const descendantInfo = published.checkpoint.toCheckpointInfo();
931
+ this.log.warn(
932
+ `Skipping checkpoint ${published.checkpoint.number} as it is a descendant of ` +
933
+ `rejected checkpoint ${rejectedAncestor.checkpointNumber} (${rejectedAncestor.reason})`,
934
+ {
935
+ checkpointNumber: published.checkpoint.number,
936
+ checkpointHash: published.checkpoint.hash(),
937
+ l1BlockNumber: published.l1.blockNumber,
938
+ l1BlockHash: published.l1.blockHash,
939
+ ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
940
+ ancestorArchiveRoot: rejectedAncestor.archiveRoot.toString(),
941
+ ancestorReason: rejectedAncestor.reason,
942
+ },
943
+ );
944
+
945
+ this.events.emit(L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected, {
946
+ type: L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected,
947
+ checkpoint: descendantInfo,
948
+ ancestorArchiveRoot: rejectedAncestor.archiveRoot,
949
+ ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
950
+ });
951
+
952
+ // Persist this chainpoint as rejected as well, so we can construct a chain of
953
+ // skipped checkpoints starting from the first one with invalid attestations.
954
+ await this.stores.blocks.addRejectedCheckpoint({
955
+ checkpointNumber: published.checkpoint.number,
956
+ archiveRoot: published.checkpoint.archive.root,
957
+ parentArchiveRoot: published.checkpoint.header.lastArchiveRoot,
958
+ slotNumber: published.checkpoint.header.slotNumber,
959
+ l1: published.l1,
960
+ reason: 'descends-from-invalid-attestations' as const,
961
+ });
962
+
844
963
  continue;
845
964
  }
846
965
 
847
966
  // Check the inHash of the checkpoint against the l1->l2 messages.
848
967
  // The messages should've been synced up to the currentL1BlockNumber and must be available for the published
849
968
  // checkpoints we just retrieved.
850
- const l1ToL2Messages = await this.store.getL1ToL2Messages(published.checkpoint.number);
969
+ const l1ToL2Messages = await this.stores.messages.getL1ToL2Messages(published.checkpoint.number);
851
970
  const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
852
971
  const publishedInHash = published.checkpoint.header.inHash;
853
972
  if (!computedInHash.equals(publishedInHash)) {
@@ -904,14 +1023,15 @@ export class ArchiverL1Synchronizer implements Traceable {
904
1023
  attestations: lastCalldataCheckpoint.attestations,
905
1024
  checkpoint: maybeValidCheckpointToPromote,
906
1025
  },
1026
+ evictProposedFrom,
907
1027
  ),
908
1028
  ),
909
1029
  );
910
1030
 
911
- if (checkpointsToAdd.length > 0) {
1031
+ if (validCheckpoints.length > 0) {
912
1032
  this.instrumentation.processNewCheckpointedBlocks(
913
- processDuration / checkpointsToAdd.length,
914
- checkpointsToAdd.flatMap(c => c.checkpoint.blocks),
1033
+ processDuration / validCheckpoints.length,
1034
+ validCheckpoints.flatMap(c => c.checkpoint.blocks),
915
1035
  );
916
1036
  }
917
1037
 
@@ -938,14 +1058,23 @@ export class ArchiverL1Synchronizer implements Traceable {
938
1058
  if (err instanceof InitialCheckpointNumberNotSequentialError) {
939
1059
  const { previousCheckpointNumber, newCheckpointNumber } = err;
940
1060
  const previousCheckpoint = previousCheckpointNumber
941
- ? await this.store.getCheckpointData(CheckpointNumber(previousCheckpointNumber))
1061
+ ? await this.stores.blocks.getCheckpointData(CheckpointNumber(previousCheckpointNumber))
942
1062
  : undefined;
943
- const updatedL1SyncPoint = previousCheckpoint?.l1.blockNumber ?? this.l1Constants.l1StartBlock;
944
- await this.store.setCheckpointSynchedL1BlockNumber(updatedL1SyncPoint);
1063
+ const lastFinalizedCheckpoint = await this.stores.blocks.getCheckpointData(
1064
+ await this.stores.blocks.getFinalizedCheckpointNumber(),
1065
+ );
1066
+ const updatedL1SyncPoint =
1067
+ previousCheckpoint?.l1.blockNumber ??
1068
+ lastFinalizedCheckpoint?.l1.blockNumber ??
1069
+ this.l1Constants.l1StartBlock;
1070
+ await this.stores.blocks.setSynchedL1BlockNumber(updatedL1SyncPoint);
945
1071
  this.log.warn(
946
1072
  `Attempting to insert checkpoint ${newCheckpointNumber} with previous block ${previousCheckpointNumber}. Rolling back L1 sync point to ${updatedL1SyncPoint} to try and fetch the missing blocks.`,
947
1073
  {
948
1074
  previousCheckpointNumber,
1075
+ previousCheckpoint: previousCheckpoint?.header.toInspect(),
1076
+ lastFinalizedCheckpoint: lastFinalizedCheckpoint?.header.toInspect(),
1077
+ l1StartBlock: this.l1Constants.l1StartBlock,
949
1078
  newCheckpointNumber,
950
1079
  updatedL1SyncPoint,
951
1080
  },
@@ -966,26 +1095,33 @@ export class ArchiverL1Synchronizer implements Traceable {
966
1095
  });
967
1096
  }
968
1097
  lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
969
- lastL1BlockWithCheckpoint = calldataCheckpoints.at(-1)?.l1.blockNumber ?? lastL1BlockWithCheckpoint;
1098
+ lastSeenCheckpoint = publishedCheckpoints.at(-1) ?? lastSeenCheckpoint;
970
1099
  } while (searchEndBlock < currentL1BlockNumber);
971
1100
 
972
1101
  // Important that we update AFTER inserting the blocks.
973
1102
  await updateProvenCheckpoint();
974
1103
 
975
- return { ...rollupStatus, lastRetrievedCheckpoint, lastL1BlockWithCheckpoint };
1104
+ return { ...rollupStatus, lastRetrievedCheckpoint, lastSeenCheckpoint };
976
1105
  }
977
1106
 
978
- /** Checks if this checkpoint matches the local proposed one, and if so, loads local data to build a synthetic published checkpoint. */
1107
+ /**
1108
+ * Checks if a specific checkpoint matches a local pending entry, and if so, loads local data to build
1109
+ * a synthetic published checkpoint (skipping blob fetch).
1110
+ *
1111
+ * Returns { diverged: true, fromCheckpointNumber } when the L1 checkpoint does NOT match local pending
1112
+ * data for that number, so the caller can evict the entire pending suffix >= fromCheckpointNumber
1113
+ * (those entries chain off the now-invalid local state) within the same addCheckpoints transaction.
1114
+ */
979
1115
  private async tryBuildPublishedCheckpointFromProposed(
980
1116
  calldataCheckpoint: RetrievedCheckpointFromCalldata | undefined,
981
- ): Promise<PublishedCheckpoint | undefined> {
982
- const proposed = await this.store.getProposedCheckpointOnly();
983
- if (
984
- this.config.skipPromoteProposedCheckpointDuringL1Sync ||
985
- !proposed ||
986
- !calldataCheckpoint ||
987
- proposed.checkpointNumber !== calldataCheckpoint.checkpointNumber
988
- ) {
1117
+ ): Promise<PublishedCheckpoint | { diverged: true; fromCheckpointNumber: CheckpointNumber } | undefined> {
1118
+ if (this.config.skipPromoteProposedCheckpointDuringL1Sync || !calldataCheckpoint) {
1119
+ return undefined;
1120
+ }
1121
+
1122
+ // Look up the specific pending entry for the checkpoint being mined, not just the tip
1123
+ const proposed = await this.stores.blocks.getProposedCheckpointByNumber(calldataCheckpoint.checkpointNumber);
1124
+ if (!proposed) {
989
1125
  return undefined;
990
1126
  }
991
1127
 
@@ -1004,7 +1140,21 @@ export class ArchiverL1Synchronizer implements Traceable {
1004
1140
  calldataArchiveRoot: calldataCheckpoint.archiveRoot.toString(),
1005
1141
  },
1006
1142
  );
1007
- return undefined;
1143
+ // Both the locally-proposed checkpoint and the L1-confirmed one are signed by the
1144
+ // slot proposer; emit a divergence event so the slasher can attribute equivocation.
1145
+ // Only emit when the slots match — uncheckpointed entries are pruned above so this
1146
+ // should always hold, but guard defensively to avoid mis-attributing a slash.
1147
+ if (proposed.header.slotNumber === calldataCheckpoint.header.slotNumber) {
1148
+ this.events.emit(L2BlockSourceEvents.CheckpointEquivocationDetected, {
1149
+ type: L2BlockSourceEvents.CheckpointEquivocationDetected,
1150
+ slotNumber: calldataCheckpoint.header.slotNumber,
1151
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
1152
+ l1ArchiveRoot: calldataCheckpoint.archiveRoot,
1153
+ proposedArchiveRoot: proposed.archive.root,
1154
+ });
1155
+ }
1156
+ // Return a divergence signal so the caller can evict pending >= this number
1157
+ return { diverged: true, fromCheckpointNumber: proposed.checkpointNumber };
1008
1158
  }
1009
1159
 
1010
1160
  this.log.debug(
@@ -1012,7 +1162,10 @@ export class ArchiverL1Synchronizer implements Traceable {
1012
1162
  { proposedHeader: proposed.header.toInspect(), proposedArchiveRoot: proposed.archive.root.toString() },
1013
1163
  );
1014
1164
 
1015
- const blocks = await this.store.getBlocks(BlockNumber(proposed.startBlock), proposed.blockCount);
1165
+ const blocks = await this.stores.blocks.getBlocks({
1166
+ from: BlockNumber(proposed.startBlock),
1167
+ limit: proposed.blockCount,
1168
+ });
1016
1169
  if (blocks.length !== proposed.blockCount) {
1017
1170
  this.log.warn(
1018
1171
  `Local proposed checkpoint ${proposed.checkpointNumber} has wrong block count (expected ${proposed.blockCount} blocks starting at ${proposed.startBlock} but got ${blocks.length})`,
@@ -1048,39 +1201,44 @@ export class ArchiverL1Synchronizer implements Traceable {
1048
1201
  blocksSynchedTo: bigint,
1049
1202
  currentL1BlockNumber: bigint,
1050
1203
  ): Promise<void> {
1051
- const { lastRetrievedCheckpoint, pendingCheckpointNumber } = status;
1052
- // Compare the last checkpoint we have (either retrieved in this round or loaded from store) with what the
1053
- // rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
1204
+ const { lastSeenCheckpoint, pendingCheckpointNumber } = status;
1205
+ // Compare the last checkpoint (valid or not) we have (either retrieved in this round or loaded from store)
1206
+ // with what the rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
1054
1207
  const latestLocalCheckpointNumber =
1055
- lastRetrievedCheckpoint?.checkpoint.number ?? (await this.store.getSynchedCheckpointNumber());
1208
+ lastSeenCheckpoint?.checkpoint.number ??
1209
+ CheckpointNumber.max(
1210
+ await this.stores.blocks.getLatestCheckpointNumber(),
1211
+ await this.stores.blocks.getLatestRejectedCheckpointNumber(),
1212
+ ) ??
1213
+ CheckpointNumber.ZERO;
1214
+
1056
1215
  if (latestLocalCheckpointNumber < pendingCheckpointNumber) {
1057
1216
  // Here we have consumed all logs until the `currentL1Block` we pinned at the beginning of the archiver loop,
1058
1217
  // but still haven't reached the pending checkpoint according to the call to the rollup contract.
1059
1218
  // We suspect an L1 reorg that added checkpoints *behind* us. If that is the case, it must have happened between
1060
1219
  // the last checkpoint we saw and the current one, so we reset the last synched L1 block number. In the edge case
1061
1220
  // we don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
1062
- let latestLocalCheckpointArchive: string | undefined = undefined;
1063
- let targetL1BlockNumber = maxBigint(currentL1BlockNumber - 64n, 0n);
1064
- if (lastRetrievedCheckpoint) {
1065
- latestLocalCheckpointArchive = lastRetrievedCheckpoint.checkpoint.archive.root.toString();
1066
- targetL1BlockNumber = lastRetrievedCheckpoint.l1.blockNumber;
1067
- } else if (latestLocalCheckpointNumber > 0) {
1068
- const checkpoint = await this.store.getRangeOfCheckpoints(latestLocalCheckpointNumber, 1).then(([c]) => c);
1069
- latestLocalCheckpointArchive = checkpoint.archive.root.toString();
1070
- targetL1BlockNumber = checkpoint.l1.blockNumber;
1071
- }
1221
+ const latestLocalCheckpoint: PublishedCheckpoint | CheckpointData | RejectedCheckpoint | undefined =
1222
+ lastSeenCheckpoint ??
1223
+ (await this.stores.blocks.getCheckpointData(latestLocalCheckpointNumber)) ??
1224
+ (await this.stores.blocks.getRejectedCheckpointByNumber(latestLocalCheckpointNumber));
1225
+
1226
+ const targetL1BlockNumber =
1227
+ latestLocalCheckpoint?.l1.blockNumber ??
1228
+ maxBigint(currentL1BlockNumber - 64n, this.l1Constants.l1StartBlock, 0n);
1229
+
1072
1230
  this.log.warn(
1073
1231
  `Failed to reach checkpoint ${pendingCheckpointNumber} at ${currentL1BlockNumber} (latest is ${latestLocalCheckpointNumber}). ` +
1074
1232
  `Rolling back last synched L1 block number to ${targetL1BlockNumber}.`,
1075
1233
  {
1076
1234
  latestLocalCheckpointNumber,
1077
- latestLocalCheckpointArchive,
1235
+ latestLocalCheckpointL1: latestLocalCheckpoint?.l1,
1078
1236
  blocksSynchedTo,
1079
1237
  currentL1BlockNumber,
1080
1238
  ...status,
1081
1239
  },
1082
1240
  );
1083
- await this.store.setCheckpointSynchedL1BlockNumber(targetL1BlockNumber);
1241
+ await this.stores.blocks.setSynchedL1BlockNumber(targetL1BlockNumber);
1084
1242
  } else {
1085
1243
  this.log.trace(`No new checkpoints behind L1 sync point to retrieve.`, {
1086
1244
  latestLocalCheckpointNumber,
@@ -1090,7 +1248,7 @@ export class ArchiverL1Synchronizer implements Traceable {
1090
1248
  }
1091
1249
 
1092
1250
  private async getCheckpointHeader(number: CheckpointNumber) {
1093
- const checkpoint = await this.store.getCheckpointData(number);
1251
+ const checkpoint = await this.stores.blocks.getCheckpointData(number);
1094
1252
  if (!checkpoint) {
1095
1253
  return undefined;
1096
1254
  }