diff --git a/src/ethereum_clis/__init__.py b/src/ethereum_clis/__init__.py index fdf9f80ae9..1e2acd4368 100644 --- a/src/ethereum_clis/__init__.py +++ b/src/ethereum_clis/__init__.py @@ -4,7 +4,7 @@ from .clis.besu import BesuTransitionTool from .clis.ethereumjs import EthereumJSTransitionTool -from .clis.evmone import EvmOneTransitionTool +from .clis.evmone import EvmoneExceptionMapper, EvmOneTransitionTool from .clis.execution_specs import ExecutionSpecsTransitionTool from .clis.geth import GethTransitionTool from .clis.nimbus import NimbusTransitionTool @@ -20,6 +20,7 @@ "EvmOneTransitionTool", "ExecutionSpecsTransitionTool", "GethTransitionTool", + "EvmoneExceptionMapper", "NimbusTransitionTool", "Result", "TransitionTool", diff --git a/src/ethereum_clis/clis/besu.py b/src/ethereum_clis/clis/besu.py index 12fb502e72..708d5b949f 100644 --- a/src/ethereum_clis/clis/besu.py +++ b/src/ethereum_clis/clis/besu.py @@ -14,6 +14,12 @@ import requests +from ethereum_test_exceptions import ( + EOFException, + ExceptionMapper, + ExceptionMessage, + TransactionException, +) from ethereum_test_forks import Fork from ethereum_test_types import Alloc, Environment, Transaction @@ -41,7 +47,7 @@ def __init__( binary: Optional[Path] = None, trace: bool = False, ): - super().__init__(binary=binary, trace=trace) + super().__init__(exception_mapper=BesuExceptionMapper(), binary=binary, trace=trace) args = [str(self.binary), "t8n", "--help"] try: result = subprocess.run(args, capture_output=True, text=True) @@ -201,3 +207,137 @@ def is_fork_supported(self, fork: Fork) -> bool: Returns True if the fork is supported by the tool """ return fork.transition_tool_name() in self.help_string + + +class BesuExceptionMapper(ExceptionMapper): + """ + Translate between EEST exceptions and error strings returned by nimbus. + """ + + @property + def _mapping_data(self): + return [ + ExceptionMessage( + TransactionException.TYPE_4_TX_CONTRACT_CREATION, + "set code transaction must not be a create transaction", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS, + "exceeds transaction sender account balance", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED, + "would exceed block maximum", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + "max fee per blob gas less than block blob gas fee", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS, + "gasPrice is less than the current BaseFee", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_PRE_FORK, + "Transaction type BLOB is invalid, accepted transaction types are [EIP1559, ACCESS_LIST, FRONTIER]", # noqa: E501 + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH, + "Only supported hash version is 0x01, sha256 hash.", + ), + # This message is the same as TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED + ExceptionMessage( + TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, + "exceed block maximum", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_ZERO_BLOBS, + "Blob transaction must have at least one versioned hash", + ), + ExceptionMessage( + TransactionException.INTRINSIC_GAS_TOO_LOW, + "intrinsic gas too low", + ), + ExceptionMessage( + TransactionException.INITCODE_SIZE_EXCEEDED, + "max initcode size exceeded", + ), + # 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" + ), + ] diff --git a/src/ethereum_clis/clis/ethereumjs.py b/src/ethereum_clis/clis/ethereumjs.py index ec3370fee6..cb461e7c16 100644 --- a/src/ethereum_clis/clis/ethereumjs.py +++ b/src/ethereum_clis/clis/ethereumjs.py @@ -6,6 +6,12 @@ from re import compile from typing import Optional +from ethereum_test_exceptions import ( + EOFException, + ExceptionMapper, + ExceptionMessage, + TransactionException, +) from ethereum_test_forks import Fork from ..transition_tool import TransitionTool @@ -31,7 +37,7 @@ def __init__( binary: Optional[Path] = None, trace: bool = False, ): - super().__init__(binary=binary, trace=trace) + super().__init__(exception_mapper=EthereumJSExceptionMapper(), binary=binary, trace=trace) def is_fork_supported(self, fork: Fork) -> bool: """ @@ -39,3 +45,137 @@ def is_fork_supported(self, fork: Fork) -> bool: Currently, EthereumJS-t8n provides no way to determine supported forks. """ return True + + +class EthereumJSExceptionMapper(ExceptionMapper): + """ + Translate between EEST exceptions and error strings returned by ethereum-js. + """ + + @property + def _mapping_data(self): + return [ + ExceptionMessage( + TransactionException.TYPE_4_TX_CONTRACT_CREATION, + "set code transaction must not be a create transaction", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS, + "insufficient funds for gas * price + value", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED, + "would exceed maximum allowance", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + "max fee per blob gas less than block blob gas fee", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS, + "max fee per gas less than block base fee", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_PRE_FORK, + "blob tx used but field env.ExcessBlobGas missing", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH, + "has invalid hash version", + ), + # This message is the same as TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED + ExceptionMessage( + TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, + "exceed maximum allowance", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_ZERO_BLOBS, + "blob transaction missing blob hashes", + ), + ExceptionMessage( + TransactionException.INTRINSIC_GAS_TOO_LOW, + "is lower than the minimum gas limit of", + ), + ExceptionMessage( + TransactionException.INITCODE_SIZE_EXCEEDED, + "max initcode size exceeded", + ), + # 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" + ), + ] diff --git a/src/ethereum_clis/clis/evmone.py b/src/ethereum_clis/clis/evmone.py index c0c834c0ec..542eac83de 100644 --- a/src/ethereum_clis/clis/evmone.py +++ b/src/ethereum_clis/clis/evmone.py @@ -6,6 +6,12 @@ from re import compile from typing import Optional +from ethereum_test_exceptions import ( + EOFException, + ExceptionMapper, + ExceptionMessage, + TransactionException, +) from ethereum_test_forks import Fork from ..transition_tool import TransitionTool @@ -30,7 +36,7 @@ def __init__( binary: Optional[Path] = None, trace: bool = False, ): - super().__init__(binary=binary, trace=trace) + super().__init__(exception_mapper=EvmoneExceptionMapper(), binary=binary, trace=trace) def is_fork_supported(self, fork: Fork) -> bool: """ @@ -38,3 +44,148 @@ def is_fork_supported(self, fork: Fork) -> bool: Currently, evmone-t8n provides no way to determine supported forks. """ return True + + +class EvmoneExceptionMapper(ExceptionMapper): + """ + Translate between EEST exceptions and error strings returned by evmone. + """ + + @property + def _mapping_data(self): + return [ + ExceptionMessage( + TransactionException.TYPE_4_TX_CONTRACT_CREATION, + "set code transaction must ", + ), + ExceptionMessage( + TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE, + "invalid authorization signature", + ), + ExceptionMessage( + TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE_S_TOO_HIGH, + "authorization signature s value too high", + ), + ExceptionMessage( + TransactionException.TYPE_4_EMPTY_AUTHORIZATION_LIST, + "empty authorization list", + ), + ExceptionMessage( + TransactionException.INTRINSIC_GAS_TOO_LOW, + "intrinsic gas too low", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED, + "lob gas limit exceeded", + ), + ExceptionMessage( + TransactionException.INITCODE_SIZE_EXCEEDED, + "max initcode size exceeded", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS, + "insufficient funds for gas * price + value", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS, + "max fee per gas less than block base fee", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + "fee per gas less than block base fee", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_PRE_FORK, + "transaction type not supported", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH, + "invalid blob hash version", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, + "blob gas limit exceeded", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_ZERO_BLOBS, + "empty blob hashes list", + ), + # 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" + ), + ] diff --git a/src/ethereum_clis/clis/execution_specs.py b/src/ethereum_clis/clis/execution_specs.py index 1cebd53dfa..85ff426d64 100644 --- a/src/ethereum_clis/clis/execution_specs.py +++ b/src/ethereum_clis/clis/execution_specs.py @@ -11,6 +11,12 @@ from tempfile import TemporaryDirectory from typing import Optional +from ethereum_test_exceptions import ( + EOFException, + ExceptionMapper, + ExceptionMessage, + TransactionException, +) from ethereum_test_forks import Fork from ..transition_tool import TransitionTool @@ -46,7 +52,9 @@ def __init__( binary: Optional[Path] = None, trace: bool = False, ): - super().__init__(binary=binary, trace=trace) + super().__init__( + exception_mapper=ExecutionSpecsExceptionMapper(), binary=binary, trace=trace + ) args = [str(self.binary), "--help"] try: result = subprocess.run(args, capture_output=True, text=True) @@ -102,3 +110,141 @@ def is_fork_supported(self, fork: Fork) -> bool: `ethereum-spec-evm` appends newlines to forks in the help string. """ return (fork.transition_tool_name() + "\n") in self.help_string + + +class ExecutionSpecsExceptionMapper(ExceptionMapper): + """ + Translate between EEST exceptions and error strings returned by execution specs t8n. + """ + + @property + def _mapping_data(self): + return [ + ExceptionMessage( + TransactionException.TYPE_4_TX_CONTRACT_CREATION, + "Failed transaction: ", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS, + "ailed transaction: ", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED, + "iled transaction: ", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + "led transaction: ", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS, + "ed transaction: ", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_PRE_FORK, + "module 'ethereum.shanghai.transactions' has no attribute 'BlobTransaction'", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH, + "d transaction: ", + ), + # This message is the same as TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED + ExceptionMessage( + TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, + " transaction: ", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_ZERO_BLOBS, + "transaction: ", + ), + ExceptionMessage( + TransactionException.INTRINSIC_GAS_TOO_LOW, + "ransaction: ", + ), + ExceptionMessage( + TransactionException.INITCODE_SIZE_EXCEEDED, + "ansaction: ", + ), + ExceptionMessage( + TransactionException.PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS, + "nsaction: ", + ), + # 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" + ), + ] diff --git a/src/ethereum_clis/clis/geth.py b/src/ethereum_clis/clis/geth.py index 905a782f90..993690affa 100644 --- a/src/ethereum_clis/clis/geth.py +++ b/src/ethereum_clis/clis/geth.py @@ -10,6 +10,12 @@ from re import compile from typing import Optional +from ethereum_test_exceptions import ( + EOFException, + ExceptionMapper, + ExceptionMessage, + TransactionException, +) from ethereum_test_fixtures import BlockchainFixture, StateFixture from ethereum_test_forks import Fork @@ -37,7 +43,7 @@ def __init__( binary: Optional[Path] = None, trace: bool = False, ): - super().__init__(binary=binary, trace=trace) + super().__init__(exception_mapper=GethExceptionMapper(), binary=binary, trace=trace) args = [str(self.binary), str(self.t8n_subcommand), "--help"] try: result = subprocess.run(args, capture_output=True, text=True) @@ -147,3 +153,137 @@ def verify_fixture( else: result_json = [] # there is no parseable format for blocktest output return result_json + + +class GethExceptionMapper(ExceptionMapper): + """ + Translate between EEST exceptions and error strings returned by geth. + """ + + @property + def _mapping_data(self): + return [ + ExceptionMessage( + TransactionException.TYPE_4_TX_CONTRACT_CREATION, + "set code transaction must not be a create transaction", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS, + "insufficient funds for gas * price + value", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED, + "would exceed maximum allowance", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + "max fee per blob gas less than block blob gas fee", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS, + "max fee per gas less than block base fee", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_PRE_FORK, + "blob tx used but field env.ExcessBlobGas missing", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH, + "has invalid hash version", + ), + # This message is the same as TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED + ExceptionMessage( + TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, + "exceed maximum allowance", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_ZERO_BLOBS, + "blob transaction missing blob hashes", + ), + ExceptionMessage( + TransactionException.INTRINSIC_GAS_TOO_LOW, + "intrinsic gas too low", + ), + ExceptionMessage( + TransactionException.INITCODE_SIZE_EXCEEDED, + "max initcode size exceeded", + ), + # 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" + ), + ] diff --git a/src/ethereum_clis/clis/nimbus.py b/src/ethereum_clis/clis/nimbus.py index 318a3df18e..aae58d9523 100644 --- a/src/ethereum_clis/clis/nimbus.py +++ b/src/ethereum_clis/clis/nimbus.py @@ -8,6 +8,12 @@ from re import compile from typing import Optional +from ethereum_test_exceptions import ( + EOFException, + ExceptionMapper, + ExceptionMessage, + TransactionException, +) from ethereum_test_forks import Fork from ..transition_tool import TransitionTool @@ -32,7 +38,7 @@ def __init__( binary: Optional[Path] = None, trace: bool = False, ): - super().__init__(binary=binary, trace=trace) + super().__init__(exception_mapper=NimbusExceptionMapper(), binary=binary, trace=trace) args = [str(self.binary), "--help"] try: result = subprocess.run(args, capture_output=True, text=True) @@ -58,3 +64,137 @@ def is_fork_supported(self, fork: Fork) -> bool: If the fork is a transition fork, we want to check the fork it transitions to. """ return fork.transition_tool_name() in self.help_string + + +class NimbusExceptionMapper(ExceptionMapper): + """ + Translate between EEST exceptions and error strings returned by nimbus. + """ + + @property + def _mapping_data(self): + return [ + ExceptionMessage( + TransactionException.TYPE_4_TX_CONTRACT_CREATION, + "set code transaction must not be a create transaction", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS, + "invalid tx: not enough cash to send", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED, + "would exceed maximum allowance", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + "max fee per blob gas less than block blob gas fee", + ), + ExceptionMessage( + TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS, + "max fee per gas less than block base fee", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_PRE_FORK, + "blob tx used but field env.ExcessBlobGas missing", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH, + "invalid tx: one of blobVersionedHash has invalid version", + ), + # This message is the same as TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED + ExceptionMessage( + TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, + "exceeds maximum allowance", + ), + ExceptionMessage( + TransactionException.TYPE_3_TX_ZERO_BLOBS, + "blob transaction missing blob hashes", + ), + ExceptionMessage( + TransactionException.INTRINSIC_GAS_TOO_LOW, + "intrinsic gas too low", + ), + ExceptionMessage( + TransactionException.INITCODE_SIZE_EXCEEDED, + "max initcode size exceeded", + ), + # 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" + ), + ] diff --git a/src/ethereum_clis/transition_tool.py b/src/ethereum_clis/transition_tool.py index 58ec2a429a..fbe8c186e5 100644 --- a/src/ethereum_clis/transition_tool.py +++ b/src/ethereum_clis/transition_tool.py @@ -15,6 +15,7 @@ from requests_unixsocket import Session # type: ignore +from ethereum_test_exceptions import ExceptionMapper from ethereum_test_fixtures import FixtureFormat, FixtureVerifier from ethereum_test_forks import Fork from ethereum_test_types import Alloc, Environment, Transaction @@ -51,12 +52,14 @@ class TransitionTool(EthereumCLI, FixtureVerifier): def __init__( self, *, + exception_mapper: ExceptionMapper, binary: Optional[Path] = None, trace: bool = False, ): """ Abstract initialization method that all subclasses must implement. """ + self.exception_mapper = exception_mapper super().__init__(binary=binary) self.trace = trace diff --git a/src/ethereum_test_exceptions/__init__.py b/src/ethereum_test_exceptions/__init__.py index 33cdb29f51..22c1c9fec1 100644 --- a/src/ethereum_test_exceptions/__init__.py +++ b/src/ethereum_test_exceptions/__init__.py @@ -3,15 +3,17 @@ """ from .engine_api import EngineAPIError -from .evmone_exceptions import EvmoneExceptionMapper +from .exception_mapper import ExceptionMapper, ExceptionMessage from .exceptions import ( BlockException, BlockExceptionInstanceOrList, EOFException, EOFExceptionInstanceOrList, + ExceptionBase, ExceptionInstanceOrList, TransactionException, TransactionExceptionInstanceOrList, + UndefinedException, ) __all__ = [ @@ -19,9 +21,12 @@ "BlockExceptionInstanceOrList", "EOFException", "EOFExceptionInstanceOrList", + "ExceptionBase", "EngineAPIError", - "EvmoneExceptionMapper", + "ExceptionMapper", + "ExceptionMessage", "ExceptionInstanceOrList", "TransactionException", + "UndefinedException", "TransactionExceptionInstanceOrList", ] diff --git a/src/ethereum_test_exceptions/evmone_exceptions.py b/src/ethereum_test_exceptions/evmone_exceptions.py deleted file mode 100644 index a23e27bf85..0000000000 --- a/src/ethereum_test_exceptions/evmone_exceptions.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -Evmone eof exceptions ENUM -> str mapper -""" - -from dataclasses import dataclass - -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: - """ - 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 diff --git a/src/ethereum_test_exceptions/exception_mapper.py b/src/ethereum_test_exceptions/exception_mapper.py new file mode 100644 index 0000000000..dadafecdf6 --- /dev/null +++ b/src/ethereum_test_exceptions/exception_mapper.py @@ -0,0 +1,56 @@ +""" +EEST Exception mapper +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from bidict import frozenbidict + +from .exceptions import ExceptionBase, 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 | None: + """Takes an exception and returns a formatted string.""" + message = self.exception_to_message_map.get(exception, None) + return message + + def message_to_exception(self, exception_string: str) -> ExceptionBase: + """Takes a string and tries to find matching exception""" + for entry in self._mapping_data: + if entry.message in exception_string: + return entry.exception + return UndefinedException.UNDEFINED_EXCEPTION diff --git a/src/ethereum_test_exceptions/exceptions.py b/src/ethereum_test_exceptions/exceptions.py index f2c19f2937..94e02c3b8e 100644 --- a/src/ethereum_test_exceptions/exceptions.py +++ b/src/ethereum_test_exceptions/exceptions.py @@ -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): """ @@ -360,7 +372,15 @@ class TransactionException(ExceptionBase): """ TYPE_4_INVALID_AUTHORITY_SIGNATURE = auto() """ - Transaction is type 4, but has an empty authorization list. + Transaction authority signature is invalid + """ + TYPE_4_INVALID_AUTHORITY_SIGNATURE_S_TOO_HIGH = auto() + """ + Transaction authority signature is invalid + """ + TYPE_4_TX_CONTRACT_CREATION = auto() + """ + Transaction is a type 4 transaction and has an empty `to`. """ diff --git a/src/ethereum_test_specs/base.py b/src/ethereum_test_specs/base.py index 8c020e1e27..3dc4e24dc3 100644 --- a/src/ethereum_test_specs/base.py +++ b/src/ethereum_test_specs/base.py @@ -7,7 +7,7 @@ from itertools import count from os import path from pathlib import Path -from typing import Callable, ClassVar, Dict, Generator, Iterator, List, Optional +from typing import Callable, ClassVar, Generator, Iterator, List, Optional import pytest from pydantic import BaseModel, Field @@ -16,7 +16,7 @@ from ethereum_test_base_types import to_hex from ethereum_test_fixtures import BaseFixture, FixtureFormat from ethereum_test_forks import Fork -from ethereum_test_types import Environment, Transaction, Withdrawal +from ethereum_test_types import Environment, Withdrawal class HashMismatchException(Exception): @@ -32,27 +32,6 @@ def __str__(self): # noqa: D105 return f"{self.message}: Expected {self.expected_hash}, got {self.actual_hash}" -def verify_transactions(txs: List[Transaction], result: Result) -> List[int]: - """ - Verify rejected transactions (if any) against the expected outcome. - Raises exception on unexpected rejections or unexpected successful txs. - """ - rejected_txs: Dict[int, str] = { - rejected_tx.index: rejected_tx.error for rejected_tx in result.rejected_transactions - } - - for i, tx in enumerate(txs): - error = rejected_txs[i] if i in rejected_txs else None - if tx.error and not error: - raise Exception(f"tx expected to fail succeeded: pos={i}, nonce={tx.nonce}") - elif not tx.error and error: - raise Exception(f"tx unexpectedly failed: {error}") - - # TODO: Also we need a way to check we actually got the - # correct error - return list(rejected_txs.keys()) - - def verify_result(result: Result, env: Environment): """ Verify that values in the t8n result match the expected values. diff --git a/src/ethereum_test_specs/blockchain.py b/src/ethereum_test_specs/blockchain.py index 3d732bda43..7501ae048b 100644 --- a/src/ethereum_test_specs/blockchain.py +++ b/src/ethereum_test_specs/blockchain.py @@ -55,8 +55,9 @@ WithdrawalRequest, ) -from .base import BaseTest, verify_result, verify_transactions +from .base import BaseTest, verify_result from .debugging import print_traces +from .helpers import verify_transactions def environment_from_parent_header(parent: "FixtureHeader") -> "Environment": @@ -440,7 +441,9 @@ def generate_block_data( ) try: - rejected_txs = verify_transactions(txs, transition_tool_output.result) + rejected_txs = verify_transactions( + t8n.exception_mapper, txs, transition_tool_output.result + ) verify_result(transition_tool_output.result, env) except Exception as e: print_traces(t8n.get_traces()) diff --git a/src/ethereum_test_specs/eof.py b/src/ethereum_test_specs/eof.py index 924e667050..3096dfff69 100644 --- a/src/ethereum_test_specs/eof.py +++ b/src/ethereum_test_specs/eof.py @@ -12,9 +12,8 @@ import pytest from pydantic import Field, model_validator -from ethereum_clis import TransitionTool +from ethereum_clis import EvmoneExceptionMapper, TransitionTool from ethereum_test_base_types import Account, Bytes -from ethereum_test_exceptions import EvmoneExceptionMapper from ethereum_test_exceptions.exceptions import EOFExceptionInstanceOrList, to_pipe_str from ethereum_test_fixtures import ( BaseFixture, diff --git a/src/ethereum_test_specs/helpers.py b/src/ethereum_test_specs/helpers.py new file mode 100644 index 0000000000..83195efcfa --- /dev/null +++ b/src/ethereum_test_specs/helpers.py @@ -0,0 +1,152 @@ +""" +Helper functions +""" + +from dataclasses import dataclass +from typing import Dict, List + +from ethereum_clis import Result +from ethereum_test_exceptions import ExceptionBase, ExceptionMapper, UndefinedException +from ethereum_test_types import Transaction + + +class TransactionExpectedToFailSucceed(Exception): + """ + Exception used when the transaction expected to return an error, did succeed + """ + + def __init__(self, index: int, nonce: int): + message = ( + f"\nTransactionException (pos={index}, nonce={nonce}):" + f"\n What: tx expected to fail succeeded!" + ) + super().__init__(message) + + +class TransactionUnexpectedFail(Exception): + """ + Exception used when the transaction expected to succeed, did fail + """ + + def __init__(self, index: int, nonce: int, message: str, exception: ExceptionBase): + message = ( + f"\nTransactionException (pos={index}, nonce={nonce}):" + f"\n What: tx unexpectedly failed!" + f'\n Error: "{message}" ({exception})' + ) + super().__init__(message) + + +class TransactionExceptionMismatch(Exception): + """ + Exception used when the actual transaction error string differs from the expected one. + """ + + def __init__( + self, + index: int, + nonce: int, + expected_message: str | None, + expected_exception: ExceptionBase, + got_message: str, + got_exception: ExceptionBase, + mapper_name: str, + ): + define_message_hint = ( + f"No message defined for {expected_exception}, please add it to {mapper_name}" + if expected_message is None + else "" + ) + define_exception_hint = ( + f"No exception defined for error message got, please add it to {mapper_name}" + if got_exception == UndefinedException.UNDEFINED_EXCEPTION + else "" + ) + message = ( + f"\nTransactionException (pos={index}, nonce={nonce}):" + f"\n What: exception mismatch!" + f'\n Want: "{expected_message}" ({expected_exception})' + f'\n Got: "{got_message}" ({got_exception})' + f"\n {define_message_hint}" + f"\n {define_exception_hint}" + ) + super().__init__(message) + + +@dataclass +class TransactionExceptionInfo: + """Info to print transaction exception error messages""" + + t8n_error_message: str | None + transaction_ind: int + tx: Transaction + + +def verify_transaction_exception( + exception_mapper: ExceptionMapper, info: TransactionExceptionInfo +): + """Verify transaction exception""" + expected_error: bool = info.tx.error is not None or ( + isinstance(info.tx.error, list) and len(info.tx.error) != 0 + ) + + # info.tx.error is expected error code defined in .py test + if expected_error and not info.t8n_error_message: + raise TransactionExpectedToFailSucceed(index=info.transaction_ind, nonce=info.tx.nonce) + elif not expected_error and info.t8n_error_message: + raise TransactionUnexpectedFail( + index=info.transaction_ind, + nonce=info.tx.nonce, + message=info.t8n_error_message, + exception=exception_mapper.message_to_exception(info.t8n_error_message), + ) + elif expected_error and info.t8n_error_message: + if isinstance(info.tx.error, List): + for expected_exception in info.tx.error: + expected_error_msg = exception_mapper.exception_to_message(expected_exception) + if expected_error_msg is None: + continue + if expected_error_msg in info.t8n_error_message: + # One of expected exceptions is found in tx error string, no error + return + + if isinstance(info.tx.error, List): + expected_exception = info.tx.error[0] + elif info.tx.error is None: + return # will never happen but removes python logic check + else: + expected_exception = info.tx.error + + expected_error_msg = exception_mapper.exception_to_message(expected_exception) + t8n_error_exception = exception_mapper.message_to_exception(info.t8n_error_message) + exception_mapper_name = exception_mapper.__class__.__name__ + + if expected_error_msg is None or expected_error_msg not in info.t8n_error_message: + raise TransactionExceptionMismatch( + index=info.transaction_ind, + nonce=info.tx.nonce, + expected_exception=expected_exception, + expected_message=expected_error_msg, + got_exception=t8n_error_exception, + got_message=info.t8n_error_message, + mapper_name=exception_mapper_name, + ) + + +def verify_transactions( + exception_mapper: ExceptionMapper, txs: List[Transaction], result: Result +) -> List[int]: + """ + Verify rejected transactions (if any) against the expected outcome. + Raises exception on unexpected rejections or unexpected successful txs. + """ + rejected_txs: Dict[int, str] = { + rejected_tx.index: rejected_tx.error for rejected_tx in result.rejected_transactions + } + + for i, tx in enumerate(txs): + error_message = rejected_txs[i] if i in rejected_txs else None + info = TransactionExceptionInfo(t8n_error_message=error_message, transaction_ind=i, tx=tx) + verify_transaction_exception(exception_mapper=exception_mapper, info=info) + + return list(rejected_txs.keys()) diff --git a/src/ethereum_test_specs/state.py b/src/ethereum_test_specs/state.py index 84f30dda74..80c7f5333e 100644 --- a/src/ethereum_test_specs/state.py +++ b/src/ethereum_test_specs/state.py @@ -2,6 +2,7 @@ Ethereum state test spec definition and filler. """ +from pprint import pprint from typing import Any, Callable, ClassVar, Dict, Generator, List, Optional, Type import pytest @@ -27,6 +28,7 @@ from .base import BaseTest from .blockchain import Block, BlockchainTest, Header from .debugging import print_traces +from .helpers import verify_transactions TARGET_BLOB_GAS_PER_BLOCK = 393216 @@ -151,6 +153,14 @@ def make_state_test_fixture( print_traces(t8n.get_traces()) raise e + try: + verify_transactions(t8n.exception_mapper, [tx], transition_tool_output.result) + except Exception as e: + print_traces(t8n.get_traces()) + pprint(transition_tool_output.result) + pprint(transition_tool_output.alloc) + raise e + return Fixture( env=FixtureEnvironment(**env.model_dump(exclude_none=True)), pre=pre_alloc, diff --git a/src/ethereum_test_specs/tests/test_expect.py b/src/ethereum_test_specs/tests/test_expect.py index 2e530da2c6..c9a574cd93 100644 --- a/src/ethereum_test_specs/tests/test_expect.py +++ b/src/ethereum_test_specs/tests/test_expect.py @@ -7,7 +7,7 @@ import pytest from ethereum_clis import ExecutionSpecsTransitionTool -from ethereum_test_base_types import Account, Address +from ethereum_test_base_types import Account, Address, TestAddress, TestPrivateKey from ethereum_test_fixtures import StateFixture from ethereum_test_forks import Fork, get_deployed_forks from ethereum_test_types import Alloc, Environment, Storage, Transaction @@ -17,12 +17,20 @@ ADDRESS_UNDER_TEST = Address(0x01) +@pytest.fixture +def tx() -> Transaction: + """ + The transaction: Set from the test's indirectly parametrized `tx` parameter. + """ + return Transaction(secret_key=TestPrivateKey) + + @pytest.fixture def pre(request) -> Alloc: """ The pre state: Set from the test's indirectly parametrized `pre` parameter. """ - return Alloc(request.param) + return Alloc(request.param | {TestAddress: Account(balance=(10**18))}) @pytest.fixture @@ -40,14 +48,13 @@ def fork() -> Fork: # noqa: D103 @pytest.fixture def state_test( # noqa: D103 - fork: Fork, pre: Mapping[Any, Any], post: Mapping[Any, Any] + pre: Mapping[Any, Any], post: Mapping[Any, Any], tx: Transaction ) -> StateTest: return StateTest( env=Environment(), pre=pre, post=post, - tx=Transaction(), - tag="post_value_mismatch", + tx=tx, ) diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py index 1f84924ebd..052314731c 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py @@ -2243,7 +2243,10 @@ def test_invalid_tx_invalid_auth_signature( to=callee_address, value=0, authorization_list=[authorization_tuple], - error=TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE, + error=[ + TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE, + TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE_S_TOO_HIGH, + ], sender=pre.fund_eoa(), ) @@ -2786,3 +2789,27 @@ def test_reset_code( ), }, ) + + +def test_contract_create( + state_test: StateTestFiller, + pre: Alloc, +): + """ + Test sending type-4 tx as a create transaction + """ + tx = Transaction( + gas_limit=100_000, + to=None, + value=0, + authorization_list=[], + error=TransactionException.TYPE_4_TX_CONTRACT_CREATION, + sender=pre.fund_eoa(), + ) + + state_test( + env=Environment(), + pre=pre, + tx=tx, + post={}, + ) diff --git a/whitelist.txt b/whitelist.txt index b3b0a689a7..0e69332479 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -249,6 +249,7 @@ kzg kzgs lastblockhash len +Levenshtein linux listdir lll