Skip to content

Commit

Permalink
feat(tests): add a (opcode) Macro class and the OOG macro (#457)
Browse files Browse the repository at this point in the history
Co-authored-by: Mario Vega <[email protected]>
Co-authored-by: danceratopz <[email protected]>
  • Loading branch information
3 people authored Mar 26, 2024
1 parent 44f0bba commit 1983444
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Test fixtures for use by clients are available for each release on the [Github r
### 🛠️ Framework

- 🐞 Fix incorrect `!=` operator for `FixedSizeBytes` ([#477](https://github.com/ethereum/execution-spec-tests/pull/477)).
- ✨ Add Macro enum that represents byte sequence of Op instructions ([#457](https://github.com/ethereum/execution-spec-tests/pull/457))

### 🔧 EVM Tools

Expand Down
3 changes: 2 additions & 1 deletion src/entry_points/evm_bytes_to_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
from typing import Any, List, Optional

from ethereum_test_tools import Macro
from ethereum_test_tools import Opcodes as Op


Expand All @@ -21,7 +22,7 @@ def process_evm_bytes(evm_bytes_hex_string: Any) -> str: # noqa: D103

opcode: Optional[Op] = None
for op in Op:
if op.int() == opcode_byte:
if not isinstance(op, Macro) and op.int() == opcode_byte:
opcode = op
break

Expand Down
3 changes: 2 additions & 1 deletion src/entry_points/tests/test_evm_bytes_to_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
from evm_bytes_to_python import process_evm_bytes

from ethereum_test_tools import Macro
from ethereum_test_tools import Opcodes as Op

basic_vector = [
Expand All @@ -31,7 +32,7 @@ def test_evm_bytes_to_python(evm_bytes, python_opcodes):
assert process_evm_bytes(evm_bytes) == python_opcodes


@pytest.mark.parametrize("opcode", list(Op))
@pytest.mark.parametrize("opcode", [op for op in Op if not isinstance(op, Macro)])
def test_individual_opcodes(opcode):
"""Test each opcode individually"""
if opcode.data_portion_length > 0:
Expand Down
3 changes: 2 additions & 1 deletion src/ethereum_test_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
TestInfo,
)
from .spec.blockchain.types import Block, Header
from .vm import Opcode, OpcodeCallArg, Opcodes
from .vm import Macro, Opcode, OpcodeCallArg, Opcodes

__all__ = (
"SPEC_TYPES",
Expand Down Expand Up @@ -84,6 +84,7 @@
"Initcode",
"JSONEncoder",
"Opcode",
"Macro",
"OpcodeCallArg",
"Opcodes",
"ReferenceSpec",
Expand Down
15 changes: 15 additions & 0 deletions src/ethereum_test_tools/tests/test_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from ..common.base_types import Address
from ..vm.opcode import Macros as Om
from ..vm.opcode import Opcodes as Op


Expand Down Expand Up @@ -141,6 +142,10 @@
b"\x60\x08\x60\x07\x60\x06\x60\x05\x60\x04\x73\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x60\x01\xf0",
),
(
Om.OOG(),
bytes([0x64, 0x17, 0x48, 0x76, 0xE8, 0x00, 0x60, 0x00, 0x20]),
),
],
)
def test_opcodes(opcodes: bytes, expected: bytes):
Expand All @@ -156,4 +161,14 @@ def test_opcodes_repr():
"""
assert f"{Op.CALL}" == "CALL"
assert f"{Op.DELEGATECALL}" == "DELEGATECALL"
assert f"{Om.OOG}" == "OOG"
assert str(Op.ADD) == "ADD"


def test_macros():
"""
Test opcode and macros interaction
"""
assert (Op.PUSH1(1) + Om.OOG) == (Op.PUSH1(1) + Op.SHA3(0, 100000000000))
for opcode in Op:
assert opcode != Om.OOG
4 changes: 3 additions & 1 deletion src/ethereum_test_tools/vm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""
Ethereum Virtual Machine related definitions and utilities.
"""
from .opcode import Opcode, OpcodeCallArg, Opcodes

from .opcode import Macro, Opcode, OpcodeCallArg, Opcodes

__all__ = (
"Opcode",
"Macro",
"OpcodeCallArg",
"Opcodes",
)
117 changes: 108 additions & 9 deletions src/ethereum_test_tools/vm/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,55 @@ def _get_int_size(n: int) -> int:
_push_opcodes_byte_list = [bytes([0x5F + x]) for x in range(33)]


class Opcode(bytes):
class OpcodeMacroBase(bytes):
"""
Base class for Macro and Opcode, inherits from bytes.
This class is designed to represent a base structure for individual evm opcodes
and opcode macros.
"""

_name_: str

def __new__(cls, *args):
"""
Since OpcodeMacroBase is never instantiated directly but through
subclassing, this method simply forwards the arguments to the
bytes constructor.
"""
return super().__new__(cls, *args)

def __call__(self, *_: Union[int, bytes, str, "Opcode", FixedSizeBytes]) -> bytes:
"""
Make OpcodeMacroBase callable, so that arguments can directly be
provided to an Opcode in order to more conveniently generate
bytecode (implemented in the subclass).
"""
# ignore opcode arguments
return bytes(self)

def __str__(self) -> str:
"""
Return the name of the opcode, assigned at Enum creation.
"""
return self._name_

def __eq__(self, other):
"""
Allows comparison between OpcodeMacroBase instances and bytes objects.
Raises:
- NotImplementedError: if the comparison is not between an OpcodeMacroBase
or a bytes object.
"""
if isinstance(other, OpcodeMacroBase):
return self._name_ == other._name_
if isinstance(other, bytes):
return bytes(self) == other
raise NotImplementedError(f"Unsupported type for comparison f{type(other)}")


class Opcode(OpcodeMacroBase):
"""
Represents a single Opcode instruction in the EVM, with extra metadata useful to parametrize
tests.
Expand All @@ -48,7 +96,6 @@ class Opcode(bytes):
pushed_stack_items: int
min_stack_height: int
data_portion_length: int
_name_: str

def __new__(
cls,
Expand All @@ -73,6 +120,7 @@ def __new__(
obj.min_stack_height = min_stack_height
obj.data_portion_length = data_portion_length
return obj
raise TypeError("Opcode constructor '__new__' didn't return an instance!")

def __call__(self, *args_t: Union[int, bytes, str, "Opcode", FixedSizeBytes]) -> bytes:
"""
Expand Down Expand Up @@ -181,11 +229,26 @@ def int(self) -> int:
"""
return int.from_bytes(self, byteorder="big")

def __str__(self) -> str:

class Macro(OpcodeMacroBase):
"""
Represents opcode macro replacement, basically holds bytes
"""

def __new__(
cls,
macro_or_bytes: Union[bytes, "Macro"],
):
"""
Return the name of the opcode, assigned at Enum creation.
Creates a new opcode macro instance.
"""
return self._name_
if type(macro_or_bytes) is Macro:
# Required because Enum class calls the base class with the instantiated object as
# parameter.
return macro_or_bytes
else:
instance = super().__new__(cls, macro_or_bytes)
return instance


OpcodeCallArg = Union[int, bytes, Opcode]
Expand Down Expand Up @@ -4533,8 +4596,8 @@ class Opcodes(Opcode, Enum):
Inputs
----
- value: value in wei to send to the new account
- offset: byte offset in the memory in bytes, the initialisation code for the new account
- size: byte size to copy (size of the initialisation code)
- offset: byte offset in the memory in bytes, the initialization code for the new account
- size: byte size to copy (size of the initialization code)
Outputs
----
Expand Down Expand Up @@ -4720,8 +4783,8 @@ class Opcodes(Opcode, Enum):
Inputs
----
- value: value in wei to send to the new account
- offset: byte offset in the memory in bytes, the initialisation code of the new account
- size: byte size to copy (size of the initialisation code)
- offset: byte offset in the memory in bytes, the initialization code of the new account
- size: byte size to copy (size of the initialization code)
- salt: 32-byte value used to create the new account at a deterministic address
Outputs
Expand Down Expand Up @@ -4861,3 +4924,39 @@ class Opcodes(Opcode, Enum):
Source: [evm.codes/#FF](https://www.evm.codes/#FF)
"""


class Macros(Macro, Enum):
"""
Enum containing all macros.
"""

OOG = Macro(Opcodes.SHA3(0, 100000000000))
"""
OOG(args)
----
Halt execution by consuming all available gas.
Inputs
----
- any input arguments are ignored
Fork
----
Frontier
Gas
----
`SHA3(0, 100000000000)` results in 19073514453125027 gas used and an OOG
exception.
Note:
If a value > `100000000000` is used as second argument, the resulting geth
trace reports gas `30` and an OOG exception.
`SHA3(0, SUB(0, 1))` causes a gas > u64 exception and an OOG exception.
Bytecode
----
SHA3(0, 100000000000)
"""
3 changes: 2 additions & 1 deletion whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ makepyfile
metafunc
modifyitems
nodeid
oog
optparser
originalname
parametrized
Expand Down Expand Up @@ -536,4 +537,4 @@ modexp

fi
url
gz
gz

0 comments on commit 1983444

Please sign in to comment.