Skip to content

Commit 14f3ecd

Browse files
author
AztecBot
committed
Merge branch 'next' into merge-train/avm
2 parents ddae41c + 30bc652 commit 14f3ecd

File tree

6 files changed

+287
-24
lines changed

6 files changed

+287
-24
lines changed

l1-contracts/src/core/interfaces/IRollup.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,13 @@ struct RollupStore {
101101
}
102102

103103
interface IRollupCore {
104-
event CheckpointProposed(uint256 indexed checkpointNumber, bytes32 indexed archive, bytes32[] versionedBlobHashes);
104+
event CheckpointProposed(
105+
uint256 indexed checkpointNumber,
106+
bytes32 indexed archive,
107+
bytes32[] versionedBlobHashes,
108+
bytes32 payloadDigest,
109+
bytes32 attestationsHash
110+
);
105111
event L2ProofVerified(uint256 indexed checkpointNumber, address indexed proverId);
106112
event CheckpointInvalidated(uint256 indexed checkpointNumber);
107113
event RewardConfigUpdated(RewardConfig rewardConfig);

l1-contracts/src/core/libraries/rollup/ProposeLib.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,9 @@ library ProposeLib {
302302
}
303303

304304
// Emit event for external listeners. Nodes rely on this event to update their state.
305-
emit IRollupCore.CheckpointProposed(checkpointNumber, _args.archive, v.blobHashes);
305+
emit IRollupCore.CheckpointProposed(
306+
checkpointNumber, _args.archive, v.blobHashes, v.payloadDigest, v.attestationsHash
307+
);
306308
}
307309

308310
/**

yarn-project/archiver/src/archiver/l1/bin/retrieve-calldata.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ async function main() {
141141
logger.info('Retrieving block header from rollup transaction...');
142142
logger.info('');
143143

144-
// For this script, we don't have blob hashes, so pass empty array
145-
const result = await retriever.getCheckpointFromRollupTx(txHash, [], CheckpointNumber(l2BlockNumber));
144+
// For this script, we don't have blob hashes or expected hashes, so pass empty arrays/objects
145+
const result = await retriever.getCheckpointFromRollupTx(txHash, [], CheckpointNumber(l2BlockNumber), {});
146146

147147
logger.info(' Successfully retrieved block header!');
148148
logger.info('');

yarn-project/archiver/src/archiver/l1/calldata_retriever.test.ts

Lines changed: 152 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,20 @@ import { withHexPrefix } from '@aztec/foundation/string';
1111
import { RollupAbi } from '@aztec/l1-artifacts';
1212
import { Signature } from '@aztec/stdlib/block';
1313
import { GasFees } from '@aztec/stdlib/gas';
14+
import { ConsensusPayload, SignatureDomainSeparator } from '@aztec/stdlib/p2p';
1415
import { CheckpointHeader } from '@aztec/stdlib/rollup';
1516
import { ContentCommitment } from '@aztec/stdlib/tx';
1617

1718
import { 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

2029
import type { ArchiverInstrumentation } from '../instrumentation.js';
2130
import { 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

Comments
 (0)