Skip to content

Commit 3bbac85

Browse files
nategrafcaposseleWollac
authored
WEB3-458: Update the Steel beacon block commit validation to always revert on invalid timestamps (#605)
On `main`, the `Beacon.parentBlockRoot(uint256 timestamp)` function returns zero bytes if the timestamp given is invalid. This allows the `Steel.validateCommitment` to return true if given a commitment suchas `Commitment { id: Encoding.encodeVersionID(block.timestamp - 1, 1), bytes32(0), configID }` where the digest is zero and the timestamp is within the last ~24 hours but does not correspond to a valid block. This violates the semantics of `validateCommitment` in that this does not commitment to a block that is in the current chain. Because the digest is zero, it does not correspond to any block and there exist no known openings. As a result, this commitment will never be produced by a correct zkVM guest using Steel. As a result, leveraging this bug to compromise the soundness of a program using Steel would require a separate bug or misuse of the Steel API. As a fix for this issue, this PR checks whether the EIP-4788 contract call reverts, and reverts with `InvalidBlockTimestamp` if so. We choose this error message as this is the only case in which the EIP-4788 contract will revert when called from the `Beacon` contract. With this, this PR removes the explicit check that the timestamp is recent, as it is redundant. As a drive-by change, this PR also drops the explicit check for the block number being too old when using execution block commitments. Instead, this checks the return value of the `blockhash` opcode, which will return zeroes if the block number is too old. This should not result in any change of behavior on Ethereum. --------- Co-authored-by: Angelo Capossele <[email protected]> Co-authored-by: Wolfgang Welz <[email protected]>
1 parent c8c18e2 commit 3bbac85

File tree

2 files changed

+157
-6
lines changed

2 files changed

+157
-6
lines changed

contracts/src/steel/Steel.sol

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ library Steel {
3030
}
3131

3232
/// @notice The version of the Commitment is incorrect.
33+
/// @dev Error signature: 0xbb2b2291
3334
error InvalidCommitmentVersion();
3435

3536
/// @notice The Commitment is too old and can no longer be validated.
37+
/// @dev Error signature: 0xcfef9a95
3638
error CommitmentTooOld();
3739

3840
/// @notice The consensus slot (version 2) commitment is not supported.
41+
/// @dev Error signature: 0x13d71698
3942
error ConsensusSlotCommitmentNotSupported();
4043

4144
/// @notice Validates if the provided Commitment matches the block hash of the given block number.
@@ -60,20 +63,19 @@ library Steel {
6063
/// @param blockHash The block hash to validate.
6164
/// @return True if the block's block hash matches the block hash, false otherwise.
6265
function validateBlockCommitment(uint256 blockNumber, bytes32 blockHash) internal view returns (bool) {
63-
if (block.number - blockNumber > 256) {
66+
// NOTE: blockhash opcode returns all zeroes if the block number is too far in the past.
67+
bytes32 blockHashResult = blockhash(blockNumber);
68+
if (blockHashResult == bytes32(0)) {
6469
revert CommitmentTooOld();
6570
}
66-
return blockHash == blockhash(blockNumber);
71+
return blockHash == blockHashResult;
6772
}
6873

6974
/// @notice Validates if the provided beacon commitment matches the block root of the given timestamp.
7075
/// @param timestamp The timestamp to compare against.
7176
/// @param blockRoot The block root to validate.
7277
/// @return True if the block's block root matches the block root, false otherwise.
7378
function validateBeaconCommitment(uint256 timestamp, bytes32 blockRoot) internal view returns (bool) {
74-
if (block.timestamp - timestamp > 12 * 8191) {
75-
revert CommitmentTooOld();
76-
}
7779
return blockRoot == Beacon.parentBlockRoot(timestamp);
7880
}
7981
}
@@ -84,12 +86,22 @@ library Beacon {
8486
/// @dev https://eips.ethereum.org/EIPS/eip-4788
8587
address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;
8688

89+
/// @notice Call to the EIP-4788 beacon roots contract failed due to an invalid block timestamp.
90+
/// @dev A block timestamp is invalid if it does not correspond to a stored block on the
91+
/// EIP-4788 contract. This can happen if the timestamp is too old, and the corresponding
92+
/// block has been evicted from the cache, if the timestamp corresponds to a
93+
/// slot with no block, or if the timestamp does not correspond to any slot at all.
94+
/// @dev Error signature: 0x4d0b0a41
95+
error InvalidBlockTimestamp();
96+
8797
/// @notice Find the root of the Beacon block corresponding to the parent of the execution block with the given timestamp.
88-
/// @return root Returns the corresponding Beacon block root or null, if no such block exists.
98+
/// @return root Returns the corresponding Beacon block root or reverts, if no such block exists.
8999
function parentBlockRoot(uint256 timestamp) internal view returns (bytes32 root) {
90100
(bool success, bytes memory result) = BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp));
91101
if (success) {
92102
return abi.decode(result, (bytes32));
103+
} else {
104+
revert InvalidBlockTimestamp();
93105
}
94106
}
95107
}

contracts/src/test/Steel.t.sol

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2025 RISC Zero, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
pragma solidity ^0.8.17;
18+
19+
import {Test} from "forge-std/Test.sol";
20+
21+
import {Steel, Beacon, Encoding} from "../steel/Steel.sol";
22+
23+
contract SteelVerifier {
24+
function validateCommitment(Steel.Commitment memory commitment) external view returns (bool) {
25+
return Steel.validateCommitment(commitment);
26+
}
27+
}
28+
29+
contract SteelTest is Test {
30+
SteelVerifier internal verifier;
31+
32+
function setUp() public {
33+
verifier = new SteelVerifier();
34+
}
35+
36+
function createCommitment(uint240 claimID, uint16 version, bytes32 digest)
37+
internal
38+
pure
39+
returns (Steel.Commitment memory)
40+
{
41+
return
42+
Steel.Commitment({id: Encoding.encodeVersionedID(claimID, version), digest: digest, configID: bytes32(0)});
43+
}
44+
45+
function test_ValidateCommitment_V0_Block_Success() public {
46+
vm.roll(block.number + 10);
47+
uint256 targetBlockNumber = block.number - 5;
48+
bytes32 targetBlockHash = blockhash(targetBlockNumber);
49+
assertTrue(targetBlockHash != bytes32(0), "Test setup: blockhash(target) is zero");
50+
51+
Steel.Commitment memory c = createCommitment(uint240(targetBlockNumber), 0, targetBlockHash);
52+
assertTrue(verifier.validateCommitment(c), "V0 valid block commitment failed");
53+
}
54+
55+
function test_ValidateCommitment_V0_Block_WrongHash() public {
56+
vm.roll(block.number + 10);
57+
uint256 targetBlockNumber = block.number - 5;
58+
bytes32 wrongHash = keccak256(abi.encodePacked("wrong_hash"));
59+
assertTrue(blockhash(targetBlockNumber) != bytes32(0), "Test setup: blockhash(target) is zero");
60+
61+
Steel.Commitment memory c = createCommitment(uint240(targetBlockNumber), 0, wrongHash);
62+
assertFalse(verifier.validateCommitment(c), "V0 wrong block hash should be invalid");
63+
}
64+
65+
function test_ValidateCommitment_V0_Block_TooOld() public {
66+
vm.roll(block.number + 300);
67+
uint256 oldBlockNumber = 1;
68+
bytes32 someHash = keccak256(abi.encodePacked("some_hash"));
69+
70+
Steel.Commitment memory c = createCommitment(uint240(oldBlockNumber), 0, someHash);
71+
vm.expectPartialRevert(Steel.CommitmentTooOld.selector);
72+
verifier.validateCommitment(c);
73+
}
74+
75+
function test_ValidateCommitment_V1_Beacon_Success() public {
76+
uint256 timestamp = 1700000000;
77+
bytes32 expectedRoot = keccak256(abi.encodePacked("beacon_root_v1"));
78+
79+
// Mock the call to Beacon.BEACON_ROOTS_ADDRESS
80+
vm.mockCall(Beacon.BEACON_ROOTS_ADDRESS, abi.encode(timestamp), abi.encode(expectedRoot));
81+
82+
Steel.Commitment memory c = createCommitment(
83+
uint240(timestamp), // claimID is timestamp for V1
84+
1,
85+
expectedRoot
86+
);
87+
assertTrue(verifier.validateCommitment(c), "V1 valid beacon commitment failed");
88+
}
89+
90+
function test_ValidateCommitment_V1_Beacon_WrongRoot() public {
91+
uint256 timestamp = 1700000000;
92+
bytes32 correctRoot = keccak256(abi.encodePacked("beacon_root_v1"));
93+
bytes32 wrongRootInCommitment = keccak256(abi.encodePacked("wrong_root"));
94+
95+
// Mock the call to Beacon.BEACON_ROOTS_ADDRESS to return the correctRoot
96+
vm.mockCall(Beacon.BEACON_ROOTS_ADDRESS, abi.encode(timestamp), abi.encode(correctRoot));
97+
98+
Steel.Commitment memory c = createCommitment(
99+
uint240(timestamp),
100+
1,
101+
wrongRootInCommitment // Commitment has the wrong root
102+
);
103+
assertFalse(verifier.validateCommitment(c), "V1 wrong beacon root should be invalid");
104+
}
105+
106+
function test_ValidateCommitment_V1_Beacon_InvalidTimestamp() public {
107+
uint256 invalidTimestamp = 1700000001;
108+
109+
// Mock the call to Beacon.BEACON_ROOTS_ADDRESS to revert
110+
vm.mockCallRevert(
111+
Beacon.BEACON_ROOTS_ADDRESS,
112+
abi.encode(invalidTimestamp),
113+
bytes("Mocked EIP-4788 Revert for invalid timestamp")
114+
);
115+
116+
Steel.Commitment memory c =
117+
createCommitment(uint240(invalidTimestamp), 1, keccak256(abi.encodePacked("any_root")));
118+
vm.expectRevert(Beacon.InvalidBlockTimestamp.selector);
119+
verifier.validateCommitment(c);
120+
}
121+
122+
function test_ValidateCommitment_V2_Reverts_ConsensusSlotNotSupported() public {
123+
uint240 claimID = 999;
124+
Steel.Commitment memory c = createCommitment(claimID, 2, keccak256(abi.encodePacked("any_digest_v2")));
125+
vm.expectRevert(Steel.ConsensusSlotCommitmentNotSupported.selector);
126+
verifier.validateCommitment(c);
127+
}
128+
129+
function test_ValidateCommitment_V3_Reverts_InvalidCommitmentVersion() public {
130+
uint240 claimID = 1000;
131+
Steel.Commitment memory c = createCommitment(
132+
claimID,
133+
3, // Any version > 2 not explicitly handled
134+
keccak256(abi.encodePacked("any_digest_v3"))
135+
);
136+
vm.expectRevert(Steel.InvalidCommitmentVersion.selector);
137+
verifier.validateCommitment(c);
138+
}
139+
}

0 commit comments

Comments
 (0)