@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
@@ -376,7 +376,8 @@ import { asyncPool } from '@aztec/foundation/async-pool';
376
376
  import { maxBigint } from '@aztec/foundation/bigint';
377
377
  import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
378
378
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
379
- import { partition, pick } from '@aztec/foundation/collection';
379
+ import { compactArray, partition, pick } from '@aztec/foundation/collection';
380
+ import { EthAddress } from '@aztec/foundation/eth-address';
380
381
  import { createLogger } from '@aztec/foundation/log';
381
382
  import { retryTimes } from '@aztec/foundation/retry';
382
383
  import { count } from '@aztec/foundation/string';
@@ -389,19 +390,20 @@ import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
389
390
  import { execInSpan, trackSpan } from '@aztec/telemetry-client';
390
391
  import { InitialCheckpointNumberNotSequentialError } from '../errors.js';
391
392
  import { getCheckpointBlobDataFromBlobs, retrieveCheckpointCalldataFromRollup, retrieveL1ToL2Message, retrieveL1ToL2Messages, retrievedToPublishedCheckpoint } from '../l1/data_retrieval.js';
393
+ import { getArchiverSynchPoint } from '../store/data_stores.js';
392
394
  import { MessageStoreError } from '../store/message_store.js';
393
395
  import { ArchiverDataStoreUpdater } from './data_store_updater.js';
394
396
  import { validateCheckpointAttestations } from './validation.js';
395
397
  _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpochPrune'), _dec2 = trackSpan('Archiver.handleL1ToL2Messages'), _dec3 = trackSpan('Archiver.handleCheckpoints');
396
398
  /**
397
399
  * Handles L1 synchronization for the archiver.
398
- * Responsible for fetching checkpoints, L1L2 messages, and handling L1 reorgs.
400
+ * Responsible for fetching checkpoints, L1 to L2 messages, and handling L1 reorgs.
399
401
  */ export class ArchiverL1Synchronizer {
400
402
  publicClient;
401
403
  debugClient;
402
404
  rollup;
403
405
  inbox;
404
- store;
406
+ stores;
405
407
  config;
406
408
  blobClient;
407
409
  epochCache;
@@ -439,12 +441,12 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
439
441
  l1Timestamp;
440
442
  updater;
441
443
  tracer;
442
- constructor(publicClient, debugClient, rollup, inbox, store, config, blobClient, epochCache, dateProvider, instrumentation, l1Constants, events, tracer, l2TipsCache, log = createLogger('archiver:l1-sync')){
444
+ constructor(publicClient, debugClient, rollup, inbox, stores, config, blobClient, epochCache, dateProvider, instrumentation, l1Constants, events, tracer, l2TipsCache, log = createLogger('archiver:l1-sync')){
443
445
  this.publicClient = publicClient;
444
446
  this.debugClient = debugClient;
445
447
  this.rollup = rollup;
446
448
  this.inbox = inbox;
447
- this.store = store;
449
+ this.stores = stores;
448
450
  this.config = config;
449
451
  this.blobClient = blobClient;
450
452
  this.epochCache = epochCache;
@@ -454,7 +456,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
454
456
  this.events = events;
455
457
  this.log = log;
456
458
  _initProto(this);
457
- this.updater = new ArchiverDataStoreUpdater(this.store, l2TipsCache, {
459
+ this.updater = new ArchiverDataStoreUpdater(this.stores, l2TipsCache, {
458
460
  rollupManaLimit: l1Constants.rollupManaLimit
459
461
  });
460
462
  this.tracer = tracer;
@@ -468,6 +470,12 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
468
470
  /** Returns the last L1 timestamp that was synced. */ getL1Timestamp() {
469
471
  return this.l1Timestamp;
470
472
  }
473
+ getSignatureContext() {
474
+ return {
475
+ chainId: this.publicClient.chain.id,
476
+ rollupAddress: EthAddress.fromString(this.rollup.address)
477
+ };
478
+ }
471
479
  /** Checks that the ethereum node we are connected to has a latest timestamp no more than the allowed drift. Throw if not. */ async testEthereumNodeSynced() {
472
480
  const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
473
481
  if (maxAllowedDelay === 0) {
@@ -500,27 +508,34 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
500
508
  this.log.trace(`No new L1 blocks since last sync at L1 block ${this.l1BlockNumber}`);
501
509
  return;
502
510
  }
503
- // Warn if the latest L1 block timestamp is too old
511
+ // Log at error if the latest L1 block timestamp is too old
504
512
  const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
505
513
  const now = this.dateProvider.nowInSeconds();
506
514
  if (maxAllowedDelay > 0 && Number(currentL1Timestamp) <= now - maxAllowedDelay) {
507
- this.log.warn(`Latest L1 block ${currentL1BlockNumber} timestamp ${currentL1Timestamp} is too old. Make sure your Ethereum node is synced.`, {
515
+ this.log.error(`Latest L1 block ${currentL1BlockNumber} timestamp ${currentL1Timestamp} is too old. Make sure your Ethereum node is synced.`, {
508
516
  currentL1BlockNumber,
509
517
  currentL1Timestamp,
510
518
  now,
511
519
  maxAllowedDelay
512
520
  });
513
521
  }
522
+ // Query finalized block on L1
523
+ const rawFinalizedL1Block = await getFinalizedL1Block(this.publicClient);
524
+ const finalizedL1Block = rawFinalizedL1Block && {
525
+ l1BlockNumber: rawFinalizedL1Block.number,
526
+ l1BlockHash: Buffer32.fromString(rawFinalizedL1Block.hash)
527
+ };
514
528
  // Load sync point for blocks defaulting to start block
515
- const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await this.store.getSynchPoint();
529
+ const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await getArchiverSynchPoint(this.stores);
516
530
  this.log.debug(`Starting new archiver sync iteration`, {
517
531
  blocksSynchedTo,
518
- currentL1BlockData
532
+ currentL1BlockData,
533
+ finalizedL1Block
519
534
  });
520
535
  // 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.
521
536
  // 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
522
537
  // block, when that wouldn't be the case, since L1 to L2 messages would need another iteration.
523
- await retryTimes(()=>this.handleL1ToL2Messages(currentL1BlockData), 'Handling L1 to L2 messages', 3, 0.1);
538
+ await retryTimes(()=>this.handleL1ToL2Messages(currentL1BlockData, finalizedL1Block), 'Handling L1 to L2 messages', 3, 0.1);
524
539
  if (currentL1BlockNumber > blocksSynchedTo) {
525
540
  // First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
526
541
  // pending chain validation status, proven checkpoint number, and synched L1 block number.
@@ -533,23 +548,16 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
533
548
  // the chain locally before we start unwinding stuff. This can be optimized by figuring out
534
549
  // up to which point we're pruning, and then requesting checkpoints up to that point only.
535
550
  const { rollupCanPrune } = await this.handleEpochPrune(rollupStatus.provenCheckpointNumber, currentL1BlockNumber, currentL1Timestamp);
536
- // If the last checkpoint we processed had an invalid attestation, we manually advance the L1 syncpoint
537
- // past it, since otherwise we'll keep downloading it and reprocessing it on every iteration until
538
- // we get a valid checkpoint to advance the syncpoint.
539
- if (!rollupStatus.validationResult?.valid && rollupStatus.lastL1BlockWithCheckpoint !== undefined) {
540
- await this.store.setCheckpointSynchedL1BlockNumber(rollupStatus.lastL1BlockWithCheckpoint);
541
- }
542
551
  // And lastly we check if we are missing any checkpoints behind us due to a possible L1 reorg.
543
552
  // We only do this if rollup cant prune on the next submission. Otherwise we will end up
544
- // re-syncing the checkpoints we have just unwound above. We also dont do this if the last checkpoint is invalid,
545
- // since the archiver will rightfully refuse to sync up to it.
546
- if (!rollupCanPrune && rollupStatus.validationResult?.valid) {
553
+ // re-syncing the checkpoints we have just unwound above.
554
+ if (!rollupCanPrune) {
547
555
  await this.checkForNewCheckpointsBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
548
556
  }
549
557
  this.instrumentation.updateL1BlockHeight(currentL1BlockNumber);
550
558
  }
551
559
  // Update the finalized L2 checkpoint based on L1 finality.
552
- await this.updateFinalizedCheckpoint();
560
+ await this.updateFinalizedCheckpoint(finalizedL1Block);
553
561
  // After syncing has completed, update the current l1 block number and timestamp,
554
562
  // otherwise we risk announcing to the world that we've synced to a given point,
555
563
  // but the corresponding blocks have not been processed (see #12631).
@@ -563,18 +571,17 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
563
571
  l1BlockNumberAtEnd
564
572
  });
565
573
  }
566
- /** Query L1 for its finalized block and update the finalized checkpoint accordingly. */ async updateFinalizedCheckpoint() {
574
+ /** Updates the finalized checkpoint using the pre-fetched finalized L1 block from the current sync iteration. */ async updateFinalizedCheckpoint(finalizedL1Block) {
567
575
  try {
568
- const finalizedL1Block = await getFinalizedL1Block(this.publicClient);
569
576
  if (!finalizedL1Block) {
570
577
  this.log.trace(`Skipping finalized checkpoint update: L1 has no finalized block yet.`);
571
578
  return;
572
579
  }
573
- const finalizedL1BlockNumber = finalizedL1Block.number;
580
+ const finalizedL1BlockNumber = finalizedL1Block.l1BlockNumber;
574
581
  const finalizedCheckpointNumber = await this.rollup.getProvenCheckpointNumber({
575
582
  blockNumber: finalizedL1BlockNumber
576
583
  });
577
- const localFinalizedCheckpointNumber = await this.store.getFinalizedCheckpointNumber();
584
+ const localFinalizedCheckpointNumber = await this.stores.blocks.getFinalizedCheckpointNumber();
578
585
  if (localFinalizedCheckpointNumber !== finalizedCheckpointNumber) {
579
586
  await this.updater.setFinalizedCheckpointNumber(finalizedCheckpointNumber);
580
587
  this.log.info(`Updated finalized chain to checkpoint ${finalizedCheckpointNumber}`, {
@@ -591,8 +598,8 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
591
598
  }
592
599
  /** Prune all proposed local blocks that should have been checkpointed by now. */ async pruneUncheckpointedBlocks(currentL1Timestamp) {
593
600
  const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
594
- this.store.getCheckpointedL2BlockNumber(),
595
- this.store.getLatestBlockNumber()
601
+ this.stores.blocks.getCheckpointedL2BlockNumber(),
602
+ this.stores.blocks.getLatestL2BlockNumber()
596
603
  ]);
597
604
  // If there are no uncheckpointed blocks, we got nothing to do
598
605
  if (lastProposedBlockNumber === lastCheckpointedBlockNumber) {
@@ -603,15 +610,17 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
603
610
  const slotAtNextL1Block = getSlotAtNextL1Block(currentL1Timestamp, this.l1Constants);
604
611
  const firstUncheckpointedBlockNumber = BlockNumber(lastCheckpointedBlockNumber + 1);
605
612
  // What's the slot of the first uncheckpointed block?
606
- const [firstUncheckpointedBlockHeader] = await this.store.getBlockHeaders(firstUncheckpointedBlockNumber, 1);
607
- const firstUncheckpointedBlockSlot = firstUncheckpointedBlockHeader?.getSlot();
613
+ const firstUncheckpointedBlockData = await this.stores.blocks.getBlockData({
614
+ number: firstUncheckpointedBlockNumber
615
+ });
616
+ const firstUncheckpointedBlockSlot = firstUncheckpointedBlockData?.header.getSlot();
608
617
  if (firstUncheckpointedBlockSlot === undefined || firstUncheckpointedBlockSlot >= slotAtNextL1Block) {
609
618
  return;
610
619
  }
611
620
  // Prune provisional blocks from slots that have ended without being checkpointed.
612
621
  // This also clears any proposed checkpoint whose blocks are being pruned.
613
622
  this.log.warn(`Pruning blocks after block ${lastCheckpointedBlockNumber} due to slot ${firstUncheckpointedBlockSlot} not being checkpointed`, {
614
- firstUncheckpointedBlockHeader: firstUncheckpointedBlockHeader.toInspect(),
623
+ firstUncheckpointedBlockHeader: firstUncheckpointedBlockData?.header.toInspect(),
615
624
  slotAtNextL1Block
616
625
  });
617
626
  const prunedBlocks = await this.updater.removeUncheckpointedBlocksAfter(lastCheckpointedBlockNumber);
@@ -639,7 +648,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
639
648
  }
640
649
  /** Checks if there'd be a reorg for the next checkpoint submission and start pruning now. */ async handleEpochPrune(provenCheckpointNumber, currentL1BlockNumber, currentL1Timestamp) {
641
650
  const rollupCanPrune = await this.canPrune(currentL1BlockNumber, currentL1Timestamp);
642
- const localPendingCheckpointNumber = await this.store.getSynchedCheckpointNumber();
651
+ const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
643
652
  const canPrune = localPendingCheckpointNumber > provenCheckpointNumber && rollupCanPrune;
644
653
  if (canPrune) {
645
654
  const timer = new Timer();
@@ -657,8 +666,8 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
657
666
  const indices = Array.from({
658
667
  length: checkpointsToUnwind
659
668
  }, (_, i)=>CheckpointNumber(i + pruneFrom));
660
- const checkpoints = (await asyncPool(BATCH_SIZE, indices, (idx)=>this.store.getCheckpointData(idx))).filter(isDefined);
661
- const newBlocks = (await asyncPool(BATCH_SIZE, checkpoints, (cp)=>this.store.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)))).filter(isDefined).flat();
669
+ const checkpoints = (await asyncPool(BATCH_SIZE, indices, (idx)=>this.stores.blocks.getCheckpointData(idx))).filter(isDefined);
670
+ const newBlocks = (await asyncPool(BATCH_SIZE, checkpoints, (cp)=>this.stores.blocks.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)))).filter(isDefined).flat();
662
671
  // Emit an event for listening services to react to the chain prune
663
672
  this.events.emit(L2BlockSourceEvents.L2PruneUnproven, {
664
673
  type: L2BlockSourceEvents.L2PruneUnproven,
@@ -667,11 +676,11 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
667
676
  });
668
677
  this.log.debug(`L2 prune from ${provenCheckpointNumber + 1} to ${localPendingCheckpointNumber} will occur on next checkpoint submission.`);
669
678
  await this.updater.removeCheckpointsAfter(provenCheckpointNumber);
670
- this.log.warn(`Removed ${count(checkpointsToUnwind, 'checkpoint')} after checkpoint ${provenCheckpointNumber} ` + `due to predicted reorg at L1 block ${currentL1BlockNumber}. ` + `Updated latest checkpoint is ${await this.store.getSynchedCheckpointNumber()}.`);
679
+ this.log.warn(`Removed ${count(checkpointsToUnwind, 'checkpoint')} after checkpoint ${provenCheckpointNumber} ` + `due to predicted reorg at L1 block ${currentL1BlockNumber}. ` + `Updated latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`);
671
680
  this.instrumentation.processPrune(timer.ms());
672
681
  // TODO(palla/reorg): Do we need to set the block synched L1 block number here?
673
682
  // Seems like the next iteration should handle this.
674
- // await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
683
+ // await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
675
684
  }
676
685
  return {
677
686
  rollupCanPrune
@@ -692,12 +701,12 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
692
701
  nextEnd
693
702
  ];
694
703
  }
695
- async handleL1ToL2Messages(currentL1Block) {
704
+ async handleL1ToL2Messages(currentL1Block, finalizedL1Block) {
696
705
  // Load the syncpoint, which may have been updated in a previous iteration
697
706
  const { messagesSynchedTo = {
698
707
  l1BlockNumber: this.l1Constants.l1StartBlock,
699
708
  l1BlockHash: this.l1Constants.l1StartBlockHash
700
- } } = await this.store.getSynchPoint();
709
+ } } = await getArchiverSynchPoint(this.stores);
701
710
  // Nothing to do if L1 block number has not moved forward
702
711
  const currentL1BlockNumber = currentL1Block.l1BlockNumber;
703
712
  if (currentL1BlockNumber <= messagesSynchedTo.l1BlockNumber) {
@@ -707,10 +716,10 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
707
716
  const remoteMessagesState = await this.inbox.getState({
708
717
  blockNumber: currentL1BlockNumber
709
718
  });
710
- const localLastMessage = await this.store.getLastL1ToL2Message();
719
+ const localLastMessage = await this.stores.messages.getLastMessage();
711
720
  if (await this.localStateMatches(localLastMessage, remoteMessagesState)) {
712
721
  this.log.trace(`Local L1 to L2 messages are already in sync with remote at L1 block ${currentL1BlockNumber}`);
713
- await this.store.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress);
722
+ await this.stores.messages.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress, finalizedL1Block);
714
723
  return true;
715
724
  }
716
725
  // If not, then we are out of sync. Most likely there are new messages on the inbox, so we try retrieving them.
@@ -732,7 +741,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
732
741
  // Note that, if there are no new messages to insert, but there was an L1 reorg that pruned out last messages,
733
742
  // we'd notice by comparing our local state with the remote one again, and seeing they don't match even after
734
743
  // our sync attempt. In this case, we also rollback our syncpoint, and trigger a retry.
735
- const localLastMessageAfterSync = await this.store.getLastL1ToL2Message();
744
+ const localLastMessageAfterSync = await this.stores.messages.getLastMessage();
736
745
  if (!await this.localStateMatches(localLastMessageAfterSync, remoteMessagesState)) {
737
746
  this.log.warn(`Local L1 to L2 messages state does not match remote after sync attempt. Rolling back syncpoint to retry.`, {
738
747
  localLastMessageAfterSync,
@@ -742,11 +751,11 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
742
751
  return false;
743
752
  }
744
753
  // Advance the syncpoint after a successful sync
745
- await this.store.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress);
754
+ await this.stores.messages.setMessageSyncState(currentL1Block, remoteMessagesState.treeInProgress, finalizedL1Block);
746
755
  return true;
747
756
  }
748
757
  /** Checks if the local rolling hash and message count matches the remote state */ async localStateMatches(localLastMessage, remoteState) {
749
- const localMessageCount = await this.store.getTotalL1ToL2MessageCount();
758
+ const localMessageCount = await this.stores.messages.getTotalL1ToL2MessageCount();
750
759
  this.log.trace(`Comparing local and remote inbox state`, {
751
760
  localMessageCount,
752
761
  localLastMessage,
@@ -764,7 +773,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
764
773
  this.log.trace(`Retrieving L1 to L2 messages in L1 blocks ${searchStartBlock}-${searchEndBlock}`);
765
774
  const messages = await retrieveL1ToL2Messages(this.inbox, searchStartBlock, searchEndBlock);
766
775
  const timer = new Timer();
767
- await this.store.addL1ToL2Messages(messages);
776
+ await this.stores.messages.addL1ToL2Messages(messages);
768
777
  const perMsg = timer.ms() / messages.length;
769
778
  this.instrumentation.processNewMessages(messages.length, perMsg);
770
779
  for (const msg of messages){
@@ -788,13 +797,15 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
788
797
  * If no common message is found, rolls back all messages and sets the syncpoint to the start block.
789
798
  */ async rollbackL1ToL2Messages(remoteMessagesState) {
790
799
  const { treeInProgress: remoteTreeInProgress, messagesRollingHash: remoteRollingHash } = remoteMessagesState;
800
+ const messagesFinalizedL1Block = await this.stores.messages.getMessagesFinalizedL1Block();
801
+ const finalizedL1BlockNumber = messagesFinalizedL1Block?.l1BlockNumber;
791
802
  // Slowly go back through our messages until we find the last common message. We could query the logs in
792
803
  // batch as an optimization, but the depth of the reorg should not be deep, and this is a very rare case,
793
804
  // so it's fine to query one log at a time.
794
805
  let commonMsg;
795
806
  let messagesToDelete = 0;
796
807
  this.log.verbose(`Searching most recent common L1 to L2 message`);
797
- for await (const localMsg of this.store.iterateL1ToL2Messages({
808
+ for await (const localMsg of this.stores.messages.iterateL1ToL2Messages({
798
809
  reverse: true
799
810
  })){
800
811
  const logCtx = {
@@ -810,6 +821,12 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
810
821
  commonMsg = localMsg;
811
822
  break;
812
823
  }
824
+ // Messages at or below the finalized L1 block cannot have been reorged — accept as common without querying L1.
825
+ if (finalizedL1BlockNumber !== undefined && localMsg.l1BlockNumber <= finalizedL1BlockNumber) {
826
+ this.log.info(`Found common L1 to L2 message at finalized L1 block ${localMsg.l1BlockNumber}`, logCtx);
827
+ commonMsg = localMsg;
828
+ break;
829
+ }
813
830
  // If there's no match with the current remote state, check if the message exists on the inbox contract at all
814
831
  // by looking at the inbox events. If the L1 reorg *added* new messages in addition to deleting existing ones,
815
832
  // then the current remote state's rolling hash will not match anything we have locally, so we need to check existence
@@ -834,19 +851,24 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
834
851
  if (messagesToDelete > 0) {
835
852
  const lastGoodIndex = commonMsg?.index;
836
853
  this.log.warn(`Rolling back all local L1 to L2 messages after index ${lastGoodIndex ?? 'initial'}`);
837
- await this.store.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
854
+ await this.stores.messages.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
838
855
  }
839
856
  // Update the syncpoint so the loop below reprocesses the changed messages. We go to the block before
840
857
  // the last common one, so we force reprocessing it, in case new messages were added on that same L1 block
841
- // after the last common message.
842
- const syncPointL1BlockNumber = commonMsg ? commonMsg.l1BlockNumber - 1n : this.l1Constants.l1StartBlock;
843
- const syncPointL1BlockHash = await this.getL1BlockHash(syncPointL1BlockNumber);
858
+ // after the last common message. Cap at the finalized L1 block: messages at or below finalized cannot
859
+ // have been reorged, so there is no need to walk back any further than that.
860
+ const syncPointL1BlockNumber = maxBigint(...compactArray([
861
+ commonMsg ? commonMsg.l1BlockNumber - 1n : undefined,
862
+ finalizedL1BlockNumber,
863
+ this.l1Constants.l1StartBlock
864
+ ]));
865
+ const syncPointL1BlockHash = syncPointL1BlockNumber === finalizedL1BlockNumber ? messagesFinalizedL1Block.l1BlockHash : await this.getL1BlockHash(syncPointL1BlockNumber);
844
866
  const messagesSyncPoint = {
845
867
  l1BlockNumber: syncPointL1BlockNumber,
846
868
  l1BlockHash: syncPointL1BlockHash
847
869
  };
848
- await this.store.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
849
- this.log.verbose(`Updated messages syncpoint to L1 block ${syncPointL1BlockNumber}`, {
870
+ await this.stores.messages.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
871
+ this.log.verbose(`Updated messages syncpoint to L1 block ${messagesSyncPoint.l1BlockNumber}`, {
850
872
  ...messagesSyncPoint,
851
873
  remoteTreeInProgress
852
874
  });
@@ -863,8 +885,8 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
863
885
  return Buffer32.fromString(block.hash);
864
886
  }
865
887
  async handleCheckpoints(blocksSynchedTo, currentL1BlockNumber, initialSyncComplete) {
866
- const localPendingCheckpointNumber = await this.store.getSynchedCheckpointNumber();
867
- const initialValidationResult = await this.store.getPendingChainValidationStatus();
888
+ const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
889
+ const initialValidationResult = await this.stores.blocks.getPendingChainValidationStatus();
868
890
  const { provenCheckpointNumber, provenArchive, pendingCheckpointNumber, pendingArchive, archiveOfMyCheckpoint: archiveForLocalPendingCheckpointNumber } = await execInSpan(this.tracer, 'Archiver.getRollupStatus', ()=>this.rollup.status(localPendingCheckpointNumber, {
869
891
  blockNumber: currentL1BlockNumber
870
892
  }));
@@ -887,7 +909,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
887
909
  // we need to set it to zero. This is an edge case because we dont have a checkpoint zero (initial checkpoint is one),
888
910
  // so localCheckpointForDestinationProvenCheckpointNumber would not be found below.
889
911
  if (provenCheckpointNumber === 0) {
890
- const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber();
912
+ const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
891
913
  if (localProvenCheckpointNumber !== provenCheckpointNumber) {
892
914
  await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
893
915
  this.log.info(`Rolled back proven chain to checkpoint ${provenCheckpointNumber}`, {
@@ -895,16 +917,16 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
895
917
  });
896
918
  }
897
919
  }
898
- const localCheckpointForDestinationProvenCheckpointNumber = await this.store.getCheckpointData(provenCheckpointNumber);
920
+ const localCheckpointForDestinationProvenCheckpointNumber = await this.stores.blocks.getCheckpointData(provenCheckpointNumber);
899
921
  // Sanity check. I've hit what seems to be a state where the proven checkpoint is set to a value greater than the latest
900
922
  // synched checkpoint when requesting L2Tips from the archiver. This is the only place where the proven checkpoint is set.
901
- const synched = await this.store.getSynchedCheckpointNumber();
923
+ const synched = await this.stores.blocks.getLatestCheckpointNumber();
902
924
  if (localCheckpointForDestinationProvenCheckpointNumber && synched < localCheckpointForDestinationProvenCheckpointNumber.checkpointNumber) {
903
925
  this.log.error(`Hit local checkpoint greater than last synched checkpoint: ${localCheckpointForDestinationProvenCheckpointNumber.checkpointNumber} > ${synched}`);
904
926
  }
905
927
  this.log.trace(`Local checkpoint for remote proven checkpoint ${provenCheckpointNumber} is ${localCheckpointForDestinationProvenCheckpointNumber?.archive.root.toString() ?? 'undefined'}`);
906
928
  if (localCheckpointForDestinationProvenCheckpointNumber && provenArchive.equals(localCheckpointForDestinationProvenCheckpointNumber.archive.root)) {
907
- const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber();
929
+ const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
908
930
  if (localProvenCheckpointNumber !== provenCheckpointNumber) {
909
931
  await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
910
932
  this.log.info(`Updated proven chain to checkpoint ${provenCheckpointNumber}`, {
@@ -929,7 +951,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
929
951
  // If we have 0 checkpoints locally and there are no checkpoints onchain there is nothing to do.
930
952
  const noCheckpoints = localPendingCheckpointNumber === 0 && pendingCheckpointNumber === 0;
931
953
  if (noCheckpoints) {
932
- await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
954
+ await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
933
955
  this.log.debug(`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}, no checkpoints on chain`);
934
956
  return rollupStatus;
935
957
  }
@@ -937,7 +959,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
937
959
  // Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there
938
960
  // are any state that could be impacted by it. If we have no checkpoints, there is no impact.
939
961
  if (localPendingCheckpointNumber > 0) {
940
- const localPendingCheckpoint = await this.store.getCheckpointData(localPendingCheckpointNumber);
962
+ const localPendingCheckpoint = await this.stores.blocks.getCheckpointData(localPendingCheckpointNumber);
941
963
  if (localPendingCheckpoint === undefined) {
942
964
  throw new Error(`Missing checkpoint ${localPendingCheckpointNumber}`);
943
965
  }
@@ -951,7 +973,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
951
973
  // However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing checkpoints.
952
974
  // We must only set this block number based on actually retrieved logs.
953
975
  // TODO(#8621): Tackle this properly when we handle L1 Re-orgs.
954
- // await this.store.setCheckpointSynchedL1BlockNumber(currentL1BlockNumber);
976
+ // await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
955
977
  this.log.debug(`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
956
978
  return rollupStatus;
957
979
  }
@@ -968,7 +990,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
968
990
  });
969
991
  let tipAfterUnwind = localPendingCheckpointNumber;
970
992
  while(true){
971
- const candidateCheckpoint = await this.store.getCheckpointData(tipAfterUnwind);
993
+ const candidateCheckpoint = await this.stores.blocks.getCheckpointData(tipAfterUnwind);
972
994
  if (candidateCheckpoint === undefined) {
973
995
  break;
974
996
  }
@@ -984,7 +1006,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
984
1006
  }
985
1007
  const checkpointsToRemove = localPendingCheckpointNumber - tipAfterUnwind;
986
1008
  await this.updater.removeCheckpointsAfter(CheckpointNumber(tipAfterUnwind));
987
- this.log.warn(`Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` + `due to mismatched checkpoint hashes at L1 block ${currentL1BlockNumber}. ` + `Updated L2 latest checkpoint is ${await this.store.getSynchedCheckpointNumber()}.`);
1009
+ this.log.warn(`Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` + `due to mismatched checkpoint hashes at L1 block ${currentL1BlockNumber}. ` + `Updated L2 latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`);
988
1010
  }
989
1011
  }
990
1012
  // Retrieve checkpoints in batches. Each batch is estimated to accommodate up to 'blockBatchSize' L1 blocks,
@@ -992,7 +1014,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
992
1014
  let searchStartBlock = blocksSynchedTo;
993
1015
  let searchEndBlock = blocksSynchedTo;
994
1016
  let lastRetrievedCheckpoint;
995
- let lastL1BlockWithCheckpoint = undefined;
1017
+ let lastSeenCheckpoint;
996
1018
  do {
997
1019
  [searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
998
1020
  this.log.trace(`Retrieving checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
@@ -1009,11 +1031,13 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1009
1031
  searchStartBlock,
1010
1032
  searchEndBlock
1011
1033
  });
1012
- // Check if the last checkpoint matches the proposed one (so we can skip blob fetch).
1013
- // We only check the last one because the proposed checkpoint is always the most recent one,
1014
- // and if it's in a multi-checkpoint batch it will always be last (sorted by L1 block number).
1034
+ // Check if the last checkpoint matches a local pending entry (so we can skip blob fetch).
1035
+ // We only check the last one; if it matches, the blob fetch is skipped for that entry.
1036
+ // TODO(palla/pipelining): We may have more than a single checkpoint to promote
1015
1037
  const lastCalldataCheckpoint = calldataCheckpoints[calldataCheckpoints.length - 1];
1016
- const checkpointToPromote = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
1038
+ const promoteResult = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
1039
+ const checkpointToPromote = promoteResult && !('diverged' in promoteResult) ? promoteResult : undefined;
1040
+ const evictProposedFrom = promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
1017
1041
  // Then fetch blobs in parallel and build the full published checkpoints
1018
1042
  const toFetchBlobs = checkpointToPromote ? calldataCheckpoints.slice(0, -1) : calldataCheckpoints;
1019
1043
  const blobFetched = await asyncPool(10, toFetchBlobs, async (checkpoint)=>retrievedToPublishedCheckpoint({
@@ -1028,15 +1052,27 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1028
1052
  const validCheckpoints = [];
1029
1053
  // Now loop through all checkpoints and validate their attestations
1030
1054
  for (const published of publishedCheckpoints){
1055
+ // Check the attestations uploaded by the publisher to L1 are correct
1056
+ // Rollup contract does not validate attestations to save on gas, so this
1057
+ // falls on the nodes to verify offchain and skip those checkpoints.
1031
1058
  const validationResult = this.config.skipValidateCheckpointAttestations ? {
1032
1059
  valid: true
1033
- } : await validateCheckpointAttestations(published, this.epochCache, this.l1Constants, this.log);
1034
- // Only update the validation result if it has changed, so we can keep track of the first invalid checkpoint
1060
+ } : await validateCheckpointAttestations(published, this.epochCache, this.l1Constants, this.getSignatureContext(), this.log);
1061
+ // Also skip the checkpoint if it builds on a previously-rejected ancestor. Without
1062
+ // this, addCheckpoints would throw InitialCheckpointNumberNotSequentialError when the
1063
+ // ancestor was skipped earlier (e.g. due to invalid attestations), the catch handler
1064
+ // would roll back the L1 sync point, and the next iteration would re-fetch and re-throw.
1065
+ const rejectedAncestor = await this.stores.blocks.getRejectedCheckpointByArchiveRoot(published.checkpoint.header.lastArchiveRoot);
1066
+ // Update the validation result if it has changed, so we can keep track of the first invalid checkpoint
1035
1067
  // in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
1036
1068
  // There is an exception though: if a checkpoint is invalidated and replaced with another invalid checkpoint,
1037
1069
  // we need to update the validation result, since we need to be able to invalidate the new one.
1038
1070
  // See test 'chain progresses if an invalid checkpoint is invalidated with an invalid one' for more info.
1039
- if (rollupStatus.validationResult?.valid !== validationResult.valid || !rollupStatus.validationResult.valid && !validationResult.valid && rollupStatus.validationResult.checkpoint.checkpointNumber === validationResult.checkpoint.checkpointNumber) {
1071
+ // Do not update the validation result if there is a rejected ancestor, since in that case we want to keep the
1072
+ // original invalidation, as the new checkpoint is extending from a previous invalid one.
1073
+ const validStatusChanged = rollupStatus.validationResult?.valid !== validationResult.valid;
1074
+ const invalidStatusWithSameCheckpointNumber = !validationResult.valid && rollupStatus.validationResult && !rollupStatus.validationResult.valid && rollupStatus.validationResult.checkpoint.checkpointNumber === validationResult.checkpoint.checkpointNumber;
1075
+ if (!rejectedAncestor && (validStatusChanged || invalidStatusWithSameCheckpointNumber)) {
1040
1076
  rollupStatus.validationResult = validationResult;
1041
1077
  }
1042
1078
  if (!validationResult.valid) {
@@ -1050,12 +1086,52 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1050
1086
  type: L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
1051
1087
  validationResult
1052
1088
  });
1089
+ // Persist a rejected-ancestor entry so any later checkpoint that builds on this one
1090
+ // is detected and skipped (rather than tripping the addCheckpoints consecutive-number
1091
+ // check and causing the sync point to roll back in a loop).
1092
+ await this.stores.blocks.addRejectedCheckpoint({
1093
+ checkpointNumber: published.checkpoint.number,
1094
+ archiveRoot: published.checkpoint.archive.root,
1095
+ parentArchiveRoot: published.checkpoint.header.lastArchiveRoot,
1096
+ slotNumber: published.checkpoint.header.slotNumber,
1097
+ l1: published.l1,
1098
+ reason: 'invalid-attestations'
1099
+ });
1100
+ continue;
1101
+ }
1102
+ if (rejectedAncestor) {
1103
+ const descendantInfo = published.checkpoint.toCheckpointInfo();
1104
+ this.log.warn(`Skipping checkpoint ${published.checkpoint.number} as it is a descendant of ` + `rejected checkpoint ${rejectedAncestor.checkpointNumber} (${rejectedAncestor.reason})`, {
1105
+ checkpointNumber: published.checkpoint.number,
1106
+ checkpointHash: published.checkpoint.hash(),
1107
+ l1BlockNumber: published.l1.blockNumber,
1108
+ l1BlockHash: published.l1.blockHash,
1109
+ ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
1110
+ ancestorArchiveRoot: rejectedAncestor.archiveRoot.toString(),
1111
+ ancestorReason: rejectedAncestor.reason
1112
+ });
1113
+ this.events.emit(L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected, {
1114
+ type: L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected,
1115
+ checkpoint: descendantInfo,
1116
+ ancestorArchiveRoot: rejectedAncestor.archiveRoot,
1117
+ ancestorCheckpointNumber: rejectedAncestor.checkpointNumber
1118
+ });
1119
+ // Persist this chainpoint as rejected as well, so we can construct a chain of
1120
+ // skipped checkpoints starting from the first one with invalid attestations.
1121
+ await this.stores.blocks.addRejectedCheckpoint({
1122
+ checkpointNumber: published.checkpoint.number,
1123
+ archiveRoot: published.checkpoint.archive.root,
1124
+ parentArchiveRoot: published.checkpoint.header.lastArchiveRoot,
1125
+ slotNumber: published.checkpoint.header.slotNumber,
1126
+ l1: published.l1,
1127
+ reason: 'descends-from-invalid-attestations'
1128
+ });
1053
1129
  continue;
1054
1130
  }
1055
1131
  // Check the inHash of the checkpoint against the l1->l2 messages.
1056
1132
  // The messages should've been synced up to the currentL1BlockNumber and must be available for the published
1057
1133
  // checkpoints we just retrieved.
1058
- const l1ToL2Messages = await this.store.getL1ToL2Messages(published.checkpoint.number);
1134
+ const l1ToL2Messages = await this.stores.messages.getL1ToL2Messages(published.checkpoint.number);
1059
1135
  const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
1060
1136
  const publishedInHash = published.checkpoint.header.inHash;
1061
1137
  if (!computedInHash.equals(publishedInHash)) {
@@ -1092,9 +1168,9 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1092
1168
  l1: lastCalldataCheckpoint.l1,
1093
1169
  attestations: lastCalldataCheckpoint.attestations,
1094
1170
  checkpoint: maybeValidCheckpointToPromote
1095
- })));
1096
- if (checkpointsToAdd.length > 0) {
1097
- this.instrumentation.processNewCheckpointedBlocks(processDuration / checkpointsToAdd.length, checkpointsToAdd.flatMap((c)=>c.checkpoint.blocks));
1171
+ }, evictProposedFrom)));
1172
+ if (validCheckpoints.length > 0) {
1173
+ this.instrumentation.processNewCheckpointedBlocks(processDuration / validCheckpoints.length, validCheckpoints.flatMap((c)=>c.checkpoint.blocks));
1098
1174
  }
1099
1175
  // If blocks were pruned due to conflict with L1 checkpoints, emit event
1100
1176
  if (result.prunedBlocks && result.prunedBlocks.length > 0) {
@@ -1117,11 +1193,15 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1117
1193
  } catch (err) {
1118
1194
  if (err instanceof InitialCheckpointNumberNotSequentialError) {
1119
1195
  const { previousCheckpointNumber, newCheckpointNumber } = err;
1120
- const previousCheckpoint = previousCheckpointNumber ? await this.store.getCheckpointData(CheckpointNumber(previousCheckpointNumber)) : undefined;
1121
- const updatedL1SyncPoint = previousCheckpoint?.l1.blockNumber ?? this.l1Constants.l1StartBlock;
1122
- await this.store.setCheckpointSynchedL1BlockNumber(updatedL1SyncPoint);
1196
+ const previousCheckpoint = previousCheckpointNumber ? await this.stores.blocks.getCheckpointData(CheckpointNumber(previousCheckpointNumber)) : undefined;
1197
+ const lastFinalizedCheckpoint = await this.stores.blocks.getCheckpointData(await this.stores.blocks.getFinalizedCheckpointNumber());
1198
+ const updatedL1SyncPoint = previousCheckpoint?.l1.blockNumber ?? lastFinalizedCheckpoint?.l1.blockNumber ?? this.l1Constants.l1StartBlock;
1199
+ await this.stores.blocks.setSynchedL1BlockNumber(updatedL1SyncPoint);
1123
1200
  this.log.warn(`Attempting to insert checkpoint ${newCheckpointNumber} with previous block ${previousCheckpointNumber}. Rolling back L1 sync point to ${updatedL1SyncPoint} to try and fetch the missing blocks.`, {
1124
1201
  previousCheckpointNumber,
1202
+ previousCheckpoint: previousCheckpoint?.header.toInspect(),
1203
+ lastFinalizedCheckpoint: lastFinalizedCheckpoint?.header.toInspect(),
1204
+ l1StartBlock: this.l1Constants.l1StartBlock,
1125
1205
  newCheckpointNumber,
1126
1206
  updatedL1SyncPoint
1127
1207
  });
@@ -1140,19 +1220,30 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1140
1220
  });
1141
1221
  }
1142
1222
  lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
1143
- lastL1BlockWithCheckpoint = calldataCheckpoints.at(-1)?.l1.blockNumber ?? lastL1BlockWithCheckpoint;
1223
+ lastSeenCheckpoint = publishedCheckpoints.at(-1) ?? lastSeenCheckpoint;
1144
1224
  }while (searchEndBlock < currentL1BlockNumber)
1145
1225
  // Important that we update AFTER inserting the blocks.
1146
1226
  await updateProvenCheckpoint();
1147
1227
  return {
1148
1228
  ...rollupStatus,
1149
1229
  lastRetrievedCheckpoint,
1150
- lastL1BlockWithCheckpoint
1230
+ lastSeenCheckpoint
1151
1231
  };
1152
1232
  }
1153
- /** Checks if this checkpoint matches the local proposed one, and if so, loads local data to build a synthetic published checkpoint. */ async tryBuildPublishedCheckpointFromProposed(calldataCheckpoint) {
1154
- const proposed = await this.store.getProposedCheckpointOnly();
1155
- if (this.config.skipPromoteProposedCheckpointDuringL1Sync || !proposed || !calldataCheckpoint || proposed.checkpointNumber !== calldataCheckpoint.checkpointNumber) {
1233
+ /**
1234
+ * Checks if a specific checkpoint matches a local pending entry, and if so, loads local data to build
1235
+ * a synthetic published checkpoint (skipping blob fetch).
1236
+ *
1237
+ * Returns { diverged: true, fromCheckpointNumber } when the L1 checkpoint does NOT match local pending
1238
+ * data for that number, so the caller can evict the entire pending suffix >= fromCheckpointNumber
1239
+ * (those entries chain off the now-invalid local state) within the same addCheckpoints transaction.
1240
+ */ async tryBuildPublishedCheckpointFromProposed(calldataCheckpoint) {
1241
+ if (this.config.skipPromoteProposedCheckpointDuringL1Sync || !calldataCheckpoint) {
1242
+ return undefined;
1243
+ }
1244
+ // Look up the specific pending entry for the checkpoint being mined, not just the tip
1245
+ const proposed = await this.stores.blocks.getProposedCheckpointByNumber(calldataCheckpoint.checkpointNumber);
1246
+ if (!proposed) {
1156
1247
  return undefined;
1157
1248
  }
1158
1249
  if (!proposed.header.equals(calldataCheckpoint.header) || !proposed.archive.root.equals(calldataCheckpoint.archiveRoot)) {
@@ -1164,13 +1255,33 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1164
1255
  calldataHeader: calldataCheckpoint.header.toInspect(),
1165
1256
  calldataArchiveRoot: calldataCheckpoint.archiveRoot.toString()
1166
1257
  });
1167
- return undefined;
1258
+ // Both the locally-proposed checkpoint and the L1-confirmed one are signed by the
1259
+ // slot proposer; emit a divergence event so the slasher can attribute equivocation.
1260
+ // Only emit when the slots match — uncheckpointed entries are pruned above so this
1261
+ // should always hold, but guard defensively to avoid mis-attributing a slash.
1262
+ if (proposed.header.slotNumber === calldataCheckpoint.header.slotNumber) {
1263
+ this.events.emit(L2BlockSourceEvents.CheckpointEquivocationDetected, {
1264
+ type: L2BlockSourceEvents.CheckpointEquivocationDetected,
1265
+ slotNumber: calldataCheckpoint.header.slotNumber,
1266
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
1267
+ l1ArchiveRoot: calldataCheckpoint.archiveRoot,
1268
+ proposedArchiveRoot: proposed.archive.root
1269
+ });
1270
+ }
1271
+ // Return a divergence signal so the caller can evict pending >= this number
1272
+ return {
1273
+ diverged: true,
1274
+ fromCheckpointNumber: proposed.checkpointNumber
1275
+ };
1168
1276
  }
1169
1277
  this.log.debug(`Building published checkpoint from proposed ${calldataCheckpoint.checkpointNumber} (skipping blob fetch)`, {
1170
1278
  proposedHeader: proposed.header.toInspect(),
1171
1279
  proposedArchiveRoot: proposed.archive.root.toString()
1172
1280
  });
1173
- const blocks = await this.store.getBlocks(BlockNumber(proposed.startBlock), proposed.blockCount);
1281
+ const blocks = await this.stores.blocks.getBlocks({
1282
+ from: BlockNumber(proposed.startBlock),
1283
+ limit: proposed.blockCount
1284
+ });
1174
1285
  if (blocks.length !== proposed.blockCount) {
1175
1286
  this.log.warn(`Local proposed checkpoint ${proposed.checkpointNumber} has wrong block count (expected ${proposed.blockCount} blocks starting at ${proposed.startBlock} but got ${blocks.length})`, {
1176
1287
  proposedCheckpointNumber: proposed.checkpointNumber,
@@ -1196,34 +1307,26 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1196
1307
  return promotedCheckpoint;
1197
1308
  }
1198
1309
  async checkForNewCheckpointsBeforeL1SyncPoint(status, blocksSynchedTo, currentL1BlockNumber) {
1199
- const { lastRetrievedCheckpoint, pendingCheckpointNumber } = status;
1200
- // Compare the last checkpoint we have (either retrieved in this round or loaded from store) with what the
1201
- // rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
1202
- const latestLocalCheckpointNumber = lastRetrievedCheckpoint?.checkpoint.number ?? await this.store.getSynchedCheckpointNumber();
1310
+ const { lastSeenCheckpoint, pendingCheckpointNumber } = status;
1311
+ // Compare the last checkpoint (valid or not) we have (either retrieved in this round or loaded from store)
1312
+ // with what the rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
1313
+ const latestLocalCheckpointNumber = lastSeenCheckpoint?.checkpoint.number ?? CheckpointNumber.max(await this.stores.blocks.getLatestCheckpointNumber(), await this.stores.blocks.getLatestRejectedCheckpointNumber()) ?? CheckpointNumber.ZERO;
1203
1314
  if (latestLocalCheckpointNumber < pendingCheckpointNumber) {
1204
1315
  // Here we have consumed all logs until the `currentL1Block` we pinned at the beginning of the archiver loop,
1205
1316
  // but still haven't reached the pending checkpoint according to the call to the rollup contract.
1206
1317
  // We suspect an L1 reorg that added checkpoints *behind* us. If that is the case, it must have happened between
1207
1318
  // the last checkpoint we saw and the current one, so we reset the last synched L1 block number. In the edge case
1208
1319
  // we don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
1209
- let latestLocalCheckpointArchive = undefined;
1210
- let targetL1BlockNumber = maxBigint(currentL1BlockNumber - 64n, 0n);
1211
- if (lastRetrievedCheckpoint) {
1212
- latestLocalCheckpointArchive = lastRetrievedCheckpoint.checkpoint.archive.root.toString();
1213
- targetL1BlockNumber = lastRetrievedCheckpoint.l1.blockNumber;
1214
- } else if (latestLocalCheckpointNumber > 0) {
1215
- const checkpoint = await this.store.getRangeOfCheckpoints(latestLocalCheckpointNumber, 1).then(([c])=>c);
1216
- latestLocalCheckpointArchive = checkpoint.archive.root.toString();
1217
- targetL1BlockNumber = checkpoint.l1.blockNumber;
1218
- }
1320
+ const latestLocalCheckpoint = lastSeenCheckpoint ?? await this.stores.blocks.getCheckpointData(latestLocalCheckpointNumber) ?? await this.stores.blocks.getRejectedCheckpointByNumber(latestLocalCheckpointNumber);
1321
+ const targetL1BlockNumber = latestLocalCheckpoint?.l1.blockNumber ?? maxBigint(currentL1BlockNumber - 64n, this.l1Constants.l1StartBlock, 0n);
1219
1322
  this.log.warn(`Failed to reach checkpoint ${pendingCheckpointNumber} at ${currentL1BlockNumber} (latest is ${latestLocalCheckpointNumber}). ` + `Rolling back last synched L1 block number to ${targetL1BlockNumber}.`, {
1220
1323
  latestLocalCheckpointNumber,
1221
- latestLocalCheckpointArchive,
1324
+ latestLocalCheckpointL1: latestLocalCheckpoint?.l1,
1222
1325
  blocksSynchedTo,
1223
1326
  currentL1BlockNumber,
1224
1327
  ...status
1225
1328
  });
1226
- await this.store.setCheckpointSynchedL1BlockNumber(targetL1BlockNumber);
1329
+ await this.stores.blocks.setSynchedL1BlockNumber(targetL1BlockNumber);
1227
1330
  } else {
1228
1331
  this.log.trace(`No new checkpoints behind L1 sync point to retrieve.`, {
1229
1332
  latestLocalCheckpointNumber,
@@ -1232,7 +1335,7 @@ _dec = trackSpan('Archiver.syncFromL1'), _dec1 = trackSpan('Archiver.handleEpoch
1232
1335
  }
1233
1336
  }
1234
1337
  async getCheckpointHeader(number) {
1235
- const checkpoint = await this.store.getCheckpointData(number);
1338
+ const checkpoint = await this.stores.blocks.getCheckpointData(number);
1236
1339
  if (!checkpoint) {
1237
1340
  return undefined;
1238
1341
  }