@aztec/archiver 5.0.0-rc.2 → 5.0.1

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 (62) hide show
  1. package/dest/archiver.d.ts +1 -1
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +28 -5
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +5 -0
  7. package/dest/factory.d.ts +8 -1
  8. package/dest/factory.d.ts.map +1 -1
  9. package/dest/factory.js +31 -0
  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/data_source_base.d.ts +1 -2
  16. package/dest/modules/data_source_base.d.ts.map +1 -1
  17. package/dest/modules/data_source_base.js +8 -25
  18. package/dest/modules/data_store_updater.d.ts +1 -1
  19. package/dest/modules/data_store_updater.d.ts.map +1 -1
  20. package/dest/modules/data_store_updater.js +4 -3
  21. package/dest/modules/instrumentation.d.ts +9 -1
  22. package/dest/modules/instrumentation.d.ts.map +1 -1
  23. package/dest/modules/instrumentation.js +23 -2
  24. package/dest/modules/l1_synchronizer.d.ts +3 -3
  25. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  26. package/dest/modules/l1_synchronizer.js +16 -2
  27. package/dest/modules/validation.d.ts +12 -8
  28. package/dest/modules/validation.d.ts.map +1 -1
  29. package/dest/modules/validation.js +4 -11
  30. package/dest/store/block_store.d.ts +13 -3
  31. package/dest/store/block_store.d.ts.map +1 -1
  32. package/dest/store/block_store.js +32 -4
  33. package/dest/store/contract_class_store.d.ts +1 -1
  34. package/dest/store/contract_class_store.d.ts.map +1 -1
  35. package/dest/store/contract_class_store.js +7 -1
  36. package/dest/store/contract_instance_store.d.ts +1 -1
  37. package/dest/store/contract_instance_store.d.ts.map +1 -1
  38. package/dest/store/contract_instance_store.js +7 -1
  39. package/dest/test/fake_l1_state.js +3 -3
  40. package/dest/test/mock_l2_block_source.d.ts +1 -1
  41. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  42. package/dest/test/mock_l2_block_source.js +5 -0
  43. package/dest/test/noop_l1_archiver.d.ts +1 -1
  44. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  45. package/dest/test/noop_l1_archiver.js +1 -1
  46. package/package.json +14 -13
  47. package/src/archiver.ts +32 -6
  48. package/src/config.ts +8 -0
  49. package/src/factory.ts +34 -0
  50. package/src/l1/calldata_retriever.ts +13 -6
  51. package/src/l1/data_retrieval.ts +6 -0
  52. package/src/modules/data_source_base.ts +8 -26
  53. package/src/modules/data_store_updater.ts +4 -3
  54. package/src/modules/instrumentation.ts +15 -2
  55. package/src/modules/l1_synchronizer.ts +29 -4
  56. package/src/modules/validation.ts +15 -18
  57. package/src/store/block_store.ts +31 -5
  58. package/src/store/contract_class_store.ts +7 -1
  59. package/src/store/contract_instance_store.ts +7 -1
  60. package/src/test/fake_l1_state.ts +3 -3
  61. package/src/test/mock_l2_block_source.ts +7 -0
  62. package/src/test/noop_l1_archiver.ts +3 -3
@@ -16,7 +16,12 @@ 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';
19
+ import {
20
+ type ArchiverEmitter,
21
+ type L2Block,
22
+ L2BlockSourceEvents,
23
+ type ValidateCheckpointResult,
24
+ } from '@aztec/stdlib/block';
20
25
  import {
21
26
  Checkpoint,
22
27
  type CheckpointData,
@@ -57,6 +62,8 @@ type RollupStatus = {
57
62
  lastRetrievedCheckpoint?: PublishedCheckpoint;
58
63
  /** Last checkpoint observed on L1 across both valid and rejected entries on this iteration */
59
64
  lastSeenCheckpoint?: { checkpointNumber: CheckpointNumber; l1: L1PublishedData };
65
+ /** Blocks added while handling checkpoints this iteration, for the aggregate block source update event. */
66
+ blocksAdded: L2Block[];
60
67
  };
61
68
 
62
69
  /**
@@ -145,7 +152,9 @@ export class ArchiverL1Synchronizer implements Traceable {
145
152
  }
146
153
 
147
154
  @trackSpan('Archiver.syncFromL1')
148
- public async syncFromL1(initialSyncComplete: boolean): Promise<void> {
155
+ public async syncFromL1(initialSyncComplete: boolean): Promise<L2Block[]> {
156
+ const blocksAdded: L2Block[] = [];
157
+
149
158
  // In between the various calls to L1, the block number can move meaning some of the following
150
159
  // calls will return data for blocks that were not present during earlier calls. To combat this
151
160
  // we ensure that all data retrieval methods only retrieve data up to the currentBlockNumber
@@ -158,7 +167,7 @@ export class ArchiverL1Synchronizer implements Traceable {
158
167
 
159
168
  if (this.l1BlockHash && currentL1BlockHash.equals(this.l1BlockHash)) {
160
169
  this.log.trace(`No new L1 blocks since last sync at L1 block ${this.l1BlockNumber}`);
161
- return;
170
+ return blocksAdded;
162
171
  }
163
172
 
164
173
  // Log at error if the latest L1 block timestamp is too old
@@ -196,6 +205,7 @@ export class ArchiverL1Synchronizer implements Traceable {
196
205
  // First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
197
206
  // pending chain validation status, proven checkpoint number, and synched L1 block number.
198
207
  const rollupStatus = await this.handleCheckpoints(blocksSynchedTo, currentL1BlockNumber, initialSyncComplete);
208
+ blocksAdded.push(...rollupStatus.blocksAdded);
199
209
 
200
210
  // Then we try pruning uncheckpointed blocks if a new slot was mined without checkpoints
201
211
  await this.pruneUncheckpointedBlocks(currentL1Timestamp);
@@ -237,6 +247,8 @@ export class ArchiverL1Synchronizer implements Traceable {
237
247
  l1TimestampAtStart: currentL1Timestamp,
238
248
  l1BlockNumberAtEnd,
239
249
  });
250
+
251
+ return blocksAdded;
240
252
  }
241
253
 
242
254
  /** Updates the finalized checkpoint using the pre-fetched finalized L1 block from the current sync iteration. */
@@ -267,7 +279,7 @@ export class ArchiverL1Synchronizer implements Traceable {
267
279
  }
268
280
 
269
281
  /** Prune all proposed local blocks that should have been checkpointed by now. */
270
- private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) {
282
+ private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint): Promise<void> {
271
283
  const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
272
284
  this.stores.blocks.getCheckpointedL2BlockNumber(),
273
285
  this.stores.blocks.getLatestL2BlockNumber(),
@@ -302,6 +314,7 @@ export class ArchiverL1Synchronizer implements Traceable {
302
314
 
303
315
  const prunedBlocks = await this.updater.removeUncheckpointedBlocksAfter(lastCheckpointedBlockNumber);
304
316
  if (prunedBlocks.length > 0) {
317
+ this.instrumentation.recordPrune('uncheckpointed');
305
318
  this.events.emit(L2BlockSourceEvents.L2PruneUncheckpointed, {
306
319
  type: L2BlockSourceEvents.L2PruneUncheckpointed,
307
320
  slotNumber: firstUncheckpointedBlockSlot,
@@ -622,6 +635,7 @@ export class ArchiverL1Synchronizer implements Traceable {
622
635
  currentL1BlockNumber: bigint,
623
636
  initialSyncComplete: boolean,
624
637
  ): Promise<RollupStatus> {
638
+ const blocksAdded: L2Block[] = [];
625
639
  const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
626
640
  const initialValidationResult: ValidateCheckpointResult | undefined =
627
641
  await this.stores.blocks.getPendingChainValidationStatus();
@@ -640,6 +654,7 @@ export class ArchiverL1Synchronizer implements Traceable {
640
654
  pendingCheckpointNumber,
641
655
  pendingArchive: pendingArchive.toString(),
642
656
  validationResult: initialValidationResult,
657
+ blocksAdded,
643
658
  };
644
659
  this.log.trace(`Retrieved rollup status at current L1 block ${currentL1BlockNumber}.`, {
645
660
  localPendingCheckpointNumber,
@@ -782,6 +797,9 @@ export class ArchiverL1Synchronizer implements Traceable {
782
797
 
783
798
  const checkpointsToRemove = localPendingCheckpointNumber - tipAfterUnwind;
784
799
  await this.updater.removeCheckpointsAfter(CheckpointNumber(tipAfterUnwind));
800
+ if (checkpointsToRemove > 0) {
801
+ this.instrumentation.recordPrune('l1_mismatch');
802
+ }
785
803
 
786
804
  this.log.warn(
787
805
  `Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` +
@@ -1067,6 +1085,11 @@ export class ArchiverL1Synchronizer implements Traceable {
1067
1085
  );
1068
1086
  }
1069
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
+
1070
1093
  // If blocks were pruned due to conflict with L1 checkpoints, emit event
1071
1094
  if (result.prunedBlocks && result.prunedBlocks.length > 0) {
1072
1095
  const prunedCheckpointNumber = result.prunedBlocks[0].checkpointNumber;
@@ -1077,6 +1100,8 @@ export class ArchiverL1Synchronizer implements Traceable {
1077
1100
  { prunedBlocks: result.prunedBlocks.map(b => b.toBlockInfo()), prunedSlotNumber, prunedCheckpointNumber },
1078
1101
  );
1079
1102
 
1103
+ this.instrumentation.recordPrune('l1_conflict');
1104
+
1080
1105
  // Emit event for listening services to react to the prune.
1081
1106
  // Note: slotNumber comes from the first pruned block. If pruned blocks theoretically spanned multiple slots,
1082
1107
  // only one slot number would be reported (though in practice all blocks in a checkpoint span a single slot).
@@ -1,4 +1,5 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
+ import type { ViemCommitteeAttestations } from '@aztec/ethereum/contracts';
2
3
  import { type CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
3
4
  import { compactArray } from '@aztec/foundation/collection';
4
5
  import type { Fr } from '@aztec/foundation/curves/bn254';
@@ -29,22 +30,6 @@ export function getAttestationInfoFromPublishedCheckpoint(
29
30
  return getAttestationInfoFromPayload(payload, attestations);
30
31
  }
31
32
 
32
- /**
33
- * Validates the attestations of a checkpoint already retrieved (with its blocks) from blobs.
34
- * Returns true if the attestations are valid and sufficient, false otherwise.
35
- */
36
- export function validateCheckpointAttestations(
37
- publishedCheckpoint: PublishedCheckpoint,
38
- epochCache: EpochCache,
39
- constants: Pick<L1RollupConstants, 'epochDuration'>,
40
- signatureContext: CoordinationSignatureContext,
41
- logger?: Logger,
42
- ): Promise<ValidateCheckpointResult> {
43
- const { checkpoint, attestations } = publishedCheckpoint;
44
- const payload = ConsensusPayload.fromCheckpoint(checkpoint, signatureContext);
45
- return validateAttestations(payload, attestations, checkpoint.toCheckpointInfo(), epochCache, constants, logger);
46
- }
47
-
48
33
  /** The subset of a calldata-only checkpoint needed to validate its committee attestations. */
49
34
  export type CalldataCheckpointForAttestations = {
50
35
  checkpointNumber: CheckpointNumber;
@@ -52,6 +37,8 @@ export type CalldataCheckpointForAttestations = {
52
37
  feeAssetPriceModifier: bigint;
53
38
  header: CheckpointHeader;
54
39
  attestations: CommitteeAttestation[];
40
+ /** The exact packed attestations tuple from L1 calldata, carried verbatim for byte-faithful invalidation. */
41
+ verbatimAttestations: ViemCommitteeAttestations;
55
42
  };
56
43
 
57
44
  /**
@@ -80,7 +67,15 @@ export function validateCheckpointAttestationsFromCalldata(
80
67
  checkpointNumber: checkpoint.checkpointNumber,
81
68
  timestamp: checkpoint.header.timestamp,
82
69
  };
83
- return validateAttestations(payload, checkpoint.attestations, checkpointInfo, epochCache, constants, logger);
70
+ return validateAttestations(
71
+ payload,
72
+ checkpoint.attestations,
73
+ checkpoint.verbatimAttestations,
74
+ checkpointInfo,
75
+ epochCache,
76
+ constants,
77
+ logger,
78
+ );
84
79
  }
85
80
 
86
81
  /**
@@ -88,9 +83,10 @@ export function validateCheckpointAttestationsFromCalldata(
88
83
  * independent of whether the checkpoint's blocks have been decoded from blobs. Returns true if the
89
84
  * attestations are valid and sufficient, false otherwise.
90
85
  */
91
- async function validateAttestations(
86
+ export async function validateAttestations(
92
87
  payload: ConsensusPayload,
93
88
  attestations: CommitteeAttestation[],
89
+ verbatimAttestations: ViemCommitteeAttestations,
94
90
  checkpointInfo: CheckpointInfo,
95
91
  epochCache: EpochCache,
96
92
  constants: Pick<L1RollupConstants, 'epochDuration'>,
@@ -137,6 +133,7 @@ async function validateAttestations(
137
133
  epoch,
138
134
  attestors,
139
135
  attestations,
136
+ verbatimAttestations,
140
137
  });
141
138
 
142
139
  for (let i = 0; i < attestorInfos.length; i++) {
@@ -306,12 +306,18 @@ export class BlockStore {
306
306
 
307
307
  /**
308
308
  * Append new checkpoints to the store's list.
309
+ * Checkpoints at the start of the batch that are already stored (e.g. re-included by an L1 reorg)
310
+ * are accepted if their archive root matches: their L1 metadata is updated but they are not
311
+ * re-inserted, and they are excluded from the returned array.
309
312
  * @param checkpoints - The L2 checkpoints to be added to the store.
310
- * @returns True if the operation is successful.
313
+ * @returns The checkpoints that were actually inserted (excluding already-stored ones).
311
314
  */
312
- async addCheckpoints(checkpoints: PublishedCheckpoint[], opts: { force?: boolean } = {}): Promise<boolean> {
315
+ async addCheckpoints(
316
+ checkpoints: PublishedCheckpoint[],
317
+ opts: { force?: boolean } = {},
318
+ ): Promise<PublishedCheckpoint[]> {
313
319
  if (checkpoints.length === 0) {
314
- return true;
320
+ return [];
315
321
  }
316
322
 
317
323
  return await this.db.transactionAsync(async () => {
@@ -324,7 +330,7 @@ export class BlockStore {
324
330
  if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
325
331
  checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
326
332
  if (checkpoints.length === 0) {
327
- return true;
333
+ return [];
328
334
  }
329
335
  // Re-check sequentiality after skipping
330
336
  const newFirstNumber = checkpoints[0].checkpoint.number;
@@ -387,7 +393,7 @@ export class BlockStore {
387
393
  }
388
394
 
389
395
  await this.advanceSynchedL1BlockNumber(checkpoints[checkpoints.length - 1].l1.blockNumber);
390
- return true;
396
+ return checkpoints;
391
397
  });
392
398
  }
393
399
 
@@ -621,6 +627,26 @@ export class BlockStore {
621
627
  return checkpoints;
622
628
  }
623
629
 
630
+ /**
631
+ * Returns up to `limit` checkpoints anchored at `fromSlot`, ordered nearest-first, walking the slot index.
632
+ * With `reverse`, takes the checkpoints at or before `fromSlot` (descending by slot); otherwise the
633
+ * checkpoints at or after it (ascending). `limit: 1, reverse: true` yields the latest checkpoint at or
634
+ * before the slot in a single range scan.
635
+ */
636
+ async getCheckpointsBySlot(fromSlot: SlotNumber, limit: number, reverse: boolean): Promise<CheckpointData[]> {
637
+ // The KV range bounds are direction-dependent: forward is [start, end), reverse is (start, end], so a
638
+ // reverse scan uses `end: fromSlot` (inclusive) with no +1 to include the checkpoint at fromSlot itself.
639
+ const range = reverse ? { end: fromSlot, reverse: true, limit } : { start: fromSlot, limit };
640
+ const result: CheckpointData[] = [];
641
+ for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync(range)) {
642
+ const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
643
+ if (checkpointStorage) {
644
+ result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
645
+ }
646
+ }
647
+ return result;
648
+ }
649
+
624
650
  /** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
625
651
  async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
626
652
  const result: CheckpointData[] = [];
@@ -50,13 +50,19 @@ export class ContractClassStore {
50
50
  ): Promise<void> {
51
51
  await this.db.transactionAsync(async () => {
52
52
  const key = contractClass.id.toString();
53
- if (await this.#contractClasses.hasAsync(key)) {
53
+ const existing = await this.#contractClasses.getAsync(key);
54
+ if (existing !== undefined) {
54
55
  // Protocol contracts are preloaded at block 0, so a later on-chain (re-)publish of a bundled
55
56
  // protocol class id is valid and must be a no-op. Keep the existing block-0 entry untouched
56
57
  // (do not bump its block number) so it survives reorgs of the publishing block.
57
58
  if (isProtocolContractClass(contractClass.id)) {
58
59
  return;
59
60
  }
61
+ // An L1 reorg can re-present an already-stored checkpoint, replaying this class at the same
62
+ // block; treat that as a no-op. A duplicate at a different block still signals double-processing.
63
+ if (deserializeContractClassPublic(existing).l2BlockNumber === blockNumber) {
64
+ return;
65
+ }
60
66
  throw new Error(`Contract class ${key} already exists, cannot add again at block ${blockNumber}`);
61
67
  }
62
68
  await this.#contractClasses.set(
@@ -78,8 +78,14 @@ export class ContractInstanceStore {
78
78
  if (isProtocolContract(contractInstance.address)) {
79
79
  return;
80
80
  }
81
+ const existingBlockNumber = await this.#contractInstancePublishedAt.getAsync(key);
82
+ // An L1 reorg can re-present an already-stored checkpoint, replaying this instance at the same
83
+ // block; treat that as a no-op. A duplicate at a different block still signals double-processing.
84
+ if (existingBlockNumber === blockNumber) {
85
+ return;
86
+ }
81
87
  throw new Error(
82
- `Contract instance at ${key} already exists (deployed at block ${await this.#contractInstancePublishedAt.getAsync(key)}), cannot add again at block ${blockNumber}`,
88
+ `Contract instance at ${key} already exists (deployed at block ${existingBlockNumber}), cannot add again at block ${blockNumber}`,
83
89
  );
84
90
  }
85
91
  await this.#contractInstances.set(key, new SerializableContractInstance(contractInstance).toBuffer());
@@ -682,7 +682,7 @@ export class FakeL1State {
682
682
  getHashedSignaturePayloadTypedData(attestationsAndSigners),
683
683
  );
684
684
 
685
- const packedAttestations = attestationsAndSigners.getPackedAttestations();
685
+ const verbatimAttestations = attestationsAndSigners.getPackedAttestations();
686
686
 
687
687
  const rollupInput = encodeFunctionData({
688
688
  abi: RollupAbi,
@@ -693,7 +693,7 @@ export class FakeL1State {
693
693
  archive,
694
694
  oracleInput: { feeAssetPriceModifier: 0n },
695
695
  },
696
- packedAttestations,
696
+ verbatimAttestations,
697
697
  attestationsAndSigners.getSigners().map(signer => signer.toString()),
698
698
  attestationsAndSignersSignature.toViemSignature(),
699
699
  blobInput,
@@ -716,7 +716,7 @@ export class FakeL1State {
716
716
 
717
717
  // Compute attestationsHash (same logic as CalldataRetriever)
718
718
  const attestationsHash = Buffer32.fromString(
719
- keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations])),
719
+ keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [verbatimAttestations])),
720
720
  );
721
721
 
722
722
  // Compute payloadDigest (same logic as CalldataRetriever)
@@ -323,6 +323,13 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource {
323
323
  if ('from' in query) {
324
324
  return this.checkpointList.slice(query.from - 1, query.from - 1 + query.limit);
325
325
  }
326
+ if ('fromSlot' in query) {
327
+ const matching = this.checkpointList.filter(c =>
328
+ query.reverse ? c.header.slotNumber <= query.fromSlot : c.header.slotNumber >= query.fromSlot,
329
+ );
330
+ const nearestFirst = query.reverse ? matching.reverse() : matching;
331
+ return nearestFirst.slice(0, query.limit);
332
+ }
326
333
  return this.getCheckpointsInEpoch(query.epoch);
327
334
  }
328
335
 
@@ -7,7 +7,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
7
7
  import { EthAddress } from '@aztec/foundation/eth-address';
8
8
  import { DateProvider } from '@aztec/foundation/timer';
9
9
  import type { FunctionsOf } from '@aztec/foundation/types';
10
- import type { ArchiverEmitter, BlockHash } from '@aztec/stdlib/block';
10
+ import type { ArchiverEmitter, BlockHash, L2Block } from '@aztec/stdlib/block';
11
11
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
12
12
  import type { BlockHeader } from '@aztec/stdlib/tx';
13
13
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
@@ -43,8 +43,8 @@ class NoopL1Synchronizer implements FunctionsOf<ArchiverL1Synchronizer> {
43
43
  testEthereumNodeSynced(): Promise<void> {
44
44
  return Promise.resolve();
45
45
  }
46
- syncFromL1(_initialSyncComplete: boolean): Promise<void> {
47
- return Promise.resolve();
46
+ syncFromL1(_initialSyncComplete: boolean): Promise<L2Block[]> {
47
+ return Promise.resolve([]);
48
48
  }
49
49
  }
50
50