From ce46627a277a7895b4718f96ed04a9c22c1b8d77 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 22:57:07 +0000 Subject: [PATCH 01/25] refactor(fw): Split EOF types tests --- src/ethereum_test_types/tests/test_eof_v1.py | 18 ++++++++++++++++++ src/ethereum_test_types/tests/test_types.py | 2 -- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ethereum_test_types/tests/test_eof_v1.py b/src/ethereum_test_types/tests/test_eof_v1.py index 506d039db0..277295d1e7 100644 --- a/src/ethereum_test_types/tests/test_eof_v1.py +++ b/src/ethereum_test_types/tests/test_eof_v1.py @@ -6,6 +6,9 @@ import pytest +from ethereum_test_base_types import to_json +from ethereum_test_base_types.pydantic import CopyValidateModel + from ..eof.v1 import AutoSection, Container, Section, SectionKind test_cases: List[Tuple[str, Container, str]] = [ @@ -819,3 +822,18 @@ def remove_comments_from_string(input_string): # Join the cleaned lines back into a single string cleaned_string = "\n".join(cleaned_lines) return cleaned_string + + +@pytest.mark.parametrize( + "model", + [ + Container(), + ], + ids=lambda model: model.__class__.__name__, +) +def test_model_copy(model: CopyValidateModel): + """ + Test that the copy method returns a correct copy of the model. + """ + assert to_json(model.copy()) == to_json(model) + assert model.copy().model_fields_set == model.model_fields_set diff --git a/src/ethereum_test_types/tests/test_types.py b/src/ethereum_test_types/tests/test_types.py index 78583d0995..8d49f7c2ab 100644 --- a/src/ethereum_test_types/tests/test_types.py +++ b/src/ethereum_test_types/tests/test_types.py @@ -10,7 +10,6 @@ from ethereum_test_base_types import Address, TestPrivateKey, to_json from ethereum_test_base_types.pydantic import CopyValidateModel -from ..eof.v1 import Container from ..types import ( AccessList, Account, @@ -877,7 +876,6 @@ def test_parsing(json_str: str, type_adapter: TypeAdapter, expected: Any): "model", [ Environment(), - Container(), ], ids=lambda model: model.__class__.__name__, ) From 391b2cc917385dbc2190d6282068a5a91f73aad9 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 22:58:10 +0000 Subject: [PATCH 02/25] feat(ethereum_test_vm): Add `EVMCodeType` --- src/ethereum_test_vm/__init__.py | 4 +++- src/ethereum_test_vm/evm_types.py | 21 +++++++++++++++++++++ whitelist.txt | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/ethereum_test_vm/evm_types.py diff --git a/src/ethereum_test_vm/__init__.py b/src/ethereum_test_vm/__init__.py index 58b4640b5b..044293e095 100644 --- a/src/ethereum_test_vm/__init__.py +++ b/src/ethereum_test_vm/__init__.py @@ -3,13 +3,15 @@ """ from .bytecode import Bytecode +from .evm_types import EVMCodeType from .opcode import Macro, Macros, Opcode, OpcodeCallArg, Opcodes, UndefinedOpcodes __all__ = ( "Bytecode", - "Opcode", + "EVMCodeType", "Macro", "Macros", + "Opcode", "OpcodeCallArg", "Opcodes", "UndefinedOpcodes", diff --git a/src/ethereum_test_vm/evm_types.py b/src/ethereum_test_vm/evm_types.py new file mode 100644 index 0000000000..274077acda --- /dev/null +++ b/src/ethereum_test_vm/evm_types.py @@ -0,0 +1,21 @@ +""" +EVM types definitions. +""" + + +from enum import Enum + + +class EVMCodeType(str, Enum): + """ + Enum representing the type of EVM code that is supported in a given fork. + """ + + LEGACY = "legacy" + EOF_V1 = "eof_v1" + + def __str__(self) -> str: + """ + Return the name of the EVM code type. + """ + return self.name diff --git a/whitelist.txt b/whitelist.txt index 6ff9a86747..64ea044cc2 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -128,6 +128,7 @@ EthereumJS ethereum's evaluatable evm +EVMCodeType evmone Evmone executables From 8e29f6982d082256cd376d45277c4987e429df95 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 31 Jul 2024 14:55:49 +0000 Subject: [PATCH 03/25] feat(types): Add `evm_code_type` as optional parameter to `deploy_contract` --- src/ethereum_test_types/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ethereum_test_types/types.py b/src/ethereum_test_types/types.py index 5474ee4ac2..05cd14602d 100644 --- a/src/ethereum_test_types/types.py +++ b/src/ethereum_test_types/types.py @@ -47,6 +47,7 @@ ) from ethereum_test_exceptions import TransactionException from ethereum_test_forks import Fork +from ethereum_test_vm import EVMCodeType # Sentinel classes @@ -269,6 +270,7 @@ def deploy_contract( balance: NumberConvertible = 0, nonce: NumberConvertible = 1, address: Address | None = None, + evm_code_type: EVMCodeType | None = None, label: str | None = None, ) -> Address: """ From 6ad72213450981969c78fde0240f22aea53751ef Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 22:58:46 +0000 Subject: [PATCH 04/25] feat(ethereum_test_forks): Add `evm_code_types` to forks feat(ethereum_test_forks): Add `call_opcodes` function --- src/ethereum_test_forks/base_fork.py | 22 +++++++- src/ethereum_test_forks/forks/forks.py | 72 +++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 2e845caa4a..b1c85f46a0 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -3,10 +3,12 @@ """ from abc import ABC, ABCMeta, abstractmethod -from typing import Any, ClassVar, List, Mapping, Optional, Protocol, Type +from typing import Any, ClassVar, List, Mapping, Optional, Protocol, Tuple, Type from semver import Version +from ethereum_test_vm import EVMCodeType, Opcodes + from .base_decorators import prefer_transition_to_method @@ -260,6 +262,24 @@ def engine_forkchoice_updated_version( """ pass + @classmethod + @abstractmethod + def evm_code_types(cls, block_number: int = 0, timestamp: int = 0) -> List[EVMCodeType]: + """ + Returns the list of EVM code types supported by the fork. + """ + pass + + @classmethod + @abstractmethod + def call_opcodes( + cls, block_number: int = 0, timestamp: int = 0 + ) -> List[Tuple[Opcodes, EVMCodeType]]: + """ + Returns the list of tuples with the call opcodes and its corresponding EVM code type. + """ + pass + # Meta information about the fork @classmethod def name(cls) -> str: diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 18ce8fa33b..6666698e8e 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -5,10 +5,12 @@ from hashlib import sha256 from os.path import realpath from pathlib import Path -from typing import List, Mapping, Optional +from typing import List, Mapping, Optional, Tuple from semver import Version +from ethereum_test_vm import EVMCodeType, Opcodes + from ..base_fork import BaseFork CURRENT_FILE = Path(realpath(__file__)) @@ -170,6 +172,25 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[int]: """ return [] + @classmethod + def evm_code_types(cls, block_number: int = 0, timestamp: int = 0) -> List[EVMCodeType]: + """ + At Genesis, only legacy EVM code is supported. + """ + return [EVMCodeType.LEGACY] + + @classmethod + def call_opcodes( + cls, block_number: int = 0, timestamp: int = 0 + ) -> List[Tuple[Opcodes, EVMCodeType]]: + """ + Returns the list of call opcodes supported by the fork. + """ + return [ + (Opcodes.CALL, EVMCodeType.LEGACY), + (Opcodes.CALLCODE, EVMCodeType.LEGACY), + ] + @classmethod def pre_allocation(cls) -> Mapping: """ @@ -201,6 +222,17 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[int]: """ return [1, 2, 3, 4] + super(Homestead, cls).precompiles(block_number, timestamp) + @classmethod + def call_opcodes( + cls, block_number: int = 0, timestamp: int = 0 + ) -> List[Tuple[Opcodes, EVMCodeType]]: + """ + At Homestead, DELEGATECALL opcode was introduced. + """ + return [(Opcodes.DELEGATECALL, EVMCodeType.LEGACY),] + super( + Homestead, cls + ).call_opcodes(block_number, timestamp) + class Byzantium(Homestead): """ @@ -224,6 +256,17 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[int]: """ return [5, 6, 7, 8] + super(Byzantium, cls).precompiles(block_number, timestamp) + @classmethod + def call_opcodes( + cls, block_number: int = 0, timestamp: int = 0 + ) -> List[Tuple[Opcodes, EVMCodeType]]: + """ + At Byzantium, STATICCALL opcode was introduced. + """ + return [(Opcodes.STATICCALL, EVMCodeType.LEGACY),] + super( + Byzantium, cls + ).call_opcodes(block_number, timestamp) + class Constantinople(Byzantium): """ @@ -625,6 +668,33 @@ class CancunEIP7692( # noqa: SC200 Cancun + EIP-7692 (EOF) fork """ + @classmethod + def evm_code_types(cls, block_number: int = 0, timestamp: int = 0) -> List[EVMCodeType]: + """ + EOF V1 is supported starting from this fork. + """ + return super(CancunEIP7692, cls,).evm_code_types( # noqa: SC200 + block_number, + timestamp, + ) + [EVMCodeType.EOF_V1] + + @classmethod + def call_opcodes( + cls, block_number: int = 0, timestamp: int = 0 + ) -> List[Tuple[Opcodes, EVMCodeType]]: + """ + EOF V1 introduces EXTCALL, EXTSTATICCALL, EXTDELEGATECALL. + """ + return [ + (Opcodes.EXTCALL, EVMCodeType.EOF_V1), + (Opcodes.EXTSTATICCALL, EVMCodeType.EOF_V1), + (Opcodes.EXTDELEGATECALL, EVMCodeType.EOF_V1), + ] + super( + CancunEIP7692, cls # noqa: SC200 + ).call_opcodes( + block_number, timestamp + ) + @classmethod def is_deployed(cls) -> bool: """ From 0e66b34f38cd3630d5d7517f139654e317dfac09 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 22:59:37 +0000 Subject: [PATCH 05/25] feat(plugins/forks): Add `with_all_evm_code_types` marker --- docs/writing_tests/test_markers.md | 42 ++++++++++++++++++++++++++++-- src/pytest_plugins/forks/forks.py | 7 +++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/writing_tests/test_markers.md b/docs/writing_tests/test_markers.md index ad2a0fb884..012718e1f8 100644 --- a/docs/writing_tests/test_markers.md +++ b/docs/writing_tests/test_markers.md @@ -53,7 +53,7 @@ import pytest @pytest.mark.with_all_tx_types @pytest.mark.valid_from("Berlin") -def test_something_with_all_tx_types(tx_type): +def test_something_with_all_tx_types(tx_type: int): pass ``` @@ -74,12 +74,50 @@ import pytest @pytest.mark.with_all_precompiles @pytest.mark.valid_from("Shanghai") -def test_something_with_all_precompiles(precompile): +def test_something_with_all_precompiles(precompile: int): pass ``` In this example, the test will be parameterized for parameter `precompile` with values `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]` for fork Shanghai, but with values `[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]` for fork Cancun (because of EIP-4844). +### pytest.mark.with_all_evm_code_types + +This marker is used to automatically parameterize a test with all EVM code types that are valid for the fork being tested. + +```python +import pytest + +@pytest.mark.with_all_evm_code_types +@pytest.mark.valid_from("Frontier") +def test_something_with_all_evm_code_types(pre: Alloc): + pass +``` + +In this example, the test will be parameterized for parameter `evm_code_type` only with value `[EVMCodeType.LEGACY]` starting on fork Frontier, and eventually it will be parametrized with with values `[EVMCodeType.LEGACY, EVMCodeType.EOF_V1]` on the EOF activation fork. + +In all calls to `pre.deploy_contract`, if the code parameter is `Bytecode` type, and `evm_code_type==EVMCodeType.EOF_V1`, the bytecode will be automatically wrapped in an EOF V1 container. + +Code wrapping might fail in the following circumstances: + +- The code contains invalid EOF V1 opcodes. +- The code does not end with a valid EOF V1 terminating opcode (such as `Op.STOP` or `Op.REVERT` or `Op.RETURN`). + +In the case where the code wrapping fails, `evm_code_type` can be added as a parameter to the test and the bytecode can be dynamically modified to be compatible with the EOF V1 container. + +```python +import pytest + +@pytest.mark.with_all_evm_code_types +@pytest.mark.valid_from("Frontier") +def test_something_with_all_evm_code_types(pre: Alloc, evm_code_type: EVMCodeType): + code = Op.SSTORE(1, 1) + if evm_code_type == EVMCodeType.EOF_V1: + # Modify the bytecode to be compatible with EOF V1 container + code += Op.STOP + pre.deploy_contract(code) + ... +``` + ## Other Markers ### pytest.mark.slow diff --git a/src/pytest_plugins/forks/forks.py b/src/pytest_plugins/forks/forks.py index ff7f180e4e..ad745665d3 100644 --- a/src/pytest_plugins/forks/forks.py +++ b/src/pytest_plugins/forks/forks.py @@ -153,6 +153,13 @@ def add_values(self, metafunc: Metafunc, fork_parametrizer: ForkParametrizer) -> fork_attribute_name="precompiles", parameter_name="precompile", ), + CovariantDescriptor( + marker_name="with_all_evm_code_types", + description="marks a test to be parametrized for all EVM code types at parameter named" + " evm_code_type of type EVMCodeType", + fork_attribute_name="evm_code_types", + parameter_name="evm_code_type", + ), ] From 67fd151bb4fa813625b25fc888c61b77dbb46acd Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 23:00:26 +0000 Subject: [PATCH 06/25] feat(plugins/filler): Add `evm_code_type` fixture --- src/pytest_plugins/filler/pre_alloc.py | 44 ++++++++++++++++++- .../filler/tests/test_pre_alloc.py | 18 ++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/pytest_plugins/filler/pre_alloc.py b/src/pytest_plugins/filler/pre_alloc.py index c9741cffac..3b868e6912 100644 --- a/src/pytest_plugins/filler/pre_alloc.py +++ b/src/pytest_plugins/filler/pre_alloc.py @@ -28,6 +28,8 @@ ) from ethereum_test_types import EOA from ethereum_test_types import Alloc as BaseAlloc +from ethereum_test_types.eof.v1 import Container +from ethereum_test_vm import EVMCodeType CONTRACT_START_ADDRESS_DEFAULT = 0x1000 CONTRACT_ADDRESS_INCREMENTS_DEFAULT = 0x100 @@ -64,6 +66,15 @@ def pytest_addoption(parser: pytest.Parser): type=str, help="The address increment value to each deployed contract by a test.", ) + pre_alloc_group.addoption( + "--evm-code-type", + action="store", + dest="evm_code_type", + default=None, + type=EVMCodeType, + choices=list(EVMCodeType), + help="Type of EVM code to deploy in each test by default.", + ) class AllocMode(IntEnum): @@ -83,6 +94,7 @@ class Alloc(BaseAlloc): _alloc_mode: AllocMode = PrivateAttr(...) _contract_address_iterator: Iterator[Address] = PrivateAttr(...) _eoa_iterator: Iterator[EOA] = PrivateAttr(...) + _evm_code_type: EVMCodeType | None = PrivateAttr(None) def __init__( self, @@ -90,6 +102,7 @@ def __init__( alloc_mode: AllocMode, contract_address_iterator: Iterator[Address], eoa_iterator: Iterator[EOA], + evm_code_type: EVMCodeType | None = None, **kwargs, ): """ @@ -99,6 +112,7 @@ def __init__( self._alloc_mode = alloc_mode self._contract_address_iterator = contract_address_iterator self._eoa_iterator = eoa_iterator + self._evm_code_type = evm_code_type def __setitem__(self, address: Address | FixedSizeBytesConvertible, account: Account | None): """ @@ -108,6 +122,19 @@ def __setitem__(self, address: Address | FixedSizeBytesConvertible, account: Acc raise ValueError("Cannot set items in strict mode") super().__setitem__(address, account) + def code_pre_processor( + self, code: BytesConvertible, *, evm_code_type: EVMCodeType | None + ) -> BytesConvertible: + """ + Pre-processes the code before setting it. + """ + if evm_code_type is None: + evm_code_type = self._evm_code_type + if evm_code_type == EVMCodeType.EOF_V1: + if not isinstance(code, Container): + return Container.Code(code) + return code + def deploy_contract( self, code: BytesConvertible, @@ -116,6 +143,7 @@ def deploy_contract( balance: NumberConvertible = 0, nonce: NumberConvertible = 1, address: Address | None = None, + evm_code_type: EVMCodeType | None = None, label: str | None = None, ) -> Address: """ @@ -139,7 +167,7 @@ def deploy_contract( Account( nonce=nonce, balance=balance, - code=code, + code=self.code_pre_processor(code, evm_code_type=evm_code_type), storage=storage, ), ) @@ -247,11 +275,24 @@ def eoa_iterator() -> Iterator[EOA]: return iter(eoa_by_index(i).copy() for i in count()) +@pytest.fixture(autouse=True) +def evm_code_type(request: pytest.FixtureRequest) -> EVMCodeType: + """ + Returns the default EVM code type for all tests (LEGACY). + """ + parameter_evm_code_type = request.config.getoption("evm_code_type") + if parameter_evm_code_type is not None: + assert type(parameter_evm_code_type) is EVMCodeType, "Invalid EVM code type" + return parameter_evm_code_type + return EVMCodeType.LEGACY + + @pytest.fixture(scope="function") def pre( alloc_mode: AllocMode, contract_address_iterator: Iterator[Address], eoa_iterator: Iterator[EOA], + evm_code_type: EVMCodeType, ) -> Alloc: """ Returns the default pre allocation for all tests (Empty alloc). @@ -260,4 +301,5 @@ def pre( alloc_mode=alloc_mode, contract_address_iterator=contract_address_iterator, eoa_iterator=eoa_iterator, + evm_code_type=evm_code_type, ) diff --git a/src/pytest_plugins/filler/tests/test_pre_alloc.py b/src/pytest_plugins/filler/tests/test_pre_alloc.py index 5c01d4ca63..e144fb597e 100644 --- a/src/pytest_plugins/filler/tests/test_pre_alloc.py +++ b/src/pytest_plugins/filler/tests/test_pre_alloc.py @@ -5,6 +5,7 @@ import pytest from ethereum_test_base_types import Address, TestAddress, TestAddress2 +from ethereum_test_vm import EVMCodeType from ethereum_test_vm import Opcodes as Op from ..pre_alloc import ( @@ -25,10 +26,11 @@ pytest.mark.parametrize("alloc_mode", [AllocMode.STRICT, AllocMode.PERMISSIVE]), pytest.mark.parametrize("contract_start_address", [CONTRACT_START_ADDRESS_DEFAULT]), pytest.mark.parametrize("contract_address_increments", [CONTRACT_ADDRESS_INCREMENTS_DEFAULT]), + pytest.mark.parametrize("evm_code_type", [EVMCodeType.LEGACY, EVMCodeType.EOF_V1]), ] -def test_alloc_deploy_contract(pre: Alloc): +def test_alloc_deploy_contract(pre: Alloc, evm_code_type: EVMCodeType): """ Test `Alloc.deploy_contract` functionallity. """ @@ -44,8 +46,18 @@ def test_alloc_deploy_contract(pre: Alloc): pre_contract_2_account = pre[contract_2] assert pre_contract_1_account is not None assert pre_contract_2_account is not None - assert pre_contract_1_account.code == bytes.fromhex("600160005500") - assert pre_contract_2_account.code == bytes.fromhex("600260005500") + if evm_code_type == EVMCodeType.LEGACY: + assert pre_contract_1_account.code == bytes.fromhex("600160005500") + assert pre_contract_2_account.code == bytes.fromhex("600260005500") + elif evm_code_type == EVMCodeType.EOF_V1: + assert pre_contract_1_account.code == ( + b"\xef\x00\x01\x01\x00\x04\x02\x00\x01\x00\x06\x04\x00\x00\x00\x00\x80\x00" + + b"\x02`\x01`\x00U\x00" + ) + assert pre_contract_2_account.code == ( + b"\xef\x00\x01\x01\x00\x04\x02\x00\x01\x00\x06\x04\x00\x00\x00\x00\x80\x00" + + b"\x02`\x02`\x00U\x00" + ) def test_alloc_fund_sender(pre: Alloc): From 2cee31ac099535ee1c539b39b898499d29414a6e Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Tue, 30 Jul 2024 00:22:40 +0000 Subject: [PATCH 07/25] feat(plugins/forks): Add `with_all_call_opcodes` marker --- docs/writing_tests/test_markers.md | 17 +++++++++++++++++ src/pytest_plugins/forks/forks.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/writing_tests/test_markers.md b/docs/writing_tests/test_markers.md index 012718e1f8..13b7aa963e 100644 --- a/docs/writing_tests/test_markers.md +++ b/docs/writing_tests/test_markers.md @@ -118,6 +118,23 @@ def test_something_with_all_evm_code_types(pre: Alloc, evm_code_type: EVMCodeTyp ... ``` +### pytest.mark.with_all_call_opcodes + +This marker is used to automatically parameterize a test with all EVM call opcodes that are valid for the fork being tested. + +```python +import pytest + +@pytest.mark.with_all_call_opcodes +@pytest.mark.valid_from("Frontier") +def test_something_with_all_call_opcodes(pre: Alloc, call_opcode: Op): + ... +``` + +In this example, the test will be parametrized for parameter `call_opcode` with values `[Op.CALL, Op.CALLCODE]` starting on fork Frontier, `[Op.CALL, Op.CALLCODE, Op.DELEGATECALL]` on fork Homestead, `[Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL]` on fork Byzantium, and eventually it will be parametrized with with values `[Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL, Op.EXTCALL, Op.EXTSTATICCALL, Op.EXTDELEGATECALL]` on the EOF activation fork. + +Parameter `evm_code_type` will also be parametrized with the correct EVM code type for the opcode under test. + ## Other Markers ### pytest.mark.slow diff --git a/src/pytest_plugins/forks/forks.py b/src/pytest_plugins/forks/forks.py index ad745665d3..00c5ceb16d 100644 --- a/src/pytest_plugins/forks/forks.py +++ b/src/pytest_plugins/forks/forks.py @@ -81,19 +81,36 @@ def get_parameter_names(self) -> List[str]: """ Return the parameter names for the test case. """ - return ["fork"] + [p.name for p in self.fork_covariant_parameters] + parameter_names = ["fork"] + for p in self.fork_covariant_parameters: + if "," in p.name: + parameter_names.extend(p.name.split(",")) + else: + parameter_names.append(p.name) + return parameter_names def get_parameter_values(self) -> List[Any]: """ Return the parameter values for the test case. """ - return [ - pytest.param(*params, marks=[self.mark] if self.mark else []) + param_value_combinations = [ + params for params in itertools.product( [self.fork], *[p.values for p in self.fork_covariant_parameters], ) ] + for i in range(len(param_value_combinations)): + # if the parameter is a tuple, we need to flatten it + param_value_combinations[i] = list( + itertools.chain.from_iterable( + [v] if not isinstance(v, tuple) else v for v in param_value_combinations[i] + ) + ) + return [ + pytest.param(*params, marks=[self.mark] if self.mark else []) + for params in param_value_combinations + ] @dataclass(kw_only=True) @@ -160,6 +177,13 @@ def add_values(self, metafunc: Metafunc, fork_parametrizer: ForkParametrizer) -> fork_attribute_name="evm_code_types", parameter_name="evm_code_type", ), + CovariantDescriptor( + marker_name="with_all_call_opcodes", + description="marks a test to be parametrized for all *CALL opcodes at parameter named" + " call_opcode, and also the appropriate EVM code type at parameter named evm_code_type", + fork_attribute_name="call_opcodes", + parameter_name="call_opcode,evm_code_type", + ), ] From df307a0864c3b16a476c2f6f7dd2fcd6ccae9764 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Tue, 30 Jul 2024 21:54:05 +0000 Subject: [PATCH 08/25] feat(fw): add `call_return_code` --- src/ethereum_test_tools/__init__.py | 2 ++ src/ethereum_test_vm/__init__.py | 2 ++ src/ethereum_test_vm/helpers.py | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/ethereum_test_vm/helpers.py diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 5d5750b7c8..68a1f7b291 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -64,6 +64,7 @@ OpcodeCallArg, Opcodes, UndefinedOpcodes, + call_return_code, ) from .code import ( @@ -135,6 +136,7 @@ "Yul", "YulCompiler", "add_kzg_version", + "call_return_code", "ceiling_division", "compute_create_address", "compute_create2_address", diff --git a/src/ethereum_test_vm/__init__.py b/src/ethereum_test_vm/__init__.py index 044293e095..d68efb068e 100644 --- a/src/ethereum_test_vm/__init__.py +++ b/src/ethereum_test_vm/__init__.py @@ -4,6 +4,7 @@ from .bytecode import Bytecode from .evm_types import EVMCodeType +from .helpers import call_return_code from .opcode import Macro, Macros, Opcode, OpcodeCallArg, Opcodes, UndefinedOpcodes __all__ = ( @@ -15,4 +16,5 @@ "OpcodeCallArg", "Opcodes", "UndefinedOpcodes", + "call_return_code", ) diff --git a/src/ethereum_test_vm/helpers.py b/src/ethereum_test_vm/helpers.py new file mode 100644 index 0000000000..9a8879da3d --- /dev/null +++ b/src/ethereum_test_vm/helpers.py @@ -0,0 +1,20 @@ +""" +Helper functions for the EVM. +""" + +from .opcode import Opcodes as Op + + +def call_return_code(opcode: Op, success: bool, *, revert: bool = False) -> int: + """ + Returns the return code for a CALL operation. + """ + if opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL]: + return int(success) + elif opcode in [Op.EXTCALL, Op.EXTDELEGATECALL, Op.EXTSTATICCALL]: + if success: + return 0 + if revert: + return 2 + return 1 + raise ValueError(f"Not a call opcode: {opcode}") From 54ad663c4232481a8fe7d54236e9b1ce2f831407 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 31 Jul 2024 15:42:53 +0000 Subject: [PATCH 09/25] fix(plugins): Add pre-alloc plugin to help --- src/pytest_plugins/help/help.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest_plugins/help/help.py b/src/pytest_plugins/help/help.py index 26feaab010..34e4fdbcd0 100644 --- a/src/pytest_plugins/help/help.py +++ b/src/pytest_plugins/help/help.py @@ -50,6 +50,7 @@ def show_test_help(config): "fork range", "filler location", "defining debug", + "pre-allocation behavior", ] elif pytest_ini.name in [ "pytest-consume.ini", From c0b2344873b0d6efbb9eb3de6ece66a25833e1de Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Tue, 30 Jul 2024 21:19:25 +0000 Subject: [PATCH 10/25] fix(tests): Incorrect import --- .../eip7620_eof_create/test_subcontainer_validation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py index e3d83c11e9..fb9321e863 100644 --- a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py @@ -6,9 +6,8 @@ from ethereum_test_tools import Account, EOFException, EOFStateTestFiller, EOFTestFiller from ethereum_test_tools.eof.v1 import Container, ContainerKind, Section -from ethereum_test_tools.eof.v1.constants import MAX_BYTECODE_SIZE +from ethereum_test_tools.eof.v1.constants import MAX_BYTECODE_SIZE, MAX_INITCODE_SIZE from ethereum_test_tools.vm.opcode import Opcodes as Op -from ethereum_test_types.eof.v1.constants import MAX_INITCODE_SIZE from ethereum_test_vm import Bytecode from .. import EOF_FORK_NAME From 8e4e4e37376136e6ee8523c79e0bab63d67995a7 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 29 Jul 2024 23:01:01 +0000 Subject: [PATCH 11/25] feat(tests): parametrize dup test with all evm code types --- tests/frontier/opcodes/test_dup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/frontier/opcodes/test_dup.py b/tests/frontier/opcodes/test_dup.py index e4f1fa4a97..ae95b2fce1 100644 --- a/tests/frontier/opcodes/test_dup.py +++ b/tests/frontier/opcodes/test_dup.py @@ -33,6 +33,7 @@ ], ids=lambda op: str(op), ) +@pytest.mark.with_all_evm_code_types def test_dup( state_test: StateTestFiller, fork: str, @@ -58,7 +59,7 @@ def test_dup( account_code += dup_opcode # Save each stack value into different keys in storage - account_code += sum(Op.PUSH1(i) + Op.SSTORE for i in range(0x11)) + account_code += sum(Op.PUSH1(i) + Op.SSTORE for i in range(0x11)) + Op.STOP account = pre.deploy_contract(account_code) From e003515e300b50ee1a668cc3b4bab282aff29d97 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 31 Jul 2024 15:13:21 +0000 Subject: [PATCH 12/25] feat(tests): EIP-4844: Precompile tests on all evm code types --- .../test_point_evaluation_precompile.py | 415 ++++++++++-------- 1 file changed, 224 insertions(+), 191 deletions(-) diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index 062bb3918f..eeb8625c01 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -17,9 +17,9 @@ - z - y - kzg_proof - - success + - result - These values correspond to a single call of the precompile, and `success` refers to + These values correspond to a single call of the precompile, and `result` refers to whether the call should succeed or fail. All other `pytest.fixture` fixtures can be parametrized to generate new combinations and test @@ -29,20 +29,25 @@ import glob import json import os -from typing import Dict, Iterator, List, Optional +from enum import Enum +from itertools import count +from typing import Dict, List, Optional import pytest from ethereum_test_tools import ( + EOA, Account, Address, + Alloc, Block, BlockchainTestFiller, + Bytecode, Environment, StateTestFiller, Storage, - TestAddress, Transaction, + call_return_code, eip_2028_transaction_data_cost, ) from ethereum_test_tools.vm.opcode import Opcodes as Op @@ -54,6 +59,16 @@ REFERENCE_SPEC_VERSION = ref_spec_4844.version +class Result(str, Enum): + """ + Result of the point evaluation precompile. + """ + + SUCCESS = "success" + FAILURE = "failure" + OUT_OF_GAS = "out_of_gas" + + @pytest.fixture def precompile_input( versioned_hash: Optional[bytes | int], @@ -82,7 +97,7 @@ def precompile_input( @pytest.fixture -def call_type() -> Op: +def call_opcode() -> Op: """ Type of call to use to call the precompile. @@ -102,105 +117,132 @@ def call_gas() -> int: return Spec.POINT_EVALUATION_PRECOMPILE_GAS +precompile_caller_storage_keys = count() +key_call_return_code = next(precompile_caller_storage_keys) +key_return_1 = next(precompile_caller_storage_keys) +key_return_2 = next(precompile_caller_storage_keys) +key_return_length = next(precompile_caller_storage_keys) +key_return_copy_1 = next(precompile_caller_storage_keys) +key_return_copy_2 = next(precompile_caller_storage_keys) + + @pytest.fixture -def precompile_caller_account(call_type: Op, call_gas: int) -> Account: +def precompile_caller_storage() -> Storage.StorageDictType: + """ + Storage for the precompile caller contract. + """ + return { + key_call_return_code: 0xBA5E, + key_return_1: 0xBA5E, + key_return_2: 0xBA5E, + key_return_length: 0xBA5E, + key_return_copy_1: 0xBA5E, + key_return_copy_2: 0xBA5E, + } + + +@pytest.fixture +def precompile_caller_code(call_opcode: Op, call_gas: int) -> Bytecode: """ Code to call the point evaluation precompile. """ precompile_caller_code = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) - if call_type == Op.CALL or call_type == Op.CALLCODE: - precompile_caller_code += Op.SSTORE( - 0, - call_type( # type: ignore # https://github.com/ethereum/execution-spec-tests/issues/348 # noqa: E501 - call_gas, - Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, - 0x00, - 0x00, - Op.CALLDATASIZE, - 0x00, - 0x40, - ), - ) # Store the result of the precompile call in storage slot 0 - elif call_type == Op.DELEGATECALL or call_type == Op.STATICCALL: - # Delegatecall and staticcall use one less argument - precompile_caller_code += Op.SSTORE( - 0, - call_type( # type: ignore # https://github.com/ethereum/execution-spec-tests/issues/348 # noqa: E501 - call_gas, - Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, - 0x00, - Op.CALLDATASIZE, - 0x00, - 0x40, - ), - ) + precompile_caller_code += Op.SSTORE( + key_call_return_code, + call_opcode( # type: ignore # https://github.com/ethereum/execution-spec-tests/issues/348 # noqa: E501 + gas=call_gas, + address=Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, + args_offset=0x00, + args_size=Op.CALLDATASIZE, + ret_offset=0x00, + ret_size=0x40, + ), + ) # Store the result of the precompile call in storage slot 0 precompile_caller_code += ( # Save the returned values into storage - Op.SSTORE(1, Op.MLOAD(0x00)) - + Op.SSTORE(2, Op.MLOAD(0x20)) + Op.SSTORE(key_return_1, Op.MLOAD(0x00)) + + Op.SSTORE(key_return_2, Op.MLOAD(0x20)) # Save the returned data length into storage - + Op.SSTORE(3, Op.RETURNDATASIZE) + + Op.SSTORE(key_return_length, Op.RETURNDATASIZE) # Save the returned data using RETURNDATACOPY into storage + Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE) - + Op.SSTORE(4, Op.MLOAD(0x00)) - + Op.SSTORE(5, Op.MLOAD(0x20)) - ) - return Account( - nonce=0, - code=precompile_caller_code, - balance=0x10**18, + + Op.SSTORE(key_return_copy_1, Op.MLOAD(0x00)) + + Op.SSTORE(key_return_copy_2, Op.MLOAD(0x20)) + + Op.STOP ) + return precompile_caller_code @pytest.fixture -def precompile_caller_address() -> Address: +def precompile_caller_balance() -> int: """ - Address of the precompile caller account. + Storage for the precompile caller contract. """ - return Address(0x100) + return 0 @pytest.fixture -def pre( - precompile_caller_account: Account, - precompile_caller_address: Address, -) -> Dict: +def precompile_caller_address( + pre: Alloc, + precompile_caller_code: Bytecode, + precompile_caller_storage: Storage.StorageDictType, + precompile_caller_balance: int, +) -> Address: + """ + Address of the code to call the point evaluation precompile. + """ + return pre.deploy_contract( + precompile_caller_code, + storage=precompile_caller_storage, + balance=precompile_caller_balance, + ) + + +@pytest.fixture +def sender(pre: Alloc) -> EOA: """ - Prepares the pre state of all test cases, by setting the balance of the - source account of all test transactions, and the precompile caller account. + Returns the sender account. """ - return { - TestAddress: Account( - nonce=0, - balance=0x10**18, - ), - precompile_caller_address: precompile_caller_account, - } + return pre.fund_eoa() @pytest.fixture def tx( precompile_caller_address: Address, precompile_input: bytes, + sender: EOA, ) -> Transaction: """ Prepares transaction used to call the precompile caller account. """ return Transaction( - ty=2, - nonce=0, + sender=sender, data=precompile_input, to=precompile_caller_address, - value=0, - gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 20, - max_fee_per_gas=7, - max_priority_fee_per_gas=0, + gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 100, ) +@pytest.fixture +def success( + result: Result, + call_opcode: Op, +) -> bool: + """ + Prepares expected success or failure for each test. + """ + if call_opcode == Op.EXTDELEGATECALL: + return False + if result == Result.OUT_OF_GAS and call_opcode in [Op.EXTCALL, Op.EXTSTATICCALL]: + return True + + return result == Result.SUCCESS + + @pytest.fixture def post( success: bool, + call_opcode: Op, precompile_caller_address: Address, precompile_input: bytes, ) -> Dict: @@ -209,29 +251,33 @@ def post( failure of the precompile call. """ expected_storage: Storage.StorageDictType = dict() + # CALL operation return code + expected_storage[key_call_return_code] = call_return_code( + call_opcode, success, revert=call_opcode != Op.EXTDELEGATECALL + ) if success: - # CALL operation success - expected_storage[0] = 1 # Success return values - expected_storage[1] = Spec.FIELD_ELEMENTS_PER_BLOB - expected_storage[2] = Spec.BLS_MODULUS + expected_storage[key_return_1] = Spec.FIELD_ELEMENTS_PER_BLOB + expected_storage[key_return_2] = Spec.BLS_MODULUS # Success return values size - expected_storage[3] = 64 + expected_storage[key_return_length] = 64 # Success return values from RETURNDATACOPY - expected_storage[4] = Spec.FIELD_ELEMENTS_PER_BLOB - expected_storage[5] = Spec.BLS_MODULUS + expected_storage[key_return_copy_1] = Spec.FIELD_ELEMENTS_PER_BLOB + expected_storage[key_return_copy_2] = Spec.BLS_MODULUS else: - # CALL operation failure - expected_storage[0] = 0 # Failure returns zero values - expected_storage[3] = 0 + expected_storage[key_return_length] = 0 # Input parameters were not overwritten since the CALL failed - expected_storage[1] = precompile_input[0:32] - expected_storage[2] = precompile_input[32:64] - expected_storage[4] = expected_storage[1] - expected_storage[5] = expected_storage[2] + expected_storage[key_return_1] = precompile_input[0:32] + expected_storage[key_return_2] = precompile_input[32:64] + expected_storage[key_return_copy_1] = expected_storage[1] + expected_storage[key_return_copy_2] = expected_storage[2] + if call_opcode in [Op.EXTCALL, Op.EXTSTATICCALL, Op.EXTDELEGATECALL]: + # Input parameters were not overwritten + expected_storage[key_return_1] = precompile_input[0:32] + expected_storage[key_return_2] = precompile_input[32:64] return { precompile_caller_address: Account( storage=expected_storage, @@ -245,11 +291,11 @@ def post( pytest.param(Spec.BLS_MODULUS - 1, 0, INF_POINT, INF_POINT, None, id="in_bounds_z"), ], ) -@pytest.mark.parametrize("success", [True]) +@pytest.mark.parametrize("result", [Result.SUCCESS]) @pytest.mark.valid_from("Cancun") -def test_valid_precompile_calls( +def test_valid_inputs( state_test: StateTestFiller, - pre: Dict, + pre: Alloc, tx: Transaction, post: Dict, ): @@ -298,11 +344,11 @@ def test_valid_precompile_calls( "correct_proof_1_incorrect_versioned_hash_version_0xff", ], ) -@pytest.mark.parametrize("success", [False]) +@pytest.mark.parametrize("result", [Result.FAILURE]) @pytest.mark.valid_from("Cancun") -def test_invalid_precompile_calls( +def test_invalid_inputs( state_test: StateTestFiller, - pre: Dict, + pre: Alloc, tx: Transaction, post: Dict, ): @@ -331,10 +377,11 @@ def kzg_point_evaluation_vector_from_dict(data: dict): raise ValueError("Missing 'input' key in data") if "output" not in data: raise ValueError("Missing 'output' key in data") - if isinstance(data["output"], bool): - success = data["output"] + output = data["output"] + if isinstance(output, bool): + result = Result.SUCCESS if output else Result.FAILURE else: - success = False + result = Result.FAILURE input = data["input"] if "commitment" not in input or not isinstance(input["commitment"], str): raise ValueError("Missing 'commitment' key in data['input']") @@ -355,7 +402,7 @@ def kzg_point_evaluation_vector_from_dict(data: dict): y, commitment, proof, - success, + result, id=name, ) @@ -413,14 +460,14 @@ def all_external_vectors() -> List: @pytest.mark.parametrize( - "z,y,kzg_commitment,kzg_proof,success", + "z,y,kzg_commitment,kzg_proof,result", all_external_vectors(), ) @pytest.mark.parametrize("versioned_hash", [None]) @pytest.mark.valid_from("Cancun") -def test_point_evaluation_precompile_external_vectors( +def test_external_vectors( state_test: StateTestFiller, - pre: Dict, + pre: Alloc, tx: Transaction, post: Dict, ): @@ -439,32 +486,24 @@ def test_point_evaluation_precompile_external_vectors( @pytest.mark.parametrize( - "call_gas,y,success", + "call_gas,y,result", [ - (Spec.POINT_EVALUATION_PRECOMPILE_GAS, 0, True), - (Spec.POINT_EVALUATION_PRECOMPILE_GAS, 1, False), - (Spec.POINT_EVALUATION_PRECOMPILE_GAS - 1, 0, False), + (Spec.POINT_EVALUATION_PRECOMPILE_GAS, 0, Result.SUCCESS), + (Spec.POINT_EVALUATION_PRECOMPILE_GAS, 1, Result.FAILURE), + (Spec.POINT_EVALUATION_PRECOMPILE_GAS - 1, 0, Result.OUT_OF_GAS), ], ids=["correct", "incorrect", "insufficient_gas"], ) -@pytest.mark.parametrize( - "call_type", - [ - Op.CALL, - Op.DELEGATECALL, - Op.CALLCODE, - Op.STATICCALL, - ], -) +@pytest.mark.with_all_call_opcodes @pytest.mark.parametrize( "z,kzg_commitment,kzg_proof,versioned_hash", [[Z, INF_POINT, INF_POINT, None]], ids=[""], ) @pytest.mark.valid_from("Cancun") -def test_point_evaluation_precompile_calls( +def test_call_opcode_types( state_test: StateTestFiller, - pre: Dict, + pre: Alloc, tx: Transaction, post: Dict, ): @@ -502,10 +541,11 @@ def test_point_evaluation_precompile_calls( ids=["correct_proof", "incorrect_proof"], ) @pytest.mark.valid_from("Cancun") -def test_point_evaluation_precompile_gas_tx_to( +def test_tx_entry_point( state_test: StateTestFiller, precompile_input: bytes, call_gas: int, + pre: Alloc, proof_correct: bool, ): """ @@ -516,12 +556,7 @@ def test_point_evaluation_precompile_gas_tx_to( - Using correct and incorrect proofs """ start_balance = 10**18 - pre = { - TestAddress: Account( - nonce=0, - balance=start_balance, - ), - } + sender = pre.fund_eoa(amount=start_balance) # Gas is appended the intrinsic gas cost of the transaction intrinsic_gas_cost = 21_000 + eip_2028_transaction_data_cost(precompile_input) @@ -538,18 +573,15 @@ def test_point_evaluation_precompile_gas_tx_to( fee_per_gas = 7 tx = Transaction( - ty=2, - nonce=0, + sender=sender, data=precompile_input, to=Address(Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS), - value=0, gas_limit=call_gas + intrinsic_gas_cost, - max_fee_per_gas=7, - max_priority_fee_per_gas=0, + gas_price=fee_per_gas, ) post = { - TestAddress: Account( + sender: Account( nonce=1, balance=start_balance - (consumed_gas * fee_per_gas), ) @@ -568,41 +600,33 @@ def test_point_evaluation_precompile_gas_tx_to( [[Z, 0, INF_POINT, INF_POINT, None]], ids=["correct_proof"], ) +@pytest.mark.parametrize("precompile_caller_storage", [{}], ids=[""]) +@pytest.mark.parametrize("precompile_caller_balance", [1], ids=[""]) +@pytest.mark.parametrize( + "precompile_caller_code", + [ + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE( + Op.NUMBER, + Op.CALL( + address=Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, + value=1, + args_size=Op.CALLDATASIZE, + ), + ) + ], + ids=[""], +) @pytest.mark.valid_at_transition_to("Cancun") -def test_point_evaluation_precompile_before_fork( +def test_precompile_before_fork( state_test: StateTestFiller, - pre: Dict, + pre: Alloc, tx: Transaction, + precompile_caller_address: Address, ): """ Test calling the Point Evaluation Precompile before the appropriate fork. """ - precompile_caller_code = Op.SSTORE( - Op.NUMBER, - Op.CALL( - Op.GAS, - Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, - 1, # Value - 0, # Zero-length calldata - 0, - 0, # Zero-length return - 0, - ), - ) - precompile_caller_address = Address(0x100) - - pre = { - TestAddress: Account( - nonce=0, - balance=0x10**18, - ), - precompile_caller_address: Account( - nonce=0, - code=precompile_caller_code, - balance=0x10**18, - ), - } - post = { precompile_caller_address: Account( storage={1: 1}, @@ -614,7 +638,6 @@ def test_point_evaluation_precompile_before_fork( } state_test( - tag="point_evaluation_precompile_before_fork", pre=pre, env=Environment(timestamp=7_500), post=post, @@ -622,61 +645,72 @@ def test_point_evaluation_precompile_before_fork( ) +FORK_TIMESTAMP = 15_000 +PRE_FORK_BLOCK_RANGE = range(999, FORK_TIMESTAMP, 1_000) + + @pytest.mark.parametrize( "z,y,kzg_commitment,kzg_proof,versioned_hash", [[Z, 0, INF_POINT, INF_POINT, None]], ids=["correct_proof"], ) +@pytest.mark.parametrize("precompile_caller_storage", [{}], ids=[""]) +@pytest.mark.parametrize("precompile_caller_balance", [len(PRE_FORK_BLOCK_RANGE)], ids=[""]) +@pytest.mark.parametrize( + "precompile_caller_code", + [ + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE( + Op.NUMBER, + Op.CALL( + address=Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, + value=1, + args_size=Op.CALLDATASIZE, + ), + ) + ], + ids=[""], +) @pytest.mark.valid_at_transition_to("Cancun") -def test_point_evaluation_precompile_during_fork( +def test_precompile_during_fork( blockchain_test: BlockchainTestFiller, - pre: Dict, - tx: Transaction, + pre: Alloc, + precompile_caller_address: Address, + precompile_input: bytes, + sender: EOA, ): """ Test calling the Point Evaluation Precompile before the appropriate fork. """ - precompile_caller_code = Op.SSTORE( - Op.NUMBER, - Op.CALL( - Op.GAS, - Spec.POINT_EVALUATION_PRECOMPILE_ADDRESS, - 1, # Value - 0, # Zero-length calldata - 0, - 0, # Zero-length return - 0, - ), - ) - precompile_caller_address = Address(0x100) - - pre = { - TestAddress: Account( - nonce=0, - balance=0x10**18, - ), - precompile_caller_address: Account( - nonce=0, - code=precompile_caller_code, - balance=0x10**18, - ), - } - - def tx_generator() -> Iterator[Transaction]: - nonce = 0 # Initial value - while True: - yield tx.with_nonce(nonce) - nonce = nonce + 1 - - iter_tx = tx_generator() - - FORK_TIMESTAMP = 15_000 - PRE_FORK_BLOCK_RANGE = range(999, FORK_TIMESTAMP, 1_000) - # Blocks before fork - blocks = [Block(timestamp=t, txs=[next(iter_tx)]) for t in PRE_FORK_BLOCK_RANGE] + blocks = [ + Block( + timestamp=t, + txs=[ + Transaction( + sender=sender, + data=precompile_input, + to=precompile_caller_address, + gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 100, + ) + ], + ) + for t in PRE_FORK_BLOCK_RANGE + ] # Block after fork - blocks += [Block(timestamp=FORK_TIMESTAMP, txs=[next(iter_tx)])] + blocks += [ + Block( + timestamp=FORK_TIMESTAMP, + txs=[ + Transaction( + sender=sender, + data=precompile_input, + to=precompile_caller_address, + gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 100, + ) + ], + ) + ] post = { precompile_caller_address: Account( @@ -689,7 +723,6 @@ def tx_generator() -> Iterator[Transaction]: } blockchain_test( - tag="point_evaluation_precompile_before_fork", pre=pre, post=post, blocks=blocks, From 35a191088759f883919e37583746f51f49f2abbf Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 31 Jul 2024 18:43:52 +0000 Subject: [PATCH 13/25] fix(fw): fix call_return_code --- src/ethereum_test_vm/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ethereum_test_vm/helpers.py b/src/ethereum_test_vm/helpers.py index 9a8879da3d..5f796ce1f4 100644 --- a/src/ethereum_test_vm/helpers.py +++ b/src/ethereum_test_vm/helpers.py @@ -15,6 +15,6 @@ def call_return_code(opcode: Op, success: bool, *, revert: bool = False) -> int: if success: return 0 if revert: - return 2 - return 1 + return 1 + return 2 raise ValueError(f"Not a call opcode: {opcode}") From a0a1ee4f9b5e52e9c614ea095c1dfbdf149d9d1e Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 31 Jul 2024 18:44:14 +0000 Subject: [PATCH 14/25] fix(plugins/forks): covariant descriptor description --- src/pytest_plugins/forks/forks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest_plugins/forks/forks.py b/src/pytest_plugins/forks/forks.py index 00c5ceb16d..521a23e15e 100644 --- a/src/pytest_plugins/forks/forks.py +++ b/src/pytest_plugins/forks/forks.py @@ -173,7 +173,7 @@ def add_values(self, metafunc: Metafunc, fork_parametrizer: ForkParametrizer) -> CovariantDescriptor( marker_name="with_all_evm_code_types", description="marks a test to be parametrized for all EVM code types at parameter named" - " evm_code_type of type EVMCodeType", + " `evm_code_type` of type `EVMCodeType`, such as `LEGACY` and `EOF_V1`", fork_attribute_name="evm_code_types", parameter_name="evm_code_type", ), From 53c76492f1e0e3cd8b59d5795cbd5ad3a7b51b9f Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Wed, 31 Jul 2024 18:44:37 +0000 Subject: [PATCH 15/25] fix(tests): fix EIP-4844 test expected outcome --- tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index eeb8625c01..8cfdfc45b6 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -253,7 +253,7 @@ def post( expected_storage: Storage.StorageDictType = dict() # CALL operation return code expected_storage[key_call_return_code] = call_return_code( - call_opcode, success, revert=call_opcode != Op.EXTDELEGATECALL + call_opcode, success, revert=call_opcode == Op.EXTDELEGATECALL ) if success: # Success return values From 4ce8cd60534d794dffe6be9e0b15e31bd067123a Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 1 Aug 2024 13:47:17 +0000 Subject: [PATCH 16/25] fix(fw): test --- src/cli/tests/test_pytest_fill_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/tests/test_pytest_fill_command.py b/src/cli/tests/test_pytest_fill_command.py index 8c383ea919..c64bdbfeaf 100644 --- a/src/cli/tests/test_pytest_fill_command.py +++ b/src/cli/tests/test_pytest_fill_command.py @@ -26,7 +26,9 @@ def test_fill_help(runner): """ result = runner.invoke(fill, ["--help"]) assert result.exit_code == pytest.ExitCode.OK - assert "[--evm-bin EVM_BIN] [--traces]" in result.output + assert "[--evm-bin EVM_BIN]" in result.output + assert "[--traces]" in result.output + assert "[--evm-code-type EVM_CODE_TYPE]" in result.output assert "--help" in result.output assert "Arguments defining evm executable behavior:" in result.output From c0602d00790367ff4bff07a4a6076175e4e73c5e Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 1 Aug 2024 15:10:05 +0000 Subject: [PATCH 17/25] fix(tools): Export `EVMCodeType` --- src/ethereum_test_tools/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 68a1f7b291..4f9c51ead1 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -58,6 +58,7 @@ ) from ethereum_test_vm import ( Bytecode, + EVMCodeType, Macro, Macros, Opcode, @@ -105,6 +106,7 @@ "EOFStateTestFiller", "EOFTest", "EOFTestFiller", + "EVMCodeType", "FixtureCollector", "Hash", "Header", From e3e76ea58411a1ad6e0acd2787633e0b2ec10399 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 1 Aug 2024 16:30:23 +0000 Subject: [PATCH 18/25] feat(tools): make `Switch` EOF compatible --- src/ethereum_test_tools/code/generators.py | 50 ++++++++++++++++------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/ethereum_test_tools/code/generators.py b/src/ethereum_test_tools/code/generators.py index 97524100db..392ecf849c 100644 --- a/src/ethereum_test_tools/code/generators.py +++ b/src/ethereum_test_tools/code/generators.py @@ -6,7 +6,7 @@ from typing import List, SupportsBytes from ethereum_test_types import ceiling_division -from ethereum_test_vm import Bytecode +from ethereum_test_vm import Bytecode, EVMCodeType from ethereum_test_vm import Opcodes as Op GAS_PER_DEPLOYED_CODE_BYTE = 0xC8 @@ -248,6 +248,7 @@ class Case: condition: Bytecode | Op action: Bytecode | Op + terminating: bool = False @dataclass @@ -267,6 +268,7 @@ class CalldataCase: value: int | str | bytes | SupportsBytes position: int = 0 condition: Bytecode | Op = field(init=False) + terminating: bool = False def __post_init__(self): """ @@ -301,18 +303,22 @@ class Switch(Bytecode): evaluates to a non-zero value is the one that is executed. """ + evm_code_type: EVMCodeType + """ + The EVM code type to use for the switch-case bytecode. + """ + def __new__( cls, *, default_action: Bytecode | Op | None = None, cases: List[Case | CalldataCase], + evm_code_type: EVMCodeType = EVMCodeType.LEGACY, ): """ Assemble the bytecode by looping over the list of cases and adding - the necessary JUMPI and JUMPDEST opcodes in order to replicate + the necessary [R]JUMPI and JUMPDEST opcodes in order to replicate switch-case behavior. - - In the future, PC usage should be replaced by using RJUMP and RJUMPI. """ # The length required to jump over subsequent actions to the final JUMPDEST at the end # of the switch-case block: @@ -320,13 +326,24 @@ def __new__( # bytecode # - add 3 to the total to account for this action's JUMP; the PC within the call # requires a "correction" of 3. - action_jump_length = sum(len(case.action) + 6 for case in cases) + 3 - # All conditions get pre-pended to this bytecode; if none are met, we reach the default - bytecode = default_action + Op.JUMP(Op.ADD(Op.PC, action_jump_length)) + bytecode = Bytecode() - # The length required to jump over the default action and its JUMP bytecode - condition_jump_length = len(bytecode) + 3 + # All conditions get pre-pended to this bytecode; if none are met, we reach the default + if evm_code_type == EVMCodeType.LEGACY: + action_jump_length = sum(len(case.action) + 6 for case in cases) + 3 + bytecode = default_action + Op.JUMP(Op.ADD(Op.PC, action_jump_length)) + # The length required to jump over the default action and its JUMP bytecode + condition_jump_length = len(bytecode) + 3 + elif evm_code_type == EVMCodeType.EOF_V1: + action_jump_length = sum( + len(case.action) + (len(Op.RJUMP[0]) if not case.terminating else 0) + for case in cases + # On not terminating cases, we need to add 3 bytes for the RJUMP + ) + bytecode = default_action + Op.RJUMP[action_jump_length] + # The length required to jump over the default action and its JUMP bytecode + condition_jump_length = len(bytecode) # Reversed: first case in the list has priority; it will become the outer-most onion layer. # We build up layers around the default_action, after 1 iteration of the loop, a simplified @@ -349,9 +366,18 @@ def __new__( # + JUMPDEST + case[0].action + JUMP() # for case in reversed(cases): - action_jump_length -= len(case.action) + 6 - action = Op.JUMPDEST + case.action + Op.JUMP(Op.ADD(Op.PC, action_jump_length)) - condition = Op.JUMPI(Op.ADD(Op.PC, condition_jump_length), case.condition) + action = case.action + if evm_code_type == EVMCodeType.LEGACY: + action_jump_length -= len(action) + 6 + action = Op.JUMPDEST + action + Op.JUMP(Op.ADD(Op.PC, action_jump_length)) + condition = Op.JUMPI(Op.ADD(Op.PC, condition_jump_length), case.condition) + elif evm_code_type == EVMCodeType.EOF_V1: + action_jump_length -= len(action) + ( + len(Op.RJUMP[0]) if not case.terminating else 0 + ) + if not case.terminating: + action += Op.RJUMP[action_jump_length] + condition = Op.RJUMPI[condition_jump_length](case.condition) # wrap the current case around the onion as its next layer bytecode = condition + bytecode + action condition_jump_length += len(condition) + len(action) From 9ef68a426afb5d6c5b4bb78fede938e3c1024c2f Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 1 Aug 2024 16:46:28 +0000 Subject: [PATCH 19/25] feat(tools): make `Conditional` EOF compatible --- src/ethereum_test_tools/code/generators.py | 70 ++++++++-------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/src/ethereum_test_tools/code/generators.py b/src/ethereum_test_tools/code/generators.py index 392ecf849c..183cd74d78 100644 --- a/src/ethereum_test_tools/code/generators.py +++ b/src/ethereum_test_tools/code/generators.py @@ -2,7 +2,7 @@ Code generating classes and functions. """ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, SupportsBytes from ethereum_test_types import ceiling_division @@ -189,27 +189,13 @@ class Conditional(Bytecode): Helper class used to generate conditional bytecode. """ - condition: Bytecode | Op - """ - Condition bytecode which must return the true or false condition of the conditional statement. - """ - - if_true: Bytecode | Op | None - """ - Bytecode to execute if the condition is true. - """ - - if_false: Bytecode | Op | None - """ - Bytecode to execute if the condition is false. - """ - def __new__( cls, *, condition: Bytecode | Op, - if_true: Bytecode | Op | None, - if_false: Bytecode | Op | None, + if_true: Bytecode | Op = Bytecode(), + if_false: Bytecode | Op = Bytecode(), + evm_code_type: EVMCodeType = EVMCodeType.LEGACY, ): """ Assemble the conditional bytecode by generating the necessary jump and @@ -218,28 +204,29 @@ def __new__( In the future, PC usage should be replaced by using RJUMP and RJUMPI """ - # First we append a jumpdest to the start of the true branch - if_true = Op.JUMPDEST + if_true + if evm_code_type == EVMCodeType.LEGACY: + # First we append a jumpdest to the start of the true branch + if_true = Op.JUMPDEST + if_true - # Then we append the unconditional jump to the end of the false branch, used to skip the - # true branch - if_false += Op.JUMP(Op.ADD(Op.PC, len(if_true) + 3)) + # Then we append the unconditional jump to the end of the false branch, used to skip + # the true branch + if_false += Op.JUMP(Op.ADD(Op.PC, len(if_true) + 3)) - # Then we need to do the conditional jump by skipping the false branch - condition = Op.JUMPI(Op.ADD(Op.PC, len(if_false) + 3), condition) + # Then we need to do the conditional jump by skipping the false branch + condition = Op.JUMPI(Op.ADD(Op.PC, len(if_false) + 3), condition) + + elif evm_code_type == EVMCodeType.EOF_V1: + if_false += Op.RJUMP[len(if_true)] + condition = Op.RJUMPI[len(if_false)](condition) # Finally we append the true and false branches, and the condition, plus the jumpdest at # the very end bytecode = condition + if_false + if_true + Op.JUMPDEST - instance = super().__new__(cls, bytecode) - instance.condition = condition - instance.if_true = if_true - instance.if_false = if_false - return instance + return super().__new__(cls, bytecode) -@dataclass +@dataclass(kw_only=True) class Case: """ Small helper class to represent a single, generic case in a `Switch` cases @@ -251,8 +238,7 @@ class Case: terminating: bool = False -@dataclass -class CalldataCase: +class CalldataCase(Case): """ Small helper class to represent a single case whose condition depends on the value of the contract's calldata in a Switch case statement. @@ -264,18 +250,12 @@ class CalldataCase: optionally `position`) and may not be set directly. """ - action: Bytecode | Op - value: int | str | bytes | SupportsBytes - position: int = 0 - condition: Bytecode | Op = field(init=False) - terminating: bool = False - - def __post_init__(self): + def __init__(self, value: int | str | Bytecode, position: int = 0, **kwargs): """ Generate the condition base on `value` and `position`. """ - self.condition = Op.EQ(Op.CALLDATALOAD(self.position), self.value) - self.action = self.action + condition = Op.EQ(Op.CALLDATALOAD(position), value) + super().__init__(condition=condition, **kwargs) class Switch(Bytecode): @@ -297,9 +277,9 @@ class Switch(Bytecode): executed. """ - cases: List[Case | CalldataCase] + cases: List[Case] """ - A list of Case or CalldataCase: The first element with a condition that + A list of Cases: The first element with a condition that evaluates to a non-zero value is the one that is executed. """ @@ -312,7 +292,7 @@ def __new__( cls, *, default_action: Bytecode | Op | None = None, - cases: List[Case | CalldataCase], + cases: List[Case], evm_code_type: EVMCodeType = EVMCodeType.LEGACY, ): """ From 4b68894405478a98ffdd63ba42fc36c0e7c7585c Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 1 Aug 2024 16:50:27 +0000 Subject: [PATCH 20/25] docs: changelog --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b179daa595..2e7e167460 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,7 @@ Test fixtures for use by clients are available for each release on the [Github r - โœจ Generated fixtures now contain the test index `index.json` by default ([#716](https://github.com/ethereum/execution-spec-tests/pull/716)). - โœจ A metadata folder `.meta/` now stores all fixture metadata files by default ([#721](https://github.com/ethereum/execution-spec-tests/pull/721)). - ๐Ÿž Fixed `fill` command index generation issue due to concurrency ([#725](https://github.com/ethereum/execution-spec-tests/pull/725)). +- โœจ Added `with_all_evm_code_types` and `with_all_call_opcodes` markers, which allow automatic parametrization of tests to EOF ([#610](https://github.com/ethereum/execution-spec-tests/pull/610)). ### ๐Ÿ”ง EVM Tools From d614fb0cec7bb3ec6bb47574a62182f1f8cc1424 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Thu, 1 Aug 2024 17:57:47 +0000 Subject: [PATCH 21/25] changelog --- docs/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2e7e167460..cbb0f1e0ef 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,9 @@ Test fixtures for use by clients are available for each release on the [Github r ### ๐Ÿงช Test Cases +- โœจ EIP-4844 test `tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py` includes an EOF test case ([#610](https://github.com/ethereum/execution-spec-tests/pull/610)). +- โœจ Example test `tests/frontier/opcodes/test_dup.py` now includes EOF parametrization ([#610](https://github.com/ethereum/execution-spec-tests/pull/610)). + ### ๐Ÿ› ๏ธ Framework - ๐Ÿž Fixed consume hive commands from spawning different hive test suites during the same test execution when using xdist ([#712](https://github.com/ethereum/execution-spec-tests/pull/712)). @@ -16,6 +19,8 @@ Test fixtures for use by clients are available for each release on the [Github r - โœจ A metadata folder `.meta/` now stores all fixture metadata files by default ([#721](https://github.com/ethereum/execution-spec-tests/pull/721)). - ๐Ÿž Fixed `fill` command index generation issue due to concurrency ([#725](https://github.com/ethereum/execution-spec-tests/pull/725)). - โœจ Added `with_all_evm_code_types` and `with_all_call_opcodes` markers, which allow automatic parametrization of tests to EOF ([#610](https://github.com/ethereum/execution-spec-tests/pull/610)). +- โœจ Code generators `Conditional` and `Switch` now support EOF by adding parameter `evm_code_type` ([#610](https://github.com/ethereum/execution-spec-tests/pull/610)). +- โœจ `fill` command now supports parameter `--evm-code-type` that can be (currently) set to `legacy` or `eof_v1` to force all test smart contracts to deployed in normal or in EOF containers ([#610](https://github.com/ethereum/execution-spec-tests/pull/610)). ### ๐Ÿ”ง EVM Tools From 765324acaab305cf08bcf314881daefe0342eb79 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Sun, 4 Aug 2024 18:03:06 +0000 Subject: [PATCH 22/25] feat(vm): Add `terminating` property to `Bytecode`/`Opcode` --- src/ethereum_test_vm/bytecode.py | 7 +++++++ src/ethereum_test_vm/opcode.py | 17 ++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/ethereum_test_vm/bytecode.py b/src/ethereum_test_vm/bytecode.py index a40678dd66..a4876c6857 100644 --- a/src/ethereum_test_vm/bytecode.py +++ b/src/ethereum_test_vm/bytecode.py @@ -30,6 +30,8 @@ class Bytecode: max_stack_height: int min_stack_height: int + terminating: bool + def __new__( cls, bytes_or_byte_code_base: "bytes | Bytecode | None" = None, @@ -38,6 +40,7 @@ def __new__( pushed_stack_items: int | None = None, max_stack_height: int | None = None, min_stack_height: int | None = None, + terminating: bool = False, name: str = "", ): """ @@ -50,6 +53,7 @@ def __new__( instance.pushed_stack_items = 0 instance.min_stack_height = 0 instance.max_stack_height = 0 + instance.terminating = False instance._name_ = name return instance @@ -62,6 +66,7 @@ def __new__( obj.pushed_stack_items = bytes_or_byte_code_base.pushed_stack_items obj.min_stack_height = bytes_or_byte_code_base.min_stack_height obj.max_stack_height = bytes_or_byte_code_base.max_stack_height + obj.terminating = bytes_or_byte_code_base.terminating obj._name_ = bytes_or_byte_code_base._name_ return obj @@ -80,6 +85,7 @@ def __new__( obj.max_stack_height = max(obj.popped_stack_items, obj.pushed_stack_items) else: obj.max_stack_height = max_stack_height + obj.terminating = terminating obj._name_ = name return obj @@ -155,6 +161,7 @@ def __add__(self, other: "Bytecode | int | None") -> "Bytecode": pushed_stack_items=c_push, min_stack_height=c_min, max_stack_height=c_max, + terminating=other.terminating, ) def __radd__(self, other: "Bytecode | int | None") -> "Bytecode": diff --git a/src/ethereum_test_vm/opcode.py b/src/ethereum_test_vm/opcode.py index d358eb7944..2acfb12c65 100644 --- a/src/ethereum_test_vm/opcode.py +++ b/src/ethereum_test_vm/opcode.py @@ -106,6 +106,7 @@ def __new__( data_portion_formatter=None, stack_properties_modifier=None, unchecked_stack=False, + terminating: bool = False, kwargs: List[str] | None = None, kwargs_defaults: KW_ARGS_DEFAULTS_TYPE = {}, ): @@ -134,6 +135,7 @@ def __new__( pushed_stack_items=pushed_stack_items, max_stack_height=max_stack_height, min_stack_height=min_stack_height, + terminating=terminating, ) obj.data_portion_length = data_portion_length obj.data_portion_formatter = data_portion_formatter @@ -207,6 +209,7 @@ def __getitem__(self, *args: "int | bytes | str | Iterable[int]") -> "Opcode": data_portion_length=0, data_portion_formatter=None, unchecked_stack=self.unchecked_stack, + terminating=self.terminating, kwargs=self.kwargs, kwargs_defaults=self.kwargs_defaults, ) @@ -426,7 +429,7 @@ class Opcodes(Opcode, Enum): Do !! NOT !! remove or modify existing opcodes from this list. """ - STOP = Opcode(0x00) + STOP = Opcode(0x00, terminating=True) """ STOP() ---- @@ -4931,7 +4934,7 @@ class Opcodes(Opcode, Enum): [ipsilon/eof/blob/main/spec/eof.md](https://github.com/ipsilon/eof/blob/main/spec/eof.md) """ - RETF = Opcode(0xE4) + RETF = Opcode(0xE4, terminating=True) """ !!! Note: This opcode is under development @@ -4956,7 +4959,7 @@ class Opcodes(Opcode, Enum): 3 """ - JUMPF = Opcode(0xE5, data_portion_length=2) + JUMPF = Opcode(0xE5, data_portion_length=2, terminating=True) """ !!! Note: This opcode is under development @@ -5130,7 +5133,7 @@ class Opcodes(Opcode, Enum): """ - RETURNCONTRACT = Opcode(0xEE, popped_stack_items=2, data_portion_length=1) + RETURNCONTRACT = Opcode(0xEE, popped_stack_items=2, data_portion_length=1, terminating=True) """ !!! Note: This opcode is under development @@ -5287,7 +5290,7 @@ class Opcodes(Opcode, Enum): Source: [evm.codes/#F2](https://www.evm.codes/#F2) """ - RETURN = Opcode(0xF3, popped_stack_items=2, kwargs=["offset", "size"]) + RETURN = Opcode(0xF3, popped_stack_items=2, kwargs=["offset", "size"], terminating=True) """ RETURN(offset, size) ---- @@ -5592,7 +5595,7 @@ class Opcodes(Opcode, Enum): 3 """ - REVERT = Opcode(0xFD, popped_stack_items=2, kwargs=["offset", "size"]) + REVERT = Opcode(0xFD, popped_stack_items=2, kwargs=["offset", "size"], terminating=True) """ REVERT(offset, size) ---- @@ -5618,7 +5621,7 @@ class Opcodes(Opcode, Enum): Source: [evm.codes/#FD](https://www.evm.codes/#FD) """ - INVALID = Opcode(0xFE) + INVALID = Opcode(0xFE, terminating=True) """ INVALID() ---- From 515e63a527c794b39420bb7c5b0907e8021a577b Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Sun, 4 Aug 2024 18:03:44 +0000 Subject: [PATCH 23/25] feat(plugins/filler): Add `Op.STOP` on non-terminating bytecodes --- src/pytest_plugins/filler/pre_alloc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pytest_plugins/filler/pre_alloc.py b/src/pytest_plugins/filler/pre_alloc.py index 3b868e6912..82778b53e4 100644 --- a/src/pytest_plugins/filler/pre_alloc.py +++ b/src/pytest_plugins/filler/pre_alloc.py @@ -29,7 +29,7 @@ from ethereum_test_types import EOA from ethereum_test_types import Alloc as BaseAlloc from ethereum_test_types.eof.v1 import Container -from ethereum_test_vm import EVMCodeType +from ethereum_test_vm import Bytecode, EVMCodeType, Opcodes CONTRACT_START_ADDRESS_DEFAULT = 0x1000 CONTRACT_ADDRESS_INCREMENTS_DEFAULT = 0x100 @@ -132,6 +132,8 @@ def code_pre_processor( evm_code_type = self._evm_code_type if evm_code_type == EVMCodeType.EOF_V1: if not isinstance(code, Container): + if isinstance(code, Bytecode) and not code.terminating: + return Container.Code(code + Opcodes.STOP) return Container.Code(code) return code From aebb48dcb78419d6bbad3c61cba95f67069c5d37 Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Sun, 4 Aug 2024 18:04:18 +0000 Subject: [PATCH 24/25] feat(tools): Use `terminating` property in `Switch` generator --- src/ethereum_test_tools/code/generators.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ethereum_test_tools/code/generators.py b/src/ethereum_test_tools/code/generators.py index 183cd74d78..0839dc6b03 100644 --- a/src/ethereum_test_tools/code/generators.py +++ b/src/ethereum_test_tools/code/generators.py @@ -235,7 +235,14 @@ class Case: condition: Bytecode | Op action: Bytecode | Op - terminating: bool = False + terminating: bool | None = None + + @property + def is_terminating(self) -> bool: + """ + Returns whether the case is terminating. + """ + return self.terminating if self.terminating is not None else self.action.terminating class CalldataCase(Case): @@ -317,7 +324,7 @@ def __new__( condition_jump_length = len(bytecode) + 3 elif evm_code_type == EVMCodeType.EOF_V1: action_jump_length = sum( - len(case.action) + (len(Op.RJUMP[0]) if not case.terminating else 0) + len(case.action) + (len(Op.RJUMP[0]) if not case.is_terminating else 0) for case in cases # On not terminating cases, we need to add 3 bytes for the RJUMP ) @@ -353,9 +360,9 @@ def __new__( condition = Op.JUMPI(Op.ADD(Op.PC, condition_jump_length), case.condition) elif evm_code_type == EVMCodeType.EOF_V1: action_jump_length -= len(action) + ( - len(Op.RJUMP[0]) if not case.terminating else 0 + len(Op.RJUMP[0]) if not case.is_terminating else 0 ) - if not case.terminating: + if not case.is_terminating: action += Op.RJUMP[action_jump_length] condition = Op.RJUMPI[condition_jump_length](case.condition) # wrap the current case around the onion as its next layer From 0961c2c3fc54294d293e323adaf562cb56215f6f Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Sun, 4 Aug 2024 18:05:28 +0000 Subject: [PATCH 25/25] fix(tests): Remove `Op.STOP` from test_dup.py --- tests/frontier/opcodes/test_dup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/frontier/opcodes/test_dup.py b/tests/frontier/opcodes/test_dup.py index ae95b2fce1..93f8079746 100644 --- a/tests/frontier/opcodes/test_dup.py +++ b/tests/frontier/opcodes/test_dup.py @@ -59,7 +59,7 @@ def test_dup( account_code += dup_opcode # Save each stack value into different keys in storage - account_code += sum(Op.PUSH1(i) + Op.SSTORE for i in range(0x11)) + Op.STOP + account_code += sum(Op.PUSH1(i) + Op.SSTORE for i in range(0x11)) account = pre.deploy_contract(account_code)