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

Test EIP-7069 Address Space Extension implications #522

Merged
merged 18 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
85 changes: 57 additions & 28 deletions src/ethereum_test_tools/vm/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -5115,6 +5115,30 @@ class Opcodes(Opcode, Enum):

"""

TXCREATE = Opcode(0xED, popped_stack_items=5, pushed_stack_items=1)
"""
!!! Note: This opcode is under development

TXCREATE(tx_initcode_hash, value, salt, input_offset, input_size)
----

Description
----

Inputs
----

Outputs
----

Fork
----

Gas
----

"""

RETURNCONTRACT = Opcode(
0xEE, popped_stack_items=2, pushed_stack_items=1, data_portion_length=1
)
Expand Down Expand Up @@ -5393,7 +5417,7 @@ class Opcodes(Opcode, Enum):

Fork
----
Prague
EOF Fork

Gas
----
Expand All @@ -5406,6 +5430,35 @@ class Opcodes(Opcode, Enum):
Source: [EIP-7069](https://eips.ethereum.org/EIPS/eip-7069)
"""

RETURNDATALOAD = Opcode(0xF7, popped_stack_items=1, pushed_stack_items=1)
"""
RETURNDATALOAD(offset) = returndata
----

Description
----
Copies 32 bytes of return data onto the operand stack

Inputs
----
- offset: The offset within the return data to start copying. There must be 32 byes of return
data at that address or an exceptional halt occurs.

Outputs
----
- returndata: the 32 bytes of return data at the offset

Fork
----
EOF Fork

Gas
----
3

Source: [EIP-7069](https://eips.ethereum.org/EIPS/eip-7069)
"""

EXTDELEGATECALL = Opcode(0xF9, popped_stack_items=3, pushed_stack_items=1)
"""
EXTDELEGATECALL(target_address, input_offset, input_size) = address
Expand All @@ -5431,7 +5484,7 @@ class Opcodes(Opcode, Enum):

Fork
----
Prague
EOF Fork

Gas
----
Expand Down Expand Up @@ -5477,7 +5530,7 @@ class Opcodes(Opcode, Enum):
Source: [evm.codes/#FA](https://www.evm.codes/#FA)
"""

EXTSTATICCALL = Opcode(0xFB, popped_stack_items=4, pushed_stack_items=1)
EXTSTATICCALL = Opcode(0xFB, popped_stack_items=3, pushed_stack_items=1)
"""
EXTSTATICCALL(target_address, input_offset, input_size) = address
----
Expand All @@ -5501,7 +5554,7 @@ class Opcodes(Opcode, Enum):

Fork
----
Prague
EOF Fork

Gas
----
Expand All @@ -5511,30 +5564,6 @@ class Opcodes(Opcode, Enum):
Source: [EIP-7069](https://eips.ethereum.org/EIPS/eip-7069)
"""

CREATE4 = Opcode(0xF7, popped_stack_items=5, pushed_stack_items=1)
"""
!!! Note: This opcode is under development

CREATE4()
----

Description
----

Inputs
----

Outputs
----

Fork
----

Gas
----

"""

REVERT = Opcode(0xFD, popped_stack_items=2)
"""
REVERT(offset, size)
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
@@ -0,0 +1,3 @@
"""
Revamped Call Instructions Tests
"""
11 changes: 11 additions & 0 deletions tests/prague/eip7692_eof_v1/eip7069_extcall/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
EOF V1 Constants used throughout all tests
"""

EOF_FORK_NAME = "CancunEIP7692"

CALL_FAILURE = 0
CALL_SUCCESS = 1
EXTCALL_SUCCESS = 0
EXTCALL_FAILURE = 1
EXTCALL_REVERT = 2
shemnon marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
"""
Tests the "Address Space Extension" aspect of EXT*CALL
"""
import itertools

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 .spec import CALL_SUCCESS, EXTCALL_FAILURE, EXTCALL_SUCCESS

REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7069.md"
REFERENCE_SPEC_VERSION = "1795943aeacc86131d5ab6bb3d65824b3b1d4cad"

pytestmark = pytest.mark.valid_from(EOF_FORK_NAME)

_address_allocation = itertools.count(0x10000)
address_entry_point = Address(next(_address_allocation))
address_caller = Address(next(_address_allocation))

_slot = itertools.count(1)
slot_top_level_call_status = next(_slot)
slot_target_call_status = next(_slot)
slot_target_returndata = next(_slot)


@pytest.mark.parametrize(
"target_address",
(
pytest.param(b"", id="zero"),
pytest.param(b"\xc0\xde", id="short"),
pytest.param(b"\x78" * 20, id="mid_20"),
pytest.param(b"\xff" * 20, id="max_20"),
pytest.param(b"\x01" + (b"\x00" * 20), id="min_ase"),
pytest.param(b"\x5a" * 28, id="mid_ase"),
pytest.param(b"\x5a" * 32, id="full_ase"),
pytest.param(b"\xff" * 32, id="max_ase"),
),
)
@pytest.mark.parametrize(
"target_account_type", ("empty", "EOA", "LegacyContract", "EOFContract"), ids=lambda x: x
)
@pytest.mark.parametrize(
"target_opcode",
(
Op.CALL,
Op.CALLCODE,
Op.STATICCALL,
Op.DELEGATECALL,
Op.EXTCALL,
Op.EXTDELEGATECALL,
Op.EXTSTATICCALL,
),
)
def test_address_space_extension(
state_test: StateTestFiller,
target_address: bytes,
target_opcode: Op,
target_account_type: str,
):
"""
Test contacts with possibly extended address and fail if address is too large
"""
env = Environment()

ase_address = len(target_address) > 20
stripped_address = target_address[-20:] if ase_address else target_address
if ase_address and target_address[0] == b"00":
raise ValueError("Test instrumentation requires target addresses trim leading zeros")

match target_opcode:
case Op.CALL | Op.CALLCODE:
call_suffix = [0, 0, 0, 0, 0]
ase_ready_opcode = False
case Op.DELEGATECALL | Op.STATICCALL:
call_suffix = [0, 0, 0, 0]
ase_ready_opcode = False
case Op.EXTCALL:
call_suffix = [0, 0, 0]
ase_ready_opcode = True
case Op.EXTDELEGATECALL | Op.EXTSTATICCALL:
call_suffix = [0, 0]
ase_ready_opcode = True
case _:
raise ValueError("Unexpected opcode ", target_opcode)

pre = {
TestAddress: Account(
balance=1000000000000000000000,
nonce=1,
),
address_entry_point: Account(
code=(
Op.MSTORE(0, Op.PUSH32(target_address))
+ Op.SSTORE(
slot_top_level_call_status,
Op.CALL(50000, address_caller, 0, 0, 32, 0, 0),
)
+ Op.STOP()
),
nonce=1,
shemnon marked this conversation as resolved.
Show resolved Hide resolved
),
address_caller: Account(
code=Container(
sections=[
Section.Code(
code=Op.SSTORE(
slot_target_call_status,
target_opcode(Op.CALLDATALOAD(0), *call_suffix),
)
+ Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE)
+ Op.SSTORE(slot_target_returndata, Op.MLOAD(0))
+ Op.STOP,
code_inputs=0,
code_outputs=NON_RETURNING_SECTION,
max_stack_height=1 + len(call_suffix),
)
],
)
if ase_ready_opcode
else Op.SSTORE(
slot_target_call_status,
target_opcode(Op.GAS, Op.CALLDATALOAD(0), *call_suffix),
)
+ Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE)
+ Op.SSTORE(slot_target_returndata, Op.MLOAD(0))
+ Op.STOP,
nonce=1,
shemnon marked this conversation as resolved.
Show resolved Hide resolved
),
}
match target_account_type:
case "empty":
# add no account
pass
case "EOA":
pre[Address(stripped_address)] = Account(code="", balance=10**18, nonce=9)
case "LegacyContract":
pre[Address(stripped_address)] = Account(
code=Op.MSTORE(0, Op.ADDRESS) + Op.RETURN(0, 32),
balance=0,
nonce=0,
)
case "EOFContract":
pre[Address(stripped_address)] = Account(
code=Container(
sections=[
Section.Code(
code=Op.MSTORE(0, Op.ADDRESS) + Op.RETURN(0, 32),
code_outputs=NON_RETURNING_SECTION,
max_stack_height=2,
)
],
),
balance=0,
nonce=0,
)

target_storage: dict[int, int | bytes | Address] = {}
shemnon marked this conversation as resolved.
Show resolved Hide resolved
match target_account_type:
case "empty" | "EOA":
target_storage[slot_target_call_status] = (
EXTCALL_SUCCESS if ase_ready_opcode else CALL_SUCCESS
shemnon marked this conversation as resolved.
Show resolved Hide resolved
)
case "LegacyContract" | "EOFContract":
match target_opcode:
case Op.CALL | Op.STATICCALL:
target_storage[slot_target_call_status] = CALL_SUCCESS
# CALL and STATICCALL call will call the stripped address
target_storage[slot_target_returndata] = stripped_address
case Op.CALLCODE | Op.DELEGATECALL:
target_storage[slot_target_call_status] = CALL_SUCCESS
# CALLCODE and DELEGATECALL call will call the stripped address
# but will change the sender to self
target_storage[slot_target_returndata] = address_caller
case Op.EXTCALL | Op.EXTSTATICCALL:
target_storage[slot_target_call_status] = EXTCALL_SUCCESS
shemnon marked this conversation as resolved.
Show resolved Hide resolved
# EXTCALL and EXTSTATICCALL will fault if calling an ASE address
target_storage[slot_target_returndata] = 0 if ase_address else stripped_address
shemnon marked this conversation as resolved.
Show resolved Hide resolved
case Op.EXTDELEGATECALL:
if not ase_address and target_account_type == "LegacyContract":
# If calling a legacy contract EXTDELEGATECALL fails
target_storage[slot_target_call_status] = EXTCALL_FAILURE
target_storage[slot_target_returndata] = 0
else:
target_storage[slot_target_call_status] = EXTCALL_SUCCESS
# EXTDELEGATECALL will fault if calling an ASE address
target_storage[slot_target_returndata] = (
0 if ase_address else address_caller
)

post = {
address_entry_point: Account(
storage={
slot_top_level_call_status: EXTCALL_SUCCESS
if ase_ready_opcode and ase_address
else CALL_SUCCESS
}
),
address_caller: Account(storage=target_storage),
}

tx = Transaction(
nonce=1,
to=address_entry_point,
gas_limit=50000000,
gas_price=10,
protected=False,
data="",
)

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