-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new(tests): EIP-2935: Serve historical block hashes from state (#564)
* feat(forks): Add history contract to Prague * new(tests): eip-2935: add file * tests: eip-2935: new test * fix(tests): EIP-2935: run HISTORY_SERVE_WINDOW tests * chengelog * refactor 1 * fix(fw): types: accept bool in `store_next` * refactor 2
- Loading branch information
Showing
7 changed files
with
249 additions
and
3 deletions.
There are no files selected for viewing
This file contains 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 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
Binary file not shown.
This file contains 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
3 changes: 3 additions & 0 deletions
3
tests/prague/eip2935_historical_block_hashes_from_state/__init__.py
This file contains 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,3 @@ | ||
""" | ||
Cross-client EIP-2935 Tests | ||
""" |
29 changes: 29 additions & 0 deletions
29
tests/prague/eip2935_historical_block_hashes_from_state/spec.py
This file contains 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,29 @@ | ||
""" | ||
Defines EIP-2935 specification constants and functions. | ||
""" | ||
from dataclasses import dataclass | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ReferenceSpec: | ||
""" | ||
Defines the reference spec version and git path. | ||
""" | ||
|
||
git_path: str | ||
version: str | ||
|
||
|
||
ref_spec_2935 = ReferenceSpec("EIPS/eip-2935.md", "3ab311ccd6029c080fb2a8b9615d493dfc093377") | ||
|
||
|
||
@dataclass(frozen=True) | ||
class Spec: | ||
""" | ||
Parameters from the EIP-2935 specifications as defined at | ||
https://eips.ethereum.org/EIPS/eip-2935 | ||
""" | ||
|
||
HISTORY_STORAGE_ADDRESS = 0x25A219378DAD9B3503C8268C9CA836A52427A4FB | ||
HISTORY_SERVE_WINDOW = 8192 | ||
BLOCKHASH_OLD_WINDOW = 256 |
200 changes: 200 additions & 0 deletions
200
tests/prague/eip2935_historical_block_hashes_from_state/test_block_hashes.py
This file contains 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,200 @@ | ||
""" | ||
abstract: Tests [EIP-2935: Serve historical block hashes from state](https://eips.ethereum.org/EIPS/eip-2935) | ||
Test [EIP-2935: Serve historical block hashes from state](https://eips.ethereum.org/EIPS/eip-2935) | ||
""" # noqa: E501 | ||
|
||
from itertools import count | ||
from typing import Dict, List | ||
|
||
import pytest | ||
|
||
from ethereum_test_tools import Account, Address, Block, BlockchainTestFiller, Environment | ||
from ethereum_test_tools import Opcodes as Op | ||
from ethereum_test_tools import Storage, TestAddress, Transaction | ||
|
||
from .spec import Spec, ref_spec_2935 | ||
|
||
REFERENCE_SPEC_GIT_PATH = ref_spec_2935.git_path | ||
REFERENCE_SPEC_VERSION = ref_spec_2935.version | ||
|
||
FORK_TIMESTAMP = 15_000 | ||
|
||
|
||
def generate_block_check_code( | ||
block_number: int | None, | ||
populated_blockhash: bool, | ||
populated_contract: bool, | ||
storage: Storage, | ||
check_contract_first: bool = False, | ||
) -> bytes: | ||
""" | ||
Generate EVM code to check that the blockhashes are correctly stored in the state. | ||
Args: | ||
block_number (int | None): The block number to check (or None to return empty code). | ||
populated_blockhash (bool): Whether the blockhash should be populated. | ||
populated_contract (bool): Whether the contract should be populated. | ||
storage (Storage): The storage object to use. | ||
check_contract_first (bool): Whether to check the contract first, for slot warming checks. | ||
""" | ||
if block_number is None: | ||
# No block number to check | ||
return b"" | ||
|
||
blockhash_key = storage.store_next(not populated_blockhash) | ||
contract_key = storage.store_next(not populated_contract) | ||
|
||
check_blockhash = Op.SSTORE(blockhash_key, Op.ISZERO(Op.BLOCKHASH(block_number))) | ||
check_contract = ( | ||
Op.MSTORE(0, block_number) | ||
+ Op.POP(Op.CALL(Op.GAS, Spec.HISTORY_STORAGE_ADDRESS, 0, 0, 32, 0, 32)) | ||
+ Op.SSTORE(contract_key, Op.ISZERO(Op.MLOAD(0))) | ||
) | ||
|
||
if check_contract_first: | ||
code = check_contract + check_blockhash | ||
else: | ||
code = check_blockhash + check_contract | ||
|
||
if populated_contract and populated_blockhash: | ||
# Both values must be equal | ||
code += Op.SSTORE(storage.store_next(True), Op.EQ(Op.MLOAD(0), Op.BLOCKHASH(block_number))) | ||
|
||
return code | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"blocks_before_fork", | ||
[ | ||
pytest.param(1, id="fork_at_1"), | ||
pytest.param(Spec.BLOCKHASH_OLD_WINDOW, id="fork_at_BLOCKHASH_OLD_WINDOW"), | ||
pytest.param( | ||
Spec.BLOCKHASH_OLD_WINDOW + 1, | ||
id="fork_at_BLOCKHASH_OLD_WINDOW_plus_1", | ||
), | ||
pytest.param( | ||
Spec.BLOCKHASH_OLD_WINDOW + 2, | ||
id="fork_at_BLOCKHASH_OLD_WINDOW_plus_2", | ||
), | ||
pytest.param( | ||
Spec.HISTORY_SERVE_WINDOW + 1, | ||
id="fork_at_HISTORY_SERVE_WINDOW_plus_1", | ||
marks=pytest.mark.slow, | ||
), | ||
], | ||
) | ||
@pytest.mark.valid_at_transition_to("Prague") | ||
def test_block_hashes_history_at_transition( | ||
blockchain_test: BlockchainTestFiller, | ||
blocks_before_fork: int, | ||
): | ||
""" | ||
Test the fork transition and that the block hashes of previous blocks, even blocks | ||
before the fork, are included in the state at the moment of the transition. | ||
""" | ||
# Fork happens at timestamp 15_000, and genesis counts as a block before fork. | ||
blocks: List[Block] = [] | ||
assert blocks_before_fork >= 1 and blocks_before_fork < FORK_TIMESTAMP | ||
|
||
pre = {TestAddress: Account(balance=10_000_000_000)} | ||
post: Dict[Address, Account] = {} | ||
tx_nonce = count(0) | ||
|
||
current_code_address = 0x10000 | ||
for i in range(1, blocks_before_fork): | ||
txs: List[Transaction] = [] | ||
if i == blocks_before_fork - 1: | ||
# On the last block before the fork, BLOCKHASH must return values for the last 256 | ||
# blocks but not for the blocks before that. | ||
# And HISTORY_STORAGE_ADDRESS should be empty. | ||
code = b"" | ||
storage = Storage() | ||
|
||
# Check the last block before the window | ||
code += generate_block_check_code( | ||
block_number=( | ||
i - Spec.BLOCKHASH_OLD_WINDOW - 1 | ||
if i > Spec.BLOCKHASH_OLD_WINDOW | ||
else None # Chain not long enough, no block to check | ||
), | ||
populated_blockhash=False, | ||
populated_contract=False, | ||
storage=storage, | ||
) | ||
|
||
# Check the first block inside the window | ||
code += generate_block_check_code( | ||
block_number=( | ||
i - Spec.BLOCKHASH_OLD_WINDOW | ||
if i > Spec.BLOCKHASH_OLD_WINDOW | ||
else 0 # Entire chain is inside the window, check genesis | ||
), | ||
populated_blockhash=True, | ||
populated_contract=False, | ||
storage=storage, | ||
) | ||
|
||
txs.append( | ||
Transaction( | ||
to=current_code_address, | ||
gas_limit=10_000_000, | ||
nonce=next(tx_nonce), | ||
) | ||
) | ||
pre[Address(current_code_address)] = Account(code=code, nonce=1) | ||
post[Address(current_code_address)] = Account(storage=storage) | ||
current_code_address += 0x100 | ||
blocks.append(Block(timestamp=i, txs=txs)) | ||
|
||
# Add the fork block | ||
current_block_number = len(blocks) + 1 | ||
txs = [] | ||
# On the block after the fork, BLOCKHASH must return values for the last | ||
# Spec.HISTORY_SERVE_WINDOW blocks. | ||
# And HISTORY_STORAGE_ADDRESS should be also serve the same values. | ||
code = b"" | ||
storage = Storage() | ||
|
||
# Check the last block before the window | ||
code += generate_block_check_code( | ||
block_number=( | ||
current_block_number - Spec.HISTORY_SERVE_WINDOW - 1 | ||
if current_block_number > Spec.HISTORY_SERVE_WINDOW | ||
else None # Chain not long enough, no block to check | ||
), | ||
populated_blockhash=False, | ||
populated_contract=False, | ||
storage=storage, | ||
) | ||
|
||
# Check the first block inside the window | ||
code += generate_block_check_code( | ||
block_number=( | ||
current_block_number - Spec.HISTORY_SERVE_WINDOW | ||
if current_block_number > Spec.HISTORY_SERVE_WINDOW | ||
else 0 # Entire chain is inside the window, check genesis | ||
), | ||
populated_blockhash=True, | ||
populated_contract=True, | ||
storage=storage, | ||
) | ||
|
||
txs.append( | ||
Transaction( | ||
to=current_code_address, | ||
gas_limit=10_000_000, | ||
nonce=next(tx_nonce), | ||
) | ||
) | ||
pre[Address(current_code_address)] = Account(code=code, nonce=1) | ||
post[Address(current_code_address)] = Account(storage=storage) | ||
current_code_address += 0x100 | ||
|
||
blocks.append(Block(timestamp=FORK_TIMESTAMP, txs=txs)) | ||
|
||
blockchain_test( | ||
genesis_environment=Environment(), | ||
pre=pre, | ||
blocks=blocks, | ||
post=post, | ||
) |