@aztec-labs/archiver 6.0.0-rc.1 → 6.0.0-rc.2

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 (42) hide show
  1. package/README.md +7 -1
  2. package/dest/errors.d.ts +20 -1
  3. package/dest/errors.d.ts.map +1 -1
  4. package/dest/errors.js +22 -0
  5. package/dest/l1/bin/retrieve-calldata.js +14 -4
  6. package/dest/l1/calldata_retriever.d.ts +15 -17
  7. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  8. package/dest/l1/calldata_retriever.js +2 -9
  9. package/dest/l1/data_retrieval.d.ts +7 -3
  10. package/dest/l1/data_retrieval.d.ts.map +1 -1
  11. package/dest/l1/data_retrieval.js +14 -9
  12. package/dest/modules/data_store_updater.js +5 -5
  13. package/dest/modules/l1_synchronizer.d.ts +1 -1
  14. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  15. package/dest/modules/l1_synchronizer.js +228 -191
  16. package/dest/modules/validation.d.ts +14 -8
  17. package/dest/modules/validation.d.ts.map +1 -1
  18. package/dest/modules/validation.js +72 -22
  19. package/dest/store/block_store.d.ts +2 -1
  20. package/dest/store/block_store.d.ts.map +1 -1
  21. package/dest/store/block_store.js +34 -14
  22. package/dest/store/contract_class_store.d.ts +1 -1
  23. package/dest/store/contract_class_store.d.ts.map +1 -1
  24. package/dest/store/contract_class_store.js +4 -2
  25. package/dest/store/contract_instance_store.d.ts +5 -4
  26. package/dest/store/contract_instance_store.d.ts.map +1 -1
  27. package/dest/store/contract_instance_store.js +20 -9
  28. package/dest/test/fake_l1_state.d.ts +3 -1
  29. package/dest/test/fake_l1_state.d.ts.map +1 -1
  30. package/dest/test/fake_l1_state.js +4 -4
  31. package/package.json +13 -13
  32. package/src/errors.ts +32 -0
  33. package/src/l1/bin/retrieve-calldata.ts +15 -4
  34. package/src/l1/calldata_retriever.ts +14 -20
  35. package/src/l1/data_retrieval.ts +22 -14
  36. package/src/modules/data_store_updater.ts +4 -4
  37. package/src/modules/l1_synchronizer.ts +294 -261
  38. package/src/modules/validation.ts +87 -28
  39. package/src/store/block_store.ts +41 -13
  40. package/src/store/contract_class_store.ts +4 -4
  41. package/src/store/contract_instance_store.ts +24 -17
  42. package/src/test/fake_l1_state.ts +9 -3
package/src/errors.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { BlockNumber, CheckpointNumber } from '@aztec-labs/foundation/branded-types';
2
2
  import type { Fr } from '@aztec-labs/foundation/curves/bn254';
3
+ import type { TxHash } from '@aztec-labs/stdlib/tx';
3
4
 
4
5
  export class NoBlobBodiesFoundError extends Error {
5
6
  constructor(l2BlockNum: number) {
@@ -80,6 +81,21 @@ export class BlockArchiveNotConsistentError extends Error {
80
81
  }
81
82
  }
82
83
 
84
+ /**
85
+ * Thrown when a block lists a tx hash already included in a lower stored block. Blocks below the one being inserted
86
+ * are its ancestors, so the tx would be included twice (and its nullifiers repeated), which no valid chain allows.
87
+ */
88
+ export class DuplicateTxHashError extends Error {
89
+ constructor(
90
+ public readonly txHash: TxHash,
91
+ public readonly blockNumber: BlockNumber,
92
+ public readonly existingBlockNumber: BlockNumber,
93
+ ) {
94
+ super(`Cannot insert block ${blockNumber}: tx ${txHash} is already included in block ${existingBlockNumber}`);
95
+ this.name = 'DuplicateTxHashError';
96
+ }
97
+ }
98
+
83
99
  export class CheckpointNotFoundError extends Error {
84
100
  constructor(checkpointNumber: number) {
85
101
  super(`Failed to find expected checkpoint number ${checkpointNumber}`);
@@ -302,3 +318,19 @@ export class UndecodableCheckpointAttestationsError extends Error {
302
318
  this.name = 'UndecodableCheckpointAttestationsError';
303
319
  }
304
320
  }
321
+
322
+ /** Thrown when a checkpoint's attestations tuple does not decode for its epoch committee. */
323
+ export class CheckpointAttestationsDecodeError extends Error {
324
+ constructor(
325
+ public readonly checkpointNumber: number,
326
+ public readonly epoch: number,
327
+ public readonly committeeSize: number,
328
+ public override readonly cause: unknown,
329
+ ) {
330
+ super(
331
+ `Attestations tuple of checkpoint ${checkpointNumber} does not decode for the committee of ${committeeSize} ` +
332
+ `recorded for epoch ${epoch} (${cause instanceof Error ? cause.message : String(cause)})`,
333
+ );
334
+ this.name = 'CheckpointAttestationsDecodeError';
335
+ }
336
+ }
@@ -5,6 +5,7 @@ import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec-labs/ethere
5
5
  import { CheckpointNumber } from '@aztec-labs/foundation/branded-types';
6
6
  import { EthAddress } from '@aztec-labs/foundation/eth-address';
7
7
  import { createLogger } from '@aztec-labs/foundation/log';
8
+ import { CommitteeAttestation } from '@aztec-labs/stdlib/block';
8
9
  import { type Hex, createPublicClient, decodeEventLog, getAbiItem, http, toEventSelector } from 'viem';
9
10
  import { mainnet } from 'viem/chains';
10
11
 
@@ -93,7 +94,6 @@ async function main() {
93
94
  const retriever = new CalldataRetriever(
94
95
  publicClient as unknown as ViemPublicClient,
95
96
  publicClient as unknown as ViemPublicDebugClient,
96
- targetCommitteeSize,
97
97
  undefined,
98
98
  logger,
99
99
  rollupAddress,
@@ -171,9 +171,20 @@ async function main() {
171
171
  logger.info(` Fee Recipient: ${result.header.feeRecipient.toString()}`);
172
172
  logger.info(` Total Mana Used: ${result.header.totalManaUsed.toString()}`);
173
173
  logger.info('');
174
- logger.info('Attestations:');
175
- logger.info(` Count: ${result.attestations.length}`);
176
- logger.info(` Non-empty attestations: ${result.attestations.filter((a: any) => !a.signature.isEmpty()).length}`);
174
+ // The target committee size is only a guess at the epoch committee size, and escape-hatch tuples may not decode.
175
+ logger.info('Attestations (packed):');
176
+ logger.info(` Signature bitmap: ${result.verbatimAttestations.signatureIndices}`);
177
+ logger.info(` Signatures or addresses: ${result.verbatimAttestations.signaturesOrAddresses}`);
178
+ try {
179
+ const attestations = CommitteeAttestation.fromPacked(result.verbatimAttestations, targetCommitteeSize);
180
+ logger.info(` Decoded for a committee of ${targetCommitteeSize}:`);
181
+ logger.info(` Count: ${attestations.length}`);
182
+ logger.info(` Non-empty attestations: ${attestations.filter(a => !a.signature.isEmpty()).length}`);
183
+ } catch (err) {
184
+ logger.info(
185
+ ` Does not decode for a committee of ${targetCommitteeSize}: ${err instanceof Error ? err.message : err}`,
186
+ );
187
+ }
177
188
 
178
189
  process.exit(0);
179
190
  } catch (error) {
@@ -12,7 +12,6 @@ import { LruSet } from '@aztec-labs/foundation/collection';
12
12
  import { Fr } from '@aztec-labs/foundation/curves/bn254';
13
13
  import { EthAddress } from '@aztec-labs/foundation/eth-address';
14
14
  import type { Logger } from '@aztec-labs/foundation/log';
15
- import { CommitteeAttestation } from '@aztec-labs/stdlib/block';
16
15
  import { computeCheckpointPayloadDigest } from '@aztec-labs/stdlib/checkpoint';
17
16
  import { CheckpointHeader } from '@aztec-labs/stdlib/rollup';
18
17
  import { type Hex, type Transaction, decodeFunctionData, hexToBytes, multicall3Abi, toFunctionSelector } from 'viem';
@@ -23,16 +22,17 @@ import { getCallsFromSpireProposer } from './spire_proposer.js';
23
22
  import { getSuccessfulCallsFromTrace } from './trace_tx.js';
24
23
  import type { CallInfo } from './types.js';
25
24
 
26
- /** Decoded checkpoint data from a propose calldata. */
27
- type CheckpointData = {
25
+ /**
26
+ * Checkpoint data extracted from a propose calldata. The attestations tuple is left packed, since decoding it
27
+ * depends on the checkpoint's epoch (escape-hatch proposers may post an arbitrary tuple).
28
+ */
29
+ export type RawCheckpointFromCalldata = {
28
30
  checkpointNumber: CheckpointNumber;
29
31
  archiveRoot: Fr;
30
32
  header: CheckpointHeader;
31
- attestations: CommitteeAttestation[];
32
33
  /**
33
- * The exact packed `CommitteeAttestations` tuple as it appears in the propose calldata, preserved
34
- * verbatim (never re-derived from {@link attestations}) so invalidation evidence stays byte-faithful to
35
- * the on-chain `attestationsHash`.
34
+ * The exact packed `CommitteeAttestations` tuple as it appears in the propose calldata. It is what the
35
+ * rollup hashed into `attestationsHash`, and repacking decoded attestations does not reproduce it.
36
36
  */
37
37
  verbatimAttestations: ViemCommitteeAttestations;
38
38
  blockHash: string;
@@ -56,7 +56,6 @@ export class CalldataRetriever {
56
56
  constructor(
57
57
  private readonly publicClient: ViemPublicClient,
58
58
  private readonly debugClient: ViemPublicDebugClient,
59
- private readonly targetCommitteeSize: number,
60
59
  private readonly instrumentation: ArchiverInstrumentation | undefined,
61
60
  private readonly logger: Logger,
62
61
  private readonly rollupAddress: EthAddress,
@@ -86,7 +85,7 @@ export class CalldataRetriever {
86
85
  attestationsHash: Hex;
87
86
  payloadDigest: Hex;
88
87
  },
89
- ): Promise<CheckpointData> {
88
+ ): Promise<RawCheckpointFromCalldata> {
90
89
  this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}`);
91
90
  const tx = await this.publicClient.getTransaction({ hash: txHash });
92
91
  return this.getCheckpointFromTx(tx, checkpointNumber, expectedHashes);
@@ -97,7 +96,7 @@ export class CalldataRetriever {
97
96
  tx: Transaction,
98
97
  checkpointNumber: CheckpointNumber,
99
98
  expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
100
- ): Promise<CheckpointData> {
99
+ ): Promise<RawCheckpointFromCalldata> {
101
100
  // Try to decode as multicall3 with hash-verified matching
102
101
  const multicall3Result = this.tryDecodeMulticall3(tx, expectedHashes, checkpointNumber, tx.blockHash!);
103
102
  if (multicall3Result) {
@@ -155,7 +154,7 @@ export class CalldataRetriever {
155
154
  expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
156
155
  checkpointNumber: CheckpointNumber,
157
156
  blockHash: Hex,
158
- ): Promise<CheckpointData | undefined> {
157
+ ): Promise<RawCheckpointFromCalldata | undefined> {
159
158
  // Try to decode as Spire Proposer multicall (extracts all wrapped calls)
160
159
  const spireWrappedCalls = await getCallsFromSpireProposer(tx, this.publicClient, this.logger);
161
160
  if (!spireWrappedCalls) {
@@ -202,7 +201,7 @@ export class CalldataRetriever {
202
201
  expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
203
202
  checkpointNumber: CheckpointNumber,
204
203
  blockHash: Hex,
205
- ): CheckpointData | undefined {
204
+ ): RawCheckpointFromCalldata | undefined {
206
205
  const txHash = tx.hash;
207
206
 
208
207
  try {
@@ -256,7 +255,7 @@ export class CalldataRetriever {
256
255
  }
257
256
 
258
257
  // Decode, verify, and build for each candidate
259
- const verified: CheckpointData[] = [];
258
+ const verified: RawCheckpointFromCalldata[] = [];
260
259
  for (const candidate of candidates) {
261
260
  const result = this.tryDecodeAndVerifyPropose(candidate, expectedHashes, checkpointNumber, blockHash);
262
261
  if (result) {
@@ -300,7 +299,7 @@ export class CalldataRetriever {
300
299
  expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
301
300
  checkpointNumber: CheckpointNumber,
302
301
  blockHash: Hex,
303
- ): CheckpointData | undefined {
302
+ ): RawCheckpointFromCalldata | undefined {
304
303
  const txHash = tx.hash;
305
304
  try {
306
305
  // Check if transaction is to the rollup address
@@ -400,7 +399,7 @@ export class CalldataRetriever {
400
399
  expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
401
400
  checkpointNumber: CheckpointNumber,
402
401
  blockHash: Hex,
403
- ): CheckpointData | undefined {
402
+ ): RawCheckpointFromCalldata | undefined {
404
403
  try {
405
404
  const { functionName, args } = decodeFunctionData({ abi: RollupAbi, data: proposeCalldata });
406
405
  if (functionName !== 'propose') {
@@ -442,23 +441,18 @@ export class CalldataRetriever {
442
441
  return undefined;
443
442
  }
444
443
 
445
- const attestations = CommitteeAttestation.fromPacked(verbatimAttestations, this.targetCommitteeSize);
446
-
447
444
  this.logger.trace(`Validated and decoded propose calldata for checkpoint ${checkpointNumber}`, {
448
445
  checkpointNumber,
449
446
  archive: decodedArgs.archive,
450
447
  header: decodedArgs.header,
451
448
  l1BlockHash: blockHash,
452
- attestations,
453
449
  verbatimAttestations,
454
- targetCommitteeSize: this.targetCommitteeSize,
455
450
  });
456
451
 
457
452
  return {
458
453
  checkpointNumber,
459
454
  archiveRoot,
460
455
  header,
461
- attestations,
462
456
  verbatimAttestations,
463
457
  blockHash,
464
458
  feeAssetPriceModifier,
@@ -46,7 +46,6 @@ type RetrievedCheckpointBase = {
46
46
  l1: L1PublishedData;
47
47
  chainId: Fr;
48
48
  version: Fr;
49
- attestations: CommitteeAttestation[];
50
49
  /**
51
50
  * The exact packed `CommitteeAttestations` tuple from the propose calldata, carried verbatim: it is what
52
51
  * the rollup hashed into `attestationsHash`, and repacking the decoded attestations does not reproduce it.
@@ -55,9 +54,12 @@ type RetrievedCheckpointBase = {
55
54
  };
56
55
 
57
56
  /** Checkpoint data as retrieved from L1 calldata and blob data. */
58
- export type RetrievedCheckpoint = RetrievedCheckpointBase & { checkpointBlobData: CheckpointBlobData };
57
+ export type RetrievedCheckpoint = RetrievedCheckpointBase & {
58
+ attestations: CommitteeAttestation[];
59
+ checkpointBlobData: CheckpointBlobData;
60
+ };
59
61
 
60
- /** Checkpoint data retrieved from L1 calldata only, without blob data. */
62
+ /** Checkpoint data retrieved from L1 calldata only, without blob data or decoded attestations. */
61
63
  export type RetrievedCheckpointFromCalldata = RetrievedCheckpointBase & {
62
64
  /** Versioned blob hashes from the checkpoint proposed event. */
63
65
  blobHashes: Buffer[];
@@ -65,6 +67,11 @@ export type RetrievedCheckpointFromCalldata = RetrievedCheckpointBase & {
65
67
  parentBeaconBlockRoot: string | undefined;
66
68
  };
67
69
 
70
+ /** A calldata-only checkpoint with its attestations decoded for its epoch committee. */
71
+ export type ResolvedCheckpointFromCalldata = RetrievedCheckpointFromCalldata & {
72
+ attestations: CommitteeAttestation[];
73
+ };
74
+
68
75
  export async function retrievedToPublishedCheckpoint({
69
76
  checkpointNumber,
70
77
  archiveRoot,
@@ -187,7 +194,7 @@ export async function retrieveCheckpointCalldataFromRollup(
187
194
  ): Promise<RetrievedCheckpointFromCalldata[]> {
188
195
  const retrievedCheckpoints: RetrievedCheckpointFromCalldata[] = [];
189
196
 
190
- let rollupConstants: { chainId: Fr; version: Fr; targetCommitteeSize: number } | undefined;
197
+ let rollupConstants: { chainId: Fr; version: Fr } | undefined;
191
198
 
192
199
  do {
193
200
  if (searchStartBlock > searchEndBlock) {
@@ -205,15 +212,10 @@ export async function retrieveCheckpointCalldataFromRollup(
205
212
  );
206
213
 
207
214
  if (rollupConstants === undefined) {
208
- const [chainId, version, targetCommitteeSize] = await Promise.all([
209
- publicClient.getChainId(),
210
- rollup.getVersion(),
211
- rollup.getTargetCommitteeSize(),
212
- ]);
215
+ const [chainId, version] = await Promise.all([publicClient.getChainId(), rollup.getVersion()]);
213
216
  rollupConstants = {
214
217
  chainId: new Fr(chainId),
215
218
  version: new Fr(version),
216
- targetCommitteeSize,
217
219
  };
218
220
  }
219
221
 
@@ -239,7 +241,7 @@ export async function retrieveCheckpointCalldataFromRollup(
239
241
  * @param publicClient - The viem public client to use for transaction retrieval.
240
242
  * @param debugClient - The viem debug client to use for trace/debug RPC methods (optional).
241
243
  * @param logs - CheckpointProposed logs.
242
- * @param rollupConstants - The rollup constants (chainId, version, targetCommitteeSize).
244
+ * @param rollupConstants - The rollup constants (chainId, version).
243
245
  * @param instrumentation - The archiver instrumentation instance.
244
246
  * @param logger - The logger instance.
245
247
  * @returns An array of calldata-only checkpoints.
@@ -249,7 +251,7 @@ async function processCheckpointProposedLogs(
249
251
  publicClient: ViemPublicClient,
250
252
  debugClient: ViemPublicDebugClient,
251
253
  logs: CheckpointProposedLog[],
252
- { chainId, version, targetCommitteeSize }: { chainId: Fr; version: Fr; targetCommitteeSize: number },
254
+ { chainId, version }: { chainId: Fr; version: Fr },
253
255
  instrumentation: ArchiverInstrumentation,
254
256
  logger: Logger,
255
257
  ): Promise<RetrievedCheckpointFromCalldata[]> {
@@ -257,7 +259,6 @@ async function processCheckpointProposedLogs(
257
259
  const calldataRetriever = new CalldataRetriever(
258
260
  publicClient,
259
261
  debugClient,
260
- targetCommitteeSize,
261
262
  instrumentation,
262
263
  logger,
263
264
  EthAddress.fromString(rollup.address),
@@ -266,6 +267,13 @@ async function processCheckpointProposedLogs(
266
267
  await asyncPool(10, logs, async log => {
267
268
  const checkpointNumber = log.args.checkpointNumber;
268
269
  const archive = log.args.archive;
270
+ // CheckpointProposed logs stay on L1 after their checkpoint is pruned or replaced, so every re-read of a range
271
+ // (a sync point rewind, a fresh sync from genesis) sees them. Rollup.archiveAt only answers up to the current
272
+ // pending tip, so a pruned or replaced checkpoint mismatches here and is dropped before its blobs are fetched or
273
+ // it is validated. A checkpoint the archiver refuses to ingest therefore blocks sync only while it is still
274
+ // canonical: the L1 prune is lazy (it runs on the next propose or an explicit prune() call), and until then
275
+ // archiveAt keeps returning the refused checkpoint's archive. Proposers send prune() from their cannot-build
276
+ // fallback even when their own sync is stuck, so the refused checkpoint is eventually dropped here.
269
277
  const archiveFromChain = await rollup.archiveAt(checkpointNumber);
270
278
  const blobHashes = log.args.versionedBlobHashes;
271
279
 
@@ -297,7 +305,7 @@ async function processCheckpointProposedLogs(
297
305
  l1BlockNumber: log.l1BlockNumber,
298
306
  checkpointNumber,
299
307
  archive: archive.toString(),
300
- attestations: checkpoint.attestations,
308
+ verbatimAttestations: checkpoint.verbatimAttestations,
301
309
  });
302
310
  } else {
303
311
  logger.warn(`Ignoring checkpoint ${checkpointNumber} due to archive root mismatch`, {
@@ -521,15 +521,15 @@ export class ArchiverDataStoreUpdater {
521
521
  const result = await this.stores.db.transactionAsync(async () => {
522
522
  const { blocksRemoved = [] } = await this.stores.blocks.removeCheckpointsAfter(checkpointNumber);
523
523
 
524
- const opResults = await Promise.all([
524
+ const [, logsDeleted, ...contractDataRemoved] = await Promise.all([
525
525
  // Prune rolls back to the last proven block, which is by definition valid
526
526
  this.stores.blocks.setPendingChainValidationStatus({ valid: true }),
527
+ this.stores.logs.deleteLogs(blocksRemoved),
527
528
  // Remove contract data for all blocks being removed
528
529
  ...blocksRemoved.map(block => this.removeContractDataFromDb(block)),
529
- this.stores.logs.deleteLogs(blocksRemoved),
530
530
  ]);
531
531
 
532
- return opResults.every(Boolean);
532
+ return logsDeleted && contractDataRemoved.every(Boolean);
533
533
  });
534
534
  await this.l2FrontierCache?.refresh();
535
535
  return result;
@@ -694,7 +694,7 @@ export class ArchiverDataStoreUpdater {
694
694
  if (operation == Operation.Store) {
695
695
  return await this.stores.contractInstances.addContractInstances(contractInstances, blockNum);
696
696
  } else if (operation == Operation.Delete) {
697
- return await this.stores.contractInstances.deleteContractInstances(contractInstances);
697
+ return await this.stores.contractInstances.deleteContractInstances(contractInstances, blockNum);
698
698
  }
699
699
  }
700
700
  return true;