@aztec/archiver 0.0.1-commit.b9865e97 → 0.0.1-commit.be03c316

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 (82) hide show
  1. package/dest/archiver.d.ts +22 -4
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +79 -35
  4. package/dest/config.d.ts +5 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +22 -5
  7. package/dest/factory.d.ts +1 -2
  8. package/dest/factory.d.ts.map +1 -1
  9. package/dest/factory.js +6 -3
  10. package/dest/l1/calldata_retriever.d.ts +8 -1
  11. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  12. package/dest/l1/calldata_retriever.js +7 -6
  13. package/dest/l1/data_retrieval.d.ts +7 -2
  14. package/dest/l1/data_retrieval.d.ts.map +1 -1
  15. package/dest/modules/contract_data_source_adapter.d.ts +2 -2
  16. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -1
  17. package/dest/modules/contract_data_source_adapter.js +1 -9
  18. package/dest/modules/data_source_base.d.ts +2 -3
  19. package/dest/modules/data_source_base.d.ts.map +1 -1
  20. package/dest/modules/data_source_base.js +9 -35
  21. package/dest/modules/data_store_updater.d.ts +1 -1
  22. package/dest/modules/data_store_updater.d.ts.map +1 -1
  23. package/dest/modules/data_store_updater.js +4 -3
  24. package/dest/modules/instrumentation.d.ts +9 -1
  25. package/dest/modules/instrumentation.d.ts.map +1 -1
  26. package/dest/modules/instrumentation.js +23 -2
  27. package/dest/modules/l1_synchronizer.d.ts +3 -3
  28. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  29. package/dest/modules/l1_synchronizer.js +78 -41
  30. package/dest/modules/validation.d.ts +29 -7
  31. package/dest/modules/validation.d.ts.map +1 -1
  32. package/dest/modules/validation.js +31 -13
  33. package/dest/store/block_store.d.ts +20 -11
  34. package/dest/store/block_store.d.ts.map +1 -1
  35. package/dest/store/block_store.js +43 -25
  36. package/dest/store/contract_class_store.d.ts +1 -1
  37. package/dest/store/contract_class_store.d.ts.map +1 -1
  38. package/dest/store/contract_class_store.js +19 -1
  39. package/dest/store/contract_instance_store.d.ts +1 -1
  40. package/dest/store/contract_instance_store.d.ts.map +1 -1
  41. package/dest/store/contract_instance_store.js +18 -1
  42. package/dest/store/log_store.d.ts +1 -1
  43. package/dest/store/log_store.d.ts.map +1 -1
  44. package/dest/store/log_store.js +3 -3
  45. package/dest/store/log_store_codec.d.ts +9 -1
  46. package/dest/store/log_store_codec.d.ts.map +1 -1
  47. package/dest/store/log_store_codec.js +9 -0
  48. package/dest/test/fake_l1_state.js +3 -3
  49. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  50. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  51. package/dest/test/mock_l1_to_l2_message_source.js +1 -2
  52. package/dest/test/mock_l2_block_source.d.ts +1 -4
  53. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  54. package/dest/test/mock_l2_block_source.js +8 -22
  55. package/dest/test/mock_structs.d.ts +1 -1
  56. package/dest/test/mock_structs.d.ts.map +1 -1
  57. package/dest/test/mock_structs.js +2 -2
  58. package/dest/test/noop_l1_archiver.d.ts +7 -3
  59. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  60. package/dest/test/noop_l1_archiver.js +9 -6
  61. package/package.json +13 -13
  62. package/src/archiver.ts +107 -41
  63. package/src/config.ts +27 -10
  64. package/src/factory.ts +12 -4
  65. package/src/l1/calldata_retriever.ts +13 -6
  66. package/src/l1/data_retrieval.ts +8 -1
  67. package/src/modules/contract_data_source_adapter.ts +1 -10
  68. package/src/modules/data_source_base.ts +9 -38
  69. package/src/modules/data_store_updater.ts +4 -3
  70. package/src/modules/instrumentation.ts +15 -2
  71. package/src/modules/l1_synchronizer.ts +120 -57
  72. package/src/modules/validation.ts +71 -15
  73. package/src/store/block_store.ts +42 -31
  74. package/src/store/contract_class_store.ts +19 -1
  75. package/src/store/contract_instance_store.ts +18 -1
  76. package/src/store/log_store.ts +3 -2
  77. package/src/store/log_store_codec.ts +11 -0
  78. package/src/test/fake_l1_state.ts +3 -3
  79. package/src/test/mock_l1_to_l2_message_source.ts +0 -1
  80. package/src/test/mock_l2_block_source.ts +8 -22
  81. package/src/test/mock_structs.ts +5 -2
  82. package/src/test/noop_l1_archiver.ts +15 -5
@@ -34,6 +34,12 @@ type CheckpointData = {
34
34
  archiveRoot: Fr;
35
35
  header: CheckpointHeader;
36
36
  attestations: CommitteeAttestation[];
37
+ /**
38
+ * The exact packed `CommitteeAttestations` tuple as it appears in the propose calldata, preserved
39
+ * verbatim (never re-derived from {@link attestations}) so invalidation evidence stays byte-faithful to
40
+ * the on-chain `attestationsHash`.
41
+ */
42
+ verbatimAttestations: ViemCommitteeAttestations;
37
43
  blockHash: string;
38
44
  feeAssetPriceModifier: bigint;
39
45
  };
@@ -406,14 +412,14 @@ export class CalldataRetriever {
406
412
  return undefined;
407
413
  }
408
414
 
409
- const [decodedArgs, packedAttestations] = args! as readonly [
415
+ const [decodedArgs, verbatimAttestations] = args! as readonly [
410
416
  { archive: Hex; oracleInput: { feeAssetPriceModifier: bigint }; header: ViemHeader },
411
417
  ViemCommitteeAttestations,
412
418
  ...unknown[],
413
419
  ];
414
420
 
415
421
  // Verify attestationsHash
416
- const computedAttestationsHash = this.computeAttestationsHash(packedAttestations);
422
+ const computedAttestationsHash = this.computeAttestationsHash(verbatimAttestations);
417
423
  if (
418
424
  !Buffer.from(hexToBytes(computedAttestationsHash)).equals(
419
425
  Buffer.from(hexToBytes(expectedHashes.attestationsHash)),
@@ -441,7 +447,7 @@ export class CalldataRetriever {
441
447
  return undefined;
442
448
  }
443
449
 
444
- const attestations = CommitteeAttestation.fromPacked(packedAttestations, this.targetCommitteeSize);
450
+ const attestations = CommitteeAttestation.fromPacked(verbatimAttestations, this.targetCommitteeSize);
445
451
 
446
452
  this.logger.trace(`Validated and decoded propose calldata for checkpoint ${checkpointNumber}`, {
447
453
  checkpointNumber,
@@ -449,7 +455,7 @@ export class CalldataRetriever {
449
455
  header: decodedArgs.header,
450
456
  l1BlockHash: blockHash,
451
457
  attestations,
452
- packedAttestations,
458
+ verbatimAttestations,
453
459
  targetCommitteeSize: this.targetCommitteeSize,
454
460
  });
455
461
 
@@ -458,6 +464,7 @@ export class CalldataRetriever {
458
464
  archiveRoot,
459
465
  header,
460
466
  attestations,
467
+ verbatimAttestations,
461
468
  blockHash,
462
469
  feeAssetPriceModifier,
463
470
  };
@@ -467,8 +474,8 @@ export class CalldataRetriever {
467
474
  }
468
475
 
469
476
  /** Computes the keccak256 hash of ABI-encoded CommitteeAttestations. */
470
- private computeAttestationsHash(packedAttestations: ViemCommitteeAttestations): Hex {
471
- return keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations]));
477
+ private computeAttestationsHash(verbatimAttestations: ViemCommitteeAttestations): Hex {
478
+ return keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [verbatimAttestations]));
472
479
  }
473
480
 
474
481
  /** Computes the keccak256 payload digest from the checkpoint header, archive root, and fee asset price modifier. */
@@ -12,6 +12,8 @@ import type {
12
12
  InboxContract,
13
13
  MessageSentLog,
14
14
  RollupContract,
15
+ ViemCommitteeAttestations,
16
+ ViemHeader,
15
17
  } from '@aztec/ethereum/contracts';
16
18
  import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
17
19
  import { asyncPool } from '@aztec/foundation/async-pool';
@@ -55,6 +57,11 @@ export type RetrievedCheckpointFromCalldata = RetrievedCheckpointBase & {
55
57
  blobHashes: Buffer[];
56
58
  /** Parent beacon block root from the L1 block, used for blob fetching. */
57
59
  parentBeaconBlockRoot: string | undefined;
60
+ /**
61
+ * The exact packed `CommitteeAttestations` tuple from the propose calldata, carried verbatim so that
62
+ * attestation validation can attach byte-faithful invalidation evidence to a negative result.
63
+ */
64
+ verbatimAttestations: ViemCommitteeAttestations;
58
65
  };
59
66
 
60
67
  export async function retrievedToPublishedCheckpoint({
@@ -460,7 +467,7 @@ export async function getProofFromSubmitProofTx(
460
467
  start: bigint;
461
468
  end: bigint;
462
469
  args: EpochProofPublicInputArgs;
463
- fees: readonly Hex[];
470
+ headers: readonly ViemHeader[];
464
471
  proof: Hex;
465
472
  },
466
473
  ];
@@ -28,16 +28,7 @@ export class ArchiverContractDataSourceAdapter implements ContractDataSource {
28
28
  return this.stores.contractClasses.getBytecodeCommitment(id);
29
29
  }
30
30
 
31
- public async getContract(
32
- address: AztecAddress,
33
- maybeTimestamp?: UInt64,
34
- ): Promise<ContractInstanceWithAddress | undefined> {
35
- let timestamp = maybeTimestamp;
36
- if (timestamp === undefined) {
37
- const latest = await this.stores.blocks.getLatestL2BlockNumber();
38
- const blockData = latest > 0 ? await this.stores.blocks.getBlockData({ number: latest }) : undefined;
39
- timestamp = blockData ? blockData.header.globalVariables.timestamp : 0n;
40
- }
31
+ public getContract(address: AztecAddress, timestamp: UInt64): Promise<ContractInstanceWithAddress | undefined> {
41
32
  return this.stores.contractInstances.getContractInstance(address, timestamp);
42
33
  }
43
34
 
@@ -224,23 +224,6 @@ export abstract class ArchiverDataSourceBase
224
224
  }
225
225
  }
226
226
 
227
- /**
228
- * Resolves a {@link CheckpointsQuery} to a concrete `{from, limit}` pair used by BlockStore,
229
- * or undefined when the epoch has no checkpoints.
230
- */
231
- private async resolveCheckpointsQuery(
232
- query: CheckpointsQuery,
233
- ): Promise<{ from: CheckpointNumber; limit: number } | undefined> {
234
- if ('from' in query) {
235
- return query;
236
- }
237
- const numbers = await this.getCheckpointNumbersForEpoch(query.epoch);
238
- if (numbers.length === 0) {
239
- return undefined;
240
- }
241
- return { from: numbers[0], limit: numbers.length };
242
- }
243
-
244
227
  public async getCheckpoint(query: CheckpointQuery): Promise<PublishedCheckpoint | undefined> {
245
228
  const number = await this.resolveCheckpointQuery(query);
246
229
  if (number === undefined || number === 0) {
@@ -254,11 +237,7 @@ export abstract class ArchiverDataSourceBase
254
237
  }
255
238
 
256
239
  public async getCheckpoints(query: CheckpointsQuery): Promise<PublishedCheckpoint[]> {
257
- const resolved = await this.resolveCheckpointsQuery(query);
258
- if (!resolved) {
259
- return [];
260
- }
261
- const checkpoints = await this.stores.blocks.getRangeOfCheckpoints(resolved.from, resolved.limit);
240
+ const checkpoints = await this.getCheckpointsData(query);
262
241
  return Promise.all(checkpoints.map(ch => this.getPublishedCheckpointFromCheckpointData(ch)));
263
242
  }
264
243
 
@@ -271,11 +250,14 @@ export abstract class ArchiverDataSourceBase
271
250
  }
272
251
 
273
252
  public async getCheckpointsData(query: CheckpointsQuery): Promise<CheckpointData[]> {
274
- const resolved = await this.resolveCheckpointsQuery(query);
275
- if (!resolved) {
276
- return [];
253
+ if ('fromSlot' in query) {
254
+ return this.stores.blocks.getCheckpointsBySlot(query.fromSlot, query.limit, query.reverse ?? false);
255
+ }
256
+ if ('from' in query) {
257
+ return this.stores.blocks.getRangeOfCheckpoints(query.from, query.limit);
277
258
  }
278
- return this.stores.blocks.getRangeOfCheckpoints(resolved.from, resolved.limit);
259
+ const numbers = await this.getCheckpointNumbersForEpoch(query.epoch);
260
+ return numbers.length > 0 ? this.stores.blocks.getRangeOfCheckpoints(numbers[0], numbers.length) : [];
279
261
  }
280
262
 
281
263
  public getProposedCheckpointData(query?: ProposedCheckpointQuery): Promise<ProposedCheckpointData | undefined> {
@@ -316,18 +298,7 @@ export abstract class ArchiverDataSourceBase
316
298
  return this.stores.contractClasses.getBytecodeCommitment(id);
317
299
  }
318
300
 
319
- public async getContract(
320
- address: AztecAddress,
321
- maybeTimestamp?: UInt64,
322
- ): Promise<ContractInstanceWithAddress | undefined> {
323
- let timestamp;
324
- if (maybeTimestamp === undefined) {
325
- const latestBlockData = await this.getBlockData({ tag: 'proposed' });
326
- timestamp = latestBlockData ? latestBlockData.header.globalVariables.timestamp : 0n;
327
- } else {
328
- timestamp = maybeTimestamp;
329
- }
330
-
301
+ public getContract(address: AztecAddress, timestamp: UInt64): Promise<ContractInstanceWithAddress | undefined> {
331
302
  return this.stores.contractInstances.getContractInstance(address, timestamp);
332
303
  }
333
304
 
@@ -114,10 +114,11 @@ export class ArchiverDataStoreUpdater {
114
114
  // Before adding checkpoints, check for conflicts with local blocks if any
115
115
  const { prunedBlocks, lastAlreadyInsertedBlockNumber } = await this.pruneMismatchingLocalBlocks(checkpoints);
116
116
 
117
- await this.stores.blocks.addCheckpoints(checkpoints);
117
+ const insertedCheckpoints = await this.stores.blocks.addCheckpoints(checkpoints);
118
118
 
119
- // Filter out blocks that were already inserted via addProposedBlock() to avoid duplicating logs/contract data
120
- const newBlocks = checkpoints
119
+ // Skip blocks already inserted via addProposedBlock() and blocks of already-stored checkpoints
120
+ // re-included by an L1 reorg: their logs/contract data were extracted when first inserted.
121
+ const newBlocks = insertedCheckpoints
121
122
  .flatMap((ch: PublishedCheckpoint) => ch.checkpoint.blocks)
122
123
  .filter(b => lastAlreadyInsertedBlockNumber === undefined || b.number > lastAlreadyInsertedBlockNumber);
123
124
 
@@ -84,7 +84,9 @@ export class ArchiverInstrumentation {
84
84
 
85
85
  this.pruneDuration = meter.createHistogram(Metrics.ARCHIVER_PRUNE_DURATION);
86
86
 
87
- this.pruneCount = createUpDownCounterWithDefault(meter, Metrics.ARCHIVER_PRUNE_COUNT);
87
+ this.pruneCount = createUpDownCounterWithDefault(meter, Metrics.ARCHIVER_PRUNE_COUNT, {
88
+ [Attributes.PRUNE_TYPE]: ['unproven', 'uncheckpointed', 'l1_conflict', 'orphan', 'l1_mismatch'],
89
+ });
88
90
 
89
91
  this.blockProposalTxTargetCount = createUpDownCounterWithDefault(
90
92
  meter,
@@ -150,10 +152,21 @@ export class ArchiverInstrumentation {
150
152
  }
151
153
 
152
154
  public processPrune(duration: number) {
153
- this.pruneCount.add(1);
155
+ this.pruneCount.add(1, { [Attributes.PRUNE_TYPE]: 'unproven' });
154
156
  this.pruneDuration.record(Math.ceil(duration));
155
157
  }
156
158
 
159
+ /**
160
+ * Records a pending-chain reorg, where the archiver dropped proposed blocks (and world-state follows by pruning). The
161
+ * type distinguishes the cause: 'uncheckpointed' (slot ended without a checkpoint), 'l1_conflict' (proposed blocks
162
+ * conflicting with an L1 checkpoint), 'orphan' (no matching proposed checkpoint arrived before the deadline), or
163
+ * 'l1_mismatch' (the local checkpointed tip diverged from L1 — an L1 reorg or a pruned/missed-proof checkpoint — so
164
+ * already-checkpointed blocks were rewound).
165
+ */
166
+ public recordPrune(pruneType: 'uncheckpointed' | 'l1_conflict' | 'orphan' | 'l1_mismatch') {
167
+ this.pruneCount.add(1, { [Attributes.PRUNE_TYPE]: pruneType });
168
+ }
169
+
157
170
  public updateLastProvenCheckpoint(checkpoint: CheckpointData) {
158
171
  const lastBlockNumberInCheckpoint = checkpoint.startBlock + checkpoint.blockCount - 1;
159
172
  this.blockHeight.record(lastBlockNumberInCheckpoint, { [Attributes.STATUS]: 'proven' });
@@ -16,8 +16,19 @@ import { retryTimes } from '@aztec/foundation/retry';
16
16
  import { count } from '@aztec/foundation/string';
17
17
  import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
18
18
  import { isDefined, isErrorClass } from '@aztec/foundation/types';
19
- import { type ArchiverEmitter, L2BlockSourceEvents, type ValidateCheckpointResult } from '@aztec/stdlib/block';
20
- import { Checkpoint, type CheckpointData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
19
+ import {
20
+ type ArchiverEmitter,
21
+ type L2Block,
22
+ L2BlockSourceEvents,
23
+ type ValidateCheckpointResult,
24
+ } from '@aztec/stdlib/block';
25
+ import {
26
+ Checkpoint,
27
+ type CheckpointData,
28
+ type CheckpointInfo,
29
+ type L1PublishedData,
30
+ PublishedCheckpoint,
31
+ } from '@aztec/stdlib/checkpoint';
21
32
  import { type L1RollupConstants, getEpochAtSlot, getSlotAtNextL1Block } from '@aztec/stdlib/epoch-helpers';
22
33
  import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
23
34
  import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
@@ -39,7 +50,7 @@ import { MessageStoreError } from '../store/message_store.js';
39
50
  import type { InboxMessage } from '../structs/inbox_message.js';
40
51
  import { ArchiverDataStoreUpdater } from './data_store_updater.js';
41
52
  import type { ArchiverInstrumentation } from './instrumentation.js';
42
- import { validateCheckpointAttestations } from './validation.js';
53
+ import { validateCheckpointAttestationsFromCalldata } from './validation.js';
43
54
 
44
55
  type RollupStatus = {
45
56
  provenCheckpointNumber: CheckpointNumber;
@@ -50,7 +61,9 @@ type RollupStatus = {
50
61
  /** Last valid checkpoint observed on L1 and synced on this iteration */
51
62
  lastRetrievedCheckpoint?: PublishedCheckpoint;
52
63
  /** Last checkpoint observed on L1 across both valid and rejected entries on this iteration */
53
- lastSeenCheckpoint?: PublishedCheckpoint;
64
+ lastSeenCheckpoint?: { checkpointNumber: CheckpointNumber; l1: L1PublishedData };
65
+ /** Blocks added while handling checkpoints this iteration, for the aggregate block source update event. */
66
+ blocksAdded: L2Block[];
54
67
  };
55
68
 
56
69
  /**
@@ -139,7 +152,9 @@ export class ArchiverL1Synchronizer implements Traceable {
139
152
  }
140
153
 
141
154
  @trackSpan('Archiver.syncFromL1')
142
- public async syncFromL1(initialSyncComplete: boolean): Promise<void> {
155
+ public async syncFromL1(initialSyncComplete: boolean): Promise<L2Block[]> {
156
+ const blocksAdded: L2Block[] = [];
157
+
143
158
  // In between the various calls to L1, the block number can move meaning some of the following
144
159
  // calls will return data for blocks that were not present during earlier calls. To combat this
145
160
  // we ensure that all data retrieval methods only retrieve data up to the currentBlockNumber
@@ -152,7 +167,7 @@ export class ArchiverL1Synchronizer implements Traceable {
152
167
 
153
168
  if (this.l1BlockHash && currentL1BlockHash.equals(this.l1BlockHash)) {
154
169
  this.log.trace(`No new L1 blocks since last sync at L1 block ${this.l1BlockNumber}`);
155
- return;
170
+ return blocksAdded;
156
171
  }
157
172
 
158
173
  // Log at error if the latest L1 block timestamp is too old
@@ -190,6 +205,7 @@ export class ArchiverL1Synchronizer implements Traceable {
190
205
  // First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
191
206
  // pending chain validation status, proven checkpoint number, and synched L1 block number.
192
207
  const rollupStatus = await this.handleCheckpoints(blocksSynchedTo, currentL1BlockNumber, initialSyncComplete);
208
+ blocksAdded.push(...rollupStatus.blocksAdded);
193
209
 
194
210
  // Then we try pruning uncheckpointed blocks if a new slot was mined without checkpoints
195
211
  await this.pruneUncheckpointedBlocks(currentL1Timestamp);
@@ -231,6 +247,8 @@ export class ArchiverL1Synchronizer implements Traceable {
231
247
  l1TimestampAtStart: currentL1Timestamp,
232
248
  l1BlockNumberAtEnd,
233
249
  });
250
+
251
+ return blocksAdded;
234
252
  }
235
253
 
236
254
  /** Updates the finalized checkpoint using the pre-fetched finalized L1 block from the current sync iteration. */
@@ -261,7 +279,7 @@ export class ArchiverL1Synchronizer implements Traceable {
261
279
  }
262
280
 
263
281
  /** Prune all proposed local blocks that should have been checkpointed by now. */
264
- private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) {
282
+ private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint): Promise<void> {
265
283
  const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
266
284
  this.stores.blocks.getCheckpointedL2BlockNumber(),
267
285
  this.stores.blocks.getLatestL2BlockNumber(),
@@ -296,6 +314,7 @@ export class ArchiverL1Synchronizer implements Traceable {
296
314
 
297
315
  const prunedBlocks = await this.updater.removeUncheckpointedBlocksAfter(lastCheckpointedBlockNumber);
298
316
  if (prunedBlocks.length > 0) {
317
+ this.instrumentation.recordPrune('uncheckpointed');
299
318
  this.events.emit(L2BlockSourceEvents.L2PruneUncheckpointed, {
300
319
  type: L2BlockSourceEvents.L2PruneUncheckpointed,
301
320
  slotNumber: firstUncheckpointedBlockSlot,
@@ -616,6 +635,7 @@ export class ArchiverL1Synchronizer implements Traceable {
616
635
  currentL1BlockNumber: bigint,
617
636
  initialSyncComplete: boolean,
618
637
  ): Promise<RollupStatus> {
638
+ const blocksAdded: L2Block[] = [];
619
639
  const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
620
640
  const initialValidationResult: ValidateCheckpointResult | undefined =
621
641
  await this.stores.blocks.getPendingChainValidationStatus();
@@ -634,6 +654,7 @@ export class ArchiverL1Synchronizer implements Traceable {
634
654
  pendingCheckpointNumber,
635
655
  pendingArchive: pendingArchive.toString(),
636
656
  validationResult: initialValidationResult,
657
+ blocksAdded,
637
658
  };
638
659
  this.log.trace(`Retrieved rollup status at current L1 block ${currentL1BlockNumber}.`, {
639
660
  localPendingCheckpointNumber,
@@ -776,6 +797,9 @@ export class ArchiverL1Synchronizer implements Traceable {
776
797
 
777
798
  const checkpointsToRemove = localPendingCheckpointNumber - tipAfterUnwind;
778
799
  await this.updater.removeCheckpointsAfter(CheckpointNumber(tipAfterUnwind));
800
+ if (checkpointsToRemove > 0) {
801
+ this.instrumentation.recordPrune('l1_mismatch');
802
+ }
779
803
 
780
804
  this.log.warn(
781
805
  `Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` +
@@ -790,7 +814,7 @@ export class ArchiverL1Synchronizer implements Traceable {
790
814
  let searchStartBlock: bigint = blocksSynchedTo;
791
815
  let searchEndBlock: bigint = blocksSynchedTo;
792
816
  let lastRetrievedCheckpoint: PublishedCheckpoint | undefined;
793
- let lastSeenCheckpoint: PublishedCheckpoint | undefined;
817
+ let lastSeenCheckpoint: { checkpointNumber: CheckpointNumber; l1: L1PublishedData } | undefined;
794
818
 
795
819
  do {
796
820
  [searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
@@ -835,37 +859,21 @@ export class ArchiverL1Synchronizer implements Traceable {
835
859
  const evictProposedFrom =
836
860
  promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
837
861
 
838
- // Then fetch blobs in parallel and build the full published checkpoints
839
- const toFetchBlobs = checkpointToPromote ? calldataCheckpoints.slice(0, -1) : calldataCheckpoints;
840
- const blobFetched = await asyncPool(10, toFetchBlobs, async checkpoint =>
841
- retrievedToPublishedCheckpoint({
842
- ...checkpoint,
843
- checkpointBlobData: await getCheckpointBlobDataFromBlobs(
844
- this.blobClient,
845
- checkpoint.l1.blockHash,
846
- checkpoint.blobHashes,
847
- checkpoint.checkpointNumber,
848
- this.log,
849
- !initialSyncComplete,
850
- checkpoint.parentBeaconBlockRoot,
851
- checkpoint.l1.timestamp,
852
- ),
853
- }),
854
- );
855
-
856
- // And add the promoted checkpoint to the list of all checkpoints
857
- const publishedCheckpoints = checkpointToPromote ? [...blobFetched, checkpointToPromote] : blobFetched;
858
- const validCheckpoints: PublishedCheckpoint[] = [];
862
+ // Validate attestations from CALLDATA before fetching any blobs. A checkpoint with invalid
863
+ // attestations (or one descending from a rejected ancestor) is rejected here without fetching its
864
+ // blobs, so a malformed blob does not throw during decode before the rejection path runs and
865
+ // stall sync. The signed consensus payload (header, archive root, fee asset price
866
+ // modifier) is fully available from calldata.
867
+ const checkpointsToIngest: RetrievedCheckpointFromCalldata[] = [];
859
868
 
860
- // Now loop through all checkpoints and validate their attestations
861
- for (const published of publishedCheckpoints) {
862
- // Check the attestations uploaded by the publisher to L1 are correct
869
+ for (const calldataCheckpoint of calldataCheckpoints) {
870
+ // Check the attestations uploaded by the publisher to L1 are correct.
863
871
  // Rollup contract does not validate attestations to save on gas, so this
864
872
  // falls on the nodes to verify offchain and skip those checkpoints.
865
873
  const validationResult = this.config.skipValidateCheckpointAttestations
866
874
  ? { valid: true as const }
867
- : await validateCheckpointAttestations(
868
- published,
875
+ : await validateCheckpointAttestationsFromCalldata(
876
+ calldataCheckpoint,
869
877
  this.epochCache,
870
878
  this.l1Constants,
871
879
  this.getSignatureContext(),
@@ -877,7 +885,7 @@ export class ArchiverL1Synchronizer implements Traceable {
877
885
  // ancestor was skipped earlier (e.g. due to invalid attestations), the catch handler
878
886
  // would roll back the L1 sync point, and the next iteration would re-fetch and re-throw.
879
887
  const rejectedAncestor = await this.stores.blocks.getRejectedCheckpointByArchiveRoot(
880
- published.checkpoint.header.lastArchiveRoot,
888
+ calldataCheckpoint.header.lastArchiveRoot,
881
889
  );
882
890
 
883
891
  // Update the validation result if it has changed, so we can keep track of the first invalid checkpoint
@@ -899,9 +907,9 @@ export class ArchiverL1Synchronizer implements Traceable {
899
907
  }
900
908
 
901
909
  if (!validationResult.valid) {
902
- this.log.warn(`Skipping checkpoint ${published.checkpoint.number} due to invalid attestations`, {
903
- checkpointHash: published.checkpoint.hash(),
904
- l1BlockNumber: published.l1.blockNumber,
910
+ this.log.warn(`Skipping checkpoint ${calldataCheckpoint.checkpointNumber} due to invalid attestations`, {
911
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
912
+ l1BlockNumber: calldataCheckpoint.l1.blockNumber,
905
913
  ...pick(validationResult, 'reason'),
906
914
  });
907
915
 
@@ -915,11 +923,11 @@ export class ArchiverL1Synchronizer implements Traceable {
915
923
  // is detected and skipped (rather than tripping the addCheckpoints consecutive-number
916
924
  // check and causing the sync point to roll back in a loop).
917
925
  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,
926
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
927
+ archiveRoot: calldataCheckpoint.archiveRoot,
928
+ parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
929
+ slotNumber: calldataCheckpoint.header.slotNumber,
930
+ l1: calldataCheckpoint.l1,
923
931
  reason: 'invalid-attestations' as const,
924
932
  });
925
933
 
@@ -927,15 +935,20 @@ export class ArchiverL1Synchronizer implements Traceable {
927
935
  }
928
936
 
929
937
  if (rejectedAncestor) {
930
- const descendantInfo = published.checkpoint.toCheckpointInfo();
938
+ const descendantInfo: CheckpointInfo = {
939
+ archive: calldataCheckpoint.archiveRoot,
940
+ lastArchive: calldataCheckpoint.header.lastArchiveRoot,
941
+ slotNumber: calldataCheckpoint.header.slotNumber,
942
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
943
+ timestamp: calldataCheckpoint.header.timestamp,
944
+ };
931
945
  this.log.warn(
932
- `Skipping checkpoint ${published.checkpoint.number} as it is a descendant of ` +
946
+ `Skipping checkpoint ${calldataCheckpoint.checkpointNumber} as it is a descendant of ` +
933
947
  `rejected checkpoint ${rejectedAncestor.checkpointNumber} (${rejectedAncestor.reason})`,
934
948
  {
935
- checkpointNumber: published.checkpoint.number,
936
- checkpointHash: published.checkpoint.hash(),
937
- l1BlockNumber: published.l1.blockNumber,
938
- l1BlockHash: published.l1.blockHash,
949
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
950
+ l1BlockNumber: calldataCheckpoint.l1.blockNumber,
951
+ l1BlockHash: calldataCheckpoint.l1.blockHash,
939
952
  ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
940
953
  ancestorArchiveRoot: rejectedAncestor.archiveRoot.toString(),
941
954
  ancestorReason: rejectedAncestor.reason,
@@ -952,17 +965,54 @@ export class ArchiverL1Synchronizer implements Traceable {
952
965
  // Persist this chainpoint as rejected as well, so we can construct a chain of
953
966
  // skipped checkpoints starting from the first one with invalid attestations.
954
967
  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,
968
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
969
+ archiveRoot: calldataCheckpoint.archiveRoot,
970
+ parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
971
+ slotNumber: calldataCheckpoint.header.slotNumber,
972
+ l1: calldataCheckpoint.l1,
960
973
  reason: 'descends-from-invalid-attestations' as const,
961
974
  });
962
975
 
963
976
  continue;
964
977
  }
965
978
 
979
+ checkpointsToIngest.push(calldataCheckpoint);
980
+ }
981
+
982
+ // Fetch blobs in parallel only for the surviving (attestation-valid, non-descendant) checkpoints,
983
+ // then build the full published checkpoints. The last calldata checkpoint may be promotable from a
984
+ // local proposed block (checkpointToPromote), in which case it carries no blob to fetch. A missing or
985
+ // undecodable blob throws and propagates, rolling back the L1 sync point so the fetch is retried.
986
+ const toFetchBlobs = checkpointToPromote
987
+ ? checkpointsToIngest.filter(c => c.checkpointNumber !== checkpointToPromote.checkpoint.number)
988
+ : checkpointsToIngest;
989
+ const blobFetched = await asyncPool(10, toFetchBlobs, async checkpoint =>
990
+ retrievedToPublishedCheckpoint({
991
+ ...checkpoint,
992
+ checkpointBlobData: await getCheckpointBlobDataFromBlobs(
993
+ this.blobClient,
994
+ checkpoint.l1.blockHash,
995
+ checkpoint.blobHashes,
996
+ checkpoint.checkpointNumber,
997
+ this.log,
998
+ !initialSyncComplete,
999
+ checkpoint.parentBeaconBlockRoot,
1000
+ checkpoint.l1.timestamp,
1001
+ ),
1002
+ }),
1003
+ );
1004
+
1005
+ // Index the built checkpoints by number so we can ingest them in calldata order, slotting in the
1006
+ // promoted checkpoint (built from a local proposed block rather than blobs).
1007
+ const publishedByNumber = new Map(blobFetched.map(published => [published.checkpoint.number, published]));
1008
+ if (checkpointToPromote) {
1009
+ publishedByNumber.set(checkpointToPromote.checkpoint.number, checkpointToPromote);
1010
+ }
1011
+
1012
+ const validCheckpoints: PublishedCheckpoint[] = [];
1013
+ for (const calldataCheckpoint of checkpointsToIngest) {
1014
+ const published = publishedByNumber.get(calldataCheckpoint.checkpointNumber)!;
1015
+
966
1016
  // Check the inHash of the checkpoint against the l1->l2 messages.
967
1017
  // The messages should've been synced up to the currentL1BlockNumber and must be available for the published
968
1018
  // checkpoints we just retrieved.
@@ -1035,6 +1085,11 @@ export class ArchiverL1Synchronizer implements Traceable {
1035
1085
  );
1036
1086
  }
1037
1087
 
1088
+ // Record blocks newly fetched from L1 checkpoint payloads as added. The promoted checkpoint (if any) is
1089
+ // excluded: its blocks were already in local archiver storage (added via the proposed-block path) so the
1090
+ // block stream does not need them re-downloaded.
1091
+ blocksAdded.push(...checkpointsToAdd.flatMap(c => c.checkpoint.blocks));
1092
+
1038
1093
  // If blocks were pruned due to conflict with L1 checkpoints, emit event
1039
1094
  if (result.prunedBlocks && result.prunedBlocks.length > 0) {
1040
1095
  const prunedCheckpointNumber = result.prunedBlocks[0].checkpointNumber;
@@ -1045,6 +1100,8 @@ export class ArchiverL1Synchronizer implements Traceable {
1045
1100
  { prunedBlocks: result.prunedBlocks.map(b => b.toBlockInfo()), prunedSlotNumber, prunedCheckpointNumber },
1046
1101
  );
1047
1102
 
1103
+ this.instrumentation.recordPrune('l1_conflict');
1104
+
1048
1105
  // Emit event for listening services to react to the prune.
1049
1106
  // Note: slotNumber comes from the first pruned block. If pruned blocks theoretically spanned multiple slots,
1050
1107
  // only one slot number would be reported (though in practice all blocks in a checkpoint span a single slot).
@@ -1095,7 +1152,9 @@ export class ArchiverL1Synchronizer implements Traceable {
1095
1152
  });
1096
1153
  }
1097
1154
  lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
1098
- lastSeenCheckpoint = publishedCheckpoints.at(-1) ?? lastSeenCheckpoint;
1155
+ // The last checkpoint seen on L1 this batch (valid or rejected), tracked from calldata since
1156
+ // rejected checkpoints are no longer built into PublishedCheckpoints.
1157
+ lastSeenCheckpoint = lastCalldataCheckpoint;
1099
1158
  } while (searchEndBlock < currentL1BlockNumber);
1100
1159
 
1101
1160
  // Important that we update AFTER inserting the blocks.
@@ -1205,7 +1264,7 @@ export class ArchiverL1Synchronizer implements Traceable {
1205
1264
  // Compare the last checkpoint (valid or not) we have (either retrieved in this round or loaded from store)
1206
1265
  // with what the rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
1207
1266
  const latestLocalCheckpointNumber =
1208
- lastSeenCheckpoint?.checkpoint.number ??
1267
+ lastSeenCheckpoint?.checkpointNumber ??
1209
1268
  CheckpointNumber.max(
1210
1269
  await this.stores.blocks.getLatestCheckpointNumber(),
1211
1270
  await this.stores.blocks.getLatestRejectedCheckpointNumber(),
@@ -1218,7 +1277,11 @@ export class ArchiverL1Synchronizer implements Traceable {
1218
1277
  // We suspect an L1 reorg that added checkpoints *behind* us. If that is the case, it must have happened between
1219
1278
  // the last checkpoint we saw and the current one, so we reset the last synched L1 block number. In the edge case
1220
1279
  // we don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
1221
- const latestLocalCheckpoint: PublishedCheckpoint | CheckpointData | RejectedCheckpoint | undefined =
1280
+ const latestLocalCheckpoint:
1281
+ | { checkpointNumber: CheckpointNumber; l1: L1PublishedData }
1282
+ | CheckpointData
1283
+ | RejectedCheckpoint
1284
+ | undefined =
1222
1285
  lastSeenCheckpoint ??
1223
1286
  (await this.stores.blocks.getCheckpointData(latestLocalCheckpointNumber)) ??
1224
1287
  (await this.stores.blocks.getRejectedCheckpointByNumber(latestLocalCheckpointNumber));