forked from ethereum/execution-spec-tests
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new(tests): EOF - EIP-5450: RJUMP* vs CALLF tests
- Loading branch information
Showing
5 changed files
with
301 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
""" | ||
EOF tests for EIP-5450 stack validation | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
""" | ||
EOF Functions 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 |
270 changes: 270 additions & 0 deletions
270
tests/prague/eip7692_eof_v1/eip5450_stack/test_code_validation.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
""" | ||
Code validation of CALLF, JUMPF, RETF opcodes in conjunction with static relative jumps | ||
""" | ||
|
||
import itertools | ||
from enum import Enum, auto, unique | ||
from typing import Tuple | ||
|
||
import pytest | ||
|
||
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_vm.bytecode import Bytecode | ||
|
||
from .. import EOF_FORK_NAME | ||
|
||
REFERENCE_SPEC_GIT_PATH = "EIPS/eip-5450.md" | ||
REFERENCE_SPEC_VERSION = "f20b164b00ae5553f7536a6d7a83a0f254455e09" | ||
|
||
pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) | ||
|
||
|
||
@unique | ||
class RjumpKind(Enum): | ||
""" | ||
Kinds of RJUMP* instruction snippets to generate. | ||
""" | ||
|
||
EMPTY_RJUMP = auto() | ||
EMPTY_RJUMPI = auto() | ||
RJUMPI_OVER_PUSH = auto() | ||
RJUMPI_OVER_NOOP = auto() | ||
RJUMPI_OVER_STOP = auto() | ||
RJUMPI_OVER_PUSH_POP = auto() | ||
RJUMPI_OVER_POP = auto() | ||
RJUMPI_OVER_NEXT = auto() | ||
RJUMPI_OVER_NEXT_NESTED = auto() | ||
RJUMPI_TO_START = auto() | ||
RJUMPV_EMPTY_AND_OVER_NEXT = auto() | ||
RJUMPV_OVER_PUSH_AND_TO_START = auto() | ||
|
||
def __str__(self) -> str: | ||
""" | ||
Returns the string representation of the enum | ||
""" | ||
return f"{self.name}" | ||
|
||
|
||
@unique | ||
class RjumpSpot(Enum): | ||
""" | ||
Possible spots in the code section layout where the RJUMP* is injected | ||
""" | ||
|
||
BEGINNING = auto() | ||
BEFORE_TERMINATION = auto() | ||
|
||
def __str__(self) -> str: | ||
""" | ||
Returns the string representation of the enum | ||
""" | ||
return f"{self.name}" | ||
|
||
|
||
def rjump_code_with(rjump_kind: RjumpKind | None, code_so_far_len: int, next_code_len: int): | ||
""" | ||
Unless `rjump_kind` is None generates a code snippet with an RJUMP* instruction. | ||
For some kinds `code_so_far_len` must be code length in bytes preceeding the snippet. | ||
For some kinds `next_code_len` must be code length in bytes of some code which follows. | ||
It is expected that the snippet and the jump target are valid, but the resulting code | ||
or its stack balance might not. | ||
""" | ||
body = Bytecode() | ||
|
||
if rjump_kind == RjumpKind.EMPTY_RJUMP: | ||
body = Op.RJUMP[0] | ||
elif rjump_kind == RjumpKind.EMPTY_RJUMPI: | ||
body = Op.RJUMPI[0](1) | ||
elif rjump_kind == RjumpKind.RJUMPI_OVER_PUSH: | ||
body = Op.RJUMPI[1](0) + Op.PUSH0 | ||
elif rjump_kind == RjumpKind.RJUMPI_OVER_NOOP: | ||
body = Op.RJUMPI[1](0) + Op.NOOP | ||
elif rjump_kind == RjumpKind.RJUMPI_OVER_STOP: | ||
body = Op.RJUMPI[1](0) + Op.STOP | ||
elif rjump_kind == RjumpKind.RJUMPI_OVER_PUSH_POP: | ||
body = Op.RJUMPI[2](0) + Op.PUSH0 + Op.POP | ||
elif rjump_kind == RjumpKind.RJUMPI_OVER_POP: | ||
body = Op.RJUMPI[1](0) + Op.POP + Op.PUSH0 | ||
elif rjump_kind == RjumpKind.RJUMPI_OVER_NEXT: | ||
body = Op.RJUMPI[next_code_len](0) | ||
elif rjump_kind == RjumpKind.RJUMPI_OVER_NEXT_NESTED: | ||
rjump_inner = Op.RJUMPI[next_code_len](0) | ||
body = Op.RJUMPI[len(rjump_inner)](0) + rjump_inner | ||
elif rjump_kind == RjumpKind.RJUMPI_TO_START: | ||
rjumpi_len = len(Op.RJUMPI[0](0)) | ||
body = Op.RJUMPI[-code_so_far_len - rjumpi_len](0) | ||
elif rjump_kind == RjumpKind.RJUMPV_EMPTY_AND_OVER_NEXT: | ||
body = Op.RJUMPV[[0, next_code_len]](0) | ||
elif rjump_kind == RjumpKind.RJUMPV_OVER_PUSH_AND_TO_START: | ||
rjumpv_two_destinations_len = len(Op.RJUMPV[[0, 0]](0)) | ||
body = Op.RJUMPV[[1, -code_so_far_len - rjumpv_two_destinations_len]](0) + Op.PUSH0 | ||
elif not rjump_kind: | ||
pass | ||
else: | ||
raise TypeError("unknown rjumps value" + str(rjump_kind)) | ||
|
||
return body | ||
|
||
|
||
def call_code_with(inputs, outputs, call: Bytecode): | ||
""" | ||
Generates a code snippet with the `call` bytecode provided and its respective input/output | ||
management. | ||
`inputs` and `outputs` are understood as those of the code section we're generating for. | ||
""" | ||
body = Bytecode() | ||
|
||
if call.popped_stack_items > inputs: | ||
body += Op.PUSH0 * (call.popped_stack_items - inputs) | ||
elif call.popped_stack_items < inputs: | ||
body += Op.POP * (inputs - call.popped_stack_items) | ||
|
||
body += call | ||
if call.pushed_stack_items < outputs: | ||
body += Op.PUSH0 * (outputs - call.pushed_stack_items) | ||
elif call.pushed_stack_items > outputs: | ||
body += Op.POP * (call.pushed_stack_items - outputs) | ||
|
||
return body | ||
|
||
|
||
def section_code_with( | ||
inputs, | ||
outputs, | ||
rjump_kind: RjumpKind | None, | ||
rjump_spot: RjumpSpot, | ||
call: Bytecode | None, | ||
termination: Bytecode, | ||
): | ||
""" | ||
Generates a code section with RJUMP* and CALLF/RETF instructions. | ||
""" | ||
code = Bytecode() | ||
|
||
if call: | ||
body = call_code_with(inputs, outputs, call) | ||
else: | ||
body = Op.POP * inputs + Op.PUSH0 * outputs | ||
|
||
if rjump_spot == RjumpSpot.BEGINNING: | ||
code += rjump_code_with(rjump_kind, 0, len(body)) | ||
|
||
code += body | ||
|
||
if rjump_spot == RjumpSpot.BEFORE_TERMINATION: | ||
# next_code_len=0 avoids jumping over the termination which never validates | ||
code += rjump_code_with(rjump_kind, len(code), next_code_len=0) | ||
|
||
code += termination | ||
|
||
code.max_stack_height = max(code.max_stack_height, inputs, outputs) | ||
if call: | ||
code.max_stack_height = max( | ||
code.max_stack_height, call.popped_stack_items, call.pushed_stack_items | ||
) | ||
return code | ||
|
||
|
||
num_sections = 3 | ||
possible_inputs_outputs = range(2) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
["inputs", "outputs"], | ||
itertools.product( | ||
list(itertools.product(*([possible_inputs_outputs] * (num_sections - 1)))), | ||
list(itertools.product(*([possible_inputs_outputs] * (num_sections - 1)))), | ||
), | ||
) | ||
@pytest.mark.parametrize( | ||
"rjump_kind", | ||
RjumpKind.__members__.values(), | ||
) | ||
# Parameter value fixed for first iteration, to cover the most important case. | ||
@pytest.mark.parametrize("rjump_section_idx", [1]) | ||
@pytest.mark.parametrize( | ||
"rjump_spot", | ||
RjumpSpot.__members__.values(), | ||
) | ||
@pytest.mark.parametrize( | ||
"rjump_stack_leeway", | ||
[0, 1], | ||
) | ||
def test_eof_validity( | ||
eof_test: EOFTestFiller, | ||
inputs: Tuple, | ||
outputs: Tuple, | ||
rjump_kind: RjumpKind, | ||
rjump_section_idx: int, | ||
rjump_spot: RjumpSpot, | ||
rjump_stack_leeway: int, | ||
): | ||
""" | ||
Test EOF container validaiton for EIP-4200 vs EIP-4750 interactions. | ||
Each test's code consists of `num_sections` code sections, which call into one another | ||
and then return. Code may include RJUMP* snippets of `rjump_kind` in various `rjump_spots`. | ||
`rjump_stack_leeway` is opportunistically added to max_stack_height and allows more cases | ||
to validate. | ||
""" | ||
# For some combinations `rjump_stack_leeway == 1` it never validates, so skipping them. | ||
assert len(inputs) == 2 and len(outputs) == 2 | ||
if rjump_stack_leeway == 1 and (inputs != (1, 1) or outputs != (1, 1)): | ||
pytest.skip("Never validates") | ||
if rjump_stack_leeway == 1 and rjump_kind in [ | ||
RjumpKind.EMPTY_RJUMP, | ||
RjumpKind.RJUMPI_OVER_PUSH, | ||
RjumpKind.RJUMPI_OVER_POP, | ||
RjumpKind.RJUMPV_OVER_PUSH_AND_TO_START, | ||
]: | ||
pytest.skip("Never validates") | ||
|
||
# Zeroth section has always 0 inputs and 0 outputs, so is excluded from param | ||
inputs = (0,) + inputs | ||
outputs = (0,) + outputs | ||
|
||
assert len(inputs) == len(outputs) | ||
assert num_sections == len(inputs) | ||
|
||
sections = [] | ||
for section_idx in range(num_sections): | ||
if section_idx == 0: | ||
call = Op.CALLF[section_idx + 1] | ||
call.popped_stack_items = inputs[section_idx + 1] | ||
call.pushed_stack_items = outputs[section_idx + 1] | ||
termination = Op.STOP | ||
elif section_idx < num_sections - 1: | ||
call = Op.CALLF[section_idx + 1] | ||
call.popped_stack_items = inputs[section_idx + 1] | ||
call.pushed_stack_items = outputs[section_idx + 1] | ||
termination = Op.RETF | ||
else: | ||
call = None | ||
termination = Op.RETF | ||
|
||
code = section_code_with( | ||
inputs[section_idx], | ||
outputs[section_idx], | ||
rjump_kind if rjump_section_idx == section_idx else None, | ||
rjump_spot, | ||
call, | ||
termination, | ||
) | ||
code.max_stack_height += rjump_stack_leeway if rjump_section_idx == section_idx else 0 | ||
|
||
sections.append(Section.Code(code)) | ||
|
||
if section_idx > 0: | ||
sections[section_idx].code_inputs = inputs[section_idx] | ||
sections[section_idx].code_outputs = outputs[section_idx] | ||
eof_test( | ||
data=bytes(Container(sections=sections)), | ||
# `empty_rjump` acts as a sanity check, it is completely stack-neutral so | ||
# should always validate.` | ||
no_expectations_on_validity=rjump_kind != "empty_rjump", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters