Skip to content

Commit

Permalink
new(tests): EOF - EIP-6206: JUMPF Tests (#540)
Browse files Browse the repository at this point in the history
* EOF JUMPF Tests

Tests the assertsions in EIP-6206.  Both container validation and
runtime execution are validated.

Signed-off-by: Danno Ferrin <[email protected]>

* reviewer comments

- move to parameterized calls
- combine eof and state tests in one go
- change file groupings

Signed-off-by: Danno Ferrin <[email protected]>

* review requested changes

Add comments to stack calculation variables.

Signed-off-by: Danno Ferrin <[email protected]>

* move

Signed-off-by: Danno Ferrin <[email protected]>

* move to symbolic storage names

Signed-off-by: Danno Ferrin <[email protected]>

* 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 <[email protected]>

---------

Signed-off-by: Danno Ferrin <[email protected]>
Co-authored-by: Mario Vega <[email protected]>
  • Loading branch information
shemnon and marioevz authored May 23, 2024
1 parent 98ee7ba commit 647a55a
Show file tree
Hide file tree
Showing 12 changed files with 613 additions and 7 deletions.
4 changes: 4 additions & 0 deletions src/ethereum_test_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
BaseTest,
BlockchainTest,
BlockchainTestFiller,
EOFStateTest,
EOFStateTestFiller,
EOFTest,
EOFTestFiller,
FixtureCollector,
Expand Down Expand Up @@ -78,6 +80,8 @@
"EngineAPIError",
"Environment",
"EOFException",
"EOFStateTest",
"EOFStateTestFiller",
"EOFTest",
"EOFTestFiller",
"FixtureCollector",
Expand Down
10 changes: 10 additions & 0 deletions src/ethereum_test_tools/exceptions/evmone_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"),
)
Expand Down
13 changes: 13 additions & 0 deletions src/ethereum_test_tools/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions src/ethereum_test_tools/spec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -19,6 +32,9 @@
"BlockchainTest",
"BlockchainTestFiller",
"BlockchainTestSpec",
"EOFStateTest",
"EOFStateTestFiller",
"EOFStateTestSpec",
"EOFTest",
"EOFTestFiller",
"EOFTestSpec",
Expand Down
2 changes: 0 additions & 2 deletions src/ethereum_test_tools/spec/base/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
121 changes: 118 additions & 3 deletions src/ethereum_test_tools/spec/eof/eof_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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,
Expand All @@ -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]
3 changes: 3 additions & 0 deletions tests/prague/eip7692_eof_v1/eip6206_jumpf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
EOF tests for EIP-6206 JUMPF
"""
13 changes: 13 additions & 0 deletions tests/prague/eip7692_eof_v1/eip6206_jumpf/helpers.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions tests/prague/eip7692_eof_v1/eip6206_jumpf/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
EOF V1 Constants used throughout all tests
"""

EOF_FORK_NAME = "Prague"
Loading

0 comments on commit 647a55a

Please sign in to comment.