Skip to content

Commit 5079dea

Browse files
committed
fix(fast-inbox): dedupe checkpoint header verification on epoch proof submission (A-1373)
submitEpochRootProof verified the supplied checkpoint headers and then hit getEpochProofPublicInputs, which verified them again, hashing every header in the epoch twice per submission. Split the assembly into a private computeEpochProofPublicInputs that takes the headers as already verified, and keep the header check on the externally reachable getter, which is the entry point off-chain callers use and must not trust caller-supplied fee fields.
1 parent b35221d commit 5079dea

1 file changed

Lines changed: 112 additions & 84 deletions

File tree

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

Lines changed: 112 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ library EpochProofLib {
109109

110110
Epoch endEpoch = assertAcceptable(_args.start, _args.end);
111111

112+
// Rehash the supplied headers against storage once, here: the public-input assembly below reads the fee
113+
// recipient/value out of them and relies on this call having run.
112114
verifyHeaders(_args.start, _args.end, _args.headers);
113115

114116
// Verify attestations for the last checkpoint in the epoch
@@ -150,6 +152,11 @@ library EpochProofLib {
150152
* own public inputs used for generating the proof vs the ones assembled
151153
* by this contract when verifying it.
152154
*
155+
* @dev The fee recipient/value public inputs are sourced from the supplied headers, so this entry point rehashes
156+
* them against storage before assembling: an off-chain caller must not walk away with public inputs built from
157+
* unverified fee fields and only discover the mismatch when the on-chain proof reverts. The submit path verifies
158+
* the headers up front and assembles via computeEpochProofPublicInputs to avoid rehashing them twice.
159+
*
153160
* @param _start - The start of the epoch (inclusive)
154161
* @param _end - The end of the epoch (inclusive)
155162
* @param _args - Array of public inputs to the proof (previousArchive, endArchive, endTimestamp, outHash, proverId)
@@ -163,6 +170,107 @@ library EpochProofLib {
163170
ProposedHeader[] calldata _headers,
164171
bytes calldata _blobPublicInputs
165172
) internal view returns (bytes32[] memory) {
173+
verifyHeaders(_start, _end, _headers);
174+
return computeEpochProofPublicInputs(_start, _end, _args, _headers, _blobPublicInputs);
175+
}
176+
177+
/**
178+
* @notice Verifies committee attestations for the last checkpoint in the epoch before accepting the epoch proof
179+
*
180+
* @dev This verification ensures that the committee has properly validated the final state of the epoch
181+
* before the proof can be accepted. The function validates that:
182+
* 1. The provided attestations match the stored attestation hash for the checkpoint
183+
* 2. The attestations have valid signatures from committee members
184+
* 3. The attestations meet the required threshold (2/3+ of committee)
185+
*
186+
* For escape hatch epochs, attestation verification is skipped since there is no committee
187+
* involvement - only the designated escape hatch proposer can propose blocks.
188+
*
189+
* @dev Errors Thrown:
190+
* - Rollup__InvalidAttestations: Provided attestations don't match stored hash or fail validation
191+
*
192+
* @param _endCheckpointNumber The last checkpoint number in the epoch to verify attestations for
193+
* @param _attestations The committee attestations containing signatures and validator information
194+
*/
195+
function verifyLastCheckpointAttestationsAndOutHash(
196+
uint256 _endCheckpointNumber,
197+
CommitteeAttestations memory _attestations,
198+
bytes32 _outHash
199+
) private {
200+
// Get the stored attestation hash and payload digest for the last checkpoint
201+
CompressedTempCheckpointLog storage checkpointLog = STFLib.getStorageTempCheckpointLog(_endCheckpointNumber);
202+
203+
// Verify that the out hash matches the stored value
204+
// The stored out hash is part of the payloadDigest that was attested to.
205+
require(checkpointLog.outHash == _outHash, Errors.Rollup__InvalidOutHash(checkpointLog.outHash, _outHash));
206+
207+
// Verify that the provided attestations match the stored hash
208+
bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations));
209+
require(providedAttestationsHash == checkpointLog.attestationsHash, Errors.Rollup__InvalidAttestations());
210+
211+
// Get the epoch for the last checkpoint
212+
Epoch epoch = STFLib.getEpochForCheckpoint(_endCheckpointNumber);
213+
214+
// Check if this is an escape hatch epoch - skip attestation verification if so
215+
// since escape hatch blocks are proposed without committee attestations.
216+
// Uses epoch-stable lookup so proof verification uses the escape hatch that was
217+
// active when the epoch started, not whatever is currently configured.
218+
{
219+
IEscapeHatch escapeHatch = ValidatorSelectionLib.getEscapeHatchForEpoch(epoch);
220+
if (address(escapeHatch) != address(0)) {
221+
(bool isOpen,) = escapeHatch.isHatchOpen(epoch);
222+
if (isOpen) {
223+
// Skip attestation verification for escape hatch epochs
224+
return;
225+
}
226+
}
227+
}
228+
229+
ValidatorSelectionLib.verifyAttestations(epoch, _attestations, checkpointLog.payloadDigest);
230+
}
231+
232+
/**
233+
* @notice Rehashes each provided checkpoint header and requires it to match the stored header hash
234+
*
235+
* @param _start The first checkpoint number in the epoch (inclusive)
236+
* @param _end The last checkpoint number in the epoch (inclusive)
237+
* @param _headers The proposed headers for each checkpoint in [_start, _end]
238+
*/
239+
function verifyHeaders(uint256 _start, uint256 _end, ProposedHeader[] calldata _headers) private view {
240+
uint256 numCheckpoints = _end - _start + 1;
241+
require(
242+
_headers.length == numCheckpoints, Errors.Rollup__InvalidCheckpointHeaderCount(numCheckpoints, _headers.length)
243+
);
244+
245+
for (uint256 i = 0; i < numCheckpoints; i++) {
246+
bytes32 expectedHeaderHash = STFLib.getHeaderHash(_start + i);
247+
bytes32 providedHeaderHash = ProposedHeaderLib.hash(_headers[i]);
248+
require(
249+
providedHeaderHash == expectedHeaderHash,
250+
Errors.Rollup__InvalidCheckpointHeader(expectedHeaderHash, providedHeaderHash)
251+
);
252+
}
253+
}
254+
255+
/**
256+
* @notice Assembles the root rollup public inputs, taking the supplied checkpoint headers as already verified
257+
*
258+
* @dev Callers must have rehashed `_headers` against the stored header hashes beforehand, since the fee
259+
* recipient/value public inputs are read straight out of them.
260+
*
261+
* @param _start - The start of the epoch (inclusive)
262+
* @param _end - The end of the epoch (inclusive)
263+
* @param _args - Array of public inputs to the proof (previousArchive, endArchive, endTimestamp, outHash, proverId)
264+
* @param _headers - The proposed checkpoint headers supplying the fee recipient and value for each checkpoint
265+
* @param _blobPublicInputs- The blob public inputs for the proof
266+
*/
267+
function computeEpochProofPublicInputs(
268+
uint256 _start,
269+
uint256 _end,
270+
PublicInputArgs calldata _args,
271+
ProposedHeader[] calldata _headers,
272+
bytes calldata _blobPublicInputs
273+
) private view returns (bytes32[] memory) {
166274
RollupStore storage rollupStore = STFLib.getStorage();
167275

168276
{
@@ -183,11 +291,6 @@ library EpochProofLib {
183291
}
184292
}
185293

186-
// The fee recipient/value below are sourced from the supplied headers, so the header hashes must be validated here
187-
// as well as on the submit path - otherwise an off-chain caller of this getter would assemble public inputs from
188-
// unverified fee fields and only discover the mismatch when the on-chain proof reverts.
189-
verifyHeaders(_start, _end, _headers);
190-
191294
bytes32[] memory publicInputs = new bytes32[](Constants.ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH);
192295

193296
// Structure of the root rollup public inputs we need to reassemble:
@@ -289,84 +392,6 @@ library EpochProofLib {
289392
return publicInputs;
290393
}
291394

292-
/**
293-
* @notice Verifies committee attestations for the last checkpoint in the epoch before accepting the epoch proof
294-
*
295-
* @dev This verification ensures that the committee has properly validated the final state of the epoch
296-
* before the proof can be accepted. The function validates that:
297-
* 1. The provided attestations match the stored attestation hash for the checkpoint
298-
* 2. The attestations have valid signatures from committee members
299-
* 3. The attestations meet the required threshold (2/3+ of committee)
300-
*
301-
* For escape hatch epochs, attestation verification is skipped since there is no committee
302-
* involvement - only the designated escape hatch proposer can propose blocks.
303-
*
304-
* @dev Errors Thrown:
305-
* - Rollup__InvalidAttestations: Provided attestations don't match stored hash or fail validation
306-
*
307-
* @param _endCheckpointNumber The last checkpoint number in the epoch to verify attestations for
308-
* @param _attestations The committee attestations containing signatures and validator information
309-
*/
310-
function verifyLastCheckpointAttestationsAndOutHash(
311-
uint256 _endCheckpointNumber,
312-
CommitteeAttestations memory _attestations,
313-
bytes32 _outHash
314-
) private {
315-
// Get the stored attestation hash and payload digest for the last checkpoint
316-
CompressedTempCheckpointLog storage checkpointLog = STFLib.getStorageTempCheckpointLog(_endCheckpointNumber);
317-
318-
// Verify that the out hash matches the stored value
319-
// The stored out hash is part of the payloadDigest that was attested to.
320-
require(checkpointLog.outHash == _outHash, Errors.Rollup__InvalidOutHash(checkpointLog.outHash, _outHash));
321-
322-
// Verify that the provided attestations match the stored hash
323-
bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations));
324-
require(providedAttestationsHash == checkpointLog.attestationsHash, Errors.Rollup__InvalidAttestations());
325-
326-
// Get the epoch for the last checkpoint
327-
Epoch epoch = STFLib.getEpochForCheckpoint(_endCheckpointNumber);
328-
329-
// Check if this is an escape hatch epoch - skip attestation verification if so
330-
// since escape hatch blocks are proposed without committee attestations.
331-
// Uses epoch-stable lookup so proof verification uses the escape hatch that was
332-
// active when the epoch started, not whatever is currently configured.
333-
{
334-
IEscapeHatch escapeHatch = ValidatorSelectionLib.getEscapeHatchForEpoch(epoch);
335-
if (address(escapeHatch) != address(0)) {
336-
(bool isOpen,) = escapeHatch.isHatchOpen(epoch);
337-
if (isOpen) {
338-
// Skip attestation verification for escape hatch epochs
339-
return;
340-
}
341-
}
342-
}
343-
344-
ValidatorSelectionLib.verifyAttestations(epoch, _attestations, checkpointLog.payloadDigest);
345-
}
346-
347-
/**
348-
* @notice Rehashes each provided checkpoint header and requires it to match the stored header hash
349-
*
350-
* @param _start The first checkpoint number in the epoch (inclusive)
351-
* @param _end The last checkpoint number in the epoch (inclusive)
352-
* @param _headers The proposed headers for each checkpoint in [_start, _end]
353-
*/
354-
function verifyHeaders(uint256 _start, uint256 _end, ProposedHeader[] calldata _headers) private view {
355-
uint256 numCheckpoints = _end - _start + 1;
356-
require(
357-
_headers.length == numCheckpoints, Errors.Rollup__InvalidCheckpointHeaderCount(numCheckpoints, _headers.length)
358-
);
359-
360-
for (uint256 i = 0; i < numCheckpoints; i++) {
361-
bytes32 expectedHeaderHash = STFLib.getHeaderHash(_start + i);
362-
bytes32 providedHeaderHash = ProposedHeaderLib.hash(_headers[i]);
363-
require(
364-
providedHeaderHash == expectedHeaderHash,
365-
Errors.Rollup__InvalidCheckpointHeader(expectedHeaderHash, providedHeaderHash)
366-
);
367-
}
368-
}
369-
370395
/**
371396
* @notice Validates that an epoch proof submission meets all acceptance criteria
372397
*
@@ -434,6 +459,9 @@ library EpochProofLib {
434459
* 2. Assembling the public inputs for the root rollup circuit
435460
* 3. Verifying the validity proof against the assembled public inputs using the configured verifier
436461
*
462+
* @dev Assumes the caller has already verified the supplied checkpoint headers against storage, so assembly skips
463+
* rehashing them.
464+
*
437465
* @dev Errors Thrown:
438466
* - Rollup__InvalidBlobProof: Batched blob proof verification failed
439467
* - Rollup__InvalidProof: validity proof verification failed
@@ -449,7 +477,7 @@ library EpochProofLib {
449477
BlobLib.validateBatchedBlob(_args.blobInputs);
450478

451479
bytes32[] memory publicInputs =
452-
getEpochProofPublicInputs(_args.start, _args.end, _args.args, _args.headers, _args.blobInputs);
480+
computeEpochProofPublicInputs(_args.start, _args.end, _args.args, _args.headers, _args.blobInputs);
453481

454482
require(rollupStore.config.epochProofVerifier.verify(_args.proof, publicInputs), Errors.Rollup__InvalidProof());
455483

0 commit comments

Comments
 (0)