Skip to content

Commit

Permalink
new(tests): EOF - EIP-7069 - RETURNDATALOAD and RETURNDATACOPY (#595)
Browse files Browse the repository at this point in the history
* Validate EIP-7676 Prepare for Address Space Extension

Validate ASE behaviors on legacy and EOF opcodes for ASE, when targeting
EOAs, Contracts, and empty accounts.  Opcodes are BALANCE, EXTCALL/CALL,
EXTDELEGATECALL/DELEGATECALL,  EXTSTATICCALL/STATICCALL, and CALLCODE.

Signed-off-by: Danno Ferrin <[email protected]>

* Reviewer requested changes

Signed-off-by: Danno Ferrin <[email protected]>

* formatting

Signed-off-by: Danno Ferrin <[email protected]>

* speling

Signed-off-by: Danno Ferrin <[email protected]>

* comment out balance from ASE checks

EIP-7676 moved the critical EXTCALL sections into the main spec.
EOF does not depend on a new BALANCE op, so don't test it.

Signed-off-by: Danno Ferrin <[email protected]>

* move to new subfolder

Signed-off-by: Danno Ferrin <[email protected]>

* Recast as EIP-7069 test

Signed-off-by: Danno Ferrin <[email protected]>

* use itertools.count instead of hard coded address

Signed-off-by: Danno Ferrin <[email protected]>

* Review changes

* Apply specific edits
* Parameterize by opcode as well

Signed-off-by: Danno Ferrin <[email protected]>

* tox fixes

Signed-off-by: Danno Ferrin <[email protected]>

* remove RETURNDATALOAD from ASE tests

Remove RETURNDATALOAD from ASE tests, to be moved to a different test

Signed-off-by: Danno Ferrin <[email protected]>

* reviewer comments

Signed-off-by: Danno Ferrin <[email protected]>

* new(tests) EIP-7069 RETURNDATALOAD and RETURNDATACOPY

Test new RETURNDATACOPY semantics across both EOF and Legacy.
Test RETURNDATALOAD semantics. Includes boundary conditions on both.

Signed-off-by: Danno Ferrin <[email protected]>

* review changes

Signed-off-by: Danno Ferrin <[email protected]>

* unneeded changes.

Signed-off-by: Danno Ferrin <[email protected]>

* docs: Changelog

---------

Signed-off-by: Danno Ferrin <[email protected]>
Co-authored-by: Mario Vega <[email protected]>
  • Loading branch information
shemnon and marioevz authored Jun 10, 2024
1 parent 5c590c6 commit de57ee5
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ Add tests for [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) ([#530](https://github.com/ethereum/execution-spec-tests/pull/530)).
- ✨ Add tests for [EIP-2935: Serve historical block hashes from state](https://eips.ethereum.org/EIPS/eip-2935) ([#564](https://github.com/ethereum/execution-spec-tests/pull/564)).
- ✨ Add tests for [EIP-4200: EOF - Static relative jumps](https://eips.ethereum.org/EIPS/eip-4200) ([#581](https://github.com/ethereum/execution-spec-tests/pull/581)).
- ✨ Add tests for [EIP-7069: EOF - Revamped CALL instructions](https://eips.ethereum.org/EIPS/eip-7069) ([#595](https://github.com/ethereum/execution-spec-tests/pull/595)).
- 🐞 Fix typos in self-destruct collision test from erroneous pytest parametrization ([#608](https://github.com/ethereum/execution-spec-tests/pull/608)).

### 🛠️ Framework
Expand Down
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,
)

0 comments on commit de57ee5

Please sign in to comment.