Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(tests) EIP-7069 RETURNDATALOAD and RETURNDATACOPY #595

Merged
merged 21 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tests/prague/eip7692_eof_v1/eip7069_extcall/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Revamped Call Instructions Tests
"""

REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7069.md"
REFERENCE_SPEC_VERSION = "1795943aeacc86131d5ab6bb3d65824b3b1d4cad"
13 changes: 13 additions & 0 deletions tests/prague/eip7692_eof_v1/eip7069_extcall/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
EOF extcall 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
2 changes: 0 additions & 2 deletions tests/prague/eip7692_eof_v1/eip7069_extcall/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
EOF V1 Constants used throughout all tests
"""

EOF_FORK_NAME = "CancunEIP7692"

CALL_FAILURE = 0
CALL_SUCCESS = 1
EXTCALL_SUCCESS = 0
Expand Down
282 changes: 282 additions & 0 deletions tests/prague/eip7692_eof_v1/eip7069_extcall/test_returndataload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
"""
abstract: Tests [EIP-7069: Revamped CALL instructions](https://eips.ethereum.org/EIPS/eip-7069)
Tests for the RETURNDATALOAD instriction
""" # noqa: E501
from typing import List

import pytest

from ethereum_test_tools import (
Account,
Address,
Environment,
StateTestFiller,
TestAddress,
Transaction,
)
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

from .. import EOF_FORK_NAME
from . import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION
from .helpers import slot_code_worked, value_code_worked

REFERENCE_SPEC_GIT_PATH = REFERENCE_SPEC_GIT_PATH
REFERENCE_SPEC_VERSION = REFERENCE_SPEC_VERSION

pytestmark = pytest.mark.valid_from(EOF_FORK_NAME)


@pytest.mark.parametrize(
["call_prefix", "opcode", "call_suffix"],
[
pytest.param([500_000], Op.CALL, [0, 0, 0, 0, 0], id="CALL"),
pytest.param([500_000], Op.CALLCODE, [0, 0, 0, 0, 0], id="CALLCODE"),
pytest.param([500_000], Op.DELEGATECALL, [0, 0, 0, 0], id="DELEGATECALL"),
pytest.param([500_000], Op.STATICCALL, [0, 0, 0, 0], id="STATICCALL"),
pytest.param([], Op.EXTCALL, [0, 0, 0], id="EXTCALL"),
pytest.param([], Op.EXTDELEGATECALL, [0, 0], id="EXTDELEGATECALL"),
pytest.param([], Op.EXTSTATICCALL, [0, 0], id="EXTSTATICCALL"),
],
ids=lambda x: x,
)
@pytest.mark.parametrize(
"return_data",
[
b"",
b"\x10" * 0x10,
b"\x20" * 0x20,
b"\x30" * 0x30,
],
ids=lambda x: "len_%x" % len(x),
)
@pytest.mark.parametrize(
"offset",
[
0,
0x10,
0x20,
0x30,
],
ids=lambda x: "offset_%x" % x,
)
@pytest.mark.parametrize(
"size",
[
0,
0x10,
0x20,
0x30,
],
ids=lambda x: "size_%x" % x,
)
def test_returndatacopy_handling(
state_test: StateTestFiller,
call_prefix: List[int],
opcode: Op,
call_suffix: List[int],
return_data: bytes,
offset: int,
size: int,
):
"""
Tests ReturnDataLoad including multiple offset conditions and differeing legacy vs. eof
boundary conditions.

entrypoint creates a "0xff" test area of memory, delegate calls to caller.
Caller is either EOF or legacy, as per parameter. Calls returner and copies the return data
based on offset and size params. Cases are expected to trigger boundary violations.

Entrypoint copies the test area to storage slots, and the expected result is asserted.
"""
env = Environment()
address_entry_point = Address(0x1000000)
address_caller = Address(0x1000001)
address_returner = Address(0x1000002)
tx = Transaction(to=address_entry_point, gas_limit=2_000_000, nonce=1)

slot_result_start = 0x1000

pre = {
TestAddress: Account(balance=10**18, nonce=tx.nonce),
address_entry_point: Account(
nonce=1,
code=Op.NOOP
# First, create a "dirty" area, so we can check zero overwrite
+ Op.MSTORE(0x00, -1) + Op.MSTORE(0x20, -1)
# call the contract under test
+ Op.DELEGATECALL(1_000_000, address_caller, 0, 0, 0, 0)
+ Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE)
# store the return data
+ Op.SSTORE(slot_result_start, Op.MLOAD(0x0))
+ Op.SSTORE(slot_result_start + 1, Op.MLOAD(0x20))
+ Op.SSTORE(slot_code_worked, value_code_worked)
+ Op.STOP,
),
address_returner: Account(
nonce=1,
code=Container(
sections=[
Section.Code(
code=Op.DATACOPY(0, 0, Op.DATASIZE) + Op.RETURN(0, Op.DATASIZE),
code_outputs=NON_RETURNING_SECTION,
max_stack_height=3,
),
Section.Data(data=return_data),
]
),
),
}

result = [0xFF] * 0x40
result[0:size] = [0] * size
extent = size - max(0, size + offset - len(return_data))
if extent > 0 and len(return_data) > 0:
result[0:extent] = [return_data[0]] * extent
post = {
address_entry_point: Account(
storage={
slot_code_worked: value_code_worked,
slot_result_start: bytes(result[:0x20]),
(slot_result_start + 0x1): bytes(result[0x20:]),
}
)
}

code_under_test: bytes = (
opcode(*call_prefix, address_returner, *call_suffix)
+ Op.RETURNDATACOPY(0, offset, size)
+ Op.SSTORE(slot_code_worked, value_code_worked)
+ Op.RETURN(0, size)
)
match opcode:
case Op.EXTCALL | Op.EXTDELEGATECALL | Op.EXTSTATICCALL:
pre[address_caller] = Account(
code=Container(
sections=[
Section.Code(
code=code_under_test,
max_stack_height=4,
)
]
)
)
case Op.CALL | Op.CALLCODE | Op.DELEGATECALL | Op.STATICCALL:
pre[address_caller] = Account(
code=code_under_test,
)
if (offset + size) > len(return_data):
post[address_entry_point] = Account(
storage={
slot_code_worked: value_code_worked,
slot_result_start: b"\xff" * 32,
slot_result_start + 1: b"\xff" * 32,
}
)

state_test(
env=env,
pre=pre,
tx=tx,
post=post,
)


@pytest.mark.parametrize(
["opcode", "call_suffix"],
[
pytest.param(Op.EXTCALL, [0, 0, 0], id="EXTCALL"),
pytest.param(Op.EXTDELEGATECALL, [0, 0], id="EXTDELEGATECALL"),
pytest.param(Op.EXTSTATICCALL, [0, 0], id="EXTSTATICCALL"),
],
ids=lambda x: x,
)
@pytest.mark.parametrize(
"return_data",
[
b"",
b"\x10" * 0x10,
b"\x20" * 0x20,
b"\x30" * 0x30,
],
ids=lambda x: "len_%x" % len(x),
)
@pytest.mark.parametrize(
"offset",
[
0,
0x10,
0x20,
0x30,
],
ids=lambda x: "offset_%x" % x,
)
def test_returndataload_handling(
state_test: StateTestFiller,
opcode: Op,
call_suffix: List[int],
return_data: bytes,
offset: int,
):
"""
Much simpler than returndatacopy, no memory or boosted call. Returner is called
and results are stored in storage slot, which is asserted for expected values.
The parameters offset and return data are configured to test boundary conditions.
"""
env = Environment()
address_entry_point = Address(0x1000000)
address_returner = Address(0x1000001)
tx = Transaction(to=address_entry_point, gas_limit=2_000_000, nonce=1)

slot_result_start = 0x1000

pre = {
TestAddress: Account(balance=10**18, nonce=tx.nonce),
address_entry_point: Account(
nonce=1,
code=Container(
sections=[
Section.Code(
code=opcode(address_returner, *call_suffix)
+ Op.SSTORE(slot_result_start, Op.RETURNDATALOAD(offset))
+ Op.SSTORE(slot_code_worked, value_code_worked)
+ Op.STOP,
max_stack_height=len(call_suffix) + 1,
)
]
),
),
address_returner: Account(
nonce=1,
code=Container(
sections=[
Section.Code(
code=Op.DATACOPY(0, 0, Op.DATASIZE) + Op.RETURN(0, Op.DATASIZE),
max_stack_height=3,
),
Section.Data(data=return_data),
]
),
),
}

result = [0] * 0x20
extent = 0x20 - max(0, 0x20 + offset - len(return_data))
if extent > 0 and len(return_data) > 0:
result[0:extent] = [return_data[0]] * extent
post = {
address_entry_point: Account(
storage={
slot_code_worked: value_code_worked,
slot_result_start: bytes(result),
}
)
}

state_test(
env=env,
pre=pre,
tx=tx,
post=post,
)