diff --git a/src/StdRlp.sol b/src/StdRlp.sol new file mode 100644 index 00000000..5c2b452b --- /dev/null +++ b/src/StdRlp.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0 <0.9.0; + +pragma experimental ABIEncoderV2; + +import {VmSafe} from "./Vm.sol"; + +// TODO: Account Fields (CodeHash, Balance, Nonce, StorageRoot). + +/// @notice A general-purpose library for working with RLP (Recursive Length Prefix) +/// encoded data in Ethereum. +/// +/// @dev This library provides utilities for decoding and working with RLP-encoded +/// data structures. Currently focused on block header parsing, but designed +/// to be extensible for other RLP use cases. +/// +/// Block header usage: +/// ```solidity +/// import {stdRlp} from "forge-std/StdRlp.sol"; +/// +/// stdRlp.BlockHeader memory header = stdRlp.getBlockHeader(block.number); +/// console.log("stateRoot:", header.stateRoot); +/// +/// bytes memory rawHeader = vm.getRawBlockHeader(block.number); +/// stdRlp.BlockHeader memory header = stdRlp.toBlockHeader(rawHeader); +/// console.log("stateRoot:", header.stateRoot); +/// ``` +library stdRlp { + VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code"))))); + + /// @notice Represents a parsed Ethereum block header with all standard fields. + /// @dev Contains all fields from modern Ethereum block headers, including + /// post-merge (baseFeePerGas), post-Shapella (withdrawalsRoot), post-Cancun + /// (blobGasUsed, excessBlobGas, parentBeaconRoot), and post-Dencun (requestsHash) fields. + struct BlockHeader { + bytes32 hash; + bytes32 parentHash; + bytes32 ommersHash; + address beneficiary; + bytes32 stateRoot; + bytes32 transactionsRoot; + bytes32 receiptsRoot; + bytes logsBloom; + uint256 difficulty; + uint256 number; + uint256 gasLimit; + uint256 gasUsed; + uint256 timestamp; + bytes extraData; + bytes32 mixHash; + uint256 nonce; + uint256 baseFeePerGas; + bytes32 withdrawalsRoot; + uint256 blobGasUsed; + uint256 excessBlobGas; + bytes32 parentBeaconRoot; + bytes32 requestsHash; + } + + /// @notice Parses a raw RLP-encoded block header into a structured `BlockHeader`. + /// @dev Uses the Foundry cheatcode `vm.fromRlp` to decode the RLP structure. + /// The block hash is computed as the keccak256 of the raw header bytes. + /// Fields are extracted in the order defined by the Ethereum specification. + /// @param rawBlockHeader The RLP-encoded block header bytes. + /// @return blockHeader The parsed block header with all fields populated. + function toBlockHeader(bytes memory rawBlockHeader) internal pure returns (BlockHeader memory blockHeader) { + bytes[] memory fields = vm.fromRlp(rawBlockHeader); + + blockHeader.hash = keccak256(rawBlockHeader); + blockHeader.parentHash = bytes32(fields[0]); + blockHeader.ommersHash = bytes32(fields[1]); + blockHeader.beneficiary = address(bytes20(fields[2])); + blockHeader.stateRoot = bytes32(fields[3]); + blockHeader.transactionsRoot = bytes32(fields[4]); + blockHeader.receiptsRoot = bytes32(fields[5]); + blockHeader.logsBloom = fields[6]; + blockHeader.difficulty = _toUint(fields[7]); + blockHeader.number = _toUint(fields[8]); + blockHeader.gasLimit = _toUint(fields[9]); + blockHeader.gasUsed = _toUint(fields[10]); + blockHeader.timestamp = _toUint(fields[11]); + blockHeader.extraData = fields[12]; + blockHeader.mixHash = bytes32(fields[13]); + blockHeader.nonce = _toUint(fields[14]); + blockHeader.baseFeePerGas = _toUint(fields[15]); + blockHeader.withdrawalsRoot = bytes32(fields[16]); + blockHeader.blobGasUsed = _toUint(fields[17]); + blockHeader.excessBlobGas = _toUint(fields[18]); + blockHeader.parentBeaconRoot = bytes32(fields[19]); + blockHeader.requestsHash = bytes32(fields[20]); + + return blockHeader; + } + + /// @notice Fetches and parses the block header for a specific block number. + /// @dev Combines `vm.getRawBlockHeader` with `toBlockHeader` for convenience. + /// This is a view function because it reads blockchain state via the vm cheatcode. + /// @param blockNumber The block number to fetch the header for. + /// @return blockHeader The parsed block header with all fields populated. + function getBlockHeader(uint256 blockNumber) internal view returns (BlockHeader memory blockHeader) { + return toBlockHeader(vm.getRawBlockHeader(blockNumber)); + } + + /// @dev Internal helper to convert variable-length bytes to uint256. + /// Handles RLP-encoded integers by treating the bytes as big-endian + /// and right-shifting to account for shorter lengths. + function _toUint(bytes memory b) internal pure returns (uint256 r) { + unchecked { + return uint256(bytes32(b)) >> (8 * (32 - b.length)); + } + } +} diff --git a/src/Test.sol b/src/Test.sol index 11b18f29..0b3e28f0 100644 --- a/src/Test.sol +++ b/src/Test.sol @@ -18,6 +18,7 @@ import {stdError} from "./StdError.sol"; import {StdInvariant} from "./StdInvariant.sol"; import {stdJson} from "./StdJson.sol"; import {stdMath} from "./StdMath.sol"; +import {stdRlp} from "./StdRlp.sol"; import {StdStorage, stdStorage} from "./StdStorage.sol"; import {StdStyle} from "./StdStyle.sol"; import {stdToml} from "./StdToml.sol"; diff --git a/src/Vm.sol b/src/Vm.sol index 20f91a14..853a2a20 100644 --- a/src/Vm.sol +++ b/src/Vm.sol @@ -1928,6 +1928,9 @@ interface VmSafe { /// Returns ENS namehash for provided string. function ensNamehash(string calldata name) external pure returns (bytes32); + /// RLP decodes an RLP payload into a list of bytes. + function fromRlp(bytes calldata rlp) external pure returns (bytes[] memory data); + /// Gets the label for the specified address. function getLabel(address account) external view returns (string memory currentLabel); @@ -1998,6 +2001,9 @@ interface VmSafe { /// Encodes a `string` value to a base64 string. function toBase64(string calldata data) external pure returns (string memory); + + /// RLP encodes a list of bytes into an RLP payload. + function toRlp(bytes[] calldata data) external pure returns (bytes memory); } /// The `Vm` interface does allow manipulation of the EVM state. These are all intended to be used diff --git a/test/StdRlp.t.sol b/test/StdRlp.t.sol new file mode 100644 index 00000000..0b353f2d --- /dev/null +++ b/test/StdRlp.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.0 <0.9.0; + +import {Test, stdRlp} from "../src/Test.sol"; + +contract StdRlpTest is Test { + // Block 23513000 RLP header from Ethereum mainnet (October 5, 2025) + // cast block 23513000 --raw + // vm.getRawBlockHeader(23513000) + bytes constant BLOCK_23513000_RAW = + hex"f90286a05b5b6bff48fe6a2167a2c70193bd1ec94e140ad09ebca6d64f468bc273518d36a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944838b106fce9647bdf1e7877bf73ce8b0bad5f97a09099fb0c9d079059756770f425953cbc7965cf14a6f99e2007ceb5fc0c86f695a00ed75b9475c7d88f3656ae833b9af1249efe1d51234ae9c318e92fb9bbc58cefa05a70f4c705bec8d642aef9d9fa593a9292a2b70e85fe303cc20f407af4814be4b901009ff9dbe477c99f97b7556c0ffaf95a41bbeb73bdee22fd307bf7739ae6d7bf37eb3defd7f3ff78d772d5d7b5fc3fb5dddfd9d3f8fe85aeffbfa92cc29aaff5b7e5edf80e6e4addfc7cdbf1ce895bbd71d71d9a756a74cdbcbf7ddca5cbf306d9fedf3b278a7f13ef3545ab52fecd7cfb4eefef63f8e8c6e5a1b8ee57874ff67f0f5beeffd7bc503f6379c76f2ff6a4fff17d7fddbbff336ef42fd1d43ad3bddee246af87a6bdfe977ffb35f57a5dfdfca566d5e5d8f7fc9cba4f4531e666f5f0f3d7bcaf65b7e7708dcdf679afe6b4add57f6f4a3e397efb9cdf9f77f0f7fbcf3bb3bc880b2b71c0b4eee1dee95f72e5ffcd9eefeffc37fecd84ba435ebdf70d80840166c7a88402aea4ea84014204dd8468e2a7cf98546974616e2028746974616e6275696c6465722e78797a29a0542df7cb12be5ee5281247c44fbef1a534899cacf6f4a7cbb005e451dd416de68800000000000000008407e356e5a0740234ba028832cd35e1e2923133d136e54fbded84e1e1f1f21747508c8edb92830c00008404100000a030297d32b4bb781dea1b6861b8b15db62f4428d1ae766615b4c29cd515df3a9da0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + stdRlp.BlockHeader expectedHeader; + + function setUp() public { + expectedHeader = stdRlp.BlockHeader({ + hash: 0x65407618ec1f44bd793f024f9ce855d7287ea4a9d7ae4c9e672362a372b9ded4, + parentHash: 0x5b5b6bff48fe6a2167a2c70193bd1ec94e140ad09ebca6d64f468bc273518d36, + ommersHash: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, + beneficiary: 0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97, + stateRoot: 0x9099fb0c9d079059756770f425953cbc7965cf14a6f99e2007ceb5fc0c86f695, + transactionsRoot: 0x0ed75b9475c7d88f3656ae833b9af1249efe1d51234ae9c318e92fb9bbc58cef, + receiptsRoot: 0x5a70f4c705bec8d642aef9d9fa593a9292a2b70e85fe303cc20f407af4814be4, + logsBloom: hex"009ff9dbe477c99f97b7556c0ffaf95a41bbeb73bdee22fd307bf7739ae6d7bf37eb3defd7f3ff78d772d5d7b5fc3fb5dddfd9d3f8fe85aeffbfa92cc29aaff5b7e5edf80e6e4addfc7cdbf1ce895bbd71d71d9a756a74cdbcbf7ddca5cbf306d9fedf3b278a7f13ef3545ab52fecd7cfb4eefef63f8e8c6e5a1b8ee57874ff67f0f5beeffd7bc503f6379c76f2ff6a4fff17d7fddbbff336ef42fd1d43ad3bddee246af87a6bdfe977ffb35f57a5dfdfca566d5e5d8f7fc9cba4f4531e666f5f0f3d7bcaf65b7e7708dcdf679afe6b4add57f6f4a3e397efb9cdf9f77f0f7fbcf3bb3bc880b2b71c0b4eee1dee95f72e5ffcd9eefeffc37fecd84ba435ebdf70d", + difficulty: 0, + number: 23513000, + gasLimit: 44999914, + gasUsed: 21103837, + timestamp: 1759684559, + extraData: hex"546974616e2028746974616e6275696c6465722e78797a29", + mixHash: 0x542df7cb12be5ee5281247c44fbef1a534899cacf6f4a7cbb005e451dd416de6, + nonce: 0, + baseFeePerGas: 132339429, + withdrawalsRoot: 0x740234ba028832cd35e1e2923133d136e54fbded84e1e1f1f21747508c8edb92, + blobGasUsed: 786432, + excessBlobGas: 68157440, + parentBeaconRoot: 0x30297d32b4bb781dea1b6861b8b15db62f4428d1ae766615b4c29cd515df3a9d, + requestsHash: 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + }); + } + + function test_GetBlockHeader_BasicFields() public view { + stdRlp.BlockHeader memory header = stdRlp.toBlockHeader(BLOCK_23513000_RAW); + + // Verify hash + assertEq(header.hash, keccak256(BLOCK_23513000_RAW), "Hash mismatch"); + + // Verify basic fields + assertEq(header.parentHash, expectedHeader.parentHash, "Parent hash mismatch"); + assertEq(header.ommersHash, expectedHeader.ommersHash, "Ommers hash mismatch"); + assertEq(header.beneficiary, expectedHeader.beneficiary, "Beneficiary mismatch"); + assertEq(header.stateRoot, expectedHeader.stateRoot, "State root mismatch"); + assertEq(header.transactionsRoot, expectedHeader.transactionsRoot, "Transactions root mismatch"); + assertEq(header.receiptsRoot, expectedHeader.receiptsRoot, "Receipts root mismatch"); + assertEq(header.difficulty, expectedHeader.difficulty, "Difficulty mismatch"); + assertEq(header.number, expectedHeader.number, "Number mismatch"); + assertEq(header.gasLimit, expectedHeader.gasLimit, "Gas limit mismatch"); + assertEq(header.gasUsed, expectedHeader.gasUsed, "Gas used mismatch"); + assertEq(header.timestamp, expectedHeader.timestamp, "Timestamp mismatch"); + assertEq(header.extraData, expectedHeader.extraData, "Extra data mismatch"); + assertEq(header.mixHash, expectedHeader.mixHash, "Mix hash mismatch"); + assertEq(header.nonce, expectedHeader.nonce, "Nonce mismatch"); + assertEq(header.baseFeePerGas, expectedHeader.baseFeePerGas, "Base fee per gas mismatch"); + assertEq(header.withdrawalsRoot, expectedHeader.withdrawalsRoot, "Withdrawals root mismatch"); + assertEq(header.blobGasUsed, expectedHeader.blobGasUsed, "Blob gas used mismatch"); + assertEq(header.excessBlobGas, expectedHeader.excessBlobGas, "Excess blob gas mismatch"); + assertEq(header.parentBeaconRoot, expectedHeader.parentBeaconRoot, "Parent beacon root mismatch"); + assertEq(header.requestsHash, expectedHeader.requestsHash, "Requests hash mismatch"); + } +}