From 647a55a65dcab025b36495f4ac6b8951ccf02993 Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Thu, 23 May 2024 17:31:04 -0600 Subject: [PATCH] new(tests): EOF - EIP-6206: JUMPF Tests (#540) * EOF JUMPF Tests Tests the assertsions in EIP-6206. Both container validation and runtime execution are validated. Signed-off-by: Danno Ferrin * reviewer comments - move to parameterized calls - combine eof and state tests in one go - change file groupings Signed-off-by: Danno Ferrin * review requested changes Add comments to stack calculation variables. Signed-off-by: Danno Ferrin * move Signed-off-by: Danno Ferrin * move to symbolic storage names Signed-off-by: Danno Ferrin * fix(tests): eip-6206: nit * fix(tests): eip-6206: EOF exception * feat(fw): Add EOFStateTest test type * feat(fw): EOF Exception STACK_HIGHER_THAN_OUTPUTS * refactor(tests): EOF - EIP-7692: Use EOFStateTest * fix(tests): EIP-6206: fix invalid container tests * fix(tests): EIP-6206: test_jumpf_target_rules logic * fix(fw): EOF - Add INVALID_NON_RETURNING_FLAG, JUMPF_DESTINATION_INCOMPATIBLE_OUTPUTS * fix(tests): EOF - EIP-6206: Use correct exceptions * merge in marioevz:eof/jumpf Signed-off-by: Danno Ferrin --------- Signed-off-by: Danno Ferrin Co-authored-by: Mario Vega --- src/ethereum_test_tools/__init__.py | 4 + .../exceptions/evmone_exceptions.py | 10 ++ .../exceptions/exceptions.py | 13 ++ src/ethereum_test_tools/spec/__init__.py | 20 ++- .../spec/base/base_test.py | 2 - src/ethereum_test_tools/spec/eof/eof_test.py | 121 ++++++++++++- .../eip7692_eof_v1/eip6206_jumpf/__init__.py | 3 + .../eip7692_eof_v1/eip6206_jumpf/helpers.py | 13 ++ .../eip7692_eof_v1/eip6206_jumpf/spec.py | 5 + .../eip6206_jumpf/test_jumpf_execution.py | 161 ++++++++++++++++++ .../eip6206_jumpf/test_jumpf_stack.py | 140 +++++++++++++++ .../eip6206_jumpf/test_jumpf_target.py | 128 ++++++++++++++ 12 files changed, 613 insertions(+), 7 deletions(-) create mode 100644 tests/prague/eip7692_eof_v1/eip6206_jumpf/__init__.py create mode 100644 tests/prague/eip7692_eof_v1/eip6206_jumpf/helpers.py create mode 100644 tests/prague/eip7692_eof_v1/eip6206_jumpf/spec.py create mode 100644 tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py create mode 100644 tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_stack.py create mode 100644 tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_target.py diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index cd1d17ccf58..54d78faca0b 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -48,6 +48,8 @@ BaseTest, BlockchainTest, BlockchainTestFiller, + EOFStateTest, + EOFStateTestFiller, EOFTest, EOFTestFiller, FixtureCollector, @@ -78,6 +80,8 @@ "EngineAPIError", "Environment", "EOFException", + "EOFStateTest", + "EOFStateTestFiller", "EOFTest", "EOFTestFiller", "FixtureCollector", diff --git a/src/ethereum_test_tools/exceptions/evmone_exceptions.py b/src/ethereum_test_tools/exceptions/evmone_exceptions.py index 6f23356fbd6..9965a78044d 100644 --- a/src/ethereum_test_tools/exceptions/evmone_exceptions.py +++ b/src/ethereum_test_tools/exceptions/evmone_exceptions.py @@ -33,6 +33,9 @@ class EvmoneExceptionMapper: EOFException.MISSING_HEADERS_TERMINATOR, "err: section_headers_not_terminated" ), ExceptionMessage(EOFException.INVALID_VERSION, "err: eof_version_unknown"), + ExceptionMessage( + EOFException.INVALID_NON_RETURNING_FLAG, "err: invalid_non_returning_flag" + ), ExceptionMessage(EOFException.INVALID_MAGIC, "err: invalid_prefix"), ExceptionMessage( EOFException.INVALID_FIRST_SECTION_TYPE, "err: invalid_first_section_type" @@ -53,6 +56,13 @@ class EvmoneExceptionMapper: ExceptionMessage( EOFException.MAX_STACK_HEIGHT_ABOVE_LIMIT, "err: max_stack_height_above_limit" ), + ExceptionMessage( + EOFException.STACK_HIGHER_THAN_OUTPUTS, "err: stack_higher_than_outputs_required" + ), + ExceptionMessage( + EOFException.JUMPF_DESTINATION_INCOMPATIBLE_OUTPUTS, + "err: jumpf_destination_incompatible_outputs", + ), ExceptionMessage(EOFException.INVALID_MAX_STACK_HEIGHT, "err: invalid_max_stack_height"), ExceptionMessage(EOFException.INVALID_DATALOADN_INDEX, "err: invalid_dataloadn_index"), ) diff --git a/src/ethereum_test_tools/exceptions/exceptions.py b/src/ethereum_test_tools/exceptions/exceptions.py index 8defeaf21ee..01be8b5397d 100644 --- a/src/ethereum_test_tools/exceptions/exceptions.py +++ b/src/ethereum_test_tools/exceptions/exceptions.py @@ -219,6 +219,10 @@ class EOFException(ExceptionBase): """ EOF container version bytes mismatch """ + INVALID_NON_RETURNING_FLAG = auto() + """ + EOF container's section has non-returning flag set incorrectly + """ INVALID_RJUMP_DESTINATION = auto() """ Code has RJUMP instruction with invalid parameters @@ -311,6 +315,15 @@ class EOFException(ExceptionBase): """ EOF container's specified max stack height is above the limit """ + STACK_HIGHER_THAN_OUTPUTS = auto() + """ + EOF container section stack height is higher than the outputs + when returning + """ + JUMPF_DESTINATION_INCOMPATIBLE_OUTPUTS = auto() + """ + EOF container section JUMPF's to a destination section with incompatible outputs + """ INVALID_MAX_STACK_HEIGHT = auto() """ EOF container section's specified max stack height does not match the actual stack height diff --git a/src/ethereum_test_tools/spec/__init__.py b/src/ethereum_test_tools/spec/__init__.py index ea2f1b2178b..5ebe48aefaa 100644 --- a/src/ethereum_test_tools/spec/__init__.py +++ b/src/ethereum_test_tools/spec/__init__.py @@ -6,11 +6,24 @@ from .base.base_test import BaseFixture, BaseTest, TestSpec from .blockchain.blockchain_test import BlockchainTest, BlockchainTestFiller, BlockchainTestSpec -from .eof.eof_test import EOFTest, EOFTestFiller, EOFTestSpec +from .eof.eof_test import ( + EOFStateTest, + EOFStateTestFiller, + EOFStateTestSpec, + EOFTest, + EOFTestFiller, + EOFTestSpec, +) from .fixture_collector import FixtureCollector, TestInfo from .state.state_test import StateTest, StateTestFiller, StateTestOnly, StateTestSpec -SPEC_TYPES: List[Type[BaseTest]] = [BlockchainTest, StateTest, StateTestOnly, EOFTest] +SPEC_TYPES: List[Type[BaseTest]] = [ + BlockchainTest, + StateTest, + StateTestOnly, + EOFTest, + EOFStateTest, +] __all__ = ( "SPEC_TYPES", @@ -19,6 +32,9 @@ "BlockchainTest", "BlockchainTestFiller", "BlockchainTestSpec", + "EOFStateTest", + "EOFStateTestFiller", + "EOFStateTestSpec", "EOFTest", "EOFTestFiller", "EOFTestSpec", diff --git a/src/ethereum_test_tools/spec/base/base_test.py b/src/ethereum_test_tools/spec/base/base_test.py index 4facab78ff3..5001508b442 100644 --- a/src/ethereum_test_tools/spec/base/base_test.py +++ b/src/ethereum_test_tools/spec/base/base_test.py @@ -153,8 +153,6 @@ def pytest_parameter_name(cls) -> str: By default, it returns the underscore separated name of the class. """ - if cls.__name__ == "EOFTest": - return "eof_test" return reduce(lambda x, y: x + ("_" if y.isupper() else "") + y, cls.__name__).lower() def get_next_transition_tool_output_path(self) -> str: diff --git a/src/ethereum_test_tools/spec/eof/eof_test.py b/src/ethereum_test_tools/spec/eof/eof_test.py index a38803b17bb..c6f43f952c1 100644 --- a/src/ethereum_test_tools/spec/eof/eof_test.py +++ b/src/ethereum_test_tools/spec/eof/eof_test.py @@ -7,14 +7,21 @@ from pathlib import Path from shutil import which from subprocess import CompletedProcess -from typing import Callable, ClassVar, Generator, List, Optional, Type +from typing import Any, Callable, ClassVar, Generator, List, Optional, Type + +import pytest +from pydantic import Field, model_validator from ethereum_test_forks import Fork -from evm_transition_tool import FixtureFormats +from evm_transition_tool import FixtureFormats, TransitionTool +from ...common import Account, Address, Alloc, Environment, Transaction from ...common.base_types import Bytes +from ...common.constants import TestAddress +from ...eof.v1 import Container from ...exceptions import EOFException, EvmoneExceptionMapper from ..base.base_test import BaseFixture, BaseTest +from ..state.state_test import StateTest from .types import Fixture, Result @@ -133,10 +140,36 @@ class EOFTest(BaseTest): expect_exception: EOFException | None = None supported_fixture_formats: ClassVar[List[FixtureFormats]] = [ - # TODO: Potentially generate a state test and blockchain test too. FixtureFormats.EOF_TEST, ] + @model_validator(mode="before") + @classmethod + def check_container_exception(cls, data: Any) -> Any: + """ + Check if the container exception matches the expected exception. + """ + if isinstance(data, dict): + container = data.get("data") + expect_exception = data.get("expect_exception") + if container is not None and isinstance(container, Container): + if container.validity_error is not None: + if expect_exception is not None: + assert container.validity_error == expect_exception, ( + f"Container validity error {container.validity_error} " + f"does not match expected exception {expect_exception}." + ) + if expect_exception is None: + data["expect_exception"] = container.validity_error + return data + + @classmethod + def pytest_parameter_name(cls) -> str: + """ + Workaround for pytest parameter name. + """ + return "eof_test" + def make_eof_test_fixture( self, *, @@ -208,6 +241,7 @@ def verify_result(self, result: CompletedProcess, expected_result: Result, code: def generate( self, *, + t8n: TransitionTool, fork: Fork, eips: Optional[List[int]] = None, fixture_format: FixtureFormats, @@ -224,3 +258,84 @@ def generate( EOFTestSpec = Callable[[str], Generator[EOFTest, None, None]] EOFTestFiller = Type[EOFTest] + + +class EOFStateTest(EOFTest): + """ + Filler type that tests EOF containers and also generates a state/blockchain test. + """ + + tx_gas_limit: int = 10_000_000 + tx_data: Bytes = Bytes(b"") + env: Environment = Field(default_factory=Environment) + container_post: Account = Field(default_factory=Account) + + supported_fixture_formats: ClassVar[List[FixtureFormats]] = [ + FixtureFormats.EOF_TEST, + FixtureFormats.STATE_TEST, + FixtureFormats.BLOCKCHAIN_TEST, + FixtureFormats.BLOCKCHAIN_TEST_HIVE, + ] + + @classmethod + def pytest_parameter_name(cls) -> str: + """ + Workaround for pytest parameter name. + """ + return "eof_state_test" + + def generate_state_test(self) -> StateTest: + """ + Generate the StateTest filler. + """ + pre = Alloc() + container_address = Address(0x100) + pre[container_address] = Account(code=self.data, nonce=1) + pre[TestAddress] = Account(balance=1_000_000_000_000_000_000_000, nonce=0) + tx = Transaction( + nonce=0, + to=container_address, + gas_limit=self.tx_gas_limit, + gas_price=10, + protected=False, + data=self.tx_data, + ) + post = Alloc() + post[container_address] = self.container_post + return StateTest( + pre=pre, + tx=tx, + env=self.env, + post=post, + ) + + def generate( + self, + *, + t8n: TransitionTool, + fork: Fork, + eips: Optional[List[int]] = None, + fixture_format: FixtureFormats, + **_, + ) -> BaseFixture: + """ + Generate the BlockchainTest fixture. + """ + if fixture_format == FixtureFormats.EOF_TEST: + return self.make_eof_test_fixture(fork=fork, eips=eips) + elif fixture_format in ( + FixtureFormats.STATE_TEST, + FixtureFormats.BLOCKCHAIN_TEST, + FixtureFormats.BLOCKCHAIN_TEST_HIVE, + ): + if self.expect_exception is not None: + pytest.skip("State tests can't be generated for invalid EOF code yet.") + return self.generate_state_test().generate( + t8n=t8n, fork=fork, fixture_format=fixture_format, eips=eips + ) + + raise Exception(f"Unknown fixture format: {fixture_format}") + + +EOFStateTestSpec = Callable[[str], Generator[EOFStateTest, None, None]] +EOFStateTestFiller = Type[EOFStateTest] diff --git a/tests/prague/eip7692_eof_v1/eip6206_jumpf/__init__.py b/tests/prague/eip7692_eof_v1/eip6206_jumpf/__init__.py new file mode 100644 index 00000000000..6b3accdbb04 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip6206_jumpf/__init__.py @@ -0,0 +1,3 @@ +""" +EOF tests for EIP-6206 JUMPF +""" diff --git a/tests/prague/eip7692_eof_v1/eip6206_jumpf/helpers.py b/tests/prague/eip7692_eof_v1/eip6206_jumpf/helpers.py new file mode 100644 index 00000000000..056278eca87 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip6206_jumpf/helpers.py @@ -0,0 +1,13 @@ +""" +EOF JumpF tests helpers +""" +import itertools + +"""Storage addresses for common testing fields""" +_slot = itertools.count() +next(_slot) # don't use slot 0 +slot_code_worked = next(_slot) +slot_last_slot = next(_slot) + +"""Storage values for common testing fields""" +value_code_worked = 0x2015 diff --git a/tests/prague/eip7692_eof_v1/eip6206_jumpf/spec.py b/tests/prague/eip7692_eof_v1/eip6206_jumpf/spec.py new file mode 100644 index 00000000000..7bf760554ff --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip6206_jumpf/spec.py @@ -0,0 +1,5 @@ +""" +EOF V1 Constants used throughout all tests +""" + +EOF_FORK_NAME = "Prague" diff --git a/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py new file mode 100644 index 00000000000..3ae60ef1807 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py @@ -0,0 +1,161 @@ +""" +EOF JUMPF tests covering simple cases. +""" +import pytest + +from ethereum_test_tools import Account, EOFException, EOFStateTestFiller +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import NON_RETURNING_SECTION +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .helpers import slot_code_worked, value_code_worked +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6206.md" +REFERENCE_SPEC_VERSION = "2f365ea0cd58faa6e26013ea77ce6d538175f7d0" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +def test_jumpf_forward( + eof_state_test: EOFStateTestFiller, +): + """Test JUMPF jumping forward""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + code=Op.JUMPF[1], + code_outputs=NON_RETURNING_SECTION, + ), + Section.Code( + Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + ], + ), + container_post=Account(storage={slot_code_worked: value_code_worked}), + tx_data=b"\1", + ) + + +def test_jumpf_backward( + eof_state_test: EOFStateTestFiller, +): + """Tests JUMPF jumping backward""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + code=Op.CALLF[2] + Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ), + Section.Code( + code=Op.RETF, + ), + Section.Code( + code=Op.JUMPF[1], + ), + ], + ), + container_post=Account(storage={slot_code_worked: value_code_worked}), + tx_data=b"\1", + ) + + +def test_jumpf_to_self( + eof_state_test: EOFStateTestFiller, +): + """Tests JUMPF jumping to self""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + code=Op.SLOAD(slot_code_worked) + + Op.ISZERO + + Op.RJUMPI[1] + + Op.STOP + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.JUMPF[0], + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2, + ) + ], + ), + container_post=Account(storage={slot_code_worked: value_code_worked}), + tx_data=b"\1", + ) + + +def test_jumpf_too_large( + eof_state_test: EOFStateTestFiller, +): + """Tests JUMPF jumping to a section outside the max section range""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + code=Op.JUMPF[1025], + code_outputs=NON_RETURNING_SECTION, + ) + ], + validity_error=EOFException.UNDEFINED_EXCEPTION, + ), + ) + + +def test_jumpf_way_too_large( + eof_state_test: EOFStateTestFiller, +): + """Tests JUMPF jumping to uint64.MAX""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + code=Op.JUMPF[0xFFFF], + code_outputs=NON_RETURNING_SECTION, + ) + ], + validity_error=EOFException.UNDEFINED_EXCEPTION, + ), + ) + + +def test_jumpf_to_nonexistent_section( + eof_state_test: EOFStateTestFiller, +): + """Tests JUMPF jumping to valid section number but where the section does not exist""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + code=Op.JUMPF[5], + code_outputs=NON_RETURNING_SECTION, + ) + ], + validity_error=EOFException.UNDEFINED_EXCEPTION, + ), + ) + + +def test_callf_to_non_returning_section( + eof_state_test: EOFStateTestFiller, +): + """Tests CALLF into a non-returning section""" + eof_state_test( + data=Container( + sections=[ + Section.Code( + code=Op.CALLF[1], + code_outputs=NON_RETURNING_SECTION, + ), + Section.Code( + code=Op.STOP, + outputs=NON_RETURNING_SECTION, + ), + ], + validity_error=EOFException.MISSING_STOP_OPCODE, + ), + ) diff --git a/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_stack.py b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_stack.py new file mode 100644 index 00000000000..f8609d8670f --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_stack.py @@ -0,0 +1,140 @@ +""" +EOF JUMPF tests covering stack validation rules. +""" +import pytest + +from ethereum_test_tools import Account, EOFException, EOFStateTestFiller +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import NON_RETURNING_SECTION +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .helpers import slot_code_worked, value_code_worked +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6206.md" +REFERENCE_SPEC_VERSION = "2f365ea0cd58faa6e26013ea77ce6d538175f7d0" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +@pytest.mark.parametrize( + "target_inputs", + [0, 2, 4], + ids=lambda x: "ti-%d" % x, +) +@pytest.mark.parametrize( + "stack_height", + [0, 2, 4], + ids=lambda x: "h-%d" % x, +) +def test_jumpf_stack_non_returning_rules( + eof_state_test: EOFStateTestFiller, + target_inputs: int, + stack_height: int, +): + """ + Tests for JUMPF validation stack rules. Non-returning section cases. + Valid cases are executed. + """ + container = Container( + name="stack-non-retuning_h-%d_ti-%d" % (stack_height, target_inputs), + sections=[ + Section.Code( + code=Op.JUMPF[1], + code_outputs=NON_RETURNING_SECTION, + ), + Section.Code( + code=Op.PUSH0 * stack_height + Op.JUMPF[2], + code_outputs=NON_RETURNING_SECTION, + max_stack_height=stack_height, + ), + Section.Code( + code=Op.POP * target_inputs + + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.STOP, + code_inputs=target_inputs, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=max(2, target_inputs), + ), + ], + ) + + if stack_height < target_inputs: + container.validity_error = EOFException.STACK_UNDERFLOW + + eof_state_test( + data=container, + container_post=Account(storage={slot_code_worked: value_code_worked}), + tx_data=b"\1", + ) + + +@pytest.mark.parametrize( + "source_outputs", + [0, 2, 4], + ids=lambda x: "so-%d" % x, +) +@pytest.mark.parametrize( + "target_outputs", + [0, 2, 4], + ids=lambda x: "to-%d" % x, +) +@pytest.mark.parametrize( + "target_inputs", + [0, 2, 4], + ids=lambda x: "to-%d" % x, +) +@pytest.mark.parametrize("stack_diff", [-1, 0, 1], ids=["less-stack", "same-stack", "more-stack"]) +def test_jumpf_stack_returning_rules( + eof_state_test: EOFStateTestFiller, + source_outputs: int, + target_outputs: int, + target_inputs: int, + stack_diff: int, +): + """ + Tests for JUMPF validation stack rules. Returning section cases. + Valid cases are executed. + """ + if target_outputs > source_outputs: + # These create invalid containers without JUMPF validation, Don't test. + return + if target_inputs == 0 and stack_diff < 0: + # Code generation is impossible for this configuration. Don't test. + return + + target_delta = target_outputs - target_inputs + container = Container( + name="stack-retuning_co-%d_to-%d_ti-%d_diff-%d" + % (source_outputs, target_outputs, target_inputs, stack_diff), + sections=[ + Section.Code( + code=Op.CALLF[1] + Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=2 + source_outputs, + ), + Section.Code( + code=Op.PUSH0 * max(0, target_inputs + stack_diff) + Op.JUMPF[2], + code_outputs=source_outputs, + max_stack_height=target_inputs, + ), + Section.Code( + code=(Op.POP * -target_delta if target_delta < 0 else Op.PUSH0 * target_delta) + + Op.RETF, + code_inputs=target_inputs, + code_outputs=target_outputs, + max_stack_height=max(target_inputs, target_outputs), + ), + ], + ) + + if stack_diff < source_outputs - target_outputs: + container.validity_error = EOFException.STACK_UNDERFLOW + elif stack_diff > source_outputs - target_outputs: + container.validity_error = EOFException.STACK_HIGHER_THAN_OUTPUTS + + eof_state_test( + data=container, + container_post=Account(storage={slot_code_worked: value_code_worked}), + tx_data=b"\1", + ) diff --git a/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_target.py b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_target.py new file mode 100644 index 00000000000..a736f7ff5ec --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_target.py @@ -0,0 +1,128 @@ +""" +EOF JUMPF tests covering JUMPF target rules. +""" + +import pytest + +from ethereum_test_tools import Account, EOFException, EOFStateTestFiller +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import NON_RETURNING_SECTION +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from .helpers import slot_code_worked, value_code_worked +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6206.md" +REFERENCE_SPEC_VERSION = "2f365ea0cd58faa6e26013ea77ce6d538175f7d0" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +@pytest.mark.parametrize( + "target_outputs", + [NON_RETURNING_SECTION, 0, 2, 4, 127], + ids=lambda x: "to-%s" % ("N" if x == NON_RETURNING_SECTION else x), +) +@pytest.mark.parametrize( + "source_outputs", + [NON_RETURNING_SECTION, 0, 2, 4, 127], + ids=lambda x: "so-%s" % ("N" if x == NON_RETURNING_SECTION else x), +) +def test_jumpf_target_rules( + eof_state_test: EOFStateTestFiller, + source_outputs: int, + target_outputs: int, +): + """ + Validate the target section rules of JUMPF, and execute valid cases. + We are not testing stack so a lot of the logic is to get correct stack values. + """ + source_non_returning = source_outputs == NON_RETURNING_SECTION + source_height = 0 if source_non_returning else source_outputs + source_section_index = 1 + + target_non_returning = target_outputs == NON_RETURNING_SECTION + target_height = 0 if target_non_returning else target_outputs + target_section_index = 2 + + # Because we are testing the target and not the stack height validation we need to do some work + # to make sure the stack passes validation. + + # `source_extra_push` is how many more pushes we need to match our stack commitments + source_extra_push = max(0, source_height - target_height) + source_section = Section.Code( + code=Op.PUSH0 * (source_height) + + Op.CALLDATALOAD(0) + + Op.RJUMPI[1] + + (Op.STOP if source_non_returning else Op.RETF) + + Op.PUSH0 * source_extra_push + + Op.JUMPF[target_section_index], + code_inputs=0, + code_outputs=source_outputs, + max_stack_height=source_height + max(1, source_extra_push), + ) + + # `delta` is how many stack items the target output is from the input height, and tracks the + # number of pushes or (if negative) pops the target needs to do to match output commitments + delta = 0 if target_non_returning or source_non_returning else target_outputs - source_height + target_section = Section.Code( + code=((Op.PUSH0 * delta) if delta >= 0 else (Op.POP * -delta)) + + Op.CALLF[3] + + (Op.STOP if target_non_returning else Op.RETF), + code_inputs=source_height, + code_outputs=target_outputs, + max_stack_height=max(source_height, source_height + delta), + ) + + base_code = ( + bytes(Op.JUMPF[source_section_index]) + if source_non_returning + else (Op.CALLF[source_section_index](0, 0) + Op.STOP) + ) + base_height = 0 if source_non_returning else 2 + source_outputs + container = Container( + name="so-%s_to-%s" + % ( + "N" if source_non_returning else source_outputs, + "N" if target_non_returning else target_outputs, + ), + sections=[ + Section.Code( + code=base_code, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=base_height, + ), + source_section, + target_section, + Section.Code( + code=Op.SSTORE(slot_code_worked, value_code_worked) + Op.RETF, + code_inputs=0, + code_outputs=0, + max_stack_height=2, + ), + ], + ) + if target_non_returning or source_non_returning: + if not target_non_returning and source_non_returning: + # both as non-returning handled above + container.validity_error = EOFException.INVALID_NON_RETURNING_FLAG + elif source_outputs < target_outputs: + container.validity_error = EOFException.JUMPF_DESTINATION_INCOMPATIBLE_OUTPUTS + + eof_state_test( + data=container, + container_post=Account(storage={slot_code_worked: value_code_worked}), + tx_data=b"\1", + ) + + +@pytest.mark.skip("Not implemented") +def test_jumpf_multi_target_rules( + eof_state_test: EOFStateTestFiller, +): + """ + NOT IMPLEMENTED: + Test a section that contains multiple JUMPF to different targets with different outputs. + """ + pass