Skip to content

Commit 1017ce5

Browse files
committed
fix: ensure outhash is checked also in the escape hatch
1 parent dc6ef70 commit 1017ce5

File tree

3 files changed

+94
-5
lines changed

3 files changed

+94
-5
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ library EpochProofLib {
295295
// Get the stored attestation hash and payload digest for the last checkpoint
296296
CompressedTempCheckpointLog storage checkpointLog = STFLib.getStorageTempCheckpointLog(_endCheckpointNumber);
297297

298+
// Verify that the out hash matches the stored value
299+
// The stored out hash is part of the payloadDigest that was attested to.
300+
require(checkpointLog.outHash == _outHash, Errors.Rollup__InvalidOutHash(checkpointLog.outHash, _outHash));
301+
298302
// Verify that the provided attestations match the stored hash
299303
bytes32 providedAttestationsHash = keccak256(abi.encode(_attestations));
300304
require(providedAttestationsHash == checkpointLog.attestationsHash, Errors.Rollup__InvalidAttestations());
@@ -317,10 +321,6 @@ library EpochProofLib {
317321
}
318322

319323
ValidatorSelectionLib.verifyAttestations(slot, epoch, _attestations, checkpointLog.payloadDigest);
320-
321-
// Verify that the out hash matches the stored value
322-
// The stored out hash is part of the payloadDigest that was attested to.
323-
require(checkpointLog.outHash == _outHash, Errors.Rollup__InvalidOutHash(checkpointLog.outHash, _outHash));
324324
}
325325

326326
/**

l1-contracts/test/escape-hatch/integration/EscapeHatchIntegrationBase.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,10 @@ abstract contract EscapeHatchIntegrationBase is ValidatorSelectionTestBase {
315315
bytes32 endArchive = rollup.archiveAt(_end);
316316

317317
PublicInputArgs memory args = PublicInputArgs({
318-
previousArchive: previousArchive, endArchive: endArchive, outHash: bytes32(0), proverId: _prover
318+
previousArchive: previousArchive,
319+
endArchive: endArchive,
320+
outHash: endFull.checkpoint.header.outHash,
321+
proverId: _prover
319322
});
320323

321324
bytes32[] memory fees = new bytes32[](Constants.AZTEC_MAX_EPOCH_DURATION * 2);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2025 Aztec Labs.
3+
pragma solidity >=0.8.27;
4+
5+
import {EscapeHatchIntegrationBase} from "../EscapeHatchIntegrationBase.sol";
6+
import {Errors} from "@aztec/core/libraries/Errors.sol";
7+
import {Epoch} from "@aztec/shared/libraries/TimeMath.sol";
8+
import {CheckpointLog, SubmitEpochRootProofArgs, PublicInputArgs} from "@aztec/core/interfaces/IRollup.sol";
9+
import {Constants} from "@aztec/core/libraries/ConstantsGen.sol";
10+
import {
11+
CommitteeAttestations,
12+
CommitteeAttestation,
13+
Signature,
14+
AttestationLib
15+
} from "@aztec/core/libraries/rollup/AttestationLib.sol";
16+
17+
/**
18+
* @title OutHashValidationSkippedTest
19+
* @notice Regression test for outHash validation being skipped during escape hatch epochs
20+
*
21+
* @dev This test verifies that the outHash is validated even when the escape hatch is open.
22+
* Previously, the early return in verifyLastCheckpointAttestationsAndOutHash() when
23+
* escape hatch was open would skip the outHash validation, allowing a malicious prover
24+
* to submit an arbitrary outHash that would be inserted into the outbox.
25+
*
26+
* Bug location: EpochProofLib.sol::verifyLastCheckpointAttestationsAndOutHash()
27+
* The early `return` when escape hatch is open skipped the outHash check at the end.
28+
*/
29+
contract OutHashValidationSkippedTest is EscapeHatchIntegrationBase {
30+
function test_RevertWhen_EscapeHatchIsOpenAndOutHashMismatch() external setup(4, 4) progressEpochsToInclusion {
31+
// Deploy and configure escape hatch
32+
_deployEscapeHatch();
33+
34+
full = load("empty_checkpoint_1");
35+
36+
// Setup escape hatch and warp to the escape hatch window
37+
_joinCandidateSet(CANDIDATE1);
38+
targetHatch = _selectCandidateForHatch();
39+
_warpToHatch(targetHatch);
40+
41+
// Verify escape hatch is open
42+
Epoch currentEpoch = rollup.getCurrentEpoch();
43+
(bool isOpen,) = escapeHatch.isHatchOpen(currentEpoch);
44+
assertTrue(isOpen, "Escape hatch should be open");
45+
46+
// Propose as escape hatch proposer
47+
_proposeWithHatch(CANDIDATE1);
48+
assertEq(rollup.getPendingCheckpointNumber(), 1, "Checkpoint should be proposed");
49+
50+
// Get the correct outHash from the checkpoint
51+
CheckpointLog memory endCheckpoint = rollup.getCheckpoint(1);
52+
bytes32 correctOutHash = endCheckpoint.outHash;
53+
54+
// Use a wrong outHash
55+
bytes32 wrongOutHash = bytes32(uint256(0xdeadbeef));
56+
57+
assertNotEq(correctOutHash, wrongOutHash, "Correct outHash should not be equal to wrong outHash");
58+
59+
PublicInputArgs memory args = PublicInputArgs({
60+
previousArchive: rollup.archiveAt(0),
61+
endArchive: rollup.archiveAt(1),
62+
outHash: wrongOutHash,
63+
proverId: address(this)
64+
});
65+
66+
bytes32[] memory fees = new bytes32[](Constants.AZTEC_MAX_EPOCH_DURATION * 2);
67+
uint256 size = 1;
68+
for (uint256 i = 0; i < size; i++) {
69+
fees[i * 2] = bytes32(uint256(uint160(bytes20(("sequencer")))));
70+
fees[i * 2 + 1] = bytes32(0);
71+
}
72+
73+
vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__InvalidOutHash.selector, correctOutHash, wrongOutHash));
74+
rollup.submitEpochRootProof(
75+
SubmitEpochRootProofArgs({
76+
start: 1,
77+
end: 1,
78+
args: args,
79+
fees: fees,
80+
attestations: CommitteeAttestations({signatureIndices: "", signaturesOrAddresses: ""}),
81+
blobInputs: full.checkpoint.batchedBlobInputs,
82+
proof: ""
83+
})
84+
);
85+
}
86+
}

0 commit comments

Comments
 (0)