Skip to content

Commit 8cbab81

Browse files
committed
wip
1 parent a6d71da commit 8cbab81

File tree

4 files changed

+189
-0
lines changed

4 files changed

+189
-0
lines changed

src/StdRlp.sol

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.6.0 <0.9.0;
3+
4+
pragma experimental ABIEncoderV2;
5+
6+
import {VmSafe} from "./Vm.sol";
7+
8+
// TODO: Account Fields (CodeHash, Balance, Nonce, StorageRoot).
9+
10+
/// @notice A general-purpose library for working with RLP (Recursive Length Prefix)
11+
/// encoded data in Ethereum.
12+
///
13+
/// @dev This library provides utilities for decoding and working with RLP-encoded
14+
/// data structures. Currently focused on block header parsing, but designed
15+
/// to be extensible for other RLP use cases.
16+
///
17+
/// Block header usage:
18+
/// ```solidity
19+
/// import {stdRlp} from "forge-std/StdRlp.sol";
20+
///
21+
/// stdRlp.BlockHeader memory header = stdRlp.getBlockHeader(block.number);
22+
/// console.log("stateRoot:", header.stateRoot);
23+
///
24+
/// bytes memory rawHeader = vm.getRawBlockHeader(block.number);
25+
/// stdRlp.BlockHeader memory header = stdRlp.toBlockHeader(rawHeader);
26+
/// console.log("stateRoot:", header.stateRoot);
27+
/// ```
28+
library stdRlp {
29+
VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code")))));
30+
31+
/// @notice Represents a parsed Ethereum block header with all standard fields.
32+
/// @dev Contains all fields from modern Ethereum block headers, including
33+
/// post-merge (baseFeePerGas), post-Shapella (withdrawalsRoot), post-Cancun
34+
/// (blobGasUsed, excessBlobGas, parentBeaconRoot), and post-Dencun (requestsHash) fields.
35+
struct BlockHeader {
36+
bytes32 hash;
37+
bytes32 parentHash;
38+
bytes32 ommersHash;
39+
address beneficiary;
40+
bytes32 stateRoot;
41+
bytes32 transactionsRoot;
42+
bytes32 receiptsRoot;
43+
bytes logsBloom;
44+
uint256 difficulty;
45+
uint256 number;
46+
uint256 gasLimit;
47+
uint256 gasUsed;
48+
uint256 timestamp;
49+
bytes extraData;
50+
bytes32 mixHash;
51+
uint256 nonce;
52+
uint256 baseFeePerGas;
53+
bytes32 withdrawalsRoot;
54+
uint256 blobGasUsed;
55+
uint256 excessBlobGas;
56+
bytes32 parentBeaconRoot;
57+
bytes32 requestsHash;
58+
}
59+
60+
/// @notice Parses a raw RLP-encoded block header into a structured `BlockHeader`.
61+
/// @dev Uses the Foundry cheatcode `vm.fromRlp` to decode the RLP structure.
62+
/// The block hash is computed as the keccak256 of the raw header bytes.
63+
/// Fields are extracted in the order defined by the Ethereum specification.
64+
/// @param rawBlockHeader The RLP-encoded block header bytes.
65+
/// @return blockHeader The parsed block header with all fields populated.
66+
function toBlockHeader(bytes memory rawBlockHeader) internal pure returns (BlockHeader memory blockHeader) {
67+
bytes[] memory fields = vm.fromRlp(rawBlockHeader);
68+
69+
blockHeader.hash = keccak256(rawBlockHeader);
70+
blockHeader.parentHash = bytes32(fields[0]);
71+
blockHeader.ommersHash = bytes32(fields[1]);
72+
blockHeader.beneficiary = address(bytes20(fields[2]));
73+
blockHeader.stateRoot = bytes32(fields[3]);
74+
blockHeader.transactionsRoot = bytes32(fields[4]);
75+
blockHeader.receiptsRoot = bytes32(fields[5]);
76+
blockHeader.logsBloom = fields[6];
77+
blockHeader.difficulty = _toUint(fields[7]);
78+
blockHeader.number = _toUint(fields[8]);
79+
blockHeader.gasLimit = _toUint(fields[9]);
80+
blockHeader.gasUsed = _toUint(fields[10]);
81+
blockHeader.timestamp = _toUint(fields[11]);
82+
blockHeader.extraData = fields[12];
83+
blockHeader.mixHash = bytes32(fields[13]);
84+
blockHeader.nonce = _toUint(fields[14]);
85+
blockHeader.baseFeePerGas = _toUint(fields[15]);
86+
blockHeader.withdrawalsRoot = bytes32(fields[16]);
87+
blockHeader.blobGasUsed = _toUint(fields[17]);
88+
blockHeader.excessBlobGas = _toUint(fields[18]);
89+
blockHeader.parentBeaconRoot = bytes32(fields[19]);
90+
blockHeader.requestsHash = bytes32(fields[20]);
91+
92+
return blockHeader;
93+
}
94+
95+
/// @notice Fetches and parses the block header for a specific block number.
96+
/// @dev Combines `vm.getRawBlockHeader` with `toBlockHeader` for convenience.
97+
/// This is a view function because it reads blockchain state via the vm cheatcode.
98+
/// @param blockNumber The block number to fetch the header for.
99+
/// @return blockHeader The parsed block header with all fields populated.
100+
function getBlockHeader(uint256 blockNumber) internal view returns (BlockHeader memory blockHeader) {
101+
return toBlockHeader(vm.getRawBlockHeader(blockNumber));
102+
}
103+
104+
/// @dev Internal helper to convert variable-length bytes to uint256.
105+
/// Handles RLP-encoded integers by treating the bytes as big-endian
106+
/// and right-shifting to account for shorter lengths.
107+
function _toUint(bytes memory b) internal pure returns (uint256 r) {
108+
unchecked {
109+
return uint256(bytes32(b)) >> (8 * (32 - b.length));
110+
}
111+
}
112+
}

src/Test.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {stdError} from "./StdError.sol";
1818
import {StdInvariant} from "./StdInvariant.sol";
1919
import {stdJson} from "./StdJson.sol";
2020
import {stdMath} from "./StdMath.sol";
21+
import {stdRlp} from "./StdRlp.sol";
2122
import {StdStorage, stdStorage} from "./StdStorage.sol";
2223
import {StdStyle} from "./StdStyle.sol";
2324
import {stdToml} from "./StdToml.sol";

src/Vm.sol

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/StdRlp.t.sol

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.7.0 <0.9.0;
3+
4+
import {Test, stdRlp} from "../src/Test.sol";
5+
6+
contract StdRlpTest is Test {
7+
// Block 23513000 RLP header from Ethereum mainnet (October 5, 2025)
8+
// cast block 23513000 --raw
9+
// vm.getRawBlockHeader(23513000)
10+
bytes constant BLOCK_23513000_RAW =
11+
hex"f90286a05b5b6bff48fe6a2167a2c70193bd1ec94e140ad09ebca6d64f468bc273518d36a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347944838b106fce9647bdf1e7877bf73ce8b0bad5f97a09099fb0c9d079059756770f425953cbc7965cf14a6f99e2007ceb5fc0c86f695a00ed75b9475c7d88f3656ae833b9af1249efe1d51234ae9c318e92fb9bbc58cefa05a70f4c705bec8d642aef9d9fa593a9292a2b70e85fe303cc20f407af4814be4b901009ff9dbe477c99f97b7556c0ffaf95a41bbeb73bdee22fd307bf7739ae6d7bf37eb3defd7f3ff78d772d5d7b5fc3fb5dddfd9d3f8fe85aeffbfa92cc29aaff5b7e5edf80e6e4addfc7cdbf1ce895bbd71d71d9a756a74cdbcbf7ddca5cbf306d9fedf3b278a7f13ef3545ab52fecd7cfb4eefef63f8e8c6e5a1b8ee57874ff67f0f5beeffd7bc503f6379c76f2ff6a4fff17d7fddbbff336ef42fd1d43ad3bddee246af87a6bdfe977ffb35f57a5dfdfca566d5e5d8f7fc9cba4f4531e666f5f0f3d7bcaf65b7e7708dcdf679afe6b4add57f6f4a3e397efb9cdf9f77f0f7fbcf3bb3bc880b2b71c0b4eee1dee95f72e5ffcd9eefeffc37fecd84ba435ebdf70d80840166c7a88402aea4ea84014204dd8468e2a7cf98546974616e2028746974616e6275696c6465722e78797a29a0542df7cb12be5ee5281247c44fbef1a534899cacf6f4a7cbb005e451dd416de68800000000000000008407e356e5a0740234ba028832cd35e1e2923133d136e54fbded84e1e1f1f21747508c8edb92830c00008404100000a030297d32b4bb781dea1b6861b8b15db62f4428d1ae766615b4c29cd515df3a9da0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
12+
13+
stdRlp.BlockHeader expectedHeader;
14+
15+
function setUp() public {
16+
expectedHeader = stdRlp.BlockHeader({
17+
hash: 0x65407618ec1f44bd793f024f9ce855d7287ea4a9d7ae4c9e672362a372b9ded4,
18+
parentHash: 0x5b5b6bff48fe6a2167a2c70193bd1ec94e140ad09ebca6d64f468bc273518d36,
19+
ommersHash: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347,
20+
beneficiary: 0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97,
21+
stateRoot: 0x9099fb0c9d079059756770f425953cbc7965cf14a6f99e2007ceb5fc0c86f695,
22+
transactionsRoot: 0x0ed75b9475c7d88f3656ae833b9af1249efe1d51234ae9c318e92fb9bbc58cef,
23+
receiptsRoot: 0x5a70f4c705bec8d642aef9d9fa593a9292a2b70e85fe303cc20f407af4814be4,
24+
logsBloom: hex"009ff9dbe477c99f97b7556c0ffaf95a41bbeb73bdee22fd307bf7739ae6d7bf37eb3defd7f3ff78d772d5d7b5fc3fb5dddfd9d3f8fe85aeffbfa92cc29aaff5b7e5edf80e6e4addfc7cdbf1ce895bbd71d71d9a756a74cdbcbf7ddca5cbf306d9fedf3b278a7f13ef3545ab52fecd7cfb4eefef63f8e8c6e5a1b8ee57874ff67f0f5beeffd7bc503f6379c76f2ff6a4fff17d7fddbbff336ef42fd1d43ad3bddee246af87a6bdfe977ffb35f57a5dfdfca566d5e5d8f7fc9cba4f4531e666f5f0f3d7bcaf65b7e7708dcdf679afe6b4add57f6f4a3e397efb9cdf9f77f0f7fbcf3bb3bc880b2b71c0b4eee1dee95f72e5ffcd9eefeffc37fecd84ba435ebdf70d",
25+
difficulty: 0,
26+
number: 23513000,
27+
gasLimit: 44999914,
28+
gasUsed: 21103837,
29+
timestamp: 1759684559,
30+
extraData: hex"546974616e2028746974616e6275696c6465722e78797a29",
31+
mixHash: 0x542df7cb12be5ee5281247c44fbef1a534899cacf6f4a7cbb005e451dd416de6,
32+
nonce: 0,
33+
baseFeePerGas: 132339429,
34+
withdrawalsRoot: 0x740234ba028832cd35e1e2923133d136e54fbded84e1e1f1f21747508c8edb92,
35+
blobGasUsed: 786432,
36+
excessBlobGas: 68157440,
37+
parentBeaconRoot: 0x30297d32b4bb781dea1b6861b8b15db62f4428d1ae766615b4c29cd515df3a9d,
38+
requestsHash: 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
39+
});
40+
}
41+
42+
function test_GetBlockHeader_BasicFields() public view {
43+
stdRlp.BlockHeader memory header = stdRlp.toBlockHeader(BLOCK_23513000_RAW);
44+
45+
// Verify hash
46+
assertEq(header.hash, keccak256(BLOCK_23513000_RAW), "Hash mismatch");
47+
48+
// Verify basic fields
49+
assertEq(header.parentHash, expectedHeader.parentHash, "Parent hash mismatch");
50+
assertEq(header.ommersHash, expectedHeader.ommersHash, "Ommers hash mismatch");
51+
assertEq(header.beneficiary, expectedHeader.beneficiary, "Beneficiary mismatch");
52+
assertEq(header.stateRoot, expectedHeader.stateRoot, "State root mismatch");
53+
assertEq(header.transactionsRoot, expectedHeader.transactionsRoot, "Transactions root mismatch");
54+
assertEq(header.receiptsRoot, expectedHeader.receiptsRoot, "Receipts root mismatch");
55+
assertEq(header.difficulty, expectedHeader.difficulty, "Difficulty mismatch");
56+
assertEq(header.number, expectedHeader.number, "Number mismatch");
57+
assertEq(header.gasLimit, expectedHeader.gasLimit, "Gas limit mismatch");
58+
assertEq(header.gasUsed, expectedHeader.gasUsed, "Gas used mismatch");
59+
assertEq(header.timestamp, expectedHeader.timestamp, "Timestamp mismatch");
60+
assertEq(header.extraData, expectedHeader.extraData, "Extra data mismatch");
61+
assertEq(header.mixHash, expectedHeader.mixHash, "Mix hash mismatch");
62+
assertEq(header.nonce, expectedHeader.nonce, "Nonce mismatch");
63+
assertEq(header.baseFeePerGas, expectedHeader.baseFeePerGas, "Base fee per gas mismatch");
64+
assertEq(header.withdrawalsRoot, expectedHeader.withdrawalsRoot, "Withdrawals root mismatch");
65+
assertEq(header.blobGasUsed, expectedHeader.blobGasUsed, "Blob gas used mismatch");
66+
assertEq(header.excessBlobGas, expectedHeader.excessBlobGas, "Excess blob gas mismatch");
67+
assertEq(header.parentBeaconRoot, expectedHeader.parentBeaconRoot, "Parent beacon root mismatch");
68+
assertEq(header.requestsHash, expectedHeader.requestsHash, "Requests hash mismatch");
69+
}
70+
}

0 commit comments

Comments
 (0)