Skip to content
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

feat(tests): extend Op with Op.OOG macro opcode #457

Merged
merged 16 commits into from
Mar 26, 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
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)
danceratopz marked this conversation as resolved.
Show resolved Hide resolved
----

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
Loading