Skip to content

Add Blockhash library following EIP-2935 #5642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-dodos-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`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.
51 changes: 51 additions & 0 deletions contracts/utils/Blockhash.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
* @dev Library for accessing historical block hashes beyond the standard 256 block limit.
* Uses EIP-2935's history storage contract which maintains a ring buffer of the last
* 8191 block hashes in state.
*
* For blocks within the last 256 blocks, it uses the native `BLOCKHASH` opcode.
* For blocks between 257 and 8191 blocks ago, it queries the EIP-2935 history storage.
* For blocks older than 8191 or future blocks, it returns zero, matching the `BLOCKHASH` behavior.
*
* NOTE: After EIP-2935 activation, it takes 8191 blocks to completely fill the history.
* Before that, only block hashes since the fork block will be available.
*/
library Blockhash {
address internal constant HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53a10cb7A02335B175320002935;

/**
* @dev Retrieves the block hash for any historical block within the supported range.
*
* NOTE: The function gracefully handles future blocks and blocks beyond the history window
* by returning zero, consistent with the EVM's native `BLOCKHASH` behavior.
*/
function blockHash(uint256 blockNumber) internal view returns (bytes32) {
uint256 current = block.number;
uint256 distance;

unchecked {
// Can only wrap around to `current + 1` given `block.number - (2**256 - 1) = block.number + 1`
distance = current - blockNumber;
}

return distance > 256 && distance <= 8191 ? _historyStorageCall(blockNumber) : blockhash(blockNumber);
}

/// @dev Internal function to query the EIP-2935 history storage contract.
function _historyStorageCall(uint256 blockNumber) private view returns (bytes32 hash) {
assembly ("memory-safe") {
// Use scratch space to allocate blockNumber
mstore(0, blockNumber) // Store the blockNumber in scratch space

// In case the history storage address is not deployed, the call will succeed
// without returndata, so the hash will be 0 just as querying `blockhash` directly.
let success := staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0, 0x20, 0, 0x20)
if and(success, gt(returndatasize(), 0)) {
hash := mload(0)
}
}
}
}
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[profile.default]
solc_version = '0.8.24'
evm_version = 'cancun'
evm_version = 'prague'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this is unnecessary since we're vm.etch'ing the bytecode anyway. I couldn't find anythign about EIP-2935 in the Foundry docs

optimizer = true
optimizer-runs = 200
src = 'contracts'
Expand Down
6 changes: 6 additions & 0 deletions hardhat/common-contracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ const INSTANCES = {
bytecode: '0x60003681823780368234f58015156014578182fd5b80825250506014600cf3',
},
},
eip2935: {
address: '0x0000F90827F1C53a10cb7A02335B175320002935',
abi: [],
bytecode:
'0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500',
},
};

const setup = (input, ethers) =>
Expand Down
100 changes: 100 additions & 0 deletions test/utils/Blockhash.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {Blockhash} from "../../contracts/utils/Blockhash.sol";

contract BlockhashTest is Test {
uint256 internal startingBlock;

address internal constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;

// See https://eips.ethereum.org/EIPS/eip-2935#bytecode
// Generated using https://www.evm.codes/playground
bytes private HISTORY_STORAGE_BYTECODE =

Check failure on line 14 in test/utils/Blockhash.t.sol

View workflow job for this annotation

GitHub Actions / lint

Variable name must be in mixedCase
hex"3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500";

function setUp() public {
vm.roll(block.number + 100);

startingBlock = block.number;
vm.etch(Blockhash.HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_BYTECODE);
}

function testFuzzRecentBlocks(uint8 offset, uint64 currentBlock, bytes32 expectedHash) public {
// Recent blocks (1-256 blocks old)
uint256 boundedOffset = uint256(offset) + 1;
vm.assume(currentBlock > boundedOffset);
vm.roll(currentBlock);

uint256 targetBlock = currentBlock - boundedOffset;
vm.setBlockhash(targetBlock, expectedHash);

bytes32 result = Blockhash.blockHash(targetBlock);
assertEq(result, blockhash(targetBlock));
assertEq(result, expectedHash);
}

function testFuzzHistoryBlocks(uint16 offset, uint256 currentBlock, bytes32 expectedHash) public {
// History blocks (257-8191 blocks old)
offset = uint16(bound(offset, 257, 8191));
vm.assume(currentBlock > offset);
vm.roll(currentBlock);

uint256 targetBlock = currentBlock - offset;
_setHistoryBlockhash(targetBlock, expectedHash);

bytes32 result = Blockhash.blockHash(targetBlock);
(bool success, bytes memory returndata) = Blockhash.HISTORY_STORAGE_ADDRESS.staticcall(
abi.encodePacked(bytes32(targetBlock))
);
assertTrue(success);
assertEq(result, abi.decode(returndata, (bytes32)));
assertEq(result, expectedHash);
}

function testFuzzVeryOldBlocks(uint256 offset, uint256 currentBlock) public {
// Very old blocks (>8191 blocks old)
offset = bound(offset, 8192, type(uint256).max);
vm.assume(currentBlock > offset);
vm.roll(currentBlock);

uint256 targetBlock = currentBlock - offset;
bytes32 result = Blockhash.blockHash(targetBlock);
assertEq(result, bytes32(0));
}

function testFuzzFutureBlocks(uint256 offset, uint256 currentBlock) public {
// Future blocks
offset = bound(offset, 1, type(uint256).max);
vm.roll(currentBlock);

unchecked {
uint256 targetBlock = currentBlock + offset;
bytes32 result = Blockhash.blockHash(targetBlock);
assertEq(result, blockhash(targetBlock));
}
}

function testUnsupportedChainsReturnZeroWhenOutOfRange() public {
vm.etch(Blockhash.HISTORY_STORAGE_ADDRESS, hex"");

vm.roll(block.number + 1000);
assertEq(Blockhash.blockHash(block.number - 1000), bytes32(0));
}

function _setHistoryBlockhash(bytes32 blockHash) internal {
_setHistoryBlockhash(block.number, blockHash);
}

function _setHistoryBlockhash(uint256 blockNumber, bytes32 blockHash) internal {
// Subtracting 1 due to bug encountered during coverage
uint256 currentBlock = block.number - 1;
vm.assume(blockNumber < type(uint256).max);
vm.roll(blockNumber + 1); // roll to the next block so the storage contract sets the parent's blockhash
vm.prank(SYSTEM_ADDRESS);
(bool success, ) = Blockhash.HISTORY_STORAGE_ADDRESS.call(abi.encode(blockHash)); // set parent's blockhash
assertTrue(success);
vm.roll(currentBlock + 1);
}
}
76 changes: 76 additions & 0 deletions test/utils/Blockhash.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture, mine, mineUpTo, setCode } = require('@nomicfoundation/hardhat-network-helpers');
const { impersonate } = require('../helpers/account');

async function fixture() {
const mock = await ethers.deployContract('$Blockhash');
return { mock };
}

const HISTORY_STORAGE_ADDRESS = '0x0000F90827F1C53a10cb7A02335B175320002935';
const SYSTEM_ADDRESS = '0xfffffffffffffffffffffffffffffffffffffffe';
const HISTORY_SERVE_WINDOW = 8191;
const BLOCKHASH_SERVE_WINDOW = 256;

describe('Blockhash', function () {
before(async function () {
Object.assign(this, await loadFixture(fixture));

impersonate(SYSTEM_ADDRESS);
this.systemSigner = await ethers.getSigner(SYSTEM_ADDRESS);
});

it('recent block', async function () {
await mine();

const mostRecentBlock = (await ethers.provider.getBlock('latest')).number;
const blockToCheck = mostRecentBlock - 1;
const fetchedHash = (await ethers.provider.getBlock(blockToCheck)).hash;
await expect(this.mock.$blockHash(blockToCheck)).to.eventually.equal(fetchedHash);
});

it('old block', async function () {
await mine();

const mostRecentBlock = await ethers.provider.getBlock('latest');

// Call the history address with the most recent block hash
await this.systemSigner.sendTransaction({
to: HISTORY_STORAGE_ADDRESS,
data: mostRecentBlock.hash,
});

await mineUpTo(mostRecentBlock.number + BLOCKHASH_SERVE_WINDOW + 10);

// Verify blockhash after setting history
await expect(this.mock.$blockHash(mostRecentBlock.number)).to.eventually.equal(mostRecentBlock.hash);
});

it('very old block', async function () {
await mine();

const mostRecentBlock = await ethers.provider.getBlock('latest');
await mineUpTo(mostRecentBlock.number + HISTORY_SERVE_WINDOW + 10);

await expect(this.mock.$blockHash(mostRecentBlock.number)).to.eventually.equal(ethers.ZeroHash);
});

it('future block', async function () {
await mine();

const mostRecentBlock = await ethers.provider.getBlock('latest');
const blockToCheck = mostRecentBlock.number + 10;
await expect(this.mock.$blockHash(blockToCheck)).to.eventually.equal(ethers.ZeroHash);
});

it('unsupported chain', async function () {
await setCode(HISTORY_STORAGE_ADDRESS, '0x00');

const mostRecentBlock = await ethers.provider.getBlock('latest');
await mineUpTo(mostRecentBlock.number + BLOCKHASH_SERVE_WINDOW + 10);

await expect(this.mock.$blockHash(mostRecentBlock.number)).to.eventually.equal(ethers.ZeroHash);
await expect(this.mock.$blockHash(mostRecentBlock.number + 20)).to.eventually.not.equal(ethers.ZeroHash);
});
});
Loading