Skip to content

Commit 4d38189

Browse files
authored
fix: ensure outhash is checked also in the escape hatch (#20063)
When the updates to set out hash root in checkpoint was merged after the escape hatch, it inserted a subtle bug. When the escape hatch is open, the check was skipped because of an early return. This meant that during the escape hatch, it was possible for someone to propose completely sound blocks, but for someone else to then prove it with a different outhash if they found soundness bugs 😱. When the updates where made in `EscapeHAtchIntegrationBase` it simple inserted `bytes32(0)` as the `outhash` meaning that the actual test was doing exactly this (since they are run without a real verifier).
2 parents dc6ef70 + 1017ce5 commit 4d38189

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)