Skip to content

Commit 8b96c25

Browse files
committed
Add BlockHeader library for parsing and verifying RLP-encoded block headers
1 parent 3daafbf commit 8b96c25

File tree

4 files changed

+271
-0
lines changed

4 files changed

+271
-0
lines changed

contracts/mocks/Stateless.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {Base58} from "../utils/Base58.sol";
1212
import {Base64} from "../utils/Base64.sol";
1313
import {BitMaps} from "../utils/structs/BitMaps.sol";
1414
import {Blockhash} from "../utils/Blockhash.sol";
15+
import {BlockHeader} from "../utils/BlockHeader.sol";
1516
import {Bytes} from "../utils/Bytes.sol";
1617
import {CAIP2} from "../utils/CAIP2.sol";
1718
import {CAIP10} from "../utils/CAIP10.sol";

contracts/utils/BlockHeader.sol

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {SafeCast} from "./math/SafeCast.sol";
6+
import {Blockhash} from "./Blockhash.sol";
7+
import {Memory} from "./Memory.sol";
8+
import {RLP} from "./RLP.sol";
9+
10+
/// @dev Library for parsing and verifying RLP-encoded block headers.
11+
library BlockHeader {
12+
using SafeCast for *;
13+
using RLP for *;
14+
15+
/// @dev List of evm versions.
16+
enum Hardforks {
17+
Homestead,
18+
DAO,
19+
TangerineWhistle,
20+
SpuriousDragon,
21+
Byzantium,
22+
Constantinople,
23+
Petersburg,
24+
Istanbul,
25+
MuirGlacier,
26+
Berlin,
27+
London,
28+
ArrowGlacier,
29+
GreyGlacier,
30+
Paris,
31+
Shanghai,
32+
Cancun,
33+
Prague,
34+
Osaka,
35+
Amsterdam
36+
}
37+
38+
/// @dev List of block header fields, in the order they are encoded in the block header RLP.
39+
enum HeaderField {
40+
ParentHash, // Since Homestead
41+
OmmersHash, // Since Homestead
42+
Coinbase, // Since Homestead
43+
StateRoot, // Since Homestead
44+
TransactionsRoot, // Since Homestead
45+
ReceiptsRoot, // Since Homestead
46+
LogsBloom, // Since Homestead
47+
Difficulty, // Since Homestead
48+
Number, // Since Homestead
49+
GasLimit, // Since Homestead
50+
GasUsed, // Since Homestead
51+
Timestamp, // Since Homestead
52+
ExtraData, // Since Homestead
53+
PrevRandao, // Since Homestead (called MixHash before Paris)
54+
Nonce, // Since Homestead
55+
BaseFeePerGas, // Since London
56+
WithdrawalsRoot, // Since Shanghai
57+
BlobGasUsed, // Since Cancun
58+
ExcessBlobGas, // Since Cancun
59+
ParentBeaconBlockRoot, // Since Cancun
60+
RequestsHash, // Since Prague
61+
BlockAccessListHash // Since Amsterdam
62+
}
63+
64+
/// @dev Thrown when the provided block header RLP does not have the expected number of fields for the given hardfork version.
65+
error InvalidBlockHeader(Hardforks expectedVersion);
66+
67+
/// @dev Verifies that the given block header RLP corresponds to a valid block header for the current chain.
68+
function verifyBlockHeader(bytes memory headerRLP) internal view returns (bool) {
69+
return Blockhash.blockHash(getNumber(headerRLP)) == keccak256(headerRLP);
70+
}
71+
72+
/// @dev Extract the parent hash from the block header RLP.
73+
function getParentHash(bytes memory headerRLP) internal pure returns (bytes32) {
74+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.ParentHash)].readBytes32();
75+
}
76+
77+
/// @dev Extract the ommers hash from the block header RLP. This is constant to keccak256(rlp([])) since EIP-3675 (Paris)
78+
function getOmmersHash(bytes memory headerRLP) internal pure returns (bytes32) {
79+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.OmmersHash)].readBytes32();
80+
}
81+
82+
/// @dev Extract the coinbase (a.k.a. beneficiary or miner) address from the block header RLP.
83+
function getCoinbase(bytes memory headerRLP) internal pure returns (address) {
84+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.Coinbase)].readAddress();
85+
}
86+
87+
/// @dev Extract the state root from the block header RLP.
88+
function getStateRoot(bytes memory headerRLP) internal pure returns (bytes32) {
89+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.StateRoot)].readBytes32();
90+
}
91+
92+
/// @dev Extract the transactions root from the block header RLP.
93+
function getTransactionsRoot(bytes memory headerRLP) internal pure returns (bytes32) {
94+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.TransactionsRoot)].readBytes32();
95+
}
96+
97+
/// @dev Extract the receipts root from the block header RLP.
98+
function getReceiptsRoot(bytes memory headerRLP) internal pure returns (bytes32) {
99+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.ReceiptsRoot)].readBytes32();
100+
}
101+
102+
/// @dev Extract the logs bloom from the block header RLP.
103+
function getLogsBloom(bytes memory headerRLP) internal pure returns (bytes memory) {
104+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.LogsBloom)].readBytes();
105+
}
106+
107+
/// @dev Extract the difficulty from the block header RLP. This is constant to 0 since EIP-3675 (Paris)
108+
function getDifficulty(bytes memory headerRLP) internal pure returns (uint256) {
109+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.Difficulty)].readUint256();
110+
}
111+
112+
/// @dev Extract the block number from the block header RLP.
113+
function getNumber(bytes memory headerRLP) internal pure returns (uint256) {
114+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.Number)].readUint256();
115+
}
116+
117+
/// @dev Extract the gas used from the block header RLP.
118+
function getGasUsed(bytes memory headerRLP) internal pure returns (uint256) {
119+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.GasUsed)].readUint256();
120+
}
121+
122+
/// @dev Extract the gas limit from the block header RLP.
123+
function getGasLimit(bytes memory headerRLP) internal pure returns (uint256) {
124+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.GasLimit)].readUint256();
125+
}
126+
127+
/// @dev Extract the timestamp from the block header RLP.
128+
function getTimestamp(bytes memory headerRLP) internal pure returns (uint256) {
129+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.Timestamp)].readUint256();
130+
}
131+
132+
/// @dev Extract the extra data from the block header RLP.
133+
function getExtraData(bytes memory headerRLP) internal pure returns (bytes memory) {
134+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.ExtraData)].readBytes();
135+
}
136+
137+
/// @dev Extract the prevRandao (a.k.a. mixHash before Paris) from the block header RLP.
138+
function getPrevRandao(bytes memory headerRLP) internal pure returns (bytes32) {
139+
return _parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.PrevRandao)].readBytes32();
140+
}
141+
142+
/// @dev Extract the nonce from the block header RLP. This is constant to 0 since EIP-3675 (Paris)
143+
function getNonce(bytes memory headerRLP) internal pure returns (bytes8) {
144+
return bytes8(_parseHeader(headerRLP, Hardforks.Homestead)[uint8(HeaderField.Nonce)].readBytes32());
145+
}
146+
147+
/// @dev Extract the base fee per gas from the block header RLP. This was introduced in London.
148+
function getBaseFeePerGas(bytes memory headerRLP) internal pure returns (uint256) {
149+
return _parseHeader(headerRLP, Hardforks.London)[uint8(HeaderField.BaseFeePerGas)].readUint256();
150+
}
151+
152+
/// @dev Extract the withdrawals root from the block header RLP. This was introduced in Shanghai.
153+
function getWithdrawalsRoot(bytes memory headerRLP) internal pure returns (bytes32) {
154+
return _parseHeader(headerRLP, Hardforks.Shanghai)[uint8(HeaderField.WithdrawalsRoot)].readBytes32();
155+
}
156+
157+
/// @dev Extract the blob gas used from the block header RLP. This was introduced in Cancun.
158+
function getBlobGasUsed(bytes memory headerRLP) internal pure returns (uint64) {
159+
return _parseHeader(headerRLP, Hardforks.Cancun)[uint8(HeaderField.BlobGasUsed)].readUint256().toUint64();
160+
}
161+
162+
/// @dev Extract the excess blob gas from the block header RLP. This was introduced in Cancun.
163+
function getExcessBlobGas(bytes memory headerRLP) internal pure returns (uint64) {
164+
return _parseHeader(headerRLP, Hardforks.Cancun)[uint8(HeaderField.ExcessBlobGas)].readUint256().toUint64();
165+
}
166+
167+
/// @dev Extract the parent beacon block root from the block header RLP. This was introduced in Cancun.
168+
function getParentBeaconBlockRoot(bytes memory headerRLP) internal pure returns (bytes32) {
169+
return _parseHeader(headerRLP, Hardforks.Cancun)[uint8(HeaderField.ParentBeaconBlockRoot)].readBytes32();
170+
}
171+
172+
/// @dev Extract the requests hash from the block header RLP. This was introduced in Prague.
173+
function getRequestsHash(bytes memory headerRLP) internal pure returns (bytes32) {
174+
return _parseHeader(headerRLP, Hardforks.Prague)[uint8(HeaderField.RequestsHash)].readBytes32();
175+
}
176+
177+
/// @dev Extract the block access list hash from the block header RLP. This will be introduced in Amsterdam.
178+
function getBlockAccessListHash(bytes memory headerRLP) internal pure returns (bytes32) {
179+
return _parseHeader(headerRLP, Hardforks.Amsterdam)[uint8(HeaderField.BlockAccessListHash)].readBytes32();
180+
}
181+
182+
/// @dev Internal function to parse the block header RLP and return the list of fields as memory slices.
183+
/// It also checks that the number of fields is at least the expected number for the given hardfork version.
184+
function _parseHeader(bytes memory headerRLP, Hardforks version) private pure returns (Memory.Slice[] memory) {
185+
Memory.Slice[] memory fields = RLP.decodeList(headerRLP);
186+
require(fields.length >= _expectedHeadersLength(version), InvalidBlockHeader(version));
187+
return fields;
188+
}
189+
190+
/// @dev Internal function to return the expected number of fields in the block header RLP for a given hardfork version.
191+
function _expectedHeadersLength(Hardforks version) private pure returns (uint8 count) {
192+
assembly ("memory-safe") {
193+
count := byte(version, 0x0F0F0F0F0F0F0F0F0F0F10101010111415151600000000000000000000000000)
194+
}
195+
}
196+
}

contracts/utils/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
2727
* {Base58}: On-chain base58 encoding and decoding.
2828
* {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648].
2929
* {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality.
30+
* {BlockHeader}: A library for parsing and verifying RLP-encoded block headers.
3031
* {Bytes}: Common operations on bytes objects.
3132
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
3233
* {Calldata}: Helpers for manipulating calldata.
@@ -118,6 +119,8 @@ Ethereum contracts have no native concept of an interface, so applications must
118119

119120
{{Blockhash}}
120121

122+
{{BlockHeader}}
123+
121124
{{Bytes}}
122125

123126
{{CAIP10}}

test/utils/BlockHeader.test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
async function fixture() {
6+
return {
7+
mock: await ethers.deployContract('$BlockHeader'),
8+
};
9+
}
10+
11+
describe('BlockHeader', function () {
12+
beforeEach(async function () {
13+
Object.assign(this, await loadFixture(fixture));
14+
});
15+
16+
for (const block of [
17+
{
18+
_version: 'osaka',
19+
_rlpEncoded:
20+
'0xf90281a031d473c19b8e0d89e0546d057506d9dea042cd1492ef90682134e6027cf8669ca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794dadb0d80178819f2319190d340ce9a924f783711a05ac60aff31a075d3d53eea5b040cc8f753bde9e107558eec5fe17df8bc110b12a08d49818fb50fbdf89c471f303685b1bdc506d824b5edc291f74a234f74a73758a0fdb89e5c12a2dca6f1d213dd82d69ec07afa07a3e1aeb76f9aac2c3a0a98f01cb90100fffffff7ffffff7ffeffffffffffffbbff7fff7edfb7fffffffffdfffffbefffb7dffdfffffffffffbf7ffffdffff7ffffffffffffbffffffffffffffffffffffeffbfffffffeffffffefffffffffdfffffffffffffffffffffffffffffffefffffffffffffffffffffffffffef7fffff7feffffffffffffbfffffff7fffffbffffffffffffffffffffffffffffff76ffefffffff7fff7bffffffffdfff7fffffffffffffbfffffefef7fffdbffffdffffefceffffffffffffffffbffdfffffffffffefffffffbffbfffffffff7fbf7ffffffffffeffffdfffffdffffffeffffffffffff7fff77ffffffffffffffffffffffffeb7fffffffffffff7ff7ffffff80840177396d84039386c784037dbb0e8469a95c3b934275696c6465724e6574202842656176657229a0f10bbd059504a06792efabaaca4af22f285ce028d54a60f9a65ea25db70eed2d8800000000000000008408342539a06bb47e5be135671fde5284a24b5f5dd933f90bb7fdb22aa764f120bdc55d164f83200000840af13e4da066573e435a75fed7dfab9b037bb87bfa2ca01e7a4914c7178145e8a9d1eba444a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
21+
hash: '0x0f23d6dee77755efe485b0870d820b1aae8cfe689d767ce7d10b6afa2b1ef14d',
22+
parentHash: '0x31d473c19b8e0d89e0546d057506d9dea042cd1492ef90682134e6027cf8669c',
23+
sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
24+
miner: '0xdadb0d80178819f2319190d340ce9a924f783711',
25+
stateRoot: '0x5ac60aff31a075d3d53eea5b040cc8f753bde9e107558eec5fe17df8bc110b12',
26+
transactionsRoot: '0x8d49818fb50fbdf89c471f303685b1bdc506d824b5edc291f74a234f74a73758',
27+
receiptsRoot: '0xfdb89e5c12a2dca6f1d213dd82d69ec07afa07a3e1aeb76f9aac2c3a0a98f01c',
28+
logsBloom:
29+
'0xfffffff7ffffff7ffeffffffffffffbbff7fff7edfb7fffffffffdfffffbefffb7dffdfffffffffffbf7ffffdffff7ffffffffffffbffffffffffffffffffffffeffbfffffffeffffffefffffffffdfffffffffffffffffffffffffffffffefffffffffffffffffffffffffffef7fffff7feffffffffffffbfffffff7fffffbffffffffffffffffffffffffffffff76ffefffffff7fff7bffffffffdfff7fffffffffffffbfffffefef7fffdbffffdffffefceffffffffffffffffbffdfffffffffffefffffffbffbfffffffff7fbf7ffffffffffeffffdfffffdffffffeffffffffffff7fff77ffffffffffffffffffffffffeb7fffffffffffff7ff7ffffff',
30+
difficulty: '0x0',
31+
number: '0x177396d',
32+
gasLimit: '0x39386c7',
33+
gasUsed: '0x37dbb0e',
34+
timestamp: '0x69a95c3b',
35+
extraData: '0x4275696c6465724e6574202842656176657229',
36+
mixHash: '0xf10bbd059504a06792efabaaca4af22f285ce028d54a60f9a65ea25db70eed2d',
37+
nonce: '0x0000000000000000',
38+
baseFeePerGas: '0x8342539',
39+
withdrawalsRoot: '0x6bb47e5be135671fde5284a24b5f5dd933f90bb7fdb22aa764f120bdc55d164f',
40+
blobGasUsed: '0x200000',
41+
excessBlobGas: '0xaf13e4d',
42+
parentBeaconBlockRoot: '0x66573e435a75fed7dfab9b037bb87bfa2ca01e7a4914c7178145e8a9d1eba444',
43+
requestsHash: '0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
44+
},
45+
]) {
46+
it(`should parse block #${Number(block.number)} correctly`, async function () {
47+
const headerRLP = block._rlpEncoded;
48+
await expect(this.mock.$getParentHash(headerRLP)).to.eventually.equal(block.parentHash);
49+
await expect(this.mock.$getOmmersHash(headerRLP)).to.eventually.equal(block.sha3Uncles);
50+
await expect(this.mock.$getCoinbase(headerRLP)).to.eventually.equal(ethers.getAddress(block.miner));
51+
await expect(this.mock.$getStateRoot(headerRLP)).to.eventually.equal(block.stateRoot);
52+
await expect(this.mock.$getTransactionsRoot(headerRLP)).to.eventually.equal(block.transactionsRoot);
53+
await expect(this.mock.$getReceiptsRoot(headerRLP)).to.eventually.equal(block.receiptsRoot);
54+
await expect(this.mock.$getLogsBloom(headerRLP)).to.eventually.equal(block.logsBloom);
55+
await expect(this.mock.$getDifficulty(headerRLP)).to.eventually.equal(block.difficulty);
56+
await expect(this.mock.$getNumber(headerRLP)).to.eventually.equal(block.number);
57+
await expect(this.mock.$getGasLimit(headerRLP)).to.eventually.equal(block.gasLimit);
58+
await expect(this.mock.$getGasUsed(headerRLP)).to.eventually.equal(block.gasUsed);
59+
await expect(this.mock.$getTimestamp(headerRLP)).to.eventually.equal(block.timestamp);
60+
await expect(this.mock.$getExtraData(headerRLP)).to.eventually.equal(block.extraData);
61+
await expect(this.mock.$getPrevRandao(headerRLP)).to.eventually.equal(block.mixHash);
62+
await expect(this.mock.$getNonce(headerRLP)).to.eventually.equal(block.nonce);
63+
await expect(this.mock.$getBaseFeePerGas(headerRLP)).to.eventually.equal(block.baseFeePerGas);
64+
await expect(this.mock.$getWithdrawalsRoot(headerRLP)).to.eventually.equal(block.withdrawalsRoot);
65+
await expect(this.mock.$getBlobGasUsed(headerRLP)).to.eventually.equal(block.blobGasUsed);
66+
await expect(this.mock.$getExcessBlobGas(headerRLP)).to.eventually.equal(block.excessBlobGas);
67+
await expect(this.mock.$getParentBeaconBlockRoot(headerRLP)).to.eventually.equal(block.parentBeaconBlockRoot);
68+
await expect(this.mock.$getRequestsHash(headerRLP)).to.eventually.equal(block.requestsHash);
69+
});
70+
}
71+
});

0 commit comments

Comments
 (0)