From 1983444bbe1a471886ef7c0e82253ffe2a4053e1 Mon Sep 17 00:00:00 2001 From: winsvega Date: Tue, 26 Mar 2024 13:44:19 +0100 Subject: [PATCH] feat(tests): add a (opcode) Macro class and the OOG macro (#457) Co-authored-by: Mario Vega Co-authored-by: danceratopz --- docs/CHANGELOG.md | 1 + src/entry_points/evm_bytes_to_python.py | 3 +- .../tests/test_evm_bytes_to_python.py | 3 +- src/ethereum_test_tools/__init__.py | 3 +- src/ethereum_test_tools/tests/test_vm.py | 15 +++ src/ethereum_test_tools/vm/__init__.py | 4 +- src/ethereum_test_tools/vm/opcode.py | 117 ++++++++++++++++-- whitelist.txt | 3 +- 8 files changed, 135 insertions(+), 14 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fb87ad2e92..4e10de2e21 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/src/entry_points/evm_bytes_to_python.py b/src/entry_points/evm_bytes_to_python.py index 930c1bc1df..8239146289 100644 --- a/src/entry_points/evm_bytes_to_python.py +++ b/src/entry_points/evm_bytes_to_python.py @@ -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 @@ -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 diff --git a/src/entry_points/tests/test_evm_bytes_to_python.py b/src/entry_points/tests/test_evm_bytes_to_python.py index df2dd7f2dc..529c31dd6b 100644 --- a/src/entry_points/tests/test_evm_bytes_to_python.py +++ b/src/entry_points/tests/test_evm_bytes_to_python.py @@ -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 = [ @@ -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: diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 7d6d144f74..639744e8e4 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -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", @@ -84,6 +84,7 @@ "Initcode", "JSONEncoder", "Opcode", + "Macro", "OpcodeCallArg", "Opcodes", "ReferenceSpec", diff --git a/src/ethereum_test_tools/tests/test_vm.py b/src/ethereum_test_tools/tests/test_vm.py index f4a14a263a..dc71ae91ed 100644 --- a/src/ethereum_test_tools/tests/test_vm.py +++ b/src/ethereum_test_tools/tests/test_vm.py @@ -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 @@ -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): @@ -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 diff --git a/src/ethereum_test_tools/vm/__init__.py b/src/ethereum_test_tools/vm/__init__.py index 9b7afda48e..c1042ef82b 100644 --- a/src/ethereum_test_tools/vm/__init__.py +++ b/src/ethereum_test_tools/vm/__init__.py @@ -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", ) diff --git a/src/ethereum_test_tools/vm/opcode.py b/src/ethereum_test_tools/vm/opcode.py index d82c39746e..e2bbb168ea 100644 --- a/src/ethereum_test_tools/vm/opcode.py +++ b/src/ethereum_test_tools/vm/opcode.py @@ -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. @@ -48,7 +96,6 @@ class Opcode(bytes): pushed_stack_items: int min_stack_height: int data_portion_length: int - _name_: str def __new__( cls, @@ -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: """ @@ -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] @@ -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 ---- @@ -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 @@ -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) + """ diff --git a/whitelist.txt b/whitelist.txt index c01bdadfbd..5707f3e8bf 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -340,6 +340,7 @@ makepyfile metafunc modifyitems nodeid +oog optparser originalname parametrized @@ -536,4 +537,4 @@ modexp fi url -gz \ No newline at end of file +gz