Skip to content

Commit ddae720

Browse files
shemnonchfastspencer-tbmarioevzwinsvega
authored
new(tests): EOF - EIP-663: EXCHANGE opcode (#544)
* 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 <[email protected]> * feat(fw): call `evmone-eofparse` on generated EOF fixtures in fill (#519) Co-authored-by: Dimitry Kh <[email protected]> Co-authored-by: danceratopz <[email protected]> * 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 <[email protected]> * EXCHANGE Exercise exchange operation Signed-off-by: Danno Ferrin <[email protected]> * speling Signed-off-by: Danno Ferrin <[email protected]> * move Signed-off-by: Danno Ferrin <[email protected]> * feat(fw): Add EXCHANGE encoder * new(tests): EOF - EIP-663: Invalid container due to invalid exchange --------- Signed-off-by: Danno Ferrin <[email protected]> Co-authored-by: Paweł Bylica <[email protected]> Co-authored-by: spencer <[email protected]> Co-authored-by: Mario Vega <[email protected]> Co-authored-by: Dimitry Kh <[email protected]> Co-authored-by: danceratopz <[email protected]>
1 parent 3baf416 commit ddae720

File tree

4 files changed

+203
-3
lines changed

4 files changed

+203
-3
lines changed

src/ethereum_test_tools/tests/test_vm.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@
261261
(Op.RJUMPV[0, 3, 6, 9], bytes.fromhex("e2030000000300060009")),
262262
(Op.RJUMPV[2, 0], bytes.fromhex("e20100020000")),
263263
(Op.RJUMPV[b"\x02\x00\x02\xFF\xFF"], bytes.fromhex("e2020002ffff")),
264+
(Op.EXCHANGE[0x2 + 0x0, 0x3 + 0x0], bytes.fromhex("e800")),
265+
(Op.EXCHANGE[0x2 + 0x0, 0x3 + 0xF], bytes.fromhex("e80f")),
266+
(Op.EXCHANGE[0x2 + 0xF, 0x3 + 0xF + 0x0], bytes.fromhex("e8f0")),
267+
(Op.EXCHANGE[0x2 + 0xF, 0x3 + 0xF + 0xF], bytes.fromhex("e8ff")),
264268
],
265269
)
266270
def test_opcodes(opcodes: bytes, expected: bytes):

src/ethereum_test_tools/vm/opcode.py

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ def __getitem__(
172172
data_portion = bytes()
173173

174174
if self.data_portion_formatter is not None:
175-
data_portion = self.data_portion_formatter(*args)
175+
if len(args) == 1 and isinstance(args[0], Iterable) and not isinstance(args[0], bytes):
176+
data_portion = self.data_portion_formatter(*args[0])
177+
else:
178+
data_portion = self.data_portion_formatter(*args)
176179
elif self.data_portion_length > 0:
177180
# For opcodes with a data portion, the first argument is the data and the rest of the
178181
# arguments form the stack.
@@ -253,8 +256,10 @@ def __call__(
253256
raise ValueError("Opcode with data portion requires at least one argument")
254257
if self.data_portion_formatter is not None:
255258
data_portion_arg = args.pop(0)
256-
assert isinstance(data_portion_arg, Iterable)
257-
data_portion = self.data_portion_formatter(*data_portion_arg)
259+
if isinstance(data_portion_arg, Iterable) and not isinstance(data_portion_arg, bytes):
260+
data_portion = self.data_portion_formatter(*data_portion_arg)
261+
else:
262+
data_portion = self.data_portion_formatter(data_portion_arg)
258263
elif self.data_portion_length > 0:
259264
# For opcodes with a data portion, the first argument is the data and the rest of the
260265
# arguments form the stack.
@@ -398,6 +403,27 @@ def _rjumpv_encoder(*args: int | bytes | Iterable[int]) -> bytes:
398403
)
399404

400405

406+
def _exchange_encoder(*args: int) -> bytes:
407+
assert 1 <= len(args) <= 2, f"Exchange opcode requires one or two arguments, got {len(args)}"
408+
if len(args) == 1:
409+
return int.to_bytes(args[0], 1, "big")
410+
# n = imm >> 4 + 1
411+
# m = imm & 0xF + 1
412+
# x = n + 1
413+
# y = n + m + 1
414+
# ...
415+
# n = x - 1
416+
# m = y - x
417+
# m = y - n - 1
418+
x, y = args
419+
assert 2 <= x <= 0x11
420+
assert x + 1 <= y <= x + 0x10
421+
n = x - 1
422+
m = y - x
423+
imm = (n - 1) << 4 | m - 1
424+
return int.to_bytes(imm, 1, "big")
425+
426+
401427
class Opcodes(Opcode, Enum):
402428
"""
403429
Enum containing all known opcodes.
@@ -4976,6 +5002,13 @@ class Opcodes(Opcode, Enum):
49765002
Description
49775003
----
49785004
5005+
- deduct 3 gas
5006+
- read uint8 operand imm
5007+
- n = imm + 1
5008+
- n‘th (1-based) stack item is duplicated at the top of the stack
5009+
- Stack validation: stack_height >= n
5010+
5011+
49795012
Inputs
49805013
----
49815014
@@ -4984,6 +5017,7 @@ class Opcodes(Opcode, Enum):
49845017
49855018
Fork
49865019
----
5020+
EOF Fork
49875021
49885022
Gas
49895023
----
@@ -5000,6 +5034,13 @@ class Opcodes(Opcode, Enum):
50005034
Description
50015035
----
50025036
5037+
- deduct 3 gas
5038+
- read uint8 operand imm
5039+
- n = imm + 1
5040+
- n + 1th stack item is swapped with the top stack item (1-based).
5041+
- Stack validation: stack_height >= n + 1
5042+
5043+
50035044
Inputs
50045045
----
50055046
@@ -5008,12 +5049,48 @@ class Opcodes(Opcode, Enum):
50085049
50095050
Fork
50105051
----
5052+
EOF Fork
50115053
50125054
Gas
50135055
----
50145056
50155057
"""
50165058

5059+
EXCHANGE = Opcode(0xE8, data_portion_formatter=_exchange_encoder)
5060+
"""
5061+
!!! Note: This opcode is under development
5062+
5063+
EXCHANGE[x, y]
5064+
----
5065+
5066+
Description
5067+
----
5068+
Exchanges two stack positions. Two nybbles, n is high 4 bits + 1, then m is 4 low bits + 1.
5069+
Exchanges tne n+1'th item with the n + m + 1 item.
5070+
5071+
Inputs x and y when the opcode is used as `EXCHANGE[x, y]`, are equal to:
5072+
- x = n + 1
5073+
- y = n + m + 1
5074+
Which each equals to 1-based stack positions swapped.
5075+
5076+
Inputs
5077+
----
5078+
n + m + 1, or ((imm >> 4) + (imm &0x0F) + 3) from the raw immediate,
5079+
5080+
Outputs
5081+
----
5082+
n + m + 1, or ((imm >> 4) + (imm &0x0F) + 3) from the raw immediate,
5083+
5084+
Fork
5085+
----
5086+
EOF_FORK
5087+
5088+
Gas
5089+
----
5090+
3
5091+
5092+
"""
5093+
50175094
CREATE3 = Opcode(0xEC, popped_stack_items=4, pushed_stack_items=1, data_portion_length=1)
50185095
"""
50195096
!!! Note: This opcode is under development
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""
2+
abstract: Tests [EIP-663: SWAPN, DUPN and EXCHANGE instructions](https://eips.ethereum.org/EIPS/eip-663)
3+
Tests for the EXCHANGE instruction.
4+
""" # noqa: E501
5+
6+
import pytest
7+
8+
from ethereum_test_tools import (
9+
Account,
10+
Environment,
11+
EOFException,
12+
EOFTestFiller,
13+
StateTestFiller,
14+
TestAddress,
15+
Transaction,
16+
)
17+
from ethereum_test_tools.eof.v1 import Container, Section
18+
from ethereum_test_tools.eof.v1.constants import NON_RETURNING_SECTION
19+
from ethereum_test_tools.vm.opcode import Opcodes as Op
20+
21+
from ..eip3540_eof_v1.spec import EOF_FORK_NAME
22+
from . import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION
23+
24+
REFERENCE_SPEC_GIT_PATH = REFERENCE_SPEC_GIT_PATH
25+
REFERENCE_SPEC_VERSION = REFERENCE_SPEC_VERSION
26+
27+
28+
@pytest.mark.valid_from(EOF_FORK_NAME)
29+
def test_exchange_all_valid_immediates(
30+
tx: Transaction,
31+
state_test: StateTestFiller,
32+
):
33+
"""
34+
Test case for all valid EXCHANGE immediates.
35+
"""
36+
n = 256
37+
s = 34
38+
values = range(0x3E8, 0x3E8 + s)
39+
40+
eof_code = Container(
41+
sections=[
42+
Section.Code(
43+
code=b"".join(Op.PUSH2(v) for v in values)
44+
+ b"".join(Op.EXCHANGE(x) for x in range(0, n))
45+
+ b"".join((Op.PUSH1(x) + Op.SSTORE) for x in range(0, s))
46+
+ Op.STOP,
47+
code_inputs=0,
48+
code_outputs=NON_RETURNING_SECTION,
49+
max_stack_height=s + 1,
50+
)
51+
],
52+
)
53+
54+
pre = {
55+
TestAddress: Account(balance=1_000_000_000),
56+
tx.to: Account(code=eof_code),
57+
}
58+
59+
# this does the same full-loop exchange
60+
values_rotated = list(range(0x3E8, 0x3E8 + s))
61+
for e in range(0, n):
62+
a = (e >> 4) + 1
63+
b = (e & 0x0F) + 1 + a
64+
temp = values_rotated[a]
65+
values_rotated[a] = values_rotated[b]
66+
values_rotated[b] = temp
67+
68+
post = {tx.to: Account(storage=dict(zip(range(0, s), reversed(values_rotated))))}
69+
70+
state_test(
71+
env=Environment(),
72+
pre=pre,
73+
post=post,
74+
tx=tx,
75+
)
76+
77+
78+
@pytest.mark.parametrize(
79+
"stack_height,x,y",
80+
[
81+
# 2 and 3 are the lowest valid values for x and y, which translates to a
82+
# zero immediate value.
83+
pytest.param(0, 2, 3, id="stack_height=0_n=1_m=1"),
84+
pytest.param(1, 2, 3, id="stack_height=1_n=1_m=1"),
85+
pytest.param(2, 2, 3, id="stack_height=2_n=1_m=1"),
86+
pytest.param(17, 2, 18, id="stack_height=17_n=1_m=16"),
87+
pytest.param(17, 17, 18, id="stack_height=17_n=16_m=1"),
88+
pytest.param(32, 17, 33, id="stack_height=32_n=16_m=16"),
89+
],
90+
)
91+
@pytest.mark.valid_from(EOF_FORK_NAME)
92+
def test_exchange_all_invalid_immediates(
93+
eof_test: EOFTestFiller,
94+
stack_height: int,
95+
x: int,
96+
y: int,
97+
):
98+
"""
99+
Test case for all invalid EXCHANGE immediates.
100+
"""
101+
eof_code = Container(
102+
sections=[
103+
Section.Code(
104+
code=b"".join(Op.PUSH2(v) for v in range(stack_height))
105+
+ Op.EXCHANGE[x, y]
106+
+ Op.POP * stack_height
107+
+ Op.STOP,
108+
code_inputs=0,
109+
code_outputs=NON_RETURNING_SECTION,
110+
max_stack_height=stack_height,
111+
)
112+
],
113+
)
114+
115+
eof_test(
116+
data=eof_code,
117+
expect_exception=EOFException.STACK_UNDERFLOW,
118+
)

whitelist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ hyperledger
180180
iat
181181
ignoreRevsFile
182182
img
183+
imm
183184
immediates
184185
incrementing
185186
init

0 commit comments

Comments
 (0)