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(exception mapper): class to verify exception strings #795

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions src/ethereum_test_exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .engine_api import EngineAPIError
from .evmone_exceptions import EvmoneExceptionMapper
from .exception_mapper import ExceptionMapper
from .exceptions import (
BlockException,
BlockExceptionInstanceOrList,
Expand All @@ -13,14 +14,17 @@
TransactionException,
TransactionExceptionInstanceOrList,
)
from .geth_exceptions import GethExceptionMapper

__all__ = [
"BlockException",
"BlockExceptionInstanceOrList",
"EOFException",
"EOFExceptionInstanceOrList",
"EngineAPIError",
"ExceptionMapper",
"EvmoneExceptionMapper",
"GethExceptionMapper",
"ExceptionInstanceOrList",
"TransactionException",
"TransactionExceptionInstanceOrList",
Expand Down
199 changes: 90 additions & 109 deletions src/ethereum_test_exceptions/evmone_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,118 +1,99 @@
"""
Evmone eof exceptions ENUM -> str mapper
Evmone exceptions ENUM -> str mapper
"""

from dataclasses import dataclass
from .exception_mapper import ExceptionMapper, ExceptionMessage
from .exceptions import EOFException, TransactionException

from bidict import frozenbidict

from .exceptions import EOFException


@dataclass
class ExceptionMessage:
"""Defines a mapping between an exception and a message."""

exception: EOFException
message: str


class EvmoneExceptionMapper:
class EvmoneExceptionMapper(ExceptionMapper):
"""
Translate between EEST exceptions and error strings returned by evmone.
"""

_mapping_data = (
# TODO EVMONE needs to differentiate when the section is missing in the header or body
ExceptionMessage(EOFException.MISSING_STOP_OPCODE, "err: no_terminating_instruction"),
ExceptionMessage(EOFException.MISSING_CODE_HEADER, "err: code_section_missing"),
ExceptionMessage(EOFException.MISSING_TYPE_HEADER, "err: type_section_missing"),
# TODO EVMONE these exceptions are too similar, this leeds to ambiguity
ExceptionMessage(EOFException.MISSING_TERMINATOR, "err: header_terminator_missing"),
ExceptionMessage(
EOFException.MISSING_HEADERS_TERMINATOR, "err: section_headers_not_terminated"
),
ExceptionMessage(EOFException.INVALID_VERSION, "err: eof_version_unknown"),
ExceptionMessage(
EOFException.INVALID_NON_RETURNING_FLAG, "err: invalid_non_returning_flag"
),
ExceptionMessage(EOFException.INVALID_MAGIC, "err: invalid_prefix"),
ExceptionMessage(
EOFException.INVALID_FIRST_SECTION_TYPE, "err: invalid_first_section_type"
),
ExceptionMessage(
EOFException.INVALID_SECTION_BODIES_SIZE, "err: invalid_section_bodies_size"
),
ExceptionMessage(EOFException.INVALID_TYPE_SECTION_SIZE, "err: invalid_type_section_size"),
ExceptionMessage(EOFException.INCOMPLETE_SECTION_SIZE, "err: incomplete_section_size"),
ExceptionMessage(EOFException.INCOMPLETE_SECTION_NUMBER, "err: incomplete_section_number"),
ExceptionMessage(EOFException.TOO_MANY_CODE_SECTIONS, "err: too_many_code_sections"),
ExceptionMessage(EOFException.ZERO_SECTION_SIZE, "err: zero_section_size"),
ExceptionMessage(EOFException.MISSING_DATA_SECTION, "err: data_section_missing"),
ExceptionMessage(EOFException.UNDEFINED_INSTRUCTION, "err: undefined_instruction"),
ExceptionMessage(
EOFException.INPUTS_OUTPUTS_NUM_ABOVE_LIMIT, "err: inputs_outputs_num_above_limit"
),
ExceptionMessage(EOFException.UNREACHABLE_INSTRUCTIONS, "err: unreachable_instructions"),
ExceptionMessage(EOFException.INVALID_RJUMP_DESTINATION, "err: invalid_rjump_destination"),
ExceptionMessage(EOFException.UNREACHABLE_CODE_SECTIONS, "err: unreachable_code_sections"),
ExceptionMessage(EOFException.STACK_UNDERFLOW, "err: stack_underflow"),
ExceptionMessage(
EOFException.MAX_STACK_HEIGHT_ABOVE_LIMIT, "err: max_stack_height_above_limit"
),
ExceptionMessage(
EOFException.STACK_HIGHER_THAN_OUTPUTS, "err: stack_higher_than_outputs_required"
),
ExceptionMessage(
EOFException.JUMPF_DESTINATION_INCOMPATIBLE_OUTPUTS,
"err: jumpf_destination_incompatible_outputs",
),
ExceptionMessage(EOFException.INVALID_MAX_STACK_HEIGHT, "err: invalid_max_stack_height"),
ExceptionMessage(EOFException.INVALID_DATALOADN_INDEX, "err: invalid_dataloadn_index"),
ExceptionMessage(EOFException.TRUNCATED_INSTRUCTION, "err: truncated_instruction"),
ExceptionMessage(
EOFException.TOPLEVEL_CONTAINER_TRUNCATED, "err: toplevel_container_truncated"
),
ExceptionMessage(EOFException.ORPHAN_SUBCONTAINER, "err: unreferenced_subcontainer"),
ExceptionMessage(
EOFException.CONTAINER_SIZE_ABOVE_LIMIT, "err: container_size_above_limit"
),
ExceptionMessage(
EOFException.INVALID_CONTAINER_SECTION_INDEX, "err: invalid_container_section_index"
),
ExceptionMessage(
EOFException.INCOMPATIBLE_CONTAINER_KIND, "err: incompatible_container_kind"
),
ExceptionMessage(EOFException.STACK_HEIGHT_MISMATCH, "err: stack_height_mismatch"),
ExceptionMessage(EOFException.TOO_MANY_CONTAINERS, "err: too_many_container_sections"),
ExceptionMessage(
EOFException.INVALID_CODE_SECTION_INDEX, "err: invalid_code_section_index"
),
)

def __init__(self) -> None:
assert len(set(entry.exception for entry in self._mapping_data)) == len(
self._mapping_data
), "Duplicate exception in _mapping_data"
assert len(set(entry.message for entry in self._mapping_data)) == len(
self._mapping_data
), "Duplicate message in _mapping_data"
self.exception_to_message_map: frozenbidict = frozenbidict(
{entry.exception: entry.message for entry in self._mapping_data}
)

def exception_to_message(self, exception: EOFException) -> str:
"""Takes an EOFException and returns a formatted string."""
message = self.exception_to_message_map.get(
exception,
f"No message defined for {exception}; please add it to {self.__class__.__name__}",
)
return message

def message_to_exception(self, exception_string: str) -> EOFException:
"""Takes a string and tries to find matching exception"""
# TODO inform tester where to add the missing exception if get uses default
exception = self.exception_to_message_map.inverse.get(
exception_string, EOFException.UNDEFINED_EXCEPTION
)
return exception
@property
def _mapping_data(self):
return [
ExceptionMessage(
TransactionException.TYPE_4_TX_CONTRACT_CREATION,
"set code transaction must ",
),
# TODO EVMONE needs to differentiate when the section is missing in the header or body
ExceptionMessage(EOFException.MISSING_STOP_OPCODE, "err: no_terminating_instruction"),
ExceptionMessage(EOFException.MISSING_CODE_HEADER, "err: code_section_missing"),
ExceptionMessage(EOFException.MISSING_TYPE_HEADER, "err: type_section_missing"),
# TODO EVMONE these exceptions are too similar, this leeds to ambiguity
ExceptionMessage(EOFException.MISSING_TERMINATOR, "err: header_terminator_missing"),
ExceptionMessage(
EOFException.MISSING_HEADERS_TERMINATOR, "err: section_headers_not_terminated"
),
ExceptionMessage(EOFException.INVALID_VERSION, "err: eof_version_unknown"),
ExceptionMessage(
EOFException.INVALID_NON_RETURNING_FLAG, "err: invalid_non_returning_flag"
),
ExceptionMessage(EOFException.INVALID_MAGIC, "err: invalid_prefix"),
ExceptionMessage(
EOFException.INVALID_FIRST_SECTION_TYPE, "err: invalid_first_section_type"
),
ExceptionMessage(
EOFException.INVALID_SECTION_BODIES_SIZE, "err: invalid_section_bodies_size"
),
ExceptionMessage(
EOFException.INVALID_TYPE_SECTION_SIZE, "err: invalid_type_section_size"
),
ExceptionMessage(EOFException.INCOMPLETE_SECTION_SIZE, "err: incomplete_section_size"),
ExceptionMessage(
EOFException.INCOMPLETE_SECTION_NUMBER, "err: incomplete_section_number"
),
ExceptionMessage(EOFException.TOO_MANY_CODE_SECTIONS, "err: too_many_code_sections"),
ExceptionMessage(EOFException.ZERO_SECTION_SIZE, "err: zero_section_size"),
ExceptionMessage(EOFException.MISSING_DATA_SECTION, "err: data_section_missing"),
ExceptionMessage(EOFException.UNDEFINED_INSTRUCTION, "err: undefined_instruction"),
ExceptionMessage(
EOFException.INPUTS_OUTPUTS_NUM_ABOVE_LIMIT, "err: inputs_outputs_num_above_limit"
),
ExceptionMessage(
EOFException.UNREACHABLE_INSTRUCTIONS, "err: unreachable_instructions"
),
ExceptionMessage(
EOFException.INVALID_RJUMP_DESTINATION, "err: invalid_rjump_destination"
),
ExceptionMessage(
EOFException.UNREACHABLE_CODE_SECTIONS, "err: unreachable_code_sections"
),
ExceptionMessage(EOFException.STACK_UNDERFLOW, "err: stack_underflow"),
ExceptionMessage(
EOFException.MAX_STACK_HEIGHT_ABOVE_LIMIT, "err: max_stack_height_above_limit"
),
ExceptionMessage(
EOFException.STACK_HIGHER_THAN_OUTPUTS, "err: stack_higher_than_outputs_required"
),
ExceptionMessage(
EOFException.JUMPF_DESTINATION_INCOMPATIBLE_OUTPUTS,
"err: jumpf_destination_incompatible_outputs",
),
ExceptionMessage(
EOFException.INVALID_MAX_STACK_HEIGHT, "err: invalid_max_stack_height"
),
ExceptionMessage(EOFException.INVALID_DATALOADN_INDEX, "err: invalid_dataloadn_index"),
ExceptionMessage(EOFException.TRUNCATED_INSTRUCTION, "err: truncated_instruction"),
ExceptionMessage(
EOFException.TOPLEVEL_CONTAINER_TRUNCATED, "err: toplevel_container_truncated"
),
ExceptionMessage(EOFException.ORPHAN_SUBCONTAINER, "err: unreferenced_subcontainer"),
ExceptionMessage(
EOFException.CONTAINER_SIZE_ABOVE_LIMIT, "err: container_size_above_limit"
),
ExceptionMessage(
EOFException.INVALID_CONTAINER_SECTION_INDEX,
"err: invalid_container_section_index",
),
ExceptionMessage(
EOFException.INCOMPATIBLE_CONTAINER_KIND, "err: incompatible_container_kind"
),
ExceptionMessage(EOFException.STACK_HEIGHT_MISMATCH, "err: stack_height_mismatch"),
ExceptionMessage(EOFException.TOO_MANY_CONTAINERS, "err: too_many_container_sections"),
ExceptionMessage(
EOFException.INVALID_CODE_SECTION_INDEX, "err: invalid_code_section_index"
),
]
90 changes: 90 additions & 0 deletions src/ethereum_test_exceptions/exception_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
EEST Exception mapper
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass

from bidict import frozenbidict

from .exceptions import ExceptionBase, TransactionException, UndefinedException


@dataclass
class ExceptionMessage:
"""Defines a mapping between an exception and a message."""

exception: ExceptionBase
message: str


class ExceptionMapper(ABC):
"""
Translate between EEST exceptions and error strings returned by client's t8n or other tools.
"""

def __init__(self) -> None:
# Ensure that the subclass has properly defined _mapping_data before accessing it
assert self._mapping_data is not None, "_mapping_data must be defined in subclass"

assert len(set(entry.exception for entry in self._mapping_data)) == len(
self._mapping_data
), "Duplicate exception in _mapping_data"
assert len(set(entry.message for entry in self._mapping_data)) == len(
self._mapping_data
), "Duplicate message in _mapping_data"
self.exception_to_message_map: frozenbidict = frozenbidict(
{entry.exception: entry.message for entry in self._mapping_data}
)

@property
@abstractmethod
def _mapping_data(self):
"""This method should be overridden in the subclass to provide mapping data."""
pass

def exception_to_message(self, exception: ExceptionBase) -> str:
"""Takes an exception and returns a formatted string."""
message = self.exception_to_message_map.get(exception, "Undefined")
return message

def message_to_exception(self, exception_string: str) -> ExceptionBase:
"""Takes a string and tries to find matching exception"""
# TODO inform tester where to add the missing exception if get uses default
exception = self.exception_to_message_map.inverse.get(
exception_string, UndefinedException.UNDEFINED_EXCEPTION
)
return exception

def check_transaction(self, tx_error_message: str | None, tx, tx_pos: int):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transaction here would cause a cycle import, and I think that is because this method should not be defined here because not all tests depend on transaction exceptions (only state_test).

Probably better to be in ethereum_test_specs/state.py.

Copy link
Member

@marioevz marioevz Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking through all the changes, I think what we could do is have a subclass called TransitionToolExceptionMapper that is specifically dedicated to map exceptions from the transition tool, and this can be used to subclass specific GethTransaitionToolExceptionMapper and so on.

Another approach could be that we don't need the subclass GethTransaitionToolExceptionMapper and only TransitionToolExceptionMapper, and then in src/evm_transition_tool/geth.py we create an instance of TransitionToolExceptionMapper that contains the map, so we avoid creating too many classes and also avoid instantiating the same GethTransaitionToolExceptionMapper every time we instantiate the t8n tool.

"""Verify transaction exception"""
exception_info = f"TransactionException (pos={tx_pos}, nonce={tx.nonce})\n"

if tx.error and not tx_error_message:
raise Exception(f"{exception_info} Error: tx expected to fail succeeded")
elif not tx.error and tx_error_message:
raise Exception(f"{exception_info} Error: tx unexpectedly failed: {tx_error_message}")
# TODO check exception list case
elif isinstance(tx.error, TransactionException) and tx_error_message:
expected_error_message = self.exception_to_message(tx.error)
error_exception = self.message_to_exception(tx_error_message)

define_message_hint = (
f"No message defined for {tx.error}, please add it to {self.__class__.__name__}"
if expected_error_message == "Undefined"
else ""
)
define_exception_hint = (
f"No exception defined for error message got, "
f"please add it to {self.__class__.__name__}"
if error_exception == UndefinedException.UNDEFINED_EXCEPTION
else ""
)

if expected_error_message not in tx_error_message:
raise Exception(
f"{exception_info}"
f"Error: exception mismatch:\n want = '{expected_error_message}' ({tx.error}),"
f"\n got = '{tx_error_message}' ({error_exception})"
f"\n {define_message_hint}"
f"\n {define_exception_hint}"
)
16 changes: 16 additions & 0 deletions src/ethereum_test_exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ def from_pipe_str(value: Any) -> str | List[str]:
return value


@unique
class UndefinedException(ExceptionBase):
"""
Default Exception
"""

UNDEFINED_EXCEPTION = auto()
"""
Exception to alert to define a proper exception
"""


@unique
class TransactionException(ExceptionBase):
"""
Expand Down Expand Up @@ -362,6 +374,10 @@ class TransactionException(ExceptionBase):
"""
Transaction is type 4, but has an empty authorization list.
"""
TYPE_4_TX_CONTRACT_CREATION = auto()
"""
Transaction is a type 4 transaction and has an empty `to`.
"""


@unique
Expand Down
Loading
Loading