Skip to content
24 changes: 18 additions & 6 deletions contracts/src/steel/Steel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ library Steel {
}

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

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

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

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

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

/// @notice Call to the EIP-4788 beacon roots contract failed due to an invalid block timestamp.
/// @dev A block timestamp is invalid if it does not correspond to a stored block on the
/// EIP-4788 contract. This can happen if the timestamp is too old, and the corresponding
/// block has been evicted from the cache, if the timestamp corresponds to a
/// slot with no block, or if the timestamp does not correspond to any slot at all.
/// @dev Error signature: 0x4d0b0a41
error InvalidBlockTimestamp();

/// @notice Find the root of the Beacon block corresponding to the parent of the execution block with the given timestamp.
/// @return root Returns the corresponding Beacon block root or null, if no such block exists.
/// @return root Returns the corresponding Beacon block root or reverts, if no such block exists.
function parentBlockRoot(uint256 timestamp) internal view returns (bytes32 root) {
(bool success, bytes memory result) = BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp));
if (success) {
return abi.decode(result, (bytes32));
} else {
revert InvalidBlockTimestamp();
}
}
}
Expand Down
139 changes: 139 additions & 0 deletions contracts/src/test/Steel.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.17;

import {Test} from "forge-std/Test.sol";

import {Steel, Beacon, Encoding} from "../steel/Steel.sol";

contract SteelVerifier {
function validateCommitment(Steel.Commitment memory commitment) external view returns (bool) {
return Steel.validateCommitment(commitment);
}
}

contract SteelTest is Test {
SteelVerifier internal verifier;

function setUp() public {
verifier = new SteelVerifier();
}

function createCommitment(uint240 claimID, uint16 version, bytes32 digest)
internal
pure
returns (Steel.Commitment memory)
{
return
Steel.Commitment({id: Encoding.encodeVersionedID(claimID, version), digest: digest, configID: bytes32(0)});
}

function test_ValidateCommitment_V0_Block_Success() public {
vm.roll(block.number + 10);
uint256 targetBlockNumber = block.number - 5;
bytes32 targetBlockHash = blockhash(targetBlockNumber);
assertTrue(targetBlockHash != bytes32(0), "Test setup: blockhash(target) is zero");

Steel.Commitment memory c = createCommitment(uint240(targetBlockNumber), 0, targetBlockHash);
assertTrue(verifier.validateCommitment(c), "V0 valid block commitment failed");
}

function test_ValidateCommitment_V0_Block_WrongHash() public {
vm.roll(block.number + 10);
uint256 targetBlockNumber = block.number - 5;
bytes32 wrongHash = keccak256(abi.encodePacked("wrong_hash"));
assertTrue(blockhash(targetBlockNumber) != bytes32(0), "Test setup: blockhash(target) is zero");

Steel.Commitment memory c = createCommitment(uint240(targetBlockNumber), 0, wrongHash);
assertFalse(verifier.validateCommitment(c), "V0 wrong block hash should be invalid");
}

function test_ValidateCommitment_V0_Block_TooOld() public {
vm.roll(block.number + 300);
uint256 oldBlockNumber = 1;
bytes32 someHash = keccak256(abi.encodePacked("some_hash"));

Steel.Commitment memory c = createCommitment(uint240(oldBlockNumber), 0, someHash);
vm.expectPartialRevert(Steel.CommitmentTooOld.selector);
verifier.validateCommitment(c);
}

function test_ValidateCommitment_V1_Beacon_Success() public {
uint256 timestamp = 1700000000;
bytes32 expectedRoot = keccak256(abi.encodePacked("beacon_root_v1"));

// Mock the call to Beacon.BEACON_ROOTS_ADDRESS
vm.mockCall(Beacon.BEACON_ROOTS_ADDRESS, abi.encode(timestamp), abi.encode(expectedRoot));

Steel.Commitment memory c = createCommitment(
uint240(timestamp), // claimID is timestamp for V1
1,
expectedRoot
);
assertTrue(verifier.validateCommitment(c), "V1 valid beacon commitment failed");
}

function test_ValidateCommitment_V1_Beacon_WrongRoot() public {
uint256 timestamp = 1700000000;
bytes32 correctRoot = keccak256(abi.encodePacked("beacon_root_v1"));
bytes32 wrongRootInCommitment = keccak256(abi.encodePacked("wrong_root"));

// Mock the call to Beacon.BEACON_ROOTS_ADDRESS to return the correctRoot
vm.mockCall(Beacon.BEACON_ROOTS_ADDRESS, abi.encode(timestamp), abi.encode(correctRoot));

Steel.Commitment memory c = createCommitment(
uint240(timestamp),
1,
wrongRootInCommitment // Commitment has the wrong root
);
assertFalse(verifier.validateCommitment(c), "V1 wrong beacon root should be invalid");
}

function test_ValidateCommitment_V1_Beacon_InvalidTimestamp() public {
uint256 invalidTimestamp = 1700000001;

// Mock the call to Beacon.BEACON_ROOTS_ADDRESS to revert
vm.mockCallRevert(
Beacon.BEACON_ROOTS_ADDRESS,
abi.encode(invalidTimestamp),
bytes("Mocked EIP-4788 Revert for invalid timestamp")
);

Steel.Commitment memory c =
createCommitment(uint240(invalidTimestamp), 1, keccak256(abi.encodePacked("any_root")));
vm.expectRevert(Beacon.InvalidBlockTimestamp.selector);
verifier.validateCommitment(c);
}

function test_ValidateCommitment_V2_Reverts_ConsensusSlotNotSupported() public {
uint240 claimID = 999;
Steel.Commitment memory c = createCommitment(claimID, 2, keccak256(abi.encodePacked("any_digest_v2")));
vm.expectRevert(Steel.ConsensusSlotCommitmentNotSupported.selector);
verifier.validateCommitment(c);
}

function test_ValidateCommitment_V3_Reverts_InvalidCommitmentVersion() public {
uint240 claimID = 1000;
Steel.Commitment memory c = createCommitment(
claimID,
3, // Any version > 2 not explicitly handled
keccak256(abi.encodePacked("any_digest_v3"))
);
vm.expectRevert(Steel.InvalidCommitmentVersion.selector);
verifier.validateCommitment(c);
}
}
Loading