Skip to content

Commit

Permalink
new(tests): EOF - EIP-663: EXCHANGE opcode (#544)
Browse files Browse the repository at this point in the history
* new(test): add tests for EOF/EIP-663 DUPN SWAPN

* improve code generation

* chore(ci): Update workflow actions to use Node.js 20 versions (#527)

* chore(ci): Update workflow actions to use Node.js 20 versions.

* chore: Add changelog.

* Add --traces support to besu (#511)

Add support for adding traces to output when using Besu.

Signed-off-by: Danno Ferrin <danno@numisight.com>

* feat(fw): call `evmone-eofparse` on generated EOF fixtures in fill (#519)

Co-authored-by: Dimitry Kh <dimitry@ethereum.org>
Co-authored-by: danceratopz <danceratopz@gmail.com>

* docs(fix): small fix to tracing report in readme cf #511 (#539)

* fix EOF return stack tests

The tests were previously corrected against a bug in Besu,

Signed-off-by: Danno Ferrin <danno@numisight.com>

* EXCHANGE

Exercise exchange operation

Signed-off-by: Danno Ferrin <danno@numisight.com>

* speling

Signed-off-by: Danno Ferrin <danno@numisight.com>

* move

Signed-off-by: Danno Ferrin <danno@numisight.com>

* feat(fw): Add EXCHANGE encoder

* new(tests): EOF - EIP-663: Invalid container due to invalid exchange

---------

Signed-off-by: Danno Ferrin <danno@numisight.com>
Co-authored-by: Paweł Bylica <pawel@ethereum.org>
Co-authored-by: spencer <spencer.taylor-brown@ethereum.org>
Co-authored-by: Mario Vega <marioevz@gmail.com>
Co-authored-by: Dimitry Kh <dimitry@ethereum.org>
Co-authored-by: danceratopz <danceratopz@gmail.com>
  • Loading branch information
6 people committed May 28, 2024
1 parent 3baf416 commit ddae720
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 3 deletions.
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 @@ -180,6 +180,7 @@ hyperledger
iat
ignoreRevsFile
img
imm
immediates
incrementing
init
Expand Down

0 comments on commit ddae720

Please sign in to comment.