Skip to content

Commit

Permalink
new(tests): EOF - EIP-3540: test all opcodes in valid code section (#634
Browse files Browse the repository at this point in the history
)

* test all opcodes in valid eof code section

* make UndefinedOpcodes global

* do not produce unreachable code on halting instructions
if opcode is halting in eof, let it act as returning opcode
to check that no exception happens

* types fix after rebase

* changelog

* feat(fw): implement hash for bytecode

* fix(tests): remove ._name_

---------

Co-authored-by: Mario Vega <[email protected]>
  • Loading branch information
2 people authored and spencer-tb committed Jun 26, 2024
1 parent e566f9a commit 8cdac5e
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 18 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ Add tests for [EIP-4200: EOF - Static relative jumps](https://eips.ethereum.org/EIPS/eip-4200) ([#581](https://github.com/ethereum/execution-spec-tests/pull/581)).
- ✨ Add tests for [EIP-7069: EOF - Revamped CALL instructions](https://eips.ethereum.org/EIPS/eip-7069) ([#595](https://github.com/ethereum/execution-spec-tests/pull/595)).
- 🐞 Fix typos in self-destruct collision test from erroneous pytest parametrization ([#608](https://github.com/ethereum/execution-spec-tests/pull/608)).
- ✨ Add tests for [EIP-3540: EOF - EVM Object Format v1](https://eips.ethereum.org/EIPS/eip-3540) ([#634](https://github.com/ethereum/execution-spec-tests/pull/634)).

### 🛠️ Framework

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 @@ -60,7 +60,7 @@
TestInfo,
)
from .spec.blockchain.types import Block, Header
from .vm import Bytecode, Macro, Macros, Opcode, OpcodeCallArg, Opcodes
from .vm import Bytecode, Macro, Macros, Opcode, OpcodeCallArg, Opcodes, UndefinedOpcodes

__all__ = (
"SPEC_TYPES",
Expand Down Expand Up @@ -96,6 +96,7 @@
"Opcode",
"OpcodeCallArg",
"Opcodes",
"UndefinedOpcodes",
"ReferenceSpec",
"ReferenceSpecTypes",
"Removable",
Expand Down
3 changes: 2 additions & 1 deletion src/ethereum_test_tools/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Ethereum Virtual Machine related definitions and utilities.
"""

from .opcode import Bytecode, Macro, Macros, Opcode, OpcodeCallArg, Opcodes
from .opcode import Bytecode, Macro, Macros, Opcode, OpcodeCallArg, Opcodes, UndefinedOpcodes

__all__ = (
"Bytecode",
Expand All @@ -11,4 +11,5 @@
"Macros",
"OpcodeCallArg",
"Opcodes",
"UndefinedOpcodes",
)
109 changes: 109 additions & 0 deletions src/ethereum_test_tools/vm/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ def __eq__(self, other):
return bytes(self) == bytes(other)
raise NotImplementedError(f"Unsupported type for comparison f{type(other)}")

def __hash__(self):
"""
Return the hash of the bytecode representation.
"""
return hash(
(
bytes(self),
self.popped_stack_items,
self.pushed_stack_items,
self.max_stack_height,
self.min_stack_height,
)
)

def __add__(self, other: "Bytecode | int | None") -> "Bytecode":
"""
Concatenate the bytecode representation with another bytecode object.
Expand Down Expand Up @@ -5802,3 +5816,98 @@ class Macros(Macro, Enum):
----
SHA3(0, 100000000000)
"""


class UndefinedOpcodes(Opcode, Enum):
"""
Enum containing all unknown opcodes (88 at the moment).
"""

OPCODE_0C = Opcode(0x0C)
OPCODE_0D = Opcode(0x0D)
OPCODE_0E = Opcode(0x0E)
OPCODE_0F = Opcode(0x0F)
OPCODE_1E = Opcode(0x1E)
OPCODE_1F = Opcode(0x1F)
OPCODE_21 = Opcode(0x21)
OPCODE_22 = Opcode(0x22)
OPCODE_23 = Opcode(0x23)
OPCODE_24 = Opcode(0x24)
OPCODE_25 = Opcode(0x25)
OPCODE_26 = Opcode(0x26)
OPCODE_27 = Opcode(0x27)
OPCODE_28 = Opcode(0x28)
OPCODE_29 = Opcode(0x29)
OPCODE_2A = Opcode(0x2A)
OPCODE_2B = Opcode(0x2B)
OPCODE_2C = Opcode(0x2C)
OPCODE_2D = Opcode(0x2D)
OPCODE_2E = Opcode(0x2E)
OPCODE_2F = Opcode(0x2F)
OPCODE_4B = Opcode(0x4B)
OPCODE_4C = Opcode(0x4C)
OPCODE_4D = Opcode(0x4D)
OPCODE_4E = Opcode(0x4E)
OPCODE_4F = Opcode(0x4F)
OPCODE_A5 = Opcode(0xA5)
OPCODE_A6 = Opcode(0xA6)
OPCODE_A7 = Opcode(0xA7)
OPCODE_A8 = Opcode(0xA8)
OPCODE_A9 = Opcode(0xA9)
OPCODE_AA = Opcode(0xAA)
OPCODE_AB = Opcode(0xAB)
OPCODE_AC = Opcode(0xAC)
OPCODE_AD = Opcode(0xAD)
OPCODE_AE = Opcode(0xAE)
OPCODE_AF = Opcode(0xAF)
OPCODE_B0 = Opcode(0xB0)
OPCODE_B1 = Opcode(0xB1)
OPCODE_B2 = Opcode(0xB2)
OPCODE_B3 = Opcode(0xB3)
OPCODE_B4 = Opcode(0xB4)
OPCODE_B5 = Opcode(0xB5)
OPCODE_B6 = Opcode(0xB6)
OPCODE_B7 = Opcode(0xB7)
OPCODE_B8 = Opcode(0xB8)
OPCODE_B9 = Opcode(0xB9)
OPCODE_BA = Opcode(0xBA)
OPCODE_BB = Opcode(0xBB)
OPCODE_BC = Opcode(0xBC)
OPCODE_BD = Opcode(0xBD)
OPCODE_BE = Opcode(0xBE)
OPCODE_BF = Opcode(0xBF)
OPCODE_C0 = Opcode(0xC0)
OPCODE_C1 = Opcode(0xC1)
OPCODE_C2 = Opcode(0xC2)
OPCODE_C3 = Opcode(0xC3)
OPCODE_C4 = Opcode(0xC4)
OPCODE_C5 = Opcode(0xC5)
OPCODE_C6 = Opcode(0xC6)
OPCODE_C7 = Opcode(0xC7)
OPCODE_C8 = Opcode(0xC8)
OPCODE_C9 = Opcode(0xC9)
OPCODE_CA = Opcode(0xCA)
OPCODE_CB = Opcode(0xCB)
OPCODE_CC = Opcode(0xCC)
OPCODE_CD = Opcode(0xCD)
OPCODE_CE = Opcode(0xCE)
OPCODE_CF = Opcode(0xCF)
OPCODE_D4 = Opcode(0xD4)
OPCODE_D5 = Opcode(0xD5)
OPCODE_D6 = Opcode(0xD6)
OPCODE_D7 = Opcode(0xD7)
OPCODE_D8 = Opcode(0xD8)
OPCODE_D9 = Opcode(0xD9)
OPCODE_DA = Opcode(0xDA)
OPCODE_DB = Opcode(0xDB)
OPCODE_DC = Opcode(0xDC)
OPCODE_DD = Opcode(0xDD)
OPCODE_DE = Opcode(0xDE)
OPCODE_DF = Opcode(0xDF)
OPCODE_E9 = Opcode(0xE9)
OPCODE_EA = Opcode(0xEA)
OPCODE_EB = Opcode(0xEB)
OPCODE_ED = Opcode(0xED)
OPCODE_EF = Opcode(0xEF)
OPCODE_F6 = Opcode(0xF6)
OPCODE_FC = Opcode(0xFC)
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""
EOF Container: check how every opcode behaves in the middle of the valid eof container code
"""

import pytest

from ethereum_test_tools import Bytecode, EOFTestFiller, Opcode
from ethereum_test_tools import Opcodes as Op
from ethereum_test_tools import UndefinedOpcodes
from ethereum_test_tools.eof.v1 import Container, EOFException, Section

from .. import EOF_FORK_NAME

REFERENCE_SPEC_GIT_PATH = "EIPS/eip-3540.md"
REFERENCE_SPEC_VERSION = "8dcb0a8c1c0102c87224308028632cc986a61183"

pytestmark = pytest.mark.valid_from(EOF_FORK_NAME)

# Invalid Opcodes will produce EOFException.UNDEFINED_INSTRUCTION when used in EOFContainer
invalid_eof_opcodes = {
Op.CODESIZE,
Op.SELFDESTRUCT,
Op.CREATE2,
Op.CODECOPY,
Op.EXTCODESIZE,
Op.EXTCODECOPY,
Op.EXTCODEHASH,
Op.JUMP,
Op.JUMPI,
Op.PC,
Op.GAS,
Op.CREATE,
Op.CALL,
Op.CALLCODE,
Op.DELEGATECALL,
Op.STATICCALL,
}

# Halting the execution opcodes can be placed without STOP instruction at the end
halting_opcodes = {
Op.STOP,
Op.JUMPF,
Op.RETURNCONTRACT,
Op.RETURN,
Op.REVERT,
Op.INVALID,
}

# Special eof opcodes that require [] operator
eof_opcodes = {
Op.DATALOADN,
Op.RJUMPV,
Op.CALLF,
Op.RETF,
Op.JUMPF,
Op.EOFCREATE,
Op.RETURNCONTRACT,
Op.EXCHANGE,
}


def expect_exception(opcode: Opcode) -> EOFException | None:
"""
Returns exception that eof container reports when having this opcode in the middle of the code
"""
if opcode in invalid_eof_opcodes or opcode in list(UndefinedOpcodes):
return EOFException.UNDEFINED_INSTRUCTION

# RETF not allowed in first section
if opcode == Op.RETF:
return EOFException.INVALID_NON_RETURNING_FLAG
return None


def make_opcode_valid_bytes(opcode: Opcode) -> Opcode | Bytecode:
"""
Construct a valid stack and bytes for the opcode
"""
code: Opcode | Bytecode
if opcode.data_portion_length == 0 and opcode.data_portion_formatter is None:
code = opcode
elif opcode == Op.CALLF:
code = opcode[1]
else:
code = opcode[0]
if opcode not in halting_opcodes:
return code + Op.STOP
return code


def eof_opcode_stack(opcode: Opcode) -> int:
"""
Eof opcode has special stack influence
"""
if opcode in eof_opcodes:
if opcode == Op.CALLF or opcode == Op.JUMPF or opcode == Op.EXCHANGE:
return 0
return 1
return 0


@pytest.mark.parametrize("opcode", list(Op) + list(UndefinedOpcodes))
def test_all_opcodes_in_container(eof_test: EOFTestFiller, opcode: Opcode):
"""
Test all opcodes inside valid container
257 because 0x5B is duplicated
"""
section_call = []
if opcode == Op.CALLF:
section_call = [
Section.Code(
code=Op.RETF,
code_inputs=0,
code_outputs=0,
max_stack_height=0,
)
]

eof_code = Container(
sections=[
Section.Code(
code=Op.PUSH1(00) * 20 + make_opcode_valid_bytes(opcode),
max_stack_height=max(
20,
20
+ opcode.pushed_stack_items
- opcode.popped_stack_items
+ eof_opcode_stack(opcode),
),
),
Section.Container(
container=Container(
sections=[
Section.Code(code=Op.STOP),
]
)
),
]
+ section_call
+ [
Section.Data("1122334455667788" * 4),
],
)

eof_test(
data=eof_code,
expect_exception=expect_exception(opcode),
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from ethereum_test_tools import EOFTestFiller, Opcode
from ethereum_test_tools import EOFTestFiller
from ethereum_test_tools import Opcodes as Op
from ethereum_test_tools.eof.v1 import Bytes, Container, EOFException, Section

Expand Down Expand Up @@ -34,21 +34,6 @@
None,
id="simple_eof_1_deploy",
),
pytest.param(
# Check that EOF1 with an illegal opcode fails
Container(
name="EOF1I0008",
sections=[
Section.Code(
code=Op.ADDRESS + Opcode(0xEF) + Op.STOP,
),
Section.Data("0x0bad60A7"),
],
),
"ef00010100040200010003040004000080000130ef000bad60A7",
EOFException.UNDEFINED_INSTRUCTION,
id="illegal_opcode_fail",
),
pytest.param(
# Check that valid EOF1 can include 0xFE, the designated invalid opcode
Container(
Expand Down
Loading

0 comments on commit 8cdac5e

Please sign in to comment.