diff --git a/src/ethereum_test_tools/tests/test_vm.py b/src/ethereum_test_tools/tests/test_vm.py index a75af52c32..01d6628506 100644 --- a/src/ethereum_test_tools/tests/test_vm.py +++ b/src/ethereum_test_tools/tests/test_vm.py @@ -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): diff --git a/src/ethereum_test_tools/vm/opcode.py b/src/ethereum_test_tools/vm/opcode.py index 1b6c78e276..05cbf51670 100644 --- a/src/ethereum_test_tools/vm/opcode.py +++ b/src/ethereum_test_tools/vm/opcode.py @@ -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. @@ -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. @@ -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. @@ -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 ---- @@ -4984,6 +5017,7 @@ class Opcodes(Opcode, Enum): Fork ---- + EOF Fork Gas ---- @@ -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 ---- @@ -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 diff --git a/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_exchange.py b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_exchange.py new file mode 100644 index 0000000000..56cbe60100 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_exchange.py @@ -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, + ) diff --git a/whitelist.txt b/whitelist.txt index da33115fa3..addac64700 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -180,6 +180,7 @@ hyperledger iat ignoreRevsFile img +imm immediates incrementing init