From f7e3cab77629757a957a6045ff3b46abf1bae751 Mon Sep 17 00:00:00 2001 From: "clandestine.eth" <96172957+0xClandestine@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:02:40 -0400 Subject: [PATCH] wip --- src/StdRlp.sol | 112 ++++++++++++++++++++++++++++++++++++++++++++++ src/Test.sol | 1 + src/Vm.sol | 88 ++++++++++++++++++------------------ test/StdRlp.t.sol | 70 +++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 45 deletions(-) create mode 100644 src/StdRlp.sol create mode 100644 test/StdRlp.t.sol diff --git a/src/StdRlp.sol b/src/StdRlp.sol new file mode 100644 index 000000000..5c2b452bd --- /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 11b18f29f..0b3e28f03 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 cd883706a..375984cd1 100644 --- a/src/Vm.sol +++ b/src/Vm.sol @@ -372,10 +372,12 @@ interface VmSafe { /// Derive a private key from a provided mnemonic string (or mnemonic file path) in the specified language /// at `{derivationPath}{index}`. - function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index, string calldata language) - external - pure - returns (uint256 privateKey); + function deriveKey( + string calldata mnemonic, + string calldata derivationPath, + uint32 index, + string calldata language + ) external pure returns (uint256 privateKey); /// Derives secp256r1 public key from the provided `privateKey`. function publicKeyP256(uint256 privateKey) external pure returns (uint256 publicKeyX, uint256 publicKeyY); @@ -430,6 +432,13 @@ interface VmSafe { /// Signs `digest` with `privateKey` using the secp256r1 curve. function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s); + /// Signs `digest` with `privateKey` on the secp256k1 curve, using the given `nonce` + /// as the raw ephemeral k value in ECDSA (instead of deriving it deterministically). + function signWithNonceUnsafe(uint256 privateKey, bytes32 digest, uint256 nonce) + external + pure + returns (uint8 v, bytes32 r, bytes32 s); + /// Signs data with a `Wallet`. function sign(Wallet calldata wallet, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); @@ -681,6 +690,12 @@ interface VmSafe { /// Returns an array of `StorageAccess` from current `vm.stateStateDiffRecording` session function getStorageAccesses() external view returns (StorageAccess[] memory storageAccesses); + /// Returns an array of storage slots occupied by the specified variable. + function getStorageSlots(address target, string calldata variableName) + external + view + returns (uint256[] memory slots); + /// Gets the gas used in the last call from the callee perspective. function lastCallGas() external view returns (Gas memory gas); @@ -850,10 +865,7 @@ interface VmSafe { function getDeployment(string calldata contractName) external view returns (address deployedAddress); /// Returns the most recent deployment for the given contract on `chainId` - function getDeployment(string calldata contractName, uint64 chainId) - external - view - returns (address deployedAddress); + function getDeployment(string calldata contractName, uint64 chainId) external view returns (address deployedAddress); /// Returns all deployments for the given contract on `chainId` /// Sorted in descending order of deployment time i.e descending order of BroadcastTxSummary.blockNumber. @@ -960,10 +972,7 @@ interface VmSafe { function parseJsonAddress(string calldata json, string calldata key) external pure returns (address); /// Parses a string of JSON data at `key` and coerces it to `address[]`. - function parseJsonAddressArray(string calldata json, string calldata key) - external - pure - returns (address[] memory); + function parseJsonAddressArray(string calldata json, string calldata key) external pure returns (address[] memory); /// Parses a string of JSON data at `key` and coerces it to `bool`. function parseJsonBool(string calldata json, string calldata key) external pure returns (bool); @@ -978,10 +987,7 @@ interface VmSafe { function parseJsonBytes32(string calldata json, string calldata key) external pure returns (bytes32); /// Parses a string of JSON data at `key` and coerces it to `bytes32[]`. - function parseJsonBytes32Array(string calldata json, string calldata key) - external - pure - returns (bytes32[] memory); + function parseJsonBytes32Array(string calldata json, string calldata key) external pure returns (bytes32[] memory); /// Parses a string of JSON data at `key` and coerces it to `bytes[]`. function parseJsonBytesArray(string calldata json, string calldata key) external pure returns (bytes[] memory); @@ -1008,10 +1014,7 @@ interface VmSafe { returns (bytes memory); /// Parses a string of JSON data and coerces it to type corresponding to `typeDescription`. - function parseJsonType(string calldata json, string calldata typeDescription) - external - pure - returns (bytes memory); + function parseJsonType(string calldata json, string calldata typeDescription) external pure returns (bytes memory); /// Parses a string of JSON data at `key` and coerces it to type corresponding to `typeDescription`. function parseJsonType(string calldata json, string calldata key, string calldata typeDescription) @@ -1378,9 +1381,7 @@ interface VmSafe { /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% /// Includes error message into revert string on failure. - function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) - external - pure; + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure; /// Asserts that two `uint256` values are equal, formatting them with decimals in failure message. function assertEqDecimal(uint256 left, uint256 right, uint256 decimals) external pure; @@ -1779,10 +1780,7 @@ interface VmSafe { function parseTomlAddress(string calldata toml, string calldata key) external pure returns (address); /// Parses a string of TOML data at `key` and coerces it to `address[]`. - function parseTomlAddressArray(string calldata toml, string calldata key) - external - pure - returns (address[] memory); + function parseTomlAddressArray(string calldata toml, string calldata key) external pure returns (address[] memory); /// Parses a string of TOML data at `key` and coerces it to `bool`. function parseTomlBool(string calldata toml, string calldata key) external pure returns (bool); @@ -1797,10 +1795,7 @@ interface VmSafe { function parseTomlBytes32(string calldata toml, string calldata key) external pure returns (bytes32); /// Parses a string of TOML data at `key` and coerces it to `bytes32[]`. - function parseTomlBytes32Array(string calldata toml, string calldata key) - external - pure - returns (bytes32[] memory); + function parseTomlBytes32Array(string calldata toml, string calldata key) external pure returns (bytes32[] memory); /// Parses a string of TOML data at `key` and coerces it to `bytes[]`. function parseTomlBytesArray(string calldata toml, string calldata key) external pure returns (bytes[] memory); @@ -1827,10 +1822,7 @@ interface VmSafe { returns (bytes memory); /// Parses a string of TOML data and coerces it to type corresponding to `typeDescription`. - function parseTomlType(string calldata toml, string calldata typeDescription) - external - pure - returns (bytes memory); + function parseTomlType(string calldata toml, string calldata typeDescription) external pure returns (bytes memory); /// Parses a string of TOML data at `key` and coerces it to type corresponding to `typeDescription`. function parseTomlType(string calldata toml, string calldata key, string calldata typeDescription) @@ -1867,10 +1859,7 @@ interface VmSafe { function bound(int256 current, int256 min, int256 max) external view returns (int256); /// Compute the address of a contract created with CREATE2 using the given CREATE2 deployer. - function computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer) - external - pure - returns (address); + function computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer) external pure returns (address); /// Compute the address of a contract created with CREATE2 using the default CREATE2 deployer. function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) external pure returns (address); @@ -1931,6 +1920,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); @@ -2001,6 +1993,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 @@ -2117,8 +2112,7 @@ interface Vm is VmSafe { function mockCallRevert(address callee, bytes calldata data, bytes calldata revertData) external; /// Reverts a call to an address with a specific `msg.value`, with specified revert data. - function mockCallRevert(address callee, uint256 msgValue, bytes calldata data, bytes calldata revertData) - external; + function mockCallRevert(address callee, uint256 msgValue, bytes calldata data, bytes calldata revertData) external; /// Reverts a call to an address with specified revert data. /// Overload to pass the function selector directly `token.approve.selector` instead of `abi.encodeWithSelector(token.approve.selector)`. @@ -2364,8 +2358,13 @@ interface Vm is VmSafe { /// Prepare an expected anonymous log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.). /// Call this function, then emit an anonymous event, then call a function. Internally after the call, we check if /// logs were emitted in the expected order with the expected topics and data (as specified by the booleans). - function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) - external; + function expectEmitAnonymous( + bool checkTopic0, + bool checkTopic1, + bool checkTopic2, + bool checkTopic3, + bool checkData + ) external; /// Same as the previous method, but also checks supplied address against emitting contract. function expectEmitAnonymous( @@ -2391,8 +2390,7 @@ interface Vm is VmSafe { function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external; /// Same as the previous method, but also checks supplied address against emitting contract. - function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter) - external; + function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter) external; /// Prepare an expected log with all topic and data checks enabled. /// Call this function, then emit an event, then call a function. Internally after the call, we check if diff --git a/test/StdRlp.t.sol b/test/StdRlp.t.sol new file mode 100644 index 000000000..0b353f2d9 --- /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"); + } +}