Skip to content

Commit

Permalink
new(tests): EOF - EIP-5450: RJUMP* vs CALLF tests (#833)
Browse files Browse the repository at this point in the history
Co-authored-by: Mario Vega <marioevz@gmail.com>
  • Loading branch information
pdobacz and marioevz authored Oct 22, 2024
1 parent 295879d commit 7758f37
Show file tree
Hide file tree
Showing 3 changed files with 339 additions and 0 deletions.
3 changes: 3 additions & 0 deletions tests/osaka/eip7692_eof_v1/eip5450_stack/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
EOF tests for EIP-5450 stack validation
"""
334 changes: 334 additions & 0 deletions tests/osaka/eip7692_eof_v1/eip5450_stack/test_code_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
"""
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_exceptions.exceptions import EOFException
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: Bytecode
) -> Tuple[Bytecode, bool, bool, bool]:
"""
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.
Also returns some traits of the snippet: `is_backwards`, `pops` and `pushes`
"""
body = Bytecode()

is_backwards = False
pops = False
pushes = False
jumps_over_next = False

if rjump_kind == RjumpKind.EMPTY_RJUMP:
body = Op.RJUMP[0]
elif rjump_kind == RjumpKind.EMPTY_RJUMPI:
body = Op.RJUMPI[0](0)
elif rjump_kind == RjumpKind.RJUMPI_OVER_PUSH:
body = Op.RJUMPI[1](0) + Op.PUSH0
pushes = True
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
pops = True
elif rjump_kind == RjumpKind.RJUMPI_OVER_NEXT:
body = Op.RJUMPI[len(next_code)](0)
jumps_over_next = True
elif rjump_kind == RjumpKind.RJUMPI_OVER_NEXT_NESTED:
rjump_inner = Op.RJUMPI[len(next_code)](0)
body = Op.RJUMPI[len(rjump_inner)](0) + rjump_inner
jumps_over_next = True
elif rjump_kind == RjumpKind.RJUMPI_TO_START:
rjumpi_len = len(Op.RJUMPI[0](0))
body = Op.RJUMPI[-code_so_far_len - rjumpi_len](0)
is_backwards = True
elif rjump_kind == RjumpKind.RJUMPV_EMPTY_AND_OVER_NEXT:
body = Op.RJUMPV[[0, len(next_code)]](0)
jumps_over_next = True
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
is_backwards = True
pushes = True
elif not rjump_kind:
pass
else:
raise TypeError("unknown rjumps value" + str(rjump_kind))

if jumps_over_next:
# This is against intuition, but if the code we're jumping over pushes, the path
# which misses it will be short of stack items, as if the RJUMP* popped and vice versa.
if next_code.pushed_stack_items > next_code.popped_stack_items:
pops = True
elif next_code.popped_stack_items > next_code.pushed_stack_items:
pushes = True

return body, is_backwards, pops, pushes


def call_code_with(inputs, outputs, call: Bytecode) -> 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: int,
outputs: int,
rjump_kind: RjumpKind | None,
rjump_spot: RjumpSpot,
call: Bytecode | None,
termination: Bytecode,
) -> Tuple[Bytecode, bool, bool, bool, bool]:
"""
Generates a code section with RJUMP* and CALLF/RETF instructions.
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)

if call:
body = call_code_with(inputs, outputs, call)
else:
body = Op.POP * inputs + Op.PUSH0 * outputs

has_invalid_back_jump = False
rjump_snippet_pushes = False
rjump_snippet_pops = False
rjump_falls_off_code = False

if rjump_spot == RjumpSpot.BEGINNING:
rjump, is_backwards, rjump_snippet_pops, rjump_snippet_pushes = rjump_code_with(
rjump_kind, 0, body
)
code += rjump

code += body

if rjump_spot == RjumpSpot.BEFORE_TERMINATION:
rjump, is_backwards, rjump_snippet_pops, rjump_snippet_pushes = rjump_code_with(
rjump_kind, len(code), next_code=termination
)
code += rjump

if is_backwards and inputs != outputs:
has_invalid_back_jump = True
if rjump_kind in [
RjumpKind.RJUMPI_OVER_NEXT,
RjumpKind.RJUMPI_OVER_NEXT_NESTED,
RjumpKind.RJUMPV_EMPTY_AND_OVER_NEXT,
]:
rjump_falls_off_code = True

code += termination

return (
code,
has_invalid_back_jump,
rjump_snippet_pops,
rjump_snippet_pushes,
rjump_falls_off_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", [0, 1])
@pytest.mark.parametrize(
"rjump_spot",
RjumpSpot.__members__.values(),
)
def test_eof_validity(
eof_test: EOFTestFiller,
inputs: Tuple[int, ...],
outputs: Tuple[int, ...],
rjump_kind: RjumpKind,
rjump_section_idx: int,
rjump_spot: RjumpSpot,
):
"""
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`.
"""
# 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) == num_sections

sections = []
container_has_invalid_back_jump = False
container_has_rjump_pops = False
container_has_rjump_pushes = False
container_has_rjump_off_code = False
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]
call.min_stack_height = call.popped_stack_items
call.max_stack_height = max(call.popped_stack_items, call.pushed_stack_items)
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]
call.min_stack_height = call.popped_stack_items
call.max_stack_height = max(call.popped_stack_items, call.pushed_stack_items)
termination = Op.RETF
else:
call = None
termination = Op.RETF

(
code,
section_has_invalid_back_jump,
rjump_snippet_pops,
rjump_snippet_pushes,
rjump_falls_off_code,
) = section_code_with(
inputs[section_idx],
outputs[section_idx],
rjump_kind if rjump_section_idx == section_idx else None,
rjump_spot,
call,
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
# 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 section_idx > 0:
sections.append(
Section.Code(
code,
code_inputs=inputs[section_idx],
code_outputs=outputs[section_idx],
)
)
else:
sections.append(Section.Code(code))

possible_exceptions = []
if container_has_invalid_back_jump:
possible_exceptions.append(EOFException.STACK_HEIGHT_MISMATCH)
if container_has_rjump_pops:
possible_exceptions.append(EOFException.STACK_UNDERFLOW)
if container_has_rjump_pushes:
possible_exceptions.append(EOFException.STACK_HIGHER_THAN_OUTPUTS)
if container_has_rjump_off_code:
possible_exceptions.append(EOFException.INVALID_RJUMP_DESTINATION)

eof_test(
data=bytes(Container(sections=sections)), expect_exception=possible_exceptions or None
)
2 changes: 2 additions & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ htmlpath
https
hyperledger
iat
idx
ignoreRevsFile
img
imm
Expand Down Expand Up @@ -618,6 +619,7 @@ gas
jumpdest
rjump
rjumpi
rjumpkind
rjumpv
RJUMPV
callf
Expand Down

0 comments on commit 7758f37

Please sign in to comment.