From e5520d9c28653ac11d1b5d2c21b8540649ecf5d2 Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Wed, 1 May 2024 21:11:50 -0600 Subject: [PATCH] EOF JUMPF Tests Tests the assertsions in EIP-6206. Both container validation and runtime execution are validated. Signed-off-by: Danno Ferrin --- src/ethereum_test_tools/vm/opcode.py | 2 +- tests/prague/eip6206_jumpf/__init__.py | 3 + tests/prague/eip6206_jumpf/contracts.py | 244 ++++++++++++++++++ tests/prague/eip6206_jumpf/spec.py | 5 + .../eip6206_jumpf/test_code_validation.py | 56 ++++ .../eip6206_jumpf/test_jumpf_execution.py | 71 +++++ .../eip7480_data_section/test_data_opcodes.py | 62 +---- 7 files changed, 381 insertions(+), 62 deletions(-) create mode 100644 tests/prague/eip6206_jumpf/__init__.py create mode 100644 tests/prague/eip6206_jumpf/contracts.py create mode 100644 tests/prague/eip6206_jumpf/spec.py create mode 100644 tests/prague/eip6206_jumpf/test_code_validation.py create mode 100644 tests/prague/eip6206_jumpf/test_jumpf_execution.py diff --git a/src/ethereum_test_tools/vm/opcode.py b/src/ethereum_test_tools/vm/opcode.py index 8b503670c3..a9288b19e7 100644 --- a/src/ethereum_test_tools/vm/opcode.py +++ b/src/ethereum_test_tools/vm/opcode.py @@ -4932,7 +4932,7 @@ class Opcodes(Opcode, Enum): 3 """ - JUMPF = Opcode(0xB1, data_portion_length=2) + JUMPF = Opcode(0xE5, data_portion_length=2) """ !!! Note: This opcode is under development diff --git a/tests/prague/eip6206_jumpf/__init__.py b/tests/prague/eip6206_jumpf/__init__.py new file mode 100644 index 0000000000..c6a92b6a52 --- /dev/null +++ b/tests/prague/eip6206_jumpf/__init__.py @@ -0,0 +1,3 @@ +""" +Cross-client EVM Object Format Tests +""" diff --git a/tests/prague/eip6206_jumpf/contracts.py b/tests/prague/eip6206_jumpf/contracts.py new file mode 100644 index 0000000000..f8f43d2446 --- /dev/null +++ b/tests/prague/eip6206_jumpf/contracts.py @@ -0,0 +1,244 @@ +""" +EOF V1 Code Validation tests +""" + +from typing import List + +from ethereum_test_tools import EOFException +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 + + +def quick_code(code, inputs=0, outputs=NON_RETURNING_SECTION, height=0): + """A sorter way to write code with section 0 defaults""" + return Section.Code( + code=code, code_inputs=inputs, code_outputs=outputs, max_stack_height=height + ) + + +def container_name(c: Container): + """ + Return the name of the container for use in pytest ids. + """ + if hasattr(c, "name"): + return c.name + else: + return c.__class__.__name__ + + +def generate_jumpf_target_rules(): + """ + Generate tests for JUMPF where we are testing the validity of the JUNMPF target. + We are not testing stack so a lot of the logic is to get correct stack values. + """ + valid = [] + invalid = [] + for current_outputs in [NON_RETURNING_SECTION, 0, 2, 4]: + current_non_returning = current_outputs == NON_RETURNING_SECTION + current_height = 0 if current_non_returning else current_outputs + for target_outputs in [NON_RETURNING_SECTION, 0, 2, 4]: + target_non_returning = target_outputs == NON_RETURNING_SECTION + target_height = 0 if target_non_returning else target_outputs + delta = ( + 0 + if target_non_returning or current_non_returning + else target_outputs - current_height + ) + current_extra_push = max(0, current_height - target_height) + current_section = Section.Code( + code=Op.PUSH0 * (current_height) + + Op.CALLDATALOAD(0) + + Op.RJUMPI[1] + + (Op.STOP if current_non_returning else Op.RETF) + + Op.PUSH0 * current_extra_push + + Op.JUMPF[2], + code_inputs=0, + code_outputs=current_outputs, + max_stack_height=current_height + max(1, current_extra_push), + ) + 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=current_height, + code_outputs=target_outputs, + max_stack_height=max(current_height, current_height + delta), + ) + + container = Container( + name="target_co-%s_to-%s" + % ( + "N" if current_non_returning else current_outputs, + "N" if target_non_returning else target_outputs, + ), + sections=[ + quick_code(Op.JUMPF[1], height=0 if current_non_returning else current_height) + if current_non_returning + else quick_code( + Op.CALLF[1](0, 0) + Op.STOP, + height=0 if current_non_returning else 2 + current_outputs, + ), + current_section, + target_section, + quick_code(Op.SSTORE(0, 1) + Op.RETF, outputs=0, height=2), + ], + ) + + # now sort validity... + if target_non_returning: + valid.append(container) + elif current_non_returning or current_outputs < target_outputs: + # both as non-returning handled above + container.validity_error = EOFException.UNDEFINED_EXCEPTION + invalid.append(container) + else: + # both are returning, and current >= target + valid.append(container) + return (valid, invalid) + + +def generate_jumpf_stack_returning_rules(): + """ + Generate tests for JUMPF where we are testing the stack rules. Returning section cases + """ + valid = [] + invalid = [] + for current_outputs in [0, 2, 4]: + for target_outputs in [x for x in [0, 2, 4] if x <= current_outputs]: + for target_inputs in [0, 2, 4]: + for stack_diff in [-1, 0, 1] if target_inputs > 0 else [0, 1]: + target_delta = target_outputs - target_inputs + container = Container( + name="stack-retuning_co-%d_to-%d_ti-%d_diff-%d" + % (current_outputs, target_outputs, target_inputs, stack_diff), + sections=[ + quick_code( + Op.CALLF[1] + Op.SSTORE(0, 1) + Op.STOP, height=2 + current_outputs + ), + quick_code( + Op.PUSH0 * max(0, target_inputs + stack_diff) + Op.JUMPF[2], + outputs=current_outputs, + height=target_inputs, + ), + quick_code( + ( + Op.POP * -target_delta + if target_delta < 0 + else Op.PUSH0 * target_delta + ) + + Op.RETF, + inputs=target_inputs, + outputs=target_outputs, + height=max(target_inputs, target_outputs), + ), + ], + ) + + if stack_diff == current_outputs - target_outputs: + valid.append(container) + else: + container.validity_error = EOFException.UNDEFINED_EXCEPTION + invalid.append(container) + + return (valid, invalid) + + +def generate_jumpf_stack_non_returning_rules(): + """ + Generate tests for JUMPF where we are testing the stack rules. Non-returning section cases. + """ + valid = [] + invalid = [] + for stack_height in [0, 2, 4]: + for target_inputs in [0, 2, 4]: + container = Container( + name="stack-non-retuning_h-%d_ti-%d" % (stack_height, target_inputs), + sections=[ + quick_code(Op.JUMPF[1]), + quick_code( + Op.PUSH0 * stack_height + Op.JUMPF[2], + height=stack_height, + ), + quick_code( + Op.POP * target_inputs + Op.SSTORE(0, 1) + Op.STOP, + inputs=target_inputs, + height=target_inputs, + ), + ], + ) + + if stack_height >= target_inputs: + valid.append(container) + else: + container.validity_error = EOFException.UNDEFINED_EXCEPTION + invalid.append(container) + + return (valid, invalid) + + +jump_forward = Container( + name="jump_forward", + sections=[quick_code(Op.JUMPF[1]), quick_code(Op.SSTORE(0, 1) + Op.STOP, height=2)], +) +jump_backward = Container( + name="jump_backward", + sections=[ + quick_code(Op.CALLF[2] + Op.SSTORE(0, 1) + Op.STOP, height=2), + quick_code(Op.RETF, outputs=0), + quick_code(Op.JUMPF[1], outputs=0), + ], +) +jump_to_self = Container( + name="jump_to_self", + sections=[ + quick_code( + Op.SLOAD(0) + Op.ISZERO + Op.RJUMPI[1] + Op.STOP + Op.SSTORE(0, 1) + Op.JUMPF[0], + height=2, + ) + ], +) +jump_too_large = Container( + name="jump_too_large", + sections=[quick_code(Op.JUMPF[1025])], + validity_error=EOFException.UNDEFINED_EXCEPTION, +) +jump_way_too_large = Container( + name="jump_way_too_large", + sections=[quick_code(Op.JUMPF[0xFFFF])], + validity_error=EOFException.UNDEFINED_EXCEPTION, +) +jump_non_existent_section = Container( + name="jump_non_existent_section", + sections=[quick_code(Op.JUMPF[5])], + validity_error=EOFException.UNDEFINED_EXCEPTION, +) +callf_non_returning = Container( + name="callf_non_returning", + sections=[quick_code(Op.CALLF[1]), quick_code(Op.STOP, outputs=NON_RETURNING_SECTION)], + validity_error=EOFException.UNDEFINED_EXCEPTION, +) + + +jumpf_targets = generate_jumpf_target_rules() +jumpf_stack_returning = generate_jumpf_stack_returning_rules() +jumpf_stack_non_returning = generate_jumpf_target_rules() + +VALID: List[Container] = [ + jump_forward, + jump_backward, + jump_to_self, + *jumpf_targets[0], + *jumpf_stack_returning[0], + *jumpf_stack_non_returning[0], +] + +INVALID: List[Container] = [ + jump_too_large, + jump_too_large, + jump_non_existent_section, + callf_non_returning, + *jumpf_targets[1], + *jumpf_stack_returning[1], + *jumpf_stack_non_returning[1], +] diff --git a/tests/prague/eip6206_jumpf/spec.py b/tests/prague/eip6206_jumpf/spec.py new file mode 100644 index 0000000000..7bf760554f --- /dev/null +++ b/tests/prague/eip6206_jumpf/spec.py @@ -0,0 +1,5 @@ +""" +EOF V1 Constants used throughout all tests +""" + +EOF_FORK_NAME = "Prague" diff --git a/tests/prague/eip6206_jumpf/test_code_validation.py b/tests/prague/eip6206_jumpf/test_code_validation.py new file mode 100644 index 0000000000..d3fc19f8b8 --- /dev/null +++ b/tests/prague/eip6206_jumpf/test_code_validation.py @@ -0,0 +1,56 @@ +""" +EOF V1 Code Validation tests +""" +import pytest + +from ethereum_test_tools import EOFTestFiller +from ethereum_test_tools.eof.v1 import Container + +from .contracts import INVALID, VALID, container_name +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6206.md" +REFERENCE_SPEC_VERSION = "a1775816657df4093787fb9fe83c2f7cc17ecf47" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +@pytest.mark.parametrize( + "container", + VALID, + ids=container_name, +) +def test_jumpf_code_validation_valid( + eof_test: EOFTestFiller, + container: Container, +): + """ + Test creating various types of valid EOF V1 contracts using legacy + initcode and a contract creating transaction. + """ + assert ( + container.validity_error is None + ), f"Valid container with validity error: {container.validity_error}" + eof_test( + data=container, + ) + + +@pytest.mark.parametrize( + "container", + INVALID, + ids=container_name, +) +def test_jumpf_code_validation_invalid( + eof_test: EOFTestFiller, + container: Container, +): + """ + Test creating various types of valid EOF V1 contracts using legacy + initcode and a contract creating transaction. + """ + assert container.validity_error is not None, "Invalid container without validity error" + eof_test( + data=container, + expect_exception=container.validity_error, + ) diff --git a/tests/prague/eip6206_jumpf/test_jumpf_execution.py b/tests/prague/eip6206_jumpf/test_jumpf_execution.py new file mode 100644 index 0000000000..2a436c6563 --- /dev/null +++ b/tests/prague/eip6206_jumpf/test_jumpf_execution.py @@ -0,0 +1,71 @@ +""" +Execution of CALLF, RETF opcodes within EOF V1 containers tests +""" + +import pytest + +from ethereum_test_tools import ( + Account, + Address, + Environment, + StateTestFiller, + TestAddress, + Transaction, +) +from ethereum_test_tools.eof.v1 import Container + +from .contracts import VALID, container_name +from .spec import EOF_FORK_NAME + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6206.md" +REFERENCE_SPEC_VERSION = "a1775816657df4093787fb9fe83c2f7cc17ecf47" + +pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) + + +@pytest.mark.parametrize( + "container", + VALID, + ids=container_name, +) +def test_jumpf_execution( + state_test: StateTestFiller, + container: Container, +): + """ + Test JUMPF valid contracts. All should end with the same canary + """ + assert ( + container.validity_error is None + ), f"Valid container with validity error: {container.validity_error}" + + env = Environment() + + pre = { + TestAddress: Account( + balance=1000000000000000000000, + nonce=1, + ), + Address(0x100): Account( + code=container, + nonce=1, + ), + } + + tx = Transaction( + nonce=1, + to=Address(0x100), + gas_limit=44_000, + gas_price=10, + protected=False, + data="1", + ) + + post = {Address(0x100): Account(storage={0: 1})} + + state_test( + env=env, + pre=pre, + post=post, + tx=tx, + ) diff --git a/tests/prague/eip7480_data_section/test_data_opcodes.py b/tests/prague/eip7480_data_section/test_data_opcodes.py index b1cd9b631f..e255bc7642 100644 --- a/tests/prague/eip7480_data_section/test_data_opcodes.py +++ b/tests/prague/eip7480_data_section/test_data_opcodes.py @@ -13,7 +13,7 @@ Transaction, ) from ethereum_test_tools.eof.v1 import Container, Section -from ethereum_test_tools.eof.v1.constants import MAX_CODE_SECTIONS, NON_RETURNING_SECTION +from ethereum_test_tools.eof.v1.constants import NON_RETURNING_SECTION from ethereum_test_tools.vm.opcode import Opcodes as Op from .spec import EOF_FORK_NAME @@ -23,66 +23,6 @@ pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) -contract_call_within_deep_nested_callf = Container( - name="contract_call_within_deep_nested_callf", - sections=[ - Section.Code( - code=(Op.CALLF[1] + Op.SSTORE(0, 1) + Op.STOP), - code_inputs=0, - code_outputs=NON_RETURNING_SECTION, - max_stack_height=2, - ) - ] - + [ - # All sections call next section and on return, store a 1 - # to their call stack height key - Section.Code( - code=(Op.CALLF[i] + Op.SSTORE(i - 1, 1) + Op.RETF), - code_inputs=0, - code_outputs=0, - max_stack_height=2, - ) - for i in range(2, MAX_CODE_SECTIONS) - ] - + [ - # Last section makes external contract call - Section.Code( - code=( - Op.EXTCALL(0x200, 0, 0, 0) + Op.SSTORE(MAX_CODE_SECTIONS - 1, Op.ISZERO) + Op.RETF - ), - code_inputs=0, - code_outputs=0, - max_stack_height=4, - ) - ], -) - -recursive_contract_call_within_deep_nested_callf = Container( - name="recursive_contract_call_within_deep_nested_callf", - sections=[ - # All sections call next section and on return, store a 1 - # to their call stack height key - Section.Code( - code=(Op.CALLF[i + 1] + Op.PUSH1(1) + Op.PUSH2(i) + Op.SSTORE + Op.STOP), - code_inputs=0, - code_outputs=NON_RETURNING_SECTION, - max_stack_height=2, - ) - for i in range(MAX_CODE_SECTIONS - 1) - ] - + [ - # Last section makes external contract call - Section.Code( - code=( - Op.SSTORE(MAX_CODE_SECTIONS - 1, Op.CALL(Op.GAS, 0x200, 0, 0, 0, 0, 0)) + Op.RETF - ), - code_inputs=0, - code_outputs=0, - max_stack_height=7, - ) - ], -) - def create_data_test(offset: int, datasize: int): """