Skip to content

Commit 6dd191a

Browse files
ernestognwarr00
andauthored
Add Blockhash library following EIP-2935 (#5642)
Co-authored-by: Arr00 <[email protected]>
1 parent 56c07e5 commit 6dd191a

File tree

7 files changed

+246
-1
lines changed

7 files changed

+246
-1
lines changed

.changeset/wet-dodos-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Blockhash`: Add a library that provides access to historical block hashes using EIP-2935's history storage, extending the standard 256-block limit to 8191 blocks.

contracts/mocks/BlockhashMock.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Blockhash} from "../utils/Blockhash.sol";
5+
6+
/// @dev This mock is required for upgradeable tests to pass
7+
contract BlockhashMock {
8+
9+
}

contracts/utils/Blockhash.sol

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
/**
5+
* @dev Library for accessing historical block hashes beyond the standard 256 block limit.
6+
* Uses EIP-2935's history storage contract which maintains a ring buffer of the last
7+
* 8191 block hashes in state.
8+
*
9+
* For blocks within the last 256 blocks, it uses the native `BLOCKHASH` opcode.
10+
* For blocks between 257 and 8191 blocks ago, it queries the EIP-2935 history storage.
11+
* For blocks older than 8191 or future blocks, it returns zero, matching the `BLOCKHASH` behavior.
12+
*
13+
* NOTE: After EIP-2935 activation, it takes 8191 blocks to completely fill the history.
14+
* Before that, only block hashes since the fork block will be available.
15+
*/
16+
library Blockhash {
17+
address internal constant HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53a10cb7A02335B175320002935;
18+
19+
/**
20+
* @dev Retrieves the block hash for any historical block within the supported range.
21+
*
22+
* NOTE: The function gracefully handles future blocks and blocks beyond the history window
23+
* by returning zero, consistent with the EVM's native `BLOCKHASH` behavior.
24+
*/
25+
function blockHash(uint256 blockNumber) internal view returns (bytes32) {
26+
uint256 current = block.number;
27+
uint256 distance;
28+
29+
unchecked {
30+
// Can only wrap around to `current + 1` given `block.number - (2**256 - 1) = block.number + 1`
31+
distance = current - blockNumber;
32+
}
33+
34+
return distance > 256 && distance <= 8191 ? _historyStorageCall(blockNumber) : blockhash(blockNumber);
35+
}
36+
37+
/// @dev Internal function to query the EIP-2935 history storage contract.
38+
function _historyStorageCall(uint256 blockNumber) private view returns (bytes32 hash) {
39+
assembly ("memory-safe") {
40+
mstore(0, blockNumber) // Store the blockNumber in scratch space
41+
42+
// In case the history storage address is not deployed, the call will succeed
43+
// without returndata, so the hash will be 0 just as querying `blockhash` directly.
44+
if and(gt(returndatasize(), 0), staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0, 0x20, 0, 0x20)) {
45+
hash := mload(0)
46+
}
47+
}
48+
}
49+
}

foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[profile.default]
22
solc_version = '0.8.24'
3-
evm_version = 'cancun'
3+
evm_version = 'prague'
44
optimizer = true
55
optimizer-runs = 200
66
src = 'contracts'

hardhat/common-contracts.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ const INSTANCES = {
4747
bytecode: '0x60003681823780368234f58015156014578182fd5b80825250506014600cf3',
4848
},
4949
},
50+
eip2935: {
51+
address: '0x0000F90827F1C53a10cb7A02335B175320002935',
52+
abi: [],
53+
bytecode:
54+
'0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500',
55+
},
5056
};
5157

5258
const setup = (input, ethers) =>

test/utils/Blockhash.t.sol

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {Blockhash} from "../../contracts/utils/Blockhash.sol";
6+
7+
contract BlockhashTest is Test {
8+
uint256 internal startingBlock;
9+
10+
address internal constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;
11+
12+
// See https://eips.ethereum.org/EIPS/eip-2935#bytecode
13+
// Generated using https://www.evm.codes/playground
14+
bytes private constant HISTORY_STORAGE_BYTECODE =
15+
hex"3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500";
16+
17+
function setUp() public {
18+
vm.roll(block.number + 100);
19+
20+
startingBlock = block.number;
21+
vm.etch(Blockhash.HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_BYTECODE);
22+
}
23+
24+
function testFuzzRecentBlocks(uint8 offset, uint64 currentBlock, bytes32 expectedHash) public {
25+
// Recent blocks (1-256 blocks old)
26+
uint256 boundedOffset = uint256(offset) + 1;
27+
vm.assume(currentBlock > boundedOffset);
28+
vm.roll(currentBlock);
29+
30+
uint256 targetBlock = currentBlock - boundedOffset;
31+
vm.setBlockhash(targetBlock, expectedHash);
32+
33+
bytes32 result = Blockhash.blockHash(targetBlock);
34+
assertEq(result, blockhash(targetBlock));
35+
assertEq(result, expectedHash);
36+
}
37+
38+
function testFuzzHistoryBlocks(uint16 offset, uint256 currentBlock, bytes32 expectedHash) public {
39+
// History blocks (257-8191 blocks old)
40+
offset = uint16(bound(offset, 257, 8191));
41+
vm.assume(currentBlock > offset);
42+
vm.roll(currentBlock);
43+
44+
uint256 targetBlock = currentBlock - offset;
45+
_setHistoryBlockhash(targetBlock, expectedHash);
46+
47+
bytes32 result = Blockhash.blockHash(targetBlock);
48+
(bool success, bytes memory returndata) = Blockhash.HISTORY_STORAGE_ADDRESS.staticcall(
49+
abi.encodePacked(bytes32(targetBlock))
50+
);
51+
assertTrue(success);
52+
assertEq(result, abi.decode(returndata, (bytes32)));
53+
assertEq(result, expectedHash);
54+
}
55+
56+
function testFuzzVeryOldBlocks(uint256 offset, uint256 currentBlock) public {
57+
// Very old blocks (>8191 blocks old)
58+
offset = bound(offset, 8192, type(uint256).max);
59+
vm.assume(currentBlock > offset);
60+
vm.roll(currentBlock);
61+
62+
uint256 targetBlock = currentBlock - offset;
63+
bytes32 result = Blockhash.blockHash(targetBlock);
64+
assertEq(result, bytes32(0));
65+
}
66+
67+
function testFuzzFutureBlocks(uint256 offset, uint256 currentBlock) public {
68+
// Future blocks
69+
offset = bound(offset, 1, type(uint256).max);
70+
vm.roll(currentBlock);
71+
72+
unchecked {
73+
uint256 targetBlock = currentBlock + offset;
74+
bytes32 result = Blockhash.blockHash(targetBlock);
75+
assertEq(result, blockhash(targetBlock));
76+
}
77+
}
78+
79+
function testUnsupportedChainsReturnZeroWhenOutOfRange() public {
80+
vm.etch(Blockhash.HISTORY_STORAGE_ADDRESS, hex"");
81+
82+
vm.roll(block.number + 1000);
83+
assertEq(Blockhash.blockHash(block.number - 1000), bytes32(0));
84+
}
85+
86+
function _setHistoryBlockhash(bytes32 blockHash) internal {
87+
_setHistoryBlockhash(block.number, blockHash);
88+
}
89+
90+
function _setHistoryBlockhash(uint256 blockNumber, bytes32 blockHash) internal {
91+
// Subtracting 1 due to bug encountered during coverage
92+
uint256 currentBlock = block.number - 1;
93+
vm.assume(blockNumber < type(uint256).max);
94+
vm.roll(blockNumber + 1); // roll to the next block so the storage contract sets the parent's blockhash
95+
vm.prank(SYSTEM_ADDRESS);
96+
(bool success, ) = Blockhash.HISTORY_STORAGE_ADDRESS.call(abi.encode(blockHash)); // set parent's blockhash
97+
assertTrue(success);
98+
vm.roll(currentBlock + 1);
99+
}
100+
}

test/utils/Blockhash.test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture, mine, mineUpTo, setCode } = require('@nomicfoundation/hardhat-network-helpers');
4+
const { impersonate } = require('../helpers/account');
5+
6+
async function fixture() {
7+
const mock = await ethers.deployContract('$Blockhash');
8+
return { mock };
9+
}
10+
11+
const HISTORY_STORAGE_ADDRESS = '0x0000F90827F1C53a10cb7A02335B175320002935';
12+
const SYSTEM_ADDRESS = '0xfffffffffffffffffffffffffffffffffffffffe';
13+
const HISTORY_SERVE_WINDOW = 8191;
14+
const BLOCKHASH_SERVE_WINDOW = 256;
15+
16+
describe('Blockhash', function () {
17+
before(async function () {
18+
Object.assign(this, await loadFixture(fixture));
19+
20+
impersonate(SYSTEM_ADDRESS);
21+
this.systemSigner = await ethers.getSigner(SYSTEM_ADDRESS);
22+
});
23+
24+
it('recent block', async function () {
25+
await mine();
26+
27+
const mostRecentBlock = (await ethers.provider.getBlock('latest')).number;
28+
const blockToCheck = mostRecentBlock - 1;
29+
const fetchedHash = (await ethers.provider.getBlock(blockToCheck)).hash;
30+
await expect(this.mock.$blockHash(blockToCheck)).to.eventually.equal(fetchedHash);
31+
});
32+
33+
it('old block', async function () {
34+
await mine();
35+
36+
const mostRecentBlock = await ethers.provider.getBlock('latest');
37+
38+
// Call the history address with the most recent block hash
39+
await this.systemSigner.sendTransaction({
40+
to: HISTORY_STORAGE_ADDRESS,
41+
data: mostRecentBlock.hash,
42+
});
43+
44+
await mineUpTo(mostRecentBlock.number + BLOCKHASH_SERVE_WINDOW + 10);
45+
46+
// Verify blockhash after setting history
47+
await expect(this.mock.$blockHash(mostRecentBlock.number)).to.eventually.equal(mostRecentBlock.hash);
48+
});
49+
50+
it('very old block', async function () {
51+
await mine();
52+
53+
const mostRecentBlock = await ethers.provider.getBlock('latest');
54+
await mineUpTo(mostRecentBlock.number + HISTORY_SERVE_WINDOW + 10);
55+
56+
await expect(this.mock.$blockHash(mostRecentBlock.number)).to.eventually.equal(ethers.ZeroHash);
57+
});
58+
59+
it('future block', async function () {
60+
await mine();
61+
62+
const mostRecentBlock = await ethers.provider.getBlock('latest');
63+
const blockToCheck = mostRecentBlock.number + 10;
64+
await expect(this.mock.$blockHash(blockToCheck)).to.eventually.equal(ethers.ZeroHash);
65+
});
66+
67+
it('unsupported chain', async function () {
68+
await setCode(HISTORY_STORAGE_ADDRESS, '0x00');
69+
70+
const mostRecentBlock = await ethers.provider.getBlock('latest');
71+
await mineUpTo(mostRecentBlock.number + BLOCKHASH_SERVE_WINDOW + 10);
72+
73+
await expect(this.mock.$blockHash(mostRecentBlock.number)).to.eventually.equal(ethers.ZeroHash);
74+
await expect(this.mock.$blockHash(mostRecentBlock.number + 20)).to.eventually.not.equal(ethers.ZeroHash);
75+
});
76+
});

0 commit comments

Comments
 (0)