diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 44411df526..ac7d7757ec 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,7 @@ Test fixtures for use by clients are available for each release on the [Github r - 🐞 Fix `fill -m yul_test` which failed to filter tests that are (dynamically) marked as a yul test ([#418](https://github.com/ethereum/execution-spec-tests/pull/418)). - 🔀 Helper methods `to_address`, `to_hash` and `to_hash_bytes` have been deprecated in favor of `Address` and `Hash`, which are automatically detected as opcode parameters and pushed to the stack in the resulting bytecode ([#422](https://github.com/ethereum/execution-spec-tests/pull/422)). - ✨ `Opcodes` enum now contains docstrings with each opcode description, including parameters and return values, which show up in many development environments ([#424](https://github.com/ethereum/execution-spec-tests/pull/424)) @ThreeHrSleep. +- 🔀 Locally calculate state root for the genesis blocks in the blockchain tests instead of calling t8n ([#450](https://github.com/ethereum/execution-spec-tests/pull/450)). ### 🔧 EVM Tools diff --git a/docs/getting_started/debugging_t8n_tools.md b/docs/getting_started/debugging_t8n_tools.md index 865d8c48f5..718c54c820 100644 --- a/docs/getting_started/debugging_t8n_tools.md +++ b/docs/getting_started/debugging_t8n_tools.md @@ -14,7 +14,7 @@ In particular, a script `t8n.sh` is generated for each call to the `t8n` command For example, running: ```console -fill tests/berlin/eip2930_access_list/ --fork Berlin \ +fill tests/berlin/eip2930_access_list/ --fork Berlin -m blockchain_test \ --evm-dump-dir=/tmp/evm-dump ``` @@ -22,56 +22,37 @@ will produce the directory structure: ```text 📂 /tmp/evm-dump -└── 📂 blockchain_tests - └── 📂 berlin__eip2930_access_list__test_acl__test_access_list - └── 📂 fork_Berlin - ├── 📂 0 - │   ├── 📄 args.py - │   ├── 📂 input - │   │   ├── 📄 alloc.json - │   │   ├── 📄 env.json - │   │   └── 📄 txs.json - │   ├── 📂 output - │   │   ├── 📄 alloc.json - │   │   ├── 📄 result.json - │   │   └── 📄 txs.rlp - │   ├── 📄 returncode.txt - │   ├── 📄 stderr.txt - │   ├── 📄 stdin.txt - │   ├── 📄 stdout.txt - │   └── 📄 t8n.sh - └── 📂 1 - ├── 📄 args.py - ├── 📂 input - │   ├── 📄 alloc.json - │   ├── 📄 env.json - │   └── 📄 txs.json - ├── 📂 output - │   ├── 📄 alloc.json - │   ├── 📄 result.json - │   └── 📄 txs.rlp - ├── 📄 returncode.txt - ├── 📄 stderr.txt - ├── 📄 stdin.txt - ├── 📄 stdout.txt - └── 📄 t8n.sh +└── 📂 berlin__eip2930_access_list__test_acl__test_access_list + └── 📂 fork_Berlin_blockchain_test + └── 📂 0 +    ├── 📄 args.py +    ├── 📂 input +    │   ├── 📄 alloc.json +    │   ├── 📄 env.json +    │   └── 📄 txs.json +    ├── 📂 output +    │   ├── 📄 alloc.json +    │   ├── 📄 result.json +    │   └── 📄 txs.rlp +    ├── 📄 returncode.txt +    ├── 📄 stderr.txt +    ├── 📄 stdin.txt +    ├── 📄 stdout.txt +    └── 📄 t8n.sh ``` -where the directories `0` and `1` correspond to the different calls made to the `t8n` tool executed during the test: +where the directory `0` is the starting index of the different calls made to the `t8n` tool executed during the test, and since the test only contains one block, there is only one directory present. -- `0` corresponds to the call used to calculate the state root of the test's initial alloc (which is why it has an empty transaction list). -- `1` corresponds to the call used to execute the first transaction or block from the test. - -Note, there may be more directories present `2`, `3`, `4`,... if the test executes more transactions/blocks. +Note, there may be more directories present `1`, `2`, `3`,... if the test executes more blocks. Each directory contains files containing information corresponding to the call, for example, the `args.py` file contains the arguments passed to the `t8n` command and the `output/alloc.json` file contains the output of the `t8n` command's `--output-alloc` flag. ### The `t8n.sh` Script -The `t8n.sh` script written to the debug directory can be used to reproduce a specific call made to the `t8n` command during the test session. For example, if a Besu `t8n-server` has been started on port `3001`, the request made by the test for first transaction can be reproduced as: +The `t8n.sh` script written to the debug directory can be used to reproduce a specific call made to the `t8n` command during the test session. For example, if a Besu `t8n-server` has been started on port `3001`, the request made by the test for first block can be reproduced as: ```console -/tmp/besu/test_access_list_fork_Berlin/1/t8n.sh 3001 +/tmp/besu/test_access_list_fork_Berlin/0/t8n.sh 3001 ``` which writes the response the from the `t8n-server` to the console output: @@ -110,7 +91,7 @@ The `--verify-fixtures` flag can be used to run go-ethereum's `evm blocktest` co For example, running: ```console -fill tests/berlin/eip2930_access_list/ --fork Berlin \ +fill tests/berlin/eip2930_access_list/ --fork Berlin -m blockchain_test \ --evm-dump-dir==/tmp/evm-dump \ --evm-bin=../evmone/build/bin/evmone-t8n \ --verify-fixtures-bin=../go-ethereum/build/bin/evm \ @@ -121,25 +102,24 @@ will additionally run the `evm blocktest` command on every JSON fixture file and ```text 📂 /tmp/evm-dump -└── 📂 blockchain_tests - └── 📂 berlin__eip2930_access_list__test_acl__test_access_list - ├── 📄 fixtures.json - ├── 📂 fork_Berlin - │   ├── 📂 0 - │   │   ├── 📄 args.py - │   │   ├── 📂 input - │   │   │   ├── 📄 alloc.json - │   │   │   ├── 📄 env.json - │   │   │   └── 📄 txs.json - │   │   ├── 📂 output - │   │   │   ├── 📄 alloc.json - │ ... ... ... - │ - ├── 📄 verify_fixtures_args.py - ├── 📄 verify_fixtures_returncode.txt - ├── 📄 verify_fixtures.sh - ├── 📄 verify_fixtures_stderr.txt - └── 📄 verify_fixtures_stdout.txt +└── 📂 berlin__eip2930_access_list__test_acl__test_access_list + ├── 📄 fixtures.json + ├── 📂 fork_Berlin_blockchain_test + │   ├── 📂 0 + │   │   ├── 📄 args.py + │   │   ├── 📂 input + │   │   │   ├── 📄 alloc.json + │   │   │   ├── 📄 env.json + │   │   │   └── 📄 txs.json + │   │   ├── 📂 output + │   │   │   ├── 📄 alloc.json + │ ... ... ... + │ + ├── 📄 verify_fixtures_args.py + ├── 📄 verify_fixtures_returncode.txt + ├── 📄 verify_fixtures.sh + ├── 📄 verify_fixtures_stderr.txt + └── 📄 verify_fixtures_stdout.txt ``` where the `verify_fixtures.sh` script can be used to reproduce the `evm blocktest` command. diff --git a/src/ethereum_test_tools/common/types.py b/src/ethereum_test_tools/common/types.py index 49523a1c7b..50a5910192 100644 --- a/src/ethereum_test_tools/common/types.py +++ b/src/ethereum_test_tools/common/types.py @@ -1,6 +1,7 @@ """ Useful types for generating Ethereum tests. """ + from copy import copy, deepcopy from dataclasses import dataclass, fields from itertools import count @@ -20,8 +21,10 @@ from coincurve.keys import PrivateKey, PublicKey from ethereum import rlp as eth_rlp -from ethereum.base_types import Uint +from ethereum.base_types import U256, Uint from ethereum.crypto.hash import keccak256 +from ethereum.frontier.fork_types import Account as FrontierAccount +from ethereum.frontier.state import State, set_account, set_storage, state_root from trie import HexaryTrie from ethereum_test_forks import Fork @@ -64,13 +67,11 @@ def __repr__(self) -> str: MIN_STORAGE_KEY_VALUE = -(2**255) -class Storage(SupportsJSON): +class Storage(SupportsJSON, dict): """ Definition of a storage in pre or post state of a test """ - data: Dict[int, int] - current_slot: Iterator[int] StorageDictType: ClassVar[TypeAlias] = Dict[ @@ -220,49 +221,43 @@ def key_value_to_string(value: int) -> str: hex_str = "0" + hex_str return "0x" + hex_str - def __init__(self, input: StorageDictType | "Storage" = {}, start_slot: int = 0): + def __init__(self, input: StorageDictType | "Storage" = {}, *, start_slot: int = 0): """ Initializes the storage using a given mapping which can have keys and values either as string or int. Strings must be valid decimal or hexadecimal (starting with 0x) numbers. """ - self.data = {} - for key in input: - value = Storage.parse_key_value(input[key]) - key = Storage.parse_key_value(key) - self.data[key] = value + super().__init__( + (Storage.parse_key_value(k), Storage.parse_key_value(v)) for k, v in input.items() + ) self.current_slot = count(start_slot) - def __len__(self) -> int: - """Returns number of elements in the storage""" - return len(self.data) - - def __iter__(self) -> Iterator[int]: - """Returns iterator of the storage""" - return iter(self.data) - - def __contains__(self, key: str | int | bytes) -> bool: + def __contains__(self, key: object) -> bool: """Checks for an item in the storage""" - key = Storage.parse_key_value(key) - return key in self.data + assert ( + isinstance(key, str) + or isinstance(key, int) + or isinstance(key, bytes) + or isinstance(key, SupportsBytes) + ) + return super().__contains__(Storage.parse_key_value(key)) - def __getitem__(self, key: str | int | bytes) -> int: + def __getitem__(self, key: str | int | bytes | SupportsBytes) -> int: """Returns an item from the storage""" - key = Storage.parse_key_value(key) - if key not in self.data: - raise KeyError() - return self.data[key] + return super().__getitem__(Storage.parse_key_value(key)) - def __setitem__(self, key: str | int | bytes, value: str | int | bytes): # noqa: SC200 + def __setitem__( + self, key: str | int | bytes | SupportsBytes, value: str | int | bytes | SupportsBytes + ): # noqa: SC200 """Sets an item in the storage""" - self.data[Storage.parse_key_value(key)] = Storage.parse_key_value(value) + super().__setitem__(Storage.parse_key_value(key), Storage.parse_key_value(value)) - def __delitem__(self, key: str | int | bytes): + def __delitem__(self, key: str | int | bytes | SupportsBytes): """Deletes an item from the storage""" - del self.data[Storage.parse_key_value(key)] + super().__delitem__(Storage.parse_key_value(key)) - def store_next(self, value: str | int | bytes) -> int: + def store_next(self, value: str | int | bytes | SupportsBytes) -> int: """ Stores a value in the storage and returns the key where the value is stored. @@ -278,9 +273,9 @@ def __json__(self, encoder: JSONEncoder) -> Mapping[str, str]: hex string formatting. """ res: Dict[str, str] = {} - for key in self.data: + for key, value in self.items(): key_repr = Storage.key_value_to_string(key) - val_repr = Storage.key_value_to_string(self.data[key]) + val_repr = Storage.key_value_to_string(value) if key_repr in res and val_repr != res[key_repr]: raise Storage.AmbiguousKeyValue( key_1=key_repr, val_1=res[key_repr], key_2=key, val_2=val_repr @@ -295,10 +290,10 @@ def contains(self, other: "Storage") -> bool: Used for comparison with test expected post state and alloc returned by the transition tool. """ - for key in other.data: - if key not in self.data: + for key in other: + if key not in self: return False - if self.data[key] != other.data[key]: + if self[key] != other[key]: return False return True @@ -310,14 +305,14 @@ def must_contain(self, address: Address, other: "Storage"): by the transition tool. Raises detailed exception when a difference is found. """ - for key in other.data: - if key not in self.data: + for key in other: + if key not in self: # storage[key]==0 is equal to missing storage if other[key] != 0: raise Storage.MissingKey(key=key) - elif self.data[key] != other.data[key]: + elif self[key] != other[key]: raise Storage.KeyValueMismatch( - address=address, key=key, want=self.data[key], got=other.data[key] + address=address, key=key, want=self[key], got=other[key] ) def must_be_equal(self, address: Address, other: "Storage"): @@ -325,24 +320,20 @@ def must_be_equal(self, address: Address, other: "Storage"): Succeeds only if "self" is equal to "other" storage. """ # Test keys contained in both storage objects - for key in self.data.keys() & other.data.keys(): - if self.data[key] != other.data[key]: + for key in self.keys() & other.keys(): + if self[key] != other[key]: raise Storage.KeyValueMismatch( - address=address, key=key, want=self.data[key], got=other.data[key] + address=address, key=key, want=self[key], got=other[key] ) # Test keys contained in either one of the storage objects - for key in self.data.keys() ^ other.data.keys(): - if key in self.data: - if self.data[key] != 0: - raise Storage.KeyValueMismatch( - address=address, key=key, want=self.data[key], got=0 - ) + for key in self.keys() ^ other.keys(): + if key in self: + if self[key] != 0: + raise Storage.KeyValueMismatch(address=address, key=key, want=self[key], got=0) - elif other.data[key] != 0: - raise Storage.KeyValueMismatch( - address=address, key=key, want=0, got=other.data[key] - ) + elif other[key] != 0: + raise Storage.KeyValueMismatch(address=address, key=key, want=0, got=other[key]) @dataclass(kw_only=True) @@ -583,10 +574,11 @@ class Alloc(dict, Mapping[Address, Account], SupportsJSON): """ def __init__(self, d: Mapping[FixedSizeBytesConvertible, Account | Dict] = {}): - for address, account in d.items(): - address = Address(address) - assert address not in self, f"Duplicate address in alloc: {address}" - self[address] = Account.from_dict(account) + super().__init__( + (Address(address), Account.from_dict(account)) for address, account in d.items() + ) + if len(self) != len(d): + raise Exception("Duplicate addresses in alloc") @classmethod def merge(cls, alloc_1: "Alloc", alloc_2: "Alloc") -> "Alloc": @@ -619,6 +611,33 @@ def __json__(self, encoder: JSONEncoder) -> Mapping[str, Any]: {Address(address): Account.from_dict(account) for address, account in self.items()} ) + def state_root(self) -> bytes: + """ + Returns the state root of the allocation. + """ + state = State() + for address, account in self.items(): + set_account( + state=state, + address=address, + account=FrontierAccount( + nonce=Uint(Number(account.nonce)) if account.nonce is not None else Uint(0), + balance=( + U256(Number(account.balance)) if account.balance is not None else U256(0) + ), + code=Bytes(account.code) if account.code is not None else b"", + ), + ) + if account.storage is not None: + for key, value in account.storage.items(): + set_storage( + state=state, + address=address, + key=Hash(key), + value=U256(Number(value)), + ) + return state_root(state) + def alloc_to_accounts(got_alloc: Dict[str, Any]) -> Mapping[str, Account]: """ diff --git a/src/ethereum_test_tools/spec/blockchain/blockchain_test.py b/src/ethereum_test_tools/spec/blockchain/blockchain_test.py index a9779f4fcf..8b0ffbdbb3 100644 --- a/src/ethereum_test_tools/spec/blockchain/blockchain_test.py +++ b/src/ethereum_test_tools/spec/blockchain/blockchain_test.py @@ -143,11 +143,7 @@ def make_genesis( ) if empty_accounts := pre_alloc.empty_accounts(): raise Exception(f"Empty accounts in pre state: {empty_accounts}") - new_alloc, state_root = t8n.calc_state_root( - alloc=to_json(pre_alloc), - fork=fork, - debug_output_path=self.get_next_transition_tool_output_path(), - ) + state_root = pre_alloc.state_root() genesis = FixtureHeader( parent_hash=Hash(0), ommers_hash=Hash(EmptyOmmersRoot), @@ -179,7 +175,7 @@ def make_genesis( withdrawals=env.withdrawals, ) - return Alloc(new_alloc), genesis_rlp, genesis + return pre_alloc, genesis_rlp, genesis def generate_block_data( self, diff --git a/src/ethereum_test_tools/tests/test_types.py b/src/ethereum_test_tools/tests/test_types.py index 54b130eac7..73c13ecf05 100644 --- a/src/ethereum_test_tools/tests/test_types.py +++ b/src/ethereum_test_tools/tests/test_types.py @@ -35,18 +35,18 @@ def test_storage(): """ s = Storage({"10": "0x10"}) - assert 10 in s.data - assert s.data[10] == 16 + assert 10 in s + assert s[10] == 16 s = Storage({"10": "10"}) - assert 10 in s.data - assert s.data[10] == 10 + assert 10 in s + assert s[10] == 10 s = Storage({10: 10}) - assert 10 in s.data - assert s.data[10] == 10 + assert 10 in s + assert s[10] == 10 iter_s = iter(Storage({10: 20, "11": "21"})) assert next(iter_s) == 10 @@ -54,8 +54,8 @@ def test_storage(): s["10"] = "0x10" s["0x10"] = "10" - assert s.data[10] == 16 - assert s.data[16] == 10 + assert s[10] == 16 + assert s[16] == 10 assert "10" in s assert "0xa" in s @@ -67,8 +67,8 @@ def test_storage(): assert 10 not in s s = Storage({-1: -1, -2: -2}) - assert s.data[-1] == -1 - assert s.data[-2] == -2 + assert s[-1] == -1 + assert s[-2] == -2 d = to_json(s) assert ( d["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"] diff --git a/src/evm_transition_tool/tests/test_evaluate.py b/src/evm_transition_tool/tests/test_evaluate.py index 57e15060cf..dbd010d23f 100644 --- a/src/evm_transition_tool/tests/test_evaluate.py +++ b/src/evm_transition_tool/tests/test_evaluate.py @@ -7,6 +7,7 @@ import pytest from ethereum_test_forks import Berlin, Fork, Istanbul, London +from ethereum_test_tools.common import Alloc from evm_transition_tool import GethTransitionTool, TransitionTool FIXTURES_ROOT = Path(os.path.join("src", "evm_transition_tool", "tests", "fixtures")) @@ -76,7 +77,7 @@ class TestEnv: env = TestEnv() env.base_fee = base_fee - assert t8n.calc_state_root(alloc=alloc, fork=fork)[1].startswith(hash) + assert Alloc(alloc).state_root().startswith(hash) @pytest.mark.parametrize("evm_tool", [GethTransitionTool]) diff --git a/src/evm_transition_tool/transition_tool.py b/src/evm_transition_tool/transition_tool.py index bbd8f5f44c..4344526536 100644 --- a/src/evm_transition_tool/transition_tool.py +++ b/src/evm_transition_tool/transition_tool.py @@ -1,6 +1,7 @@ """ Transition tool abstract class. """ + import json import os import shutil @@ -562,49 +563,6 @@ def evaluate( debug_output_path=debug_output_path, ) - def calc_state_root( - self, *, alloc: Any, fork: Fork, debug_output_path: str = "" - ) -> Tuple[Dict, bytes]: - """ - Calculate the state root for the given `alloc`. - """ - env: Dict[str, Any] = { - "currentCoinbase": "0x0000000000000000000000000000000000000000", - "currentDifficulty": "0x0", - "currentGasLimit": "0x0", - "currentNumber": "0", - "currentTimestamp": "0", - } - - if fork.header_base_fee_required(0, 0): - env["currentBaseFee"] = "7" - - if fork.header_prev_randao_required(0, 0): - env["currentRandom"] = "0" - - if fork.header_withdrawals_required(0, 0): - env["withdrawals"] = [] - - if fork.header_excess_blob_gas_required(0, 0): - env["currentExcessBlobGas"] = "0" - - if fork.header_beacon_root_required(0, 0): - env[ - "parentBeaconBlockRoot" - ] = "0x0000000000000000000000000000000000000000000000000000000000000000" - - new_alloc, result = self.evaluate( - alloc=alloc, - txs=[], - env=env, - fork_name=fork.transition_tool_name(block_number=0, timestamp=0), - debug_output_path=debug_output_path, - ) - state_root = result.get("stateRoot") - if state_root is None or not isinstance(state_root, str): - raise Exception("Unable to calculate state root") - return new_alloc, bytes.fromhex(state_root[2:]) - def verify_fixture( self, fixture_format: FixtureFormats, fixture_path: Path, debug_output_path: Optional[Path] ):