diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index effd18146b..373fc10e77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,7 +157,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Install Dependencies run: forge install - name: Run prep scripts and forge fmt diff --git a/docs/utils/blockhashlib.md b/docs/utils/blockhashlib.md index 8bae3100c6..21559d46df 100644 --- a/docs/utils/blockhashlib.md +++ b/docs/utils/blockhashlib.md @@ -9,6 +9,40 @@ Library for accessing block hashes way beyond the 256-block limit. +## Structs + +### ShortHeader + +```solidity +struct ShortHeader { + bytes32 parentHash; + bytes32 stateRoot; + bytes32 transactionsRoot; + bytes32 receiptsRoot; + bytes32[8] logsBloom; +} +``` + +Ethereum block header fields relevant to historical MPT proofs. + +## Custom Errors + +### BlockHashMismatch() + +```solidity +error BlockHashMismatch() +``` + +The keccak256 of the RLP-encoded block header does not equal to the block hash. + +### InvalidBlockHeaderEncoding() + +```solidity +error InvalidBlockHeaderEncoding() +``` + +The block header is not properly RLP-encoded. + ## Constants ### HISTORY_STORAGE_ADDRESS @@ -34,4 +68,34 @@ function blockHash(uint256 blockNumber) Retrieves the block hash for any historical block within the supported range. The function gracefully handles future blocks and blocks beyond the history window by returning zero, -consistent with the EVM's native `BLOCKHASH` behavior. \ No newline at end of file +consistent with the EVM's native `BLOCKHASH` behavior. + +### verifyBlock(bytes,uint256) + +```solidity +function verifyBlock(bytes calldata encodedHeader, uint256 blockNumber) + internal + view + returns (bytes32 result) +``` + +Reverts if `keccak256(encodedHeader) != blockHash(blockNumber)`, +where `encodedHeader` is a RLP-encoded block header. +Else, returns `blockHash(blockNumber)`. + +### toShortHeader(bytes) + +```solidity +function toShortHeader(bytes calldata encodedHeader) + internal + pure + returns (ShortHeader memory result) +``` + +Retrieves the most relevant fields for MPT proofs from an RLP-encoded block header. +Leading fields are always present and have fixed offsets and lengths. +This function efficiently extracts the fields without full RLP decoding. +For the specification of field order and lengths, please refer to +prefix. 6 of the Ethereum Yellow Paper: +(https://ethereum.github.io/yellowpaper/paper.pdf) +and the Ethereum Wiki (https://epf.wiki/#/wiki/EL/RLP). \ No newline at end of file diff --git a/src/utils/BlockHashLib.sol b/src/utils/BlockHashLib.sol index c26fbabb25..4c4d586f53 100644 --- a/src/utils/BlockHashLib.sol +++ b/src/utils/BlockHashLib.sol @@ -5,6 +5,29 @@ pragma solidity ^0.8.4; /// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/BlockHashLib.sol) /// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Blockhash.sol) library BlockHashLib { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRUCTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Ethereum block header fields relevant to historical MPT proofs. + struct ShortHeader { + bytes32 parentHash; + bytes32 stateRoot; + bytes32 transactionsRoot; + bytes32 receiptsRoot; + bytes32[8] logsBloom; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The keccak256 of the RLP-encoded block header does not equal to the block hash. + error BlockHashMismatch(); + + /// @dev The block header is not properly RLP-encoded. + error InvalidBlockHeaderEncoding(); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -35,4 +58,54 @@ library BlockHashLib { result := mload(0x00) } } + + /// @dev Reverts if `keccak256(encodedHeader) != blockHash(blockNumber)`, + /// where `encodedHeader` is a RLP-encoded block header. + /// Else, returns `blockHash(blockNumber)`. + function verifyBlock(bytes calldata encodedHeader, uint256 blockNumber) + internal + view + returns (bytes32 result) + { + result = blockHash(blockNumber); + /// @solidity memory-safe-assembly + assembly { + calldatacopy(mload(0x40), encodedHeader.offset, encodedHeader.length) + if iszero(eq(result, keccak256(mload(0x40), encodedHeader.length))) { + mstore(0x00, 0xe42b5e7e) // `BlockHashMismatch()`. + revert(0x1c, 0x04) + } + } + } + + /// @dev Retrieves the most relevant fields for MPT proofs from an RLP-encoded block header. + /// Leading fields are always present and have fixed offsets and lengths. + /// This function efficiently extracts the fields without full RLP decoding. + /// For the specification of field order and lengths, please refer to + /// prefix. 6 of the Ethereum Yellow Paper: + /// (https://ethereum.github.io/yellowpaper/paper.pdf) + /// and the Ethereum Wiki (https://epf.wiki/#/wiki/EL/RLP). + function toShortHeader(bytes calldata encodedHeader) + internal + pure + returns (ShortHeader memory result) + { + /// @solidity memory-safe-assembly + assembly { + mstore(result, calldataload(add(4, encodedHeader.offset))) // `parentHash`. + mstore(add(0x20, result), calldataload(add(91, encodedHeader.offset))) // `stateRoot`. + mstore(add(0x40, result), calldataload(add(124, encodedHeader.offset))) // `transactionsRoot`. + mstore(add(0x60, result), calldataload(add(157, encodedHeader.offset))) // `receiptsRoot`. + calldatacopy(mload(add(0x80, result)), add(192, encodedHeader.offset), 0x100) // `logsBloom`. + if iszero( // Just perform some minimal light bounds checking. + and( + gt(encodedHeader.length, 447), // `0x100 + 192 - 1`. + eq(byte(0, calldataload(encodedHeader.offset)), 0xf9) // `0xff < len < 0x10000`. + ) + ) { + mstore(0x00, 0x1a27c4e4) // `InvalidBlockHeaderEncoding()`. + revert(0x1c, 0x04) + } + } + } } diff --git a/src/utils/g/BlockHashLib.sol b/src/utils/g/BlockHashLib.sol new file mode 100644 index 0000000000..7a01bf4386 --- /dev/null +++ b/src/utils/g/BlockHashLib.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +// This file is auto-generated. + +/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ +/* STRUCTS */ +/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + +/// @dev Ethereum block header fields relevant to historical MPT proofs. +struct ShortHeader { + bytes32 parentHash; + bytes32 stateRoot; + bytes32 transactionsRoot; + bytes32 receiptsRoot; + bytes32[8] logsBloom; +} + +using BlockHashLib for ShortHeader global; + +/// @notice Library for accessing block hashes way beyond the 256-block limit. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/g/BlockHashLib.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Blockhash.sol) +library BlockHashLib { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The keccak256 of the RLP-encoded block header does not equal to the block hash. + error BlockHashMismatch(); + + /// @dev The block header is not properly RLP-encoded. + error InvalidBlockHeaderEncoding(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Address of the EIP-2935 history storage contract. + /// See: https://eips.ethereum.org/EIPS/eip-2935 + address internal constant HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53a10cb7A02335B175320002935; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Retrieves the block hash for any historical block within the supported range. + /// The function gracefully handles future blocks and blocks beyond the history window by returning zero, + /// consistent with the EVM's native `BLOCKHASH` behavior. + function blockHash(uint256 blockNumber) internal view returns (bytes32 result) { + unchecked { + // If `blockNumber + 256` overflows: + // - Typical chain height (`block.number > 255`) -> `staticcall` -> 0. + // - Very early chain (`block.number <= 255`) -> `blockhash` -> 0. + if (block.number <= blockNumber + 256) return blockhash(blockNumber); + } + /// @solidity memory-safe-assembly + assembly { + mstore(0x20, blockNumber) + mstore(0x00, 0) + pop(staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0x20, 0x20, 0x00, 0x20)) + result := mload(0x00) + } + } + + /// @dev Reverts if `keccak256(encodedHeader) != blockHash(blockNumber)`, + /// where `encodedHeader` is a RLP-encoded block header. + /// Else, returns `blockHash(blockNumber)`. + function verifyBlock(bytes calldata encodedHeader, uint256 blockNumber) + internal + view + returns (bytes32 result) + { + result = blockHash(blockNumber); + /// @solidity memory-safe-assembly + assembly { + calldatacopy(mload(0x40), encodedHeader.offset, encodedHeader.length) + if iszero(eq(result, keccak256(mload(0x40), encodedHeader.length))) { + mstore(0x00, 0xe42b5e7e) // `BlockHashMismatch()`. + revert(0x1c, 0x04) + } + } + } + + /// @dev Retrieves the most relevant fields for MPT proofs from an RLP-encoded block header. + /// Leading fields are always present and have fixed offsets and lengths. + /// This function efficiently extracts the fields without full RLP decoding. + /// For the specification of field order and lengths, please refer to + /// prefix. 6 of the Ethereum Yellow Paper: + /// (https://ethereum.github.io/yellowpaper/paper.pdf) + /// and the Ethereum Wiki (https://epf.wiki/#/wiki/EL/RLP). + function toShortHeader(bytes calldata encodedHeader) + internal + pure + returns (ShortHeader memory result) + { + /// @solidity memory-safe-assembly + assembly { + mstore(result, calldataload(add(4, encodedHeader.offset))) // `parentHash`. + mstore(add(0x20, result), calldataload(add(91, encodedHeader.offset))) // `stateRoot`. + mstore(add(0x40, result), calldataload(add(124, encodedHeader.offset))) // `transactionsRoot`. + mstore(add(0x60, result), calldataload(add(157, encodedHeader.offset))) // `receiptsRoot`. + calldatacopy(mload(add(0x80, result)), add(192, encodedHeader.offset), 0x100) // `logsBloom`. + if iszero( // Just perform some minimal light bounds checking. + and( + gt(encodedHeader.length, 447), // `0x100 + 192 - 1`. + eq(byte(0, calldataload(encodedHeader.offset)), 0xf9) // `0xff < len < 0x10000`. + ) + ) { + mstore(0x00, 0x1a27c4e4) // `InvalidBlockHeaderEncoding()`. + revert(0x1c, 0x04) + } + } + } +} diff --git a/test/BlockHashLib.t.sol b/test/BlockHashLib.t.sol index c372aa8ec9..aa6ecc53e7 100644 --- a/test/BlockHashLib.t.sol +++ b/test/BlockHashLib.t.sol @@ -3,8 +3,29 @@ pragma solidity ^0.8.4; import "./utils/SoladyTest.sol"; import {BlockHashLib} from "../src/utils/BlockHashLib.sol"; +import {LibRLP} from "../src/utils/LibRLP.sol"; contract BlockHashLibTest is SoladyTest { + using LibRLP for *; + + struct BlockHeader { + bytes32 parentHash; + bytes32 ommersHash; + bytes20 beneficiary; + bytes32 stateRoot; + bytes32 transactionsRoot; + bytes32 receiptsRoot; + bytes32[8] logsBloom; + bytes32 difficultyOrPrevrandao; + uint256 number; + uint256 gasLimit; + uint256 gasUsed; + uint256 timestamp; + bytes extraData; + bytes32 mixHash; + bytes8 nonce; + } + uint256 internal startingBlock; address internal constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; @@ -12,6 +33,15 @@ contract BlockHashLibTest is SoladyTest { bytes private constant _HISTORY_STORAGE_BYTECODE = hex"3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500"; + // cast block 23270177 --raw + // vm.getRawBlockHeader(23270177) + bytes private constant _ETH_BLOCK_23270177 = + hex"f9027da01581f4448b16694d5a728161cd65f8c80b88f5352a6f5bd2d2315b970582958da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794dadb0d80178819f2319190d340ce9a924f783711a010d2afa5dabcf2dbfe3aa82b758427938e07880bd6fef3c82c404d0dd7c3f0f3a0f81230c715a462c827898bf2e337982907a7af90e5be20f911785bda05dab93ca0740f11bc75cf25e40d78d892d2e03083eaa573e5b4c26913fcc1b833db854c94b9010085f734fb06ea8fe377abbcb2e27f9ac99751ba817dc327327db101fd76f964ed0b7ca161f148fc165b9e5b575dc7473f17f4b8ebbf4a7b02b3e1e642197f27b2af54680834449abaf833619ac7d18afb50b19d5f6944dca0dc952edfdd9837573783c339ee6a36353ce6e536eaaf29fcd569c426091d4e24568dc353347f98c74fb6f8c91d68d358467c437563f66566377fe6c3f9e8301dbeb5fc7e7adee7a85ef5f8fa905cedbaf26601e21ba91646cac4034601e51d889d49739ee6990943a6a41927660f68e1f50b9f9209ee29551a7dae478d88e0547eefc83334ea770bb6fbac620fc47479c2c59389622bf32f55e36a75e56a5fc47c38bf8ef211fc0e8084016313218402af50e883fc53b78468b5ea9b974275696c6465724e657420284e65746865726d696e6429a0580ca94e91c0e7aef26ffb0c86f6ae48ef40df6dd1629f203a1930e0ce0be9d188000000000000000084479c1e2aa00345740e1b79edb2fbb3a20220e1a497ea9bb82aaba7dc7a881f7f3cae8a8ea38080a06675ad2a40134499a753924a04b75898ae09efc6fba6b3d7a506203042cb7611a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + // keccak256(_ETH_BLOCK_23270177) + bytes32 private constant _ETH_BLOCK_HASH_23270177 = + 0x5def79dc43d588fafa396f3fbf0bcfb9bf83eaf8003f4508a626b6d3e806b29f; + function testBlockHash( uint256 simulationBlockNumber, uint256 queryBlockNumber, @@ -57,4 +87,91 @@ contract BlockHashLibTest is SoladyTest { result := mload(0x00) } } + + function testVerifyBlock() public { + vm.roll(23270177 + 1); + vm.setBlockhash(23270177, _ETH_BLOCK_HASH_23270177); + assertEq(this.verifyBlock(_ETH_BLOCK_23270177, 23270177), _ETH_BLOCK_HASH_23270177); + } + + function verifyBlock(bytes calldata blockHeader, uint256 blockNumber) + public + view + returns (bytes32) + { + return BlockHashLib.verifyBlock(blockHeader, blockNumber); + } + + function _randomBlockHeader() internal returns (BlockHeader memory b, bytes memory encoded) { + if (_randomChance(2)) { + b.parentHash = bytes32(_random()); + b.ommersHash = bytes32(_random()); + b.beneficiary = bytes20(bytes32(_random())); + b.stateRoot = bytes32(_random()); + b.transactionsRoot = bytes32(_random()); + b.receiptsRoot = bytes32(_random()); + } + if (_randomChance(2)) { + for (uint256 i; i < 8; ++i) { + b.logsBloom[i] = bytes32(_random()); + } + } + if (_randomChance(2)) { + b.difficultyOrPrevrandao = bytes32(_random()); + b.number = _random(); + b.gasLimit = _random(); + b.gasUsed = _random(); + b.timestamp = _random(); + b.extraData = _truncateBytes(_randomBytes(), 32); + b.mixHash = bytes32(_random()); + b.nonce = bytes8(bytes32(_random())); + } + + LibRLP.List memory l; + l.p(abi.encodePacked(b.parentHash)); + l.p(abi.encodePacked(b.ommersHash)); + l.p(abi.encodePacked(b.beneficiary)); + l.p(abi.encodePacked(b.stateRoot)); + l.p(abi.encodePacked(b.transactionsRoot)); + l.p(abi.encodePacked(b.receiptsRoot)); + l.p(abi.encodePacked(b.logsBloom)); + l.p(abi.encodePacked(b.difficultyOrPrevrandao)); + l.p(b.number); + l.p(b.gasLimit); + l.p(b.gasUsed); + l.p(b.timestamp); + l.p(b.extraData); + l.p(abi.encodePacked(b.mixHash)); + l.p(abi.encodePacked(b.nonce)); + encoded = l.encode(); + } + + function testToShortHeader(bytes32) public { + (BlockHeader memory b, bytes memory encoded) = _randomBlockHeader(); + BlockHashLib.ShortHeader memory s = + this.toShortHeader(_truncateBytes(_randomBytes(), 128), encoded); + assertEq(s.parentHash, b.parentHash); + assertEq(s.stateRoot, b.stateRoot); + assertEq(s.transactionsRoot, b.transactionsRoot); + assertEq(s.receiptsRoot, b.receiptsRoot); + for (uint256 i; i < 8; ++i) { + assertEq(s.logsBloom[i], b.logsBloom[i]); + } + } + + function toShortHeader(bytes calldata, bytes calldata encodedHeader) + public + view + returns (BlockHashLib.ShortHeader memory result) + { + _misalignFreeMemoryPointer(); + _brutalizeMemory(); + result = BlockHashLib.toShortHeader(encodedHeader); + _checkMemory(); + } + + function testRandomBlockHeader(bytes32) public { + (, bytes memory encoded) = _randomBlockHeader(); + assertEq(uint8(bytes1(encoded[0])), 0xf9); + } }