-
Notifications
You must be signed in to change notification settings - Fork 12.1k
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
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f5aec41
Add Blockhash library following EIP-2935
ernestognw e5062ff
Merge branch 'master' into feature/EIP-2935
ernestognw aba1a63
Add failing tests
ernestognw 568f692
nits
ernestognw 3184a68
fix tests
arr00 92992e8
fix coverage
arr00 1f2d0f6
add test to ensure unsupported chains fail gracefully
arr00 1e95107
fix return value on unsupported chains
arr00 eb7b88f
add hardhat testing
arr00 fc6444d
add blockhash mock
arr00 2ad2485
fix lint
arr00 e8b16c5
fix pragma
arr00 58940d8
Simplify and fix lint
ernestognw 1c2fab1
Fix and evaluation order
ernestognw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
|
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.