Add Blockhash library following EIP-2935 (#5642)
Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
This commit is contained in:
5
.changeset/wet-dodos-reply.md
Normal file
5
.changeset/wet-dodos-reply.md
Normal file
@ -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.
|
||||
9
contracts/mocks/BlockhashMock.sol
Normal file
9
contracts/mocks/BlockhashMock.sol
Normal file
@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Blockhash} from "../utils/Blockhash.sol";
|
||||
|
||||
/// @dev This mock is required for upgradeable tests to pass
|
||||
contract BlockhashMock {
|
||||
|
||||
}
|
||||
49
contracts/utils/Blockhash.sol
Normal file
49
contracts/utils/Blockhash.sol
Normal file
@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @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") {
|
||||
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.
|
||||
if and(gt(returndatasize(), 0), staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0, 0x20, 0, 0x20)) {
|
||||
hash := mload(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
[profile.default]
|
||||
solc_version = '0.8.24'
|
||||
evm_version = 'cancun'
|
||||
evm_version = 'prague'
|
||||
optimizer = true
|
||||
optimizer-runs = 200
|
||||
src = 'contracts'
|
||||
|
||||
@ -47,6 +47,12 @@ const INSTANCES = {
|
||||
bytecode: '0x60003681823780368234f58015156014578182fd5b80825250506014600cf3',
|
||||
},
|
||||
},
|
||||
eip2935: {
|
||||
address: '0x0000F90827F1C53a10cb7A02335B175320002935',
|
||||
abi: [],
|
||||
bytecode:
|
||||
'0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500',
|
||||
},
|
||||
};
|
||||
|
||||
const setup = (input, ethers) =>
|
||||
|
||||
100
test/utils/Blockhash.t.sol
Normal file
100
test/utils/Blockhash.t.sol
Normal file
@ -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 constant HISTORY_STORAGE_BYTECODE =
|
||||
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
test/utils/Blockhash.test.js
Normal file
76
test/utils/Blockhash.test.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user