From 3baf416d25245888961ddc40d7251afdac38e3ee Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Fri, 24 May 2024 16:14:42 -0600 Subject: [PATCH] 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 --- docs/CHANGELOG.md | 2 + src/ethereum_test_forks/forks/forks.py | 16 +- .../forks/history_contract.bin | Bin 0 -> 54 bytes src/ethereum_test_tools/common/types.py | 2 +- .../__init__.py | 3 + .../spec.py | 29 +++ .../test_block_hashes.py | 200 ++++++++++++++++++ 7 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 src/ethereum_test_forks/forks/history_contract.bin create mode 100644 tests/prague/eip2935_historical_block_hashes_from_state/__init__.py create mode 100644 tests/prague/eip2935_historical_block_hashes_from_state/spec.py create mode 100644 tests/prague/eip2935_historical_block_hashes_from_state/test_block_hashes.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b39b34ff8c..18e02f76e6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,8 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add tests for [EIP-6110: Supply validator deposits on chain](https://eips.ethereum.org/EIPS/eip-6110) ([#530](https://github.com/ethereum/execution-spec-tests/pull/530)). - ✨ Add tests for [EIP-7002: Execution layer triggerable withdrawals](https://eips.ethereum.org/EIPS/eip-7002) ([#530](https://github.com/ethereum/execution-spec-tests/pull/530)). - ✨ Add tests for [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) ([#530](https://github.com/ethereum/execution-spec-tests/pull/530)). +- ✨ Add tests for [EIP-2935: Serve historical block hashes from state +](https://eips.ethereum.org/EIPS/eip-2935) ([#564](https://github.com/ethereum/execution-spec-tests/pull/564)). ### 🛠️ Framework diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 5abdbc0fff..dfebff1ef8 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -508,8 +508,8 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[int]: @classmethod def pre_allocation_blockchain(cls) -> Mapping: """ - Prague requires pre-allocation of the beacon chain deposit contract for EIP-6110, and - the exits contract for EIP-7002. + Prague requires pre-allocation of the beacon chain deposit contract for EIP-6110, + the exits contract for EIP-7002, and the history storage contract for EIP-2935. """ new_allocation = {} @@ -542,6 +542,18 @@ def pre_allocation_blockchain(cls) -> Mapping: }, } ) + + # Add the history storage contract + with open(CURRENT_FOLDER / "history_contract.bin", mode="rb") as f: + new_allocation.update( + { + 0x25A219378DAD9B3503C8268C9CA836A52427A4FB: { + "nonce": 1, + "code": f.read(), + } + } + ) + return new_allocation | super(Prague, cls).pre_allocation_blockchain() @classmethod diff --git a/src/ethereum_test_forks/forks/history_contract.bin b/src/ethereum_test_forks/forks/history_contract.bin new file mode 100644 index 0000000000000000000000000000000000000000..250b34be25d3a48604c1a117adb0537d2b1b8cfe GIT binary patch literal 54 ycmYdLFcVBL3{PNmW{x)%OwbIEH%(MvU~~pCfsA-lwvhOs1cms|(eZHlF9QH)l@DtG literal 0 HcmV?d00001 diff --git a/src/ethereum_test_tools/common/types.py b/src/ethereum_test_tools/common/types.py index cd9db3168f..293130ee69 100644 --- a/src/ethereum_test_tools/common/types.py +++ b/src/ethereum_test_tools/common/types.py @@ -252,7 +252,7 @@ def keys(self) -> set[StorageKeyValueType]: return set(self.root.keys()) def store_next( - self, value: StorageKeyValueTypeConvertible | StorageKeyValueType + self, value: StorageKeyValueTypeConvertible | StorageKeyValueType | bool ) -> StorageKeyValueType: """ Stores a value in the storage and returns the key where the value is stored. diff --git a/tests/prague/eip2935_historical_block_hashes_from_state/__init__.py b/tests/prague/eip2935_historical_block_hashes_from_state/__init__.py new file mode 100644 index 0000000000..276289784b --- /dev/null +++ b/tests/prague/eip2935_historical_block_hashes_from_state/__init__.py @@ -0,0 +1,3 @@ +""" +Cross-client EIP-2935 Tests +""" diff --git a/tests/prague/eip2935_historical_block_hashes_from_state/spec.py b/tests/prague/eip2935_historical_block_hashes_from_state/spec.py new file mode 100644 index 0000000000..0757c98a23 --- /dev/null +++ b/tests/prague/eip2935_historical_block_hashes_from_state/spec.py @@ -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 diff --git a/tests/prague/eip2935_historical_block_hashes_from_state/test_block_hashes.py b/tests/prague/eip2935_historical_block_hashes_from_state/test_block_hashes.py new file mode 100644 index 0000000000..4872ce7594 --- /dev/null +++ b/tests/prague/eip2935_historical_block_hashes_from_state/test_block_hashes.py @@ -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, + )