From cfbfea9821e505a13227da85eb09f7d1642dc7f0 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 1 Nov 2024 17:20:46 +0100 Subject: [PATCH] new(tests): EOF - EIP-4200 EIP-6206 RJUMPI with JUMPF (#928) * new(tests): RJUMPI over an additional RETF * fix(fw): Mark JUMPF as unchecked like CALLF * new(tests): EOF - EIP-6206 - JUMPF/RJUMPx interactions * RJUMPF/CALLF test: feedback (.__members__ not needed) * Spot and remove dead code for new tests * Apply suggestions from code review * fix(tests): tox --------- Co-authored-by: Mario Vega --- src/ethereum_test_vm/opcode.py | 2 +- .../eip5450_stack/test_code_validation.py | 141 ++++++++++++++++-- whitelist.txt | 1 + 3 files changed, 128 insertions(+), 16 deletions(-) diff --git a/src/ethereum_test_vm/opcode.py b/src/ethereum_test_vm/opcode.py index 195c93809b..dbac86b0f0 100644 --- a/src/ethereum_test_vm/opcode.py +++ b/src/ethereum_test_vm/opcode.py @@ -4959,7 +4959,7 @@ class Opcodes(Opcode, Enum): 3 """ - JUMPF = Opcode(0xE5, data_portion_length=2, terminating=True) + JUMPF = Opcode(0xE5, data_portion_length=2, terminating=True, unchecked_stack=True) """ !!! Note: This opcode is under development diff --git a/tests/osaka/eip7692_eof_v1/eip5450_stack/test_code_validation.py b/tests/osaka/eip7692_eof_v1/eip5450_stack/test_code_validation.py index 6a2f852e36..63ea929528 100644 --- a/tests/osaka/eip7692_eof_v1/eip5450_stack/test_code_validation.py +++ b/tests/osaka/eip7692_eof_v1/eip5450_stack/test_code_validation.py @@ -12,6 +12,7 @@ from ethereum_test_tools import EOFTestFiller from ethereum_test_tools.eof.v1 import Container, Section from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_types.eof.v1.constants import NON_RETURNING_SECTION from ethereum_test_vm.bytecode import Bytecode from .. import EOF_FORK_NAME @@ -40,6 +41,7 @@ class RjumpKind(Enum): RJUMPI_TO_START = auto() RJUMPV_EMPTY_AND_OVER_NEXT = auto() RJUMPV_OVER_PUSH_AND_TO_START = auto() + RJUMPI_OVER_RETF = auto() def __str__(self) -> str: """ @@ -119,6 +121,8 @@ def rjump_code_with( body = Op.RJUMPV[[1, -code_so_far_len - rjumpv_two_destinations_len]](0) + Op.PUSH0 is_backwards = True pushes = True + elif rjump_kind == RjumpKind.RJUMPI_OVER_RETF: + body = Op.RJUMPI[1](0) + Op.RETF elif not rjump_kind: pass else: @@ -172,7 +176,8 @@ def section_code_with( Also returns some traits of the section: `has_invalid_back_jump`, `rjump_snippet_pops`, `rjump_snippet_pushes`, `rjump_falls_off_code` """ - code = Bytecode(min_stack_height=inputs, max_stack_height=inputs) + code = Bytecode() + code.pushed_stack_items, code.max_stack_height = (inputs, inputs) if call: body = call_code_with(inputs, outputs, call) @@ -188,6 +193,11 @@ def section_code_with( rjump, is_backwards, rjump_snippet_pops, rjump_snippet_pushes = rjump_code_with( rjump_kind, 0, body ) + if rjump_kind == RjumpKind.RJUMPI_OVER_RETF: + if inputs > outputs: + rjump_snippet_pushes = True + elif outputs > inputs: + rjump_snippet_pops = True code += rjump code += body @@ -200,11 +210,16 @@ def section_code_with( if is_backwards and inputs != outputs: has_invalid_back_jump = True + + if rjump_spot == RjumpSpot.BEFORE_TERMINATION or ( + rjump_spot == RjumpSpot.BEGINNING and len(termination) == 0 + ): if rjump_kind in [ RjumpKind.RJUMPI_OVER_NEXT, RjumpKind.RJUMPI_OVER_NEXT_NESTED, RjumpKind.RJUMPV_EMPTY_AND_OVER_NEXT, ]: + # Jump over termination or jump over body, but there is nothing after the body. rjump_falls_off_code = True code += termination @@ -231,15 +246,15 @@ def section_code_with( ) @pytest.mark.parametrize( "rjump_kind", - RjumpKind.__members__.values(), + RjumpKind, ) # Parameter value fixed for first iteration, to cover the most important case. @pytest.mark.parametrize("rjump_section_idx", [0, 1]) @pytest.mark.parametrize( "rjump_spot", - RjumpSpot.__members__.values(), + RjumpSpot, ) -def test_eof_validity( +def test_rjumps_callf_retf( eof_test: EOFTestFiller, inputs: Tuple[int, ...], outputs: Tuple[int, ...], @@ -264,6 +279,10 @@ def test_eof_validity( container_has_rjump_pops = False container_has_rjump_pushes = False container_has_rjump_off_code = False + container_has_section_0_retf = ( + rjump_section_idx == 0 and rjump_kind == RjumpKind.RJUMPI_OVER_RETF + ) + for section_idx in range(num_sections): if section_idx == 0: call = Op.CALLF[section_idx + 1] @@ -298,15 +317,15 @@ def test_eof_validity( termination, ) - container_has_invalid_back_jump = ( - container_has_invalid_back_jump or section_has_invalid_back_jump - ) - container_has_rjump_pops = container_has_rjump_pops or rjump_snippet_pops + if section_has_invalid_back_jump: + container_has_invalid_back_jump = True + if rjump_snippet_pops: + container_has_rjump_pops = True # Pushes to the stack never affect the zeroth section, because it `STOP`s and not `RETF`s. - container_has_rjump_pushes = container_has_rjump_pushes or ( - rjump_snippet_pushes and section_idx != 0 - ) - container_has_rjump_off_code = container_has_rjump_off_code or rjump_falls_off_code + if rjump_snippet_pushes and section_idx != 0: + container_has_rjump_pushes = True + if rjump_falls_off_code: + container_has_rjump_off_code = True if section_idx > 0: sections.append( @@ -328,7 +347,99 @@ def test_eof_validity( possible_exceptions.append(EOFException.STACK_HIGHER_THAN_OUTPUTS) if container_has_rjump_off_code: possible_exceptions.append(EOFException.INVALID_RJUMP_DESTINATION) + if container_has_section_0_retf: + possible_exceptions.append(EOFException.INVALID_NON_RETURNING_FLAG) - eof_test( - data=bytes(Container(sections=sections)), expect_exception=possible_exceptions or None - ) + eof_test(data=Container(sections=sections), expect_exception=possible_exceptions or None) + + +@pytest.mark.parametrize( + "inputs", itertools.product(*([possible_inputs_outputs] * (num_sections - 1))) +) +@pytest.mark.parametrize( + "rjump_kind", + RjumpKind, +) +# Parameter value fixed for first iteration, to cover the most important case. +@pytest.mark.parametrize("rjump_section_idx", [0, 1]) +@pytest.mark.parametrize( + "rjump_spot", + # `termination` is empty for JUMPF codes, because JUMPF serves as one. Spot + # `BEFORE_TERMINATION` is unreachable code. + [k for k in RjumpSpot if k not in [RjumpSpot.BEFORE_TERMINATION]], +) +def test_rjumps_jumpf_nonreturning( + eof_test: EOFTestFiller, + inputs: Tuple[int, ...], + rjump_kind: RjumpKind, + rjump_section_idx: int, + rjump_spot: RjumpSpot, +): + """ + Test EOF container validaiton for EIP-4200 vs EIP-6206 interactions on non-returning + functions. + """ + # Zeroth section has always 0 inputs and 0 outputs, so is excluded from param + inputs = (0,) + inputs + + sections = [] + container_has_rjump_pops = False + container_has_rjump_off_code = False + container_has_non_returning_retf = False + + for section_idx in range(num_sections): + if section_idx < num_sections - 1: + call = Op.JUMPF[section_idx + 1] + call.popped_stack_items = inputs[section_idx + 1] + call.pushed_stack_items = 0 + call.min_stack_height = call.popped_stack_items + call.max_stack_height = max(call.popped_stack_items, call.pushed_stack_items) + termination = Bytecode() + else: + call = None + termination = Op.STOP + + # `section_has_invalid_back_jump` - never happens: we excluded RJUMP from the end + # `rjump_snippet_pushes` - never happens: we never RETF where too large stack would fail + ( + code, + _section_has_invalid_back_jump, + rjump_snippet_pops, + _rjump_snippet_pushes, + rjump_falls_off_code, + ) = section_code_with( + inputs[section_idx], + 0, + rjump_kind if rjump_section_idx == section_idx else None, + rjump_spot, + call, + termination, + ) + + if rjump_snippet_pops: + container_has_rjump_pops = True + if rjump_falls_off_code: + container_has_rjump_off_code = True + if rjump_kind == RjumpKind.RJUMPI_OVER_RETF: + container_has_non_returning_retf = True + + if section_idx > 0: + sections.append( + Section.Code( + code, + code_inputs=inputs[section_idx], + code_outputs=NON_RETURNING_SECTION, + ) + ) + else: + sections.append(Section.Code(code)) + + possible_exceptions = [] + if container_has_rjump_pops: + possible_exceptions.append(EOFException.STACK_UNDERFLOW) + if container_has_rjump_off_code: + possible_exceptions.append(EOFException.INVALID_RJUMP_DESTINATION) + if container_has_non_returning_retf: + possible_exceptions.append(EOFException.INVALID_NON_RETURNING_FLAG) + + eof_test(data=Container(sections=sections), expect_exception=possible_exceptions or None) diff --git a/whitelist.txt b/whitelist.txt index 7eaa4fa2f2..1e8486f13b 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -631,6 +631,7 @@ jumpdest rjump rjumpi rjumpkind +rjumps rjumpv RJUMPV callf