Skip to content

new(tests): EOF - EIP-663: EXCHANGE opcode #544

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

Merged
merged 15 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/ethereum_test_tools/tests/test_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@
(Op.RJUMPV[0, 3, 6, 9], bytes.fromhex("e2030000000300060009")),
(Op.RJUMPV[2, 0], bytes.fromhex("e20100020000")),
(Op.RJUMPV[b"\x02\x00\x02\xFF\xFF"], bytes.fromhex("e2020002ffff")),
(Op.EXCHANGE[0x2 + 0x0, 0x3 + 0x0], bytes.fromhex("e800")),
(Op.EXCHANGE[0x2 + 0x0, 0x3 + 0xF], bytes.fromhex("e80f")),
(Op.EXCHANGE[0x2 + 0xF, 0x3 + 0xF + 0x0], bytes.fromhex("e8f0")),
(Op.EXCHANGE[0x2 + 0xF, 0x3 + 0xF + 0xF], bytes.fromhex("e8ff")),
],
)
def test_opcodes(opcodes: bytes, expected: bytes):
Expand Down
83 changes: 80 additions & 3 deletions src/ethereum_test_tools/vm/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ def __getitem__(
data_portion = bytes()

if self.data_portion_formatter is not None:
data_portion = self.data_portion_formatter(*args)
if len(args) == 1 and isinstance(args[0], Iterable) and not isinstance(args[0], bytes):
data_portion = self.data_portion_formatter(*args[0])
else:
data_portion = self.data_portion_formatter(*args)
elif self.data_portion_length > 0:
# For opcodes with a data portion, the first argument is the data and the rest of the
# arguments form the stack.
Expand Down Expand Up @@ -253,8 +256,10 @@ def __call__(
raise ValueError("Opcode with data portion requires at least one argument")
if self.data_portion_formatter is not None:
data_portion_arg = args.pop(0)
assert isinstance(data_portion_arg, Iterable)
data_portion = self.data_portion_formatter(*data_portion_arg)
if isinstance(data_portion_arg, Iterable) and not isinstance(data_portion_arg, bytes):
data_portion = self.data_portion_formatter(*data_portion_arg)
else:
data_portion = self.data_portion_formatter(data_portion_arg)
elif self.data_portion_length > 0:
# For opcodes with a data portion, the first argument is the data and the rest of the
# arguments form the stack.
Expand Down Expand Up @@ -398,6 +403,27 @@ def _rjumpv_encoder(*args: int | bytes | Iterable[int]) -> bytes:
)


def _exchange_encoder(*args: int) -> bytes:
assert 1 <= len(args) <= 2, f"Exchange opcode requires one or two arguments, got {len(args)}"
if len(args) == 1:
return int.to_bytes(args[0], 1, "big")
# n = imm >> 4 + 1
# m = imm & 0xF + 1
# x = n + 1
# y = n + m + 1
# ...
# n = x - 1
# m = y - x
# m = y - n - 1
x, y = args
assert 2 <= x <= 0x11
assert x + 1 <= y <= x + 0x10
n = x - 1
m = y - x
imm = (n - 1) << 4 | m - 1
return int.to_bytes(imm, 1, "big")


class Opcodes(Opcode, Enum):
"""
Enum containing all known opcodes.
Expand Down Expand Up @@ -4976,6 +5002,13 @@ class Opcodes(Opcode, Enum):
Description
----

- deduct 3 gas
- read uint8 operand imm
- n = imm + 1
- nβ€˜th (1-based) stack item is duplicated at the top of the stack
- Stack validation: stack_height >= n


Inputs
----

Expand All @@ -4984,6 +5017,7 @@ class Opcodes(Opcode, Enum):

Fork
----
EOF Fork

Gas
----
Expand All @@ -5000,6 +5034,13 @@ class Opcodes(Opcode, Enum):
Description
----

- deduct 3 gas
- read uint8 operand imm
- n = imm + 1
- n + 1th stack item is swapped with the top stack item (1-based).
- Stack validation: stack_height >= n + 1


Inputs
----

Expand All @@ -5008,12 +5049,48 @@ class Opcodes(Opcode, Enum):

Fork
----
EOF Fork

Gas
----

"""

EXCHANGE = Opcode(0xE8, data_portion_formatter=_exchange_encoder)
"""
!!! Note: This opcode is under development

EXCHANGE[x, y]
----

Description
----
Exchanges two stack positions. Two nybbles, n is high 4 bits + 1, then m is 4 low bits + 1.
Exchanges tne n+1'th item with the n + m + 1 item.

Inputs x and y when the opcode is used as `EXCHANGE[x, y]`, are equal to:
- x = n + 1
- y = n + m + 1
Which each equals to 1-based stack positions swapped.

Inputs
----
n + m + 1, or ((imm >> 4) + (imm &0x0F) + 3) from the raw immediate,

Outputs
----
n + m + 1, or ((imm >> 4) + (imm &0x0F) + 3) from the raw immediate,

Fork
----
EOF_FORK

Gas
----
3

"""

CREATE3 = Opcode(0xEC, popped_stack_items=4, pushed_stack_items=1, data_portion_length=1)
"""
!!! Note: This opcode is under development
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
abstract: Tests [EIP-663: SWAPN, DUPN and EXCHANGE instructions](https://eips.ethereum.org/EIPS/eip-663)
Tests for the EXCHANGE instruction.
""" # noqa: E501

import pytest

from ethereum_test_tools import (
Account,
Environment,
EOFException,
EOFTestFiller,
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 ..eip3540_eof_v1.spec import EOF_FORK_NAME
from . import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION

REFERENCE_SPEC_GIT_PATH = REFERENCE_SPEC_GIT_PATH
REFERENCE_SPEC_VERSION = REFERENCE_SPEC_VERSION


@pytest.mark.valid_from(EOF_FORK_NAME)
def test_exchange_all_valid_immediates(
tx: Transaction,
state_test: StateTestFiller,
):
"""
Test case for all valid EXCHANGE immediates.
"""
n = 256
s = 34
values = range(0x3E8, 0x3E8 + s)

eof_code = Container(
sections=[
Section.Code(
code=b"".join(Op.PUSH2(v) for v in values)
+ b"".join(Op.EXCHANGE(x) for x in range(0, n))
+ b"".join((Op.PUSH1(x) + Op.SSTORE) for x in range(0, s))
+ Op.STOP,
code_inputs=0,
code_outputs=NON_RETURNING_SECTION,
max_stack_height=s + 1,
)
],
)

pre = {
TestAddress: Account(balance=1_000_000_000),
tx.to: Account(code=eof_code),
}

# this does the same full-loop exchange
values_rotated = list(range(0x3E8, 0x3E8 + s))
for e in range(0, n):
a = (e >> 4) + 1
b = (e & 0x0F) + 1 + a
temp = values_rotated[a]
values_rotated[a] = values_rotated[b]
values_rotated[b] = temp

post = {tx.to: Account(storage=dict(zip(range(0, s), reversed(values_rotated))))}

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


@pytest.mark.parametrize(
"stack_height,x,y",
[
# 2 and 3 are the lowest valid values for x and y, which translates to a
# zero immediate value.
pytest.param(0, 2, 3, id="stack_height=0_n=1_m=1"),
pytest.param(1, 2, 3, id="stack_height=1_n=1_m=1"),
pytest.param(2, 2, 3, id="stack_height=2_n=1_m=1"),
pytest.param(17, 2, 18, id="stack_height=17_n=1_m=16"),
pytest.param(17, 17, 18, id="stack_height=17_n=16_m=1"),
pytest.param(32, 17, 33, id="stack_height=32_n=16_m=16"),
],
)
@pytest.mark.valid_from(EOF_FORK_NAME)
def test_exchange_all_invalid_immediates(
eof_test: EOFTestFiller,
stack_height: int,
x: int,
y: int,
):
"""
Test case for all invalid EXCHANGE immediates.
"""
eof_code = Container(
sections=[
Section.Code(
code=b"".join(Op.PUSH2(v) for v in range(stack_height))
+ Op.EXCHANGE[x, y]
+ Op.POP * stack_height
+ Op.STOP,
code_inputs=0,
code_outputs=NON_RETURNING_SECTION,
max_stack_height=stack_height,
)
],
)

eof_test(
data=eof_code,
expect_exception=EOFException.STACK_UNDERFLOW,
)
1 change: 1 addition & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ hyperledger
iat
ignoreRevsFile
img
imm
immediates
incrementing
init
Expand Down