@aztec/archiver 3.0.0-nightly.20251214 → 3.0.0-nightly.20251216

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.
@@ -13,9 +13,20 @@ import {
13
13
  TallySlashingProposerAbi,
14
14
  } from '@aztec/l1-artifacts';
15
15
  import { CommitteeAttestation } from '@aztec/stdlib/block';
16
+ import { ConsensusPayload, SignatureDomainSeparator } from '@aztec/stdlib/p2p';
16
17
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
17
18
 
18
- import { type Hex, type Transaction, decodeFunctionData, hexToBytes, multicall3Abi, toFunctionSelector } from 'viem';
19
+ import {
20
+ type AbiParameter,
21
+ type Hex,
22
+ type Transaction,
23
+ decodeFunctionData,
24
+ encodeAbiParameters,
25
+ hexToBytes,
26
+ keccak256,
27
+ multicall3Abi,
28
+ toFunctionSelector,
29
+ } from 'viem';
19
30
 
20
31
  import type { ArchiverInstrumentation } from '../instrumentation.js';
21
32
  import { getSuccessfulCallsFromDebug } from './debug_tx.js';
@@ -56,12 +67,17 @@ export class CalldataRetriever {
56
67
  * @param txHash - Hash of the tx that published it.
57
68
  * @param blobHashes - Blob hashes for the checkpoint.
58
69
  * @param checkpointNumber - Checkpoint number.
70
+ * @param expectedHashes - Optional expected hashes from the CheckpointProposed event for validation
59
71
  * @returns Checkpoint header and metadata from the calldata, deserialized
60
72
  */
61
73
  async getCheckpointFromRollupTx(
62
74
  txHash: `0x${string}`,
63
75
  blobHashes: Buffer[],
64
76
  checkpointNumber: CheckpointNumber,
77
+ expectedHashes: {
78
+ attestationsHash?: Hex;
79
+ payloadDigest?: Hex;
80
+ },
65
81
  ): Promise<{
66
82
  checkpointNumber: CheckpointNumber;
67
83
  archiveRoot: Fr;
@@ -69,10 +85,14 @@ export class CalldataRetriever {
69
85
  attestations: CommitteeAttestation[];
70
86
  blockHash: string;
71
87
  }> {
72
- this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}`);
88
+ this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}`, {
89
+ willValidateHashes: !!expectedHashes.attestationsHash || !!expectedHashes.payloadDigest,
90
+ hasAttestationsHash: !!expectedHashes.attestationsHash,
91
+ hasPayloadDigest: !!expectedHashes.payloadDigest,
92
+ });
73
93
  const tx = await this.publicClient.getTransaction({ hash: txHash });
74
94
  const proposeCalldata = await this.getProposeCallData(tx, checkpointNumber);
75
- return this.decodeAndBuildCheckpoint(proposeCalldata, tx.blockHash!, checkpointNumber);
95
+ return this.decodeAndBuildCheckpoint(proposeCalldata, tx.blockHash!, checkpointNumber, expectedHashes);
76
96
  }
77
97
 
78
98
  /** Gets rollup propose calldata from a transaction */
@@ -324,17 +344,59 @@ export class CalldataRetriever {
324
344
  return calls[0].input;
325
345
  }
326
346
 
347
+ /**
348
+ * Extracts the CommitteeAttestations struct definition from RollupAbi.
349
+ * Finds the _attestations parameter by name in the propose function.
350
+ * Lazy-loaded to avoid issues during module initialization.
351
+ */
352
+ private getCommitteeAttestationsStructDef(): AbiParameter {
353
+ const proposeFunction = RollupAbi.find(item => item.type === 'function' && item.name === 'propose') as
354
+ | { type: 'function'; name: string; inputs: readonly AbiParameter[] }
355
+ | undefined;
356
+
357
+ if (!proposeFunction) {
358
+ throw new Error('propose function not found in RollupAbi');
359
+ }
360
+
361
+ // Find the _attestations parameter by name, not by index
362
+ const attestationsParam = proposeFunction.inputs.find(param => param.name === '_attestations');
363
+
364
+ if (!attestationsParam) {
365
+ throw new Error('_attestations parameter not found in propose function');
366
+ }
367
+
368
+ if (attestationsParam.type !== 'tuple') {
369
+ throw new Error(`Expected _attestations parameter to be a tuple, got ${attestationsParam.type}`);
370
+ }
371
+
372
+ // Extract the tuple components (struct fields)
373
+ const tupleParam = attestationsParam as unknown as {
374
+ type: 'tuple';
375
+ components?: readonly AbiParameter[];
376
+ };
377
+
378
+ return {
379
+ type: 'tuple',
380
+ components: tupleParam.components || [],
381
+ } as AbiParameter;
382
+ }
383
+
327
384
  /**
328
385
  * Decodes propose calldata and builds the checkpoint header structure.
329
386
  * @param proposeCalldata - The propose function calldata
330
387
  * @param blockHash - The L1 block hash containing this transaction
331
388
  * @param checkpointNumber - The checkpoint number
389
+ * @param expectedHashes - Optional expected hashes from the CheckpointProposed event for validation
332
390
  * @returns The decoded checkpoint header and metadata
333
391
  */
334
392
  protected decodeAndBuildCheckpoint(
335
393
  proposeCalldata: Hex,
336
394
  blockHash: Hex,
337
395
  checkpointNumber: CheckpointNumber,
396
+ expectedHashes: {
397
+ attestationsHash?: Hex;
398
+ payloadDigest?: Hex;
399
+ },
338
400
  ): {
339
401
  checkpointNumber: CheckpointNumber;
340
402
  archiveRoot: Fr;
@@ -365,6 +427,57 @@ export class CalldataRetriever {
365
427
  ];
366
428
 
367
429
  const attestations = CommitteeAttestation.fromPacked(packedAttestations, this.targetCommitteeSize);
430
+ const header = CheckpointHeader.fromViem(decodedArgs.header);
431
+ const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
432
+
433
+ // Validate attestationsHash if provided (skip for backwards compatibility with older events)
434
+ if (expectedHashes.attestationsHash) {
435
+ // Compute attestationsHash: keccak256(abi.encode(CommitteeAttestations))
436
+ const computedAttestationsHash = keccak256(
437
+ encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations]),
438
+ );
439
+
440
+ // Compare as buffers to avoid case-sensitivity and string comparison issues
441
+ const computedBuffer = Buffer.from(hexToBytes(computedAttestationsHash));
442
+ const expectedBuffer = Buffer.from(hexToBytes(expectedHashes.attestationsHash));
443
+
444
+ if (!computedBuffer.equals(expectedBuffer)) {
445
+ throw new Error(
446
+ `Attestations hash mismatch for checkpoint ${checkpointNumber}: ` +
447
+ `computed=${computedAttestationsHash}, expected=${expectedHashes.attestationsHash}`,
448
+ );
449
+ }
450
+
451
+ this.logger.trace(`Validated attestationsHash for checkpoint ${checkpointNumber}`, {
452
+ computedAttestationsHash,
453
+ expectedAttestationsHash: expectedHashes.attestationsHash,
454
+ });
455
+ }
456
+
457
+ // Validate payloadDigest if provided (skip for backwards compatibility with older events)
458
+ if (expectedHashes.payloadDigest) {
459
+ // Use ConsensusPayload to compute the digest - this ensures we match the exact logic
460
+ // used by the network for signing and verification
461
+ const consensusPayload = new ConsensusPayload(header, archiveRoot);
462
+ const payloadToSign = consensusPayload.getPayloadToSign(SignatureDomainSeparator.blockAttestation);
463
+ const computedPayloadDigest = keccak256(payloadToSign);
464
+
465
+ // Compare as buffers to avoid case-sensitivity and string comparison issues
466
+ const computedBuffer = Buffer.from(hexToBytes(computedPayloadDigest));
467
+ const expectedBuffer = Buffer.from(hexToBytes(expectedHashes.payloadDigest));
468
+
469
+ if (!computedBuffer.equals(expectedBuffer)) {
470
+ throw new Error(
471
+ `Payload digest mismatch for checkpoint ${checkpointNumber}: ` +
472
+ `computed=${computedPayloadDigest}, expected=${expectedHashes.payloadDigest}`,
473
+ );
474
+ }
475
+
476
+ this.logger.trace(`Validated payloadDigest for checkpoint ${checkpointNumber}`, {
477
+ computedPayloadDigest,
478
+ expectedPayloadDigest: expectedHashes.payloadDigest,
479
+ });
480
+ }
368
481
 
369
482
  this.logger.trace(`Decoded propose calldata`, {
370
483
  checkpointNumber,
@@ -376,9 +489,6 @@ export class CalldataRetriever {
376
489
  targetCommitteeSize: this.targetCommitteeSize,
377
490
  });
378
491
 
379
- const header = CheckpointHeader.fromViem(decodedArgs.header);
380
- const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
381
-
382
492
  return {
383
493
  checkpointNumber,
384
494
  archiveRoot,
@@ -261,10 +261,17 @@ async function processCheckpointProposedLogs(
261
261
 
262
262
  // The value from the event and contract will match only if the checkpoint is in the chain.
263
263
  if (archive === archiveFromChain) {
264
+ // Build expected hashes object (fields may be undefined for backwards compatibility with older events)
265
+ const expectedHashes = {
266
+ attestationsHash: log.args.attestationsHash,
267
+ payloadDigest: log.args.payloadDigest,
268
+ };
269
+
264
270
  const checkpoint = await calldataRetriever.getCheckpointFromRollupTx(
265
271
  log.transactionHash!,
266
272
  blobHashes,
267
273
  checkpointNumber,
274
+ expectedHashes,
268
275
  );
269
276
  const checkpointBlobData = await getCheckpointBlobDataFromBlobs(
270
277
  blobSinkClient,