@@ -11,11 +11,20 @@ import { withHexPrefix } from '@aztec/foundation/string';
1111import { RollupAbi } from '@aztec/l1-artifacts' ;
1212import { Signature } from '@aztec/stdlib/block' ;
1313import { GasFees } from '@aztec/stdlib/gas' ;
14+ import { ConsensusPayload , SignatureDomainSeparator } from '@aztec/stdlib/p2p' ;
1415import { CheckpointHeader } from '@aztec/stdlib/rollup' ;
1516import { ContentCommitment } from '@aztec/stdlib/tx' ;
1617
1718import { type MockProxy , mock } from 'jest-mock-extended' ;
18- import { type Hex , type Transaction , encodeFunctionData , multicall3Abi , toFunctionSelector } from 'viem' ;
19+ import {
20+ type Hex ,
21+ type Transaction ,
22+ encodeAbiParameters ,
23+ encodeFunctionData ,
24+ keccak256 ,
25+ multicall3Abi ,
26+ toFunctionSelector ,
27+ } from 'viem' ;
1928
2029import type { ArchiverInstrumentation } from '../instrumentation.js' ;
2130import { CalldataRetriever } from './calldata_retriever.js' ;
@@ -43,8 +52,13 @@ class TestCalldataRetriever extends CalldataRetriever {
4352 return await super . extractCalldataViaTrace ( txHash ) ;
4453 }
4554
46- public override decodeAndBuildCheckpoint ( proposeCalldata : Hex , blockHash : Hex , checkpointNumber : CheckpointNumber ) {
47- return super . decodeAndBuildCheckpoint ( proposeCalldata , blockHash , checkpointNumber ) ;
55+ public override decodeAndBuildCheckpoint (
56+ proposeCalldata : Hex ,
57+ blockHash : Hex ,
58+ checkpointNumber : CheckpointNumber ,
59+ expectedHashes : { attestationsHash ?: Hex ; payloadDigest ?: Hex } ,
60+ ) {
61+ return super . decodeAndBuildCheckpoint ( proposeCalldata , blockHash , checkpointNumber , expectedHashes ) ;
4862 }
4963}
5064
@@ -145,7 +159,7 @@ describe('CalldataRetriever', () => {
145159
146160 publicClient . getTransaction . mockResolvedValue ( tx ) ;
147161
148- const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber ) ;
162+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ;
149163
150164 expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
151165 expect ( result . header ) . toBeInstanceOf ( CheckpointHeader ) ;
@@ -168,7 +182,7 @@ describe('CalldataRetriever', () => {
168182
169183 publicClient . getTransaction . mockResolvedValue ( tx ) ;
170184
171- const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber ) ;
185+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ;
172186
173187 expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
174188 expect ( result . header ) . toBeInstanceOf ( CheckpointHeader ) ;
@@ -210,7 +224,7 @@ describe('CalldataRetriever', () => {
210224 } ,
211225 ] ) ;
212226
213- const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber ) ;
227+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ;
214228
215229 expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
216230 expect ( debugClient . request ) . toHaveBeenCalledWith ( { method : 'trace_transaction' , params : [ txHash ] } ) ;
@@ -234,18 +248,142 @@ describe('CalldataRetriever', () => {
234248 // Mock both trace methods to fail
235249 debugClient . request . mockRejectedValue ( new Error ( `Method not available` ) ) ;
236250
237- await expect ( retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber ) ) . rejects . toThrow (
251+ await expect ( retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ) . rejects . toThrow (
238252 'Failed to trace transaction' ,
239253 ) ;
240254 } ) ;
241255
242256 it ( 'should throw when transaction retrieval fails' , async ( ) => {
243257 publicClient . getTransaction . mockRejectedValue ( new Error ( 'Transaction not found' ) ) ;
244258
245- await expect ( retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber ) ) . rejects . toThrow (
259+ await expect ( retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ) . rejects . toThrow (
246260 'Transaction not found' ,
247261 ) ;
248262 } ) ;
263+
264+ it ( 'should validate attestationsHash when provided' , async ( ) => {
265+ const attestations = makeViemCommitteeAttestations ( ) ;
266+ const proposeCalldata = makeProposeCalldata ( undefined , attestations ) ;
267+ const tx = makeMulticall3Transaction ( [ { target : rollupAddress . toString ( ) , callData : proposeCalldata } ] ) ;
268+
269+ publicClient . getTransaction . mockResolvedValue ( tx ) ;
270+
271+ // Compute the expected attestationsHash
272+ const expectedAttestationsHash = keccak256 (
273+ encodeAbiParameters (
274+ [
275+ {
276+ type : 'tuple' ,
277+ components : [
278+ { name : 'signatureIndices' , type : 'bytes' } ,
279+ { name : 'signaturesOrAddresses' , type : 'bytes' } ,
280+ ] ,
281+ } ,
282+ ] ,
283+ [
284+ {
285+ signatureIndices : attestations . signatureIndices ,
286+ signaturesOrAddresses : attestations . signaturesOrAddresses ,
287+ } ,
288+ ] ,
289+ ) ,
290+ ) ;
291+
292+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , {
293+ attestationsHash : expectedAttestationsHash ,
294+ } ) ;
295+
296+ expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
297+ expect ( result . header ) . toBeInstanceOf ( CheckpointHeader ) ;
298+ } ) ;
299+
300+ it ( 'should throw when attestationsHash does not match' , async ( ) => {
301+ const attestations = makeViemCommitteeAttestations ( ) ;
302+ const proposeCalldata = makeProposeCalldata ( undefined , attestations ) ;
303+ const tx = makeMulticall3Transaction ( [ { target : rollupAddress . toString ( ) , callData : proposeCalldata } ] ) ;
304+
305+ publicClient . getTransaction . mockResolvedValue ( tx ) ;
306+
307+ // Use a different (wrong) attestationsHash
308+ const wrongAttestationsHash = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex ;
309+
310+ await expect (
311+ retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , {
312+ attestationsHash : wrongAttestationsHash ,
313+ } ) ,
314+ ) . rejects . toThrow ( 'Attestations hash mismatch' ) ;
315+ } ) ;
316+
317+ it ( 'should work with empty expectedHashes for backwards compatibility' , async ( ) => {
318+ const proposeCalldata = makeProposeCalldata ( ) ;
319+ const tx = makeMulticall3Transaction ( [ { target : rollupAddress . toString ( ) , callData : proposeCalldata } ] ) ;
320+
321+ publicClient . getTransaction . mockResolvedValue ( tx ) ;
322+
323+ // Call with empty expectedHashes (simulating old event format without hash fields)
324+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ;
325+
326+ expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
327+ expect ( result . header ) . toBeInstanceOf ( CheckpointHeader ) ;
328+ // Should succeed without validation when hashes are not provided
329+ } ) ;
330+
331+ it ( 'should validate payloadDigest when provided' , async ( ) => {
332+ const header = makeViemHeader ( ) ;
333+ const attestations = makeViemCommitteeAttestations ( ) ;
334+ const archiveRoot = Fr . random ( ) ;
335+ const archive = archiveRoot . toString ( ) as Hex ;
336+ const feeAssetPriceModifier = BigInt ( 0 ) ;
337+
338+ // Create propose calldata with known values
339+ const proposeCalldata = encodeFunctionData ( {
340+ abi : RollupAbi ,
341+ functionName : 'propose' ,
342+ args : [
343+ {
344+ archive,
345+ oracleInput : { feeAssetPriceModifier } ,
346+ header,
347+ } ,
348+ attestations ,
349+ [ ] , // signers
350+ Signature . random ( ) . toViemSignature ( ) ,
351+ '0x' as Hex , // blobInput
352+ ] ,
353+ } ) ;
354+
355+ const tx = makeMulticall3Transaction ( [ { target : rollupAddress . toString ( ) , callData : proposeCalldata } ] ) ;
356+ publicClient . getTransaction . mockResolvedValue ( tx ) ;
357+
358+ // Compute the expected payloadDigest using ConsensusPayload (same logic as the validator)
359+ const checkpointHeader = CheckpointHeader . fromViem ( header ) ;
360+ const consensusPayload = new ConsensusPayload ( checkpointHeader , archiveRoot ) ;
361+ const payloadToSign = consensusPayload . getPayloadToSign ( SignatureDomainSeparator . blockAttestation ) ;
362+ const expectedPayloadDigest = keccak256 ( payloadToSign ) ;
363+
364+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , {
365+ payloadDigest : expectedPayloadDigest ,
366+ } ) ;
367+
368+ expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
369+ expect ( result . header ) . toBeInstanceOf ( CheckpointHeader ) ;
370+ } ) ;
371+
372+ it ( 'should throw when payloadDigest does not match' , async ( ) => {
373+ const proposeCalldata = makeProposeCalldata ( ) ;
374+ const tx = makeMulticall3Transaction ( [ { target : rollupAddress . toString ( ) , callData : proposeCalldata } ] ) ;
375+
376+ publicClient . getTransaction . mockResolvedValue ( tx ) ;
377+
378+ // Use a different (wrong) payloadDigest
379+ const wrongPayloadDigest = '0x0000000000000000000000000000000000000000000000000000000000000002' as Hex ;
380+
381+ await expect (
382+ retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , {
383+ payloadDigest : wrongPayloadDigest ,
384+ } ) ,
385+ ) . rejects . toThrow ( 'Payload digest mismatch' ) ;
386+ } ) ;
249387 } ) ;
250388 describe ( 'tryDecodeMulticall3' , ( ) => {
251389 it ( 'should decode simple multicall3 with single propose call' , ( ) => {
@@ -944,7 +1082,7 @@ describe('CalldataRetriever', () => {
9441082 it ( 'should correctly decode propose calldata and build checkpoint' , ( ) => {
9451083 const proposeCalldata = makeProposeCalldata ( ) ;
9461084
947- const result = retriever . decodeAndBuildCheckpoint ( proposeCalldata , blockHash , checkpointNumber ) ;
1085+ const result = retriever . decodeAndBuildCheckpoint ( proposeCalldata , blockHash , checkpointNumber , { } ) ;
9481086
9491087 expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
9501088 expect ( result . header ) . toBeInstanceOf ( CheckpointHeader ) ;
@@ -957,7 +1095,7 @@ describe('CalldataRetriever', () => {
9571095 const attestations = makeViemCommitteeAttestations ( ) ;
9581096 const proposeCalldata = makeProposeCalldata ( undefined , attestations ) ;
9591097
960- const result = retriever . decodeAndBuildCheckpoint ( proposeCalldata , blockHash , checkpointNumber ) ;
1098+ const result = retriever . decodeAndBuildCheckpoint ( proposeCalldata , blockHash , checkpointNumber , { } ) ;
9611099
9621100 expect ( result . attestations ) . toHaveLength ( TARGET_COMMITTEE_SIZE ) ;
9631101 } ) ;
@@ -968,13 +1106,13 @@ describe('CalldataRetriever', () => {
9681106 ) ;
9691107 const invalidCalldata = ( invalidateBadSelector + '0' . repeat ( 120 ) ) as Hex ;
9701108
971- expect ( ( ) => retriever . decodeAndBuildCheckpoint ( invalidCalldata , blockHash , checkpointNumber ) ) . toThrow ( ) ;
1109+ expect ( ( ) => retriever . decodeAndBuildCheckpoint ( invalidCalldata , blockHash , checkpointNumber , { } ) ) . toThrow ( ) ;
9721110 } ) ;
9731111
9741112 it ( 'should throw when calldata is malformed' , ( ) => {
9751113 const malformedCalldata = '0xinvalid' as Hex ;
9761114
977- expect ( ( ) => retriever . decodeAndBuildCheckpoint ( malformedCalldata , blockHash , checkpointNumber ) ) . toThrow ( ) ;
1115+ expect ( ( ) => retriever . decodeAndBuildCheckpoint ( malformedCalldata , blockHash , checkpointNumber , { } ) ) . toThrow ( ) ;
9781116 } ) ;
9791117 } ) ;
9801118
@@ -987,7 +1125,7 @@ describe('CalldataRetriever', () => {
9871125
9881126 publicClient . getTransaction . mockResolvedValue ( tx ) ;
9891127
990- const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber ) ;
1128+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ;
9911129
9921130 expect ( result ) . toBeDefined ( ) ;
9931131 expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
@@ -1060,7 +1198,7 @@ describe('CalldataRetriever', () => {
10601198 ( '0x000000000000000000000000' + SPIRE_PROPOSER_EXPECTED_IMPLEMENTATION . slice ( 2 ) ) as Hex ,
10611199 ) ;
10621200
1063- const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber ) ;
1201+ const result = await retriever . getCheckpointFromRollupTx ( txHash , [ ] , checkpointNumber , { } ) ;
10641202
10651203 expect ( result ) . toBeDefined ( ) ;
10661204 expect ( result . checkpointNumber ) . toBe ( checkpointNumber ) ;
0 commit comments