From 8d3b241cbeed3aa267f5552b6668003905808806 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Wed, 17 Jun 2026 09:41:19 -0400 Subject: [PATCH 1/2] fix(specs): check chain id --- .../client_clis/clis/execution_specs.py | 4 ++ src/ethereum/forks/amsterdam/exceptions.py | 16 ++++- src/ethereum/forks/amsterdam/fork.py | 11 ++- src/ethereum/forks/amsterdam/transactions.py | 30 +++++++-- .../forks/arrow_glacier/exceptions.py | 16 ++++- src/ethereum/forks/arrow_glacier/fork.py | 11 ++- .../forks/arrow_glacier/transactions.py | 30 +++++++-- src/ethereum/forks/berlin/exceptions.py | 16 +++++ src/ethereum/forks/berlin/fork.py | 11 ++- src/ethereum/forks/berlin/transactions.py | 30 +++++++-- src/ethereum/forks/bpo1/exceptions.py | 16 ++++- src/ethereum/forks/bpo1/fork.py | 11 ++- src/ethereum/forks/bpo1/transactions.py | 30 +++++++-- src/ethereum/forks/bpo2/exceptions.py | 16 ++++- src/ethereum/forks/bpo2/fork.py | 11 ++- src/ethereum/forks/bpo2/transactions.py | 30 +++++++-- src/ethereum/forks/bpo3/exceptions.py | 16 ++++- src/ethereum/forks/bpo3/fork.py | 11 ++- src/ethereum/forks/bpo3/transactions.py | 30 +++++++-- src/ethereum/forks/bpo4/exceptions.py | 16 ++++- src/ethereum/forks/bpo4/fork.py | 11 ++- src/ethereum/forks/bpo4/transactions.py | 30 +++++++-- src/ethereum/forks/bpo5/exceptions.py | 16 ++++- src/ethereum/forks/bpo5/fork.py | 11 ++- src/ethereum/forks/bpo5/transactions.py | 30 +++++++-- src/ethereum/forks/byzantium/exceptions.py | 21 ++++++ src/ethereum/forks/byzantium/fork.py | 11 ++- src/ethereum/forks/byzantium/transactions.py | 23 +++++-- src/ethereum/forks/cancun/exceptions.py | 16 ++++- src/ethereum/forks/cancun/fork.py | 11 ++- src/ethereum/forks/cancun/transactions.py | 30 +++++++-- .../forks/constantinople/exceptions.py | 21 ++++++ src/ethereum/forks/constantinople/fork.py | 11 ++- .../forks/constantinople/transactions.py | 23 +++++-- src/ethereum/forks/gray_glacier/exceptions.py | 16 ++++- src/ethereum/forks/gray_glacier/fork.py | 11 ++- .../forks/gray_glacier/transactions.py | 30 +++++++-- src/ethereum/forks/istanbul/exceptions.py | 21 ++++++ src/ethereum/forks/istanbul/fork.py | 11 ++- src/ethereum/forks/istanbul/transactions.py | 23 +++++-- src/ethereum/forks/london/exceptions.py | 16 ++++- src/ethereum/forks/london/fork.py | 11 ++- src/ethereum/forks/london/transactions.py | 30 +++++++-- src/ethereum/forks/muir_glacier/exceptions.py | 21 ++++++ src/ethereum/forks/muir_glacier/fork.py | 11 ++- .../forks/muir_glacier/transactions.py | 23 +++++-- src/ethereum/forks/osaka/exceptions.py | 16 ++++- src/ethereum/forks/osaka/fork.py | 11 ++- src/ethereum/forks/osaka/transactions.py | 30 +++++++-- src/ethereum/forks/paris/exceptions.py | 16 ++++- src/ethereum/forks/paris/fork.py | 11 ++- src/ethereum/forks/paris/transactions.py | 30 +++++++-- src/ethereum/forks/prague/exceptions.py | 16 ++++- src/ethereum/forks/prague/fork.py | 11 ++- src/ethereum/forks/prague/transactions.py | 30 +++++++-- src/ethereum/forks/shanghai/exceptions.py | 16 ++++- src/ethereum/forks/shanghai/fork.py | 11 ++- src/ethereum/forks/shanghai/transactions.py | 30 +++++++-- .../forks/spurious_dragon/exceptions.py | 21 ++++++ src/ethereum/forks/spurious_dragon/fork.py | 11 ++- .../forks/spurious_dragon/transactions.py | 23 +++++-- tests/frontier/validation/test_transaction.py | 59 ++++++++++++++++ .../eip1559_fee_market_change/test_tx_type.py | 67 ++++++++++++++++++- 63 files changed, 1110 insertions(+), 150 deletions(-) create mode 100644 src/ethereum/forks/byzantium/exceptions.py create mode 100644 src/ethereum/forks/constantinople/exceptions.py create mode 100644 src/ethereum/forks/istanbul/exceptions.py create mode 100644 src/ethereum/forks/muir_glacier/exceptions.py create mode 100644 src/ethereum/forks/spurious_dragon/exceptions.py diff --git a/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py b/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py index 55434840aeb..be3b9939a1a 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py +++ b/packages/testing/src/execution_testing/client_clis/clis/execution_specs.py @@ -204,6 +204,10 @@ class ExecutionSpecsExceptionMapper(ExceptionMapper): TransactionException.INTRINSIC_GAS_BELOW_FLOOR_GAS_COST: ( "InsufficientTransactionGasError" ), + TransactionException.INVALID_SIGNATURE_VRS: ( + "InvalidSignatureError('bad" + ), + TransactionException.INVALID_CHAINID: ("WrongChainId"), TransactionException.INITCODE_SIZE_EXCEEDED: "InitCodeTooLargeError", TransactionException.PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS: ( "PriorityFeeGreaterThanMaxFeeError" diff --git a/src/ethereum/forks/amsterdam/exceptions.py b/src/ethereum/forks/amsterdam/exceptions.py index 1c6c14cc33d..6ef5651cfa8 100644 --- a/src/ethereum/forks/amsterdam/exceptions.py +++ b/src/ethereum/forks/amsterdam/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidBlock, InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/amsterdam/fork.py b/src/ethereum/forks/amsterdam/fork.py index e72f40d8633..722463959f2 100644 --- a/src/ethereum/forks/amsterdam/fork.py +++ b/src/ethereum/forks/amsterdam/fork.py @@ -58,6 +58,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, BlockAccessIndex, VersionedHash from .requests import ( @@ -86,6 +87,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -575,7 +577,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/amsterdam/transactions.py b/src/ethereum/forks/amsterdam/transactions.py index 598f3552710..fd0cc7b1566 100644 --- a/src/ethereum/forks/amsterdam/transactions.py +++ b/src/ethereum/forks/amsterdam/transactions.py @@ -735,7 +735,25 @@ def count_tokens_in_data(data: bytes) -> Uint: return num_zeros + num_non_zeros * Uint(4) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -762,14 +780,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/arrow_glacier/exceptions.py b/src/ethereum/forks/arrow_glacier/exceptions.py index 59968e94e27..906dc947e1b 100644 --- a/src/ethereum/forks/arrow_glacier/exceptions.py +++ b/src/ethereum/forks/arrow_glacier/exceptions.py @@ -4,11 +4,25 @@ from typing import Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/arrow_glacier/fork.py b/src/ethereum/forks/arrow_glacier/fork.py index eed78cbdfb2..484d474c983 100644 --- a/src/ethereum/forks/arrow_glacier/fork.py +++ b/src/ethereum/forks/arrow_glacier/fork.py @@ -42,6 +42,7 @@ from .exceptions import ( InsufficientMaxFeePerGasError, PriorityFeeGreaterThanMaxFeeError, + WrongChainIdError, ) from .state_tracker import ( BlockState, @@ -61,6 +62,7 @@ FeeMarketTransaction, LegacyTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -490,7 +492,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketTransaction): diff --git a/src/ethereum/forks/arrow_glacier/transactions.py b/src/ethereum/forks/arrow_glacier/transactions.py index 902128e634c..b0b5bc9fa04 100644 --- a/src/ethereum/forks/arrow_glacier/transactions.py +++ b/src/ethereum/forks/arrow_glacier/transactions.py @@ -376,7 +376,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost + access_list_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -403,14 +421,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/berlin/exceptions.py b/src/ethereum/forks/berlin/exceptions.py index 5781a2c1c35..c451cad48c9 100644 --- a/src/ethereum/forks/berlin/exceptions.py +++ b/src/ethereum/forks/berlin/exceptions.py @@ -4,9 +4,25 @@ from typing import Final +from ethereum_types.numeric import U64 + from ethereum.exceptions import InvalidTransaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/berlin/fork.py b/src/ethereum/forks/berlin/fork.py index 69a176907ec..14f72b34c54 100644 --- a/src/ethereum/forks/berlin/fork.py +++ b/src/ethereum/forks/berlin/fork.py @@ -39,6 +39,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt, encode_receipt from .bloom import logs_bloom +from .exceptions import WrongChainIdError from .state_tracker import ( BlockState, TransactionState, @@ -56,6 +57,7 @@ AccessListTransaction, LegacyTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -403,7 +405,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) max_gas_fee = tx.gas * tx.gas_price diff --git a/src/ethereum/forks/berlin/transactions.py b/src/ethereum/forks/berlin/transactions.py index 1c4bf3ca2f0..a36ed5e5da1 100644 --- a/src/ethereum/forks/berlin/transactions.py +++ b/src/ethereum/forks/berlin/transactions.py @@ -292,7 +292,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost + access_list_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -319,14 +337,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/bpo1/exceptions.py b/src/ethereum/forks/bpo1/exceptions.py index 3074a1f738f..8d409eaaa02 100644 --- a/src/ethereum/forks/bpo1/exceptions.py +++ b/src/ethereum/forks/bpo1/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/bpo1/fork.py b/src/ethereum/forks/bpo1/fork.py index a256625deb7..c2c43c631dc 100644 --- a/src/ethereum/forks/bpo1/fork.py +++ b/src/ethereum/forks/bpo1/fork.py @@ -48,6 +48,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, VersionedHash from .requests import ( @@ -75,6 +76,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -483,7 +485,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/bpo1/transactions.py b/src/ethereum/forks/bpo1/transactions.py index acd3e8b07c3..ad60842ee81 100644 --- a/src/ethereum/forks/bpo1/transactions.py +++ b/src/ethereum/forks/bpo1/transactions.py @@ -651,7 +651,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: ) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -678,14 +696,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/bpo2/exceptions.py b/src/ethereum/forks/bpo2/exceptions.py index 3074a1f738f..8d409eaaa02 100644 --- a/src/ethereum/forks/bpo2/exceptions.py +++ b/src/ethereum/forks/bpo2/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/bpo2/fork.py b/src/ethereum/forks/bpo2/fork.py index a256625deb7..c2c43c631dc 100644 --- a/src/ethereum/forks/bpo2/fork.py +++ b/src/ethereum/forks/bpo2/fork.py @@ -48,6 +48,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, VersionedHash from .requests import ( @@ -75,6 +76,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -483,7 +485,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/bpo2/transactions.py b/src/ethereum/forks/bpo2/transactions.py index 983dcefa419..569d6867270 100644 --- a/src/ethereum/forks/bpo2/transactions.py +++ b/src/ethereum/forks/bpo2/transactions.py @@ -651,7 +651,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: ) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -678,14 +696,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/bpo3/exceptions.py b/src/ethereum/forks/bpo3/exceptions.py index 3074a1f738f..8d409eaaa02 100644 --- a/src/ethereum/forks/bpo3/exceptions.py +++ b/src/ethereum/forks/bpo3/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/bpo3/fork.py b/src/ethereum/forks/bpo3/fork.py index a256625deb7..c2c43c631dc 100644 --- a/src/ethereum/forks/bpo3/fork.py +++ b/src/ethereum/forks/bpo3/fork.py @@ -48,6 +48,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, VersionedHash from .requests import ( @@ -75,6 +76,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -483,7 +485,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/bpo3/transactions.py b/src/ethereum/forks/bpo3/transactions.py index 6592faf2ea9..258364835f8 100644 --- a/src/ethereum/forks/bpo3/transactions.py +++ b/src/ethereum/forks/bpo3/transactions.py @@ -651,7 +651,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: ) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -678,14 +696,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/bpo4/exceptions.py b/src/ethereum/forks/bpo4/exceptions.py index 3074a1f738f..8d409eaaa02 100644 --- a/src/ethereum/forks/bpo4/exceptions.py +++ b/src/ethereum/forks/bpo4/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/bpo4/fork.py b/src/ethereum/forks/bpo4/fork.py index a256625deb7..c2c43c631dc 100644 --- a/src/ethereum/forks/bpo4/fork.py +++ b/src/ethereum/forks/bpo4/fork.py @@ -48,6 +48,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, VersionedHash from .requests import ( @@ -75,6 +76,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -483,7 +485,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/bpo4/transactions.py b/src/ethereum/forks/bpo4/transactions.py index 1a916ab853c..5e86fc63f13 100644 --- a/src/ethereum/forks/bpo4/transactions.py +++ b/src/ethereum/forks/bpo4/transactions.py @@ -651,7 +651,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: ) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -678,14 +696,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/bpo5/exceptions.py b/src/ethereum/forks/bpo5/exceptions.py index 3074a1f738f..8d409eaaa02 100644 --- a/src/ethereum/forks/bpo5/exceptions.py +++ b/src/ethereum/forks/bpo5/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/bpo5/fork.py b/src/ethereum/forks/bpo5/fork.py index a256625deb7..c2c43c631dc 100644 --- a/src/ethereum/forks/bpo5/fork.py +++ b/src/ethereum/forks/bpo5/fork.py @@ -48,6 +48,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, VersionedHash from .requests import ( @@ -75,6 +76,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -483,7 +485,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/bpo5/transactions.py b/src/ethereum/forks/bpo5/transactions.py index 8c765b0fd5d..2aded367166 100644 --- a/src/ethereum/forks/bpo5/transactions.py +++ b/src/ethereum/forks/bpo5/transactions.py @@ -651,7 +651,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: ) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -678,14 +696,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/byzantium/exceptions.py b/src/ethereum/forks/byzantium/exceptions.py new file mode 100644 index 00000000000..1ba2a07726c --- /dev/null +++ b/src/ethereum/forks/byzantium/exceptions.py @@ -0,0 +1,21 @@ +""" +Exceptions specific to this fork. +""" + +from ethereum_types.numeric import U64 + +from ethereum.exceptions import InvalidTransaction + + +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual diff --git a/src/ethereum/forks/byzantium/fork.py b/src/ethereum/forks/byzantium/fork.py index 1c705256b96..a83087f4f0a 100644 --- a/src/ethereum/forks/byzantium/fork.py +++ b/src/ethereum/forks/byzantium/fork.py @@ -38,6 +38,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom +from .exceptions import WrongChainIdError from .state_tracker import ( BlockState, TransactionState, @@ -53,6 +54,7 @@ ) from .transactions import ( Transaction, + chain_id, get_transaction_hash, recover_sender, validate_transaction, @@ -398,7 +400,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) max_gas_fee = tx.gas * tx.gas_price diff --git a/src/ethereum/forks/byzantium/transactions.py b/src/ethereum/forks/byzantium/transactions.py index 8272ebea080..f599f94e062 100644 --- a/src/ethereum/forks/byzantium/transactions.py +++ b/src/ethereum/forks/byzantium/transactions.py @@ -147,7 +147,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if tx.v == 27 or tx.v == 28: + return None + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + return U64((tx.v - U256(35)) >> U256(1)) + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -172,11 +185,11 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( - r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) + r, s, v, signing_hash_155(tx, tx_chain_id) ) return Address(keccak256(public_key)[12:32]) diff --git a/src/ethereum/forks/cancun/exceptions.py b/src/ethereum/forks/cancun/exceptions.py index 5b46b805561..3256a5a7fcf 100644 --- a/src/ethereum/forks/cancun/exceptions.py +++ b/src/ethereum/forks/cancun/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/cancun/fork.py b/src/ethereum/forks/cancun/fork.py index 753912fd9d5..67e3b0197d0 100644 --- a/src/ethereum/forks/cancun/fork.py +++ b/src/ethereum/forks/cancun/fork.py @@ -46,6 +46,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import VersionedHash from .state_tracker import ( @@ -67,6 +68,7 @@ FeeMarketTransaction, LegacyTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -446,7 +448,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/cancun/transactions.py b/src/ethereum/forks/cancun/transactions.py index a0d80d8c67a..d65da512655 100644 --- a/src/ethereum/forks/cancun/transactions.py +++ b/src/ethereum/forks/cancun/transactions.py @@ -500,7 +500,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost + access_list_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -527,14 +545,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/constantinople/exceptions.py b/src/ethereum/forks/constantinople/exceptions.py new file mode 100644 index 00000000000..1ba2a07726c --- /dev/null +++ b/src/ethereum/forks/constantinople/exceptions.py @@ -0,0 +1,21 @@ +""" +Exceptions specific to this fork. +""" + +from ethereum_types.numeric import U64 + +from ethereum.exceptions import InvalidTransaction + + +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual diff --git a/src/ethereum/forks/constantinople/fork.py b/src/ethereum/forks/constantinople/fork.py index 209fe029e46..62654a56386 100644 --- a/src/ethereum/forks/constantinople/fork.py +++ b/src/ethereum/forks/constantinople/fork.py @@ -38,6 +38,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom +from .exceptions import WrongChainIdError from .state_tracker import ( BlockState, TransactionState, @@ -53,6 +54,7 @@ ) from .transactions import ( Transaction, + chain_id, get_transaction_hash, recover_sender, validate_transaction, @@ -398,7 +400,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) max_gas_fee = tx.gas * tx.gas_price diff --git a/src/ethereum/forks/constantinople/transactions.py b/src/ethereum/forks/constantinople/transactions.py index 8272ebea080..f599f94e062 100644 --- a/src/ethereum/forks/constantinople/transactions.py +++ b/src/ethereum/forks/constantinople/transactions.py @@ -147,7 +147,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if tx.v == 27 or tx.v == 28: + return None + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + return U64((tx.v - U256(35)) >> U256(1)) + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -172,11 +185,11 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( - r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) + r, s, v, signing_hash_155(tx, tx_chain_id) ) return Address(keccak256(public_key)[12:32]) diff --git a/src/ethereum/forks/gray_glacier/exceptions.py b/src/ethereum/forks/gray_glacier/exceptions.py index 59968e94e27..906dc947e1b 100644 --- a/src/ethereum/forks/gray_glacier/exceptions.py +++ b/src/ethereum/forks/gray_glacier/exceptions.py @@ -4,11 +4,25 @@ from typing import Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/gray_glacier/fork.py b/src/ethereum/forks/gray_glacier/fork.py index 78a88531754..fca169f4123 100644 --- a/src/ethereum/forks/gray_glacier/fork.py +++ b/src/ethereum/forks/gray_glacier/fork.py @@ -42,6 +42,7 @@ from .exceptions import ( InsufficientMaxFeePerGasError, PriorityFeeGreaterThanMaxFeeError, + WrongChainIdError, ) from .state_tracker import ( BlockState, @@ -61,6 +62,7 @@ FeeMarketTransaction, LegacyTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -490,7 +492,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketTransaction): diff --git a/src/ethereum/forks/gray_glacier/transactions.py b/src/ethereum/forks/gray_glacier/transactions.py index 902128e634c..b0b5bc9fa04 100644 --- a/src/ethereum/forks/gray_glacier/transactions.py +++ b/src/ethereum/forks/gray_glacier/transactions.py @@ -376,7 +376,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost + access_list_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -403,14 +421,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/istanbul/exceptions.py b/src/ethereum/forks/istanbul/exceptions.py new file mode 100644 index 00000000000..1ba2a07726c --- /dev/null +++ b/src/ethereum/forks/istanbul/exceptions.py @@ -0,0 +1,21 @@ +""" +Exceptions specific to this fork. +""" + +from ethereum_types.numeric import U64 + +from ethereum.exceptions import InvalidTransaction + + +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual diff --git a/src/ethereum/forks/istanbul/fork.py b/src/ethereum/forks/istanbul/fork.py index 5d5dd2686d1..da84266e3f9 100644 --- a/src/ethereum/forks/istanbul/fork.py +++ b/src/ethereum/forks/istanbul/fork.py @@ -38,6 +38,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom +from .exceptions import WrongChainIdError from .state_tracker import ( BlockState, TransactionState, @@ -53,6 +54,7 @@ ) from .transactions import ( Transaction, + chain_id, get_transaction_hash, recover_sender, validate_transaction, @@ -398,7 +400,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) max_gas_fee = tx.gas * tx.gas_price diff --git a/src/ethereum/forks/istanbul/transactions.py b/src/ethereum/forks/istanbul/transactions.py index 8272ebea080..f599f94e062 100644 --- a/src/ethereum/forks/istanbul/transactions.py +++ b/src/ethereum/forks/istanbul/transactions.py @@ -147,7 +147,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if tx.v == 27 or tx.v == 28: + return None + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + return U64((tx.v - U256(35)) >> U256(1)) + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -172,11 +185,11 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( - r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) + r, s, v, signing_hash_155(tx, tx_chain_id) ) return Address(keccak256(public_key)[12:32]) diff --git a/src/ethereum/forks/london/exceptions.py b/src/ethereum/forks/london/exceptions.py index 59968e94e27..906dc947e1b 100644 --- a/src/ethereum/forks/london/exceptions.py +++ b/src/ethereum/forks/london/exceptions.py @@ -4,11 +4,25 @@ from typing import Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/london/fork.py b/src/ethereum/forks/london/fork.py index d5ce75bd533..c457b885c6d 100644 --- a/src/ethereum/forks/london/fork.py +++ b/src/ethereum/forks/london/fork.py @@ -43,6 +43,7 @@ from .exceptions import ( InsufficientMaxFeePerGasError, PriorityFeeGreaterThanMaxFeeError, + WrongChainIdError, ) from .state_tracker import ( BlockState, @@ -62,6 +63,7 @@ FeeMarketTransaction, LegacyTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -499,7 +501,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketTransaction): diff --git a/src/ethereum/forks/london/transactions.py b/src/ethereum/forks/london/transactions.py index 902128e634c..b0b5bc9fa04 100644 --- a/src/ethereum/forks/london/transactions.py +++ b/src/ethereum/forks/london/transactions.py @@ -376,7 +376,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost + access_list_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -403,14 +421,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/muir_glacier/exceptions.py b/src/ethereum/forks/muir_glacier/exceptions.py new file mode 100644 index 00000000000..1ba2a07726c --- /dev/null +++ b/src/ethereum/forks/muir_glacier/exceptions.py @@ -0,0 +1,21 @@ +""" +Exceptions specific to this fork. +""" + +from ethereum_types.numeric import U64 + +from ethereum.exceptions import InvalidTransaction + + +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual diff --git a/src/ethereum/forks/muir_glacier/fork.py b/src/ethereum/forks/muir_glacier/fork.py index 0b6ada29506..e64fcf08654 100644 --- a/src/ethereum/forks/muir_glacier/fork.py +++ b/src/ethereum/forks/muir_glacier/fork.py @@ -38,6 +38,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom +from .exceptions import WrongChainIdError from .state_tracker import ( BlockState, TransactionState, @@ -53,6 +54,7 @@ ) from .transactions import ( Transaction, + chain_id, get_transaction_hash, recover_sender, validate_transaction, @@ -398,7 +400,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) max_gas_fee = tx.gas * tx.gas_price diff --git a/src/ethereum/forks/muir_glacier/transactions.py b/src/ethereum/forks/muir_glacier/transactions.py index 8272ebea080..f599f94e062 100644 --- a/src/ethereum/forks/muir_glacier/transactions.py +++ b/src/ethereum/forks/muir_glacier/transactions.py @@ -147,7 +147,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if tx.v == 27 or tx.v == 28: + return None + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + return U64((tx.v - U256(35)) >> U256(1)) + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -172,11 +185,11 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( - r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) + r, s, v, signing_hash_155(tx, tx_chain_id) ) return Address(keccak256(public_key)[12:32]) diff --git a/src/ethereum/forks/osaka/exceptions.py b/src/ethereum/forks/osaka/exceptions.py index 3074a1f738f..8d409eaaa02 100644 --- a/src/ethereum/forks/osaka/exceptions.py +++ b/src/ethereum/forks/osaka/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/osaka/fork.py b/src/ethereum/forks/osaka/fork.py index a256625deb7..c2c43c631dc 100644 --- a/src/ethereum/forks/osaka/fork.py +++ b/src/ethereum/forks/osaka/fork.py @@ -48,6 +48,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, VersionedHash from .requests import ( @@ -75,6 +76,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -483,7 +485,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/osaka/transactions.py b/src/ethereum/forks/osaka/transactions.py index b0a90152275..58b4f03c253 100644 --- a/src/ethereum/forks/osaka/transactions.py +++ b/src/ethereum/forks/osaka/transactions.py @@ -655,7 +655,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: ) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -682,14 +700,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/paris/exceptions.py b/src/ethereum/forks/paris/exceptions.py index 59968e94e27..906dc947e1b 100644 --- a/src/ethereum/forks/paris/exceptions.py +++ b/src/ethereum/forks/paris/exceptions.py @@ -4,11 +4,25 @@ from typing import Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/paris/fork.py b/src/ethereum/forks/paris/fork.py index a5609b3353c..318cbdf2163 100644 --- a/src/ethereum/forks/paris/fork.py +++ b/src/ethereum/forks/paris/fork.py @@ -41,6 +41,7 @@ from .exceptions import ( InsufficientMaxFeePerGasError, PriorityFeeGreaterThanMaxFeeError, + WrongChainIdError, ) from .state_tracker import ( BlockState, @@ -58,6 +59,7 @@ FeeMarketTransaction, LegacyTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -389,7 +391,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketTransaction): diff --git a/src/ethereum/forks/paris/transactions.py b/src/ethereum/forks/paris/transactions.py index 902128e634c..b0b5bc9fa04 100644 --- a/src/ethereum/forks/paris/transactions.py +++ b/src/ethereum/forks/paris/transactions.py @@ -376,7 +376,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost + access_list_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -403,14 +421,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/prague/exceptions.py b/src/ethereum/forks/prague/exceptions.py index 898d96bebaf..18d22306281 100644 --- a/src/ethereum/forks/prague/exceptions.py +++ b/src/ethereum/forks/prague/exceptions.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction @@ -12,6 +12,20 @@ from .transactions import Transaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/prague/fork.py b/src/ethereum/forks/prague/fork.py index f48695cf5c6..f5c4f81a6f8 100644 --- a/src/ethereum/forks/prague/fork.py +++ b/src/ethereum/forks/prague/fork.py @@ -47,6 +47,7 @@ NoBlobDataError, PriorityFeeGreaterThanMaxFeeError, TransactionTypeContractCreationError, + WrongChainIdError, ) from .fork_types import Authorization, VersionedHash from .requests import ( @@ -74,6 +75,7 @@ LegacyTransaction, SetCodeTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -471,7 +473,14 @@ def check_transaction( if tx_blob_gas_used > blob_gas_available: raise BlobGasLimitExceededError("blob gas limit exceeded") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketCapableTransaction): diff --git a/src/ethereum/forks/prague/transactions.py b/src/ethereum/forks/prague/transactions.py index ed549ec1d39..2c5dd7d1b1f 100644 --- a/src/ethereum/forks/prague/transactions.py +++ b/src/ethereum/forks/prague/transactions.py @@ -646,7 +646,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> IntrinsicGasCost: ) -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -673,14 +691,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/shanghai/exceptions.py b/src/ethereum/forks/shanghai/exceptions.py index 8dc6c4d0e1c..3929980023b 100644 --- a/src/ethereum/forks/shanghai/exceptions.py +++ b/src/ethereum/forks/shanghai/exceptions.py @@ -4,11 +4,25 @@ from typing import Final -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U64, Uint from ethereum.exceptions import InvalidTransaction +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual + + class TransactionTypeError(InvalidTransaction): """ Unknown [EIP-2718] transaction type byte. diff --git a/src/ethereum/forks/shanghai/fork.py b/src/ethereum/forks/shanghai/fork.py index 73a34017d55..c038d947014 100644 --- a/src/ethereum/forks/shanghai/fork.py +++ b/src/ethereum/forks/shanghai/fork.py @@ -41,6 +41,7 @@ from .exceptions import ( InsufficientMaxFeePerGasError, PriorityFeeGreaterThanMaxFeeError, + WrongChainIdError, ) from .state_tracker import ( BlockState, @@ -58,6 +59,7 @@ FeeMarketTransaction, LegacyTransaction, Transaction, + chain_id, decode_transaction, encode_transaction, get_transaction_hash, @@ -393,7 +395,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) if isinstance(tx, FeeMarketTransaction): diff --git a/src/ethereum/forks/shanghai/transactions.py b/src/ethereum/forks/shanghai/transactions.py index 2b76014505b..b76058eaf39 100644 --- a/src/ethereum/forks/shanghai/transactions.py +++ b/src/ethereum/forks/shanghai/transactions.py @@ -386,7 +386,25 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost + access_list_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if isinstance(tx, LegacyTransaction): + if tx.v == 27 or tx.v == 28: + return None + + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + + return U64((tx.v - U256(35)) >> U256(1)) + else: + return tx.chain_id + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -413,14 +431,14 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( r, s, - v - U256(35) - chain_id_x2, - signing_hash_155(tx, chain_id), + v, + signing_hash_155(tx, tx_chain_id), ) elif isinstance(tx, AccessListTransaction): if tx.y_parity not in (U256(0), U256(1)): diff --git a/src/ethereum/forks/spurious_dragon/exceptions.py b/src/ethereum/forks/spurious_dragon/exceptions.py new file mode 100644 index 00000000000..1ba2a07726c --- /dev/null +++ b/src/ethereum/forks/spurious_dragon/exceptions.py @@ -0,0 +1,21 @@ +""" +Exceptions specific to this fork. +""" + +from ethereum_types.numeric import U64 + +from ethereum.exceptions import InvalidTransaction + + +class WrongChainIdError(InvalidTransaction): + """ + Chain identifier from a transaction does not match the executing chain. See + [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + + def __init__(self, expected: U64, actual: U64): + super().__init__(f"expected chain_id `{expected}` but got `{actual}`") + self.expected = expected + self.actual = actual diff --git a/src/ethereum/forks/spurious_dragon/fork.py b/src/ethereum/forks/spurious_dragon/fork.py index b82b9fc12bc..01c8b269d16 100644 --- a/src/ethereum/forks/spurious_dragon/fork.py +++ b/src/ethereum/forks/spurious_dragon/fork.py @@ -38,6 +38,7 @@ from . import vm from .blocks import Block, Header, Log, Receipt from .bloom import logs_bloom +from .exceptions import WrongChainIdError from .state_tracker import ( BlockState, TransactionState, @@ -53,6 +54,7 @@ ) from .transactions import ( Transaction, + chain_id, get_transaction_hash, recover_sender, validate_transaction, @@ -394,7 +396,14 @@ def check_transaction( gas_available = block_env.block_gas_limit - block_output.block_gas_used if tx.gas > gas_available: raise GasUsedExceedsLimitError("gas used exceeds limit") - sender_address = recover_sender(block_env.chain_id, tx) + tx_chain_id = chain_id(tx) + if tx_chain_id is not None and tx_chain_id != block_env.chain_id: + raise WrongChainIdError( + expected=block_env.chain_id, + actual=tx_chain_id, + ) + + sender_address = recover_sender(tx) sender_account = get_account(tx_state, sender_address) max_gas_fee = tx.gas * tx.gas_price diff --git a/src/ethereum/forks/spurious_dragon/transactions.py b/src/ethereum/forks/spurious_dragon/transactions.py index 8272ebea080..f599f94e062 100644 --- a/src/ethereum/forks/spurious_dragon/transactions.py +++ b/src/ethereum/forks/spurious_dragon/transactions.py @@ -147,7 +147,20 @@ def calculate_intrinsic_cost(tx: Transaction) -> Uint: return GasCosts.TX_BASE + data_cost + create_cost -def recover_sender(chain_id: U64, tx: Transaction) -> Address: +def chain_id(tx: Transaction) -> None | U64: + """ + Extract the chain identifier from a transaction. See [EIP-155]. + + [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + """ + if tx.v == 27 or tx.v == 28: + return None + if tx.v < U256(35): + raise InvalidSignatureError("bad v") + return U64((tx.v - U256(35)) >> U256(1)) + + +def recover_sender(tx: Transaction) -> Address: """ Extracts the sender address from a transaction. @@ -172,11 +185,11 @@ def recover_sender(chain_id: U64, tx: Transaction) -> Address: r, s, v - U256(27), signing_hash_pre155(tx) ) else: - chain_id_x2 = U256(chain_id) * U256(2) - if v != U256(35) + chain_id_x2 and v != U256(36) + chain_id_x2: - raise InvalidSignatureError("bad v") + assert v >= U256(35), "call chain_id before recover_sender" + tx_chain_id = U64((v - U256(35)) >> U256(1)) + v = (v - U256(35)) & U256(1) public_key = secp256k1_recover( - r, s, v - U256(35) - chain_id_x2, signing_hash_155(tx, chain_id) + r, s, v, signing_hash_155(tx, tx_chain_id) ) return Address(keccak256(public_key)[12:32]) diff --git a/tests/frontier/validation/test_transaction.py b/tests/frontier/validation/test_transaction.py index 3e3f487317f..39613ae4ec6 100644 --- a/tests/frontier/validation/test_transaction.py +++ b/tests/frontier/validation/test_transaction.py @@ -8,6 +8,7 @@ StateTestFiller, Storage, Transaction, + add_kzg_version, ) from execution_testing.base_types.base_types import ZeroPaddedHexNumber from execution_testing.exceptions.exceptions import TransactionException @@ -196,3 +197,61 @@ def test_sender_balance_insufficient_state_test( post={contract: Account(storage=storage)}, tx=tx, ) + + +SECP256K1N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + +@pytest.mark.valid_from("Frontier") +@pytest.mark.exception_test +@pytest.mark.eels_base_coverage +@pytest.mark.with_all_tx_types +@pytest.mark.parametrize( + ("v", "r", "s"), + [ + # Other than 27/28, anything less than 35 for v is invalid. + (34, 1, 1), + # Equal to or above these values are invalid. + (27, SECP256K1N, 1), + pytest.param(27, 1, SECP256K1N, id="s=SECP256K1N"), + pytest.param( + 27, + 1, + (SECP256K1N // 2) + 1, + id="s=SECP256K1N//2+1", + marks=pytest.mark.valid_from("Homestead"), + ), + ], +) +def test_bad_v_r_s( + state_test: StateTestFiller, + pre: Alloc, + tx_type: int, + v: int, + r: int, + s: int, +) -> None: + """ + The v/y_parity component of a signature must be 35 or greater (if it isn't + 27/28). + """ + to = pre.fund_eoa(0xDEADBEEE) + + blob_versioned_hashes = add_kzg_version([0], 1) if tx_type == 3 else None + tx = Transaction( + sender=pre.fund_eoa(), + to=to, + error=TransactionException.INVALID_SIGNATURE_VRS, + ty=tx_type, + blob_versioned_hashes=blob_versioned_hashes, + value=1, + v=v, + r=r, + s=s, + ) + + state_test( + pre=pre, + post={to: Account(balance=0xDEADBEEE)}, + tx=tx, + ) diff --git a/tests/london/eip1559_fee_market_change/test_tx_type.py b/tests/london/eip1559_fee_market_change/test_tx_type.py index c8ade2fb6b6..6a831d0203a 100644 --- a/tests/london/eip1559_fee_market_change/test_tx_type.py +++ b/tests/london/eip1559_fee_market_change/test_tx_type.py @@ -1,18 +1,21 @@ """Test the tx type validation for EIP-1559.""" -from typing import Generator +from typing import Final, Generator, Sequence import pytest from execution_testing import ( Account, Alloc, + ChainConfig, Fork, ParameterSet, StateTestFiller, Transaction, TransactionException, + TransactionType, ) from execution_testing import Opcodes as Op +from execution_testing.base_types import Hash from .spec import ref_spec_1559 @@ -70,3 +73,65 @@ def test_eip1559_tx_validity( post[sender] = pre[sender] # type: ignore state_test(pre=pre, post=post, tx=tx) + + +TX_TYPES: Final[Sequence[object]] = [ + pytest.param(TransactionType.LEGACY, None), + pytest.param( + TransactionType.ACCESS_LIST, + None, + marks=[pytest.mark.valid_from("Berlin")], + ), + pytest.param( + TransactionType.BASE_FEE, + None, + marks=[pytest.mark.valid_from("London")], + ), + pytest.param( + TransactionType.BLOB_TRANSACTION, + [0], + marks=[pytest.mark.valid_from("Cancun")], + ), + pytest.param( + TransactionType.SET_CODE, + None, + marks=[pytest.mark.valid_from("Prague")], + ), +] + +if len(TX_TYPES) != len(TransactionType): + raise Exception("missing tx type") + + +@pytest.mark.valid_from("SpuriousDragon") +@pytest.mark.exception_test +@pytest.mark.parametrize(("tx_type", "blob_versioned_hashes"), TX_TYPES) +def test_invalid_chain_id( + state_test: StateTestFiller, + pre: Alloc, + chain_config: ChainConfig, + tx_type: int, + blob_versioned_hashes: None | Sequence[Hash], +) -> None: + """ + Test that a transaction with a different chain id is not valid. + """ + to = pre.fund_eoa(0xDEADBEEE) + + tx = Transaction( + sender=pre.fund_eoa(), + value=1, + chain_id=chain_config.chain_id + 1, + ty=tx_type, + to=to, + error=TransactionException.INVALID_CHAINID, + blob_versioned_hashes=blob_versioned_hashes, + ) + + state_test( + pre=pre, + tx=tx, + post={ + to: Account(balance=0xDEADBEEE), + }, + ) From fa9fde7d9800844e9d5e158840ba4294175a4925 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Wed, 17 Jun 2026 10:25:09 -0400 Subject: [PATCH 2/2] fix(tests): add exception map for invalid chain ids Co-authored-by: Mario Vega Zavala --- .../client_clis/clis/besu.py | 3 ++ .../client_clis/clis/erigon.py | 1 + .../client_clis/clis/ethrex.py | 3 ++ .../client_clis/clis/geth.py | 1 + .../client_clis/clis/nethermind.py | 3 ++ .../client_clis/clis/reth.py | 1 + .../eip1559_fee_market_change/test_tx_type.py | 37 ++----------------- 7 files changed, 16 insertions(+), 33 deletions(-) diff --git a/packages/testing/src/execution_testing/client_clis/clis/besu.py b/packages/testing/src/execution_testing/client_clis/clis/besu.py index ce0789cd399..6325b830ef9 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/besu.py +++ b/packages/testing/src/execution_testing/client_clis/clis/besu.py @@ -326,6 +326,9 @@ class BesuExceptionMapper(ExceptionMapper): "transaction invalid max priority fee per gas cannot be greater " "than max fee per gas" ), + TransactionException.INVALID_CHAINID: ( + "transaction invalid transaction was meant for chain id" + ), TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH: ( "Invalid versionedHash" ), diff --git a/packages/testing/src/execution_testing/client_clis/clis/erigon.py b/packages/testing/src/execution_testing/client_clis/clis/erigon.py index e0a77b5f71a..3ef96449490 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/erigon.py +++ b/packages/testing/src/execution_testing/client_clis/clis/erigon.py @@ -53,6 +53,7 @@ class ErigonExceptionMapper(ExceptionMapper): TransactionException.NONCE_MISMATCH_TOO_LOW: "nonce too low", TransactionException.NONCE_MISMATCH_TOO_HIGH: "nonce too high", TransactionException.GAS_ALLOWANCE_EXCEEDED: "gas limit reached", + TransactionException.INVALID_CHAINID: "invalid chain id for signer", TransactionException.TYPE_3_TX_PRE_FORK: ( "blob txn is not supported by signer" ), diff --git a/packages/testing/src/execution_testing/client_clis/clis/ethrex.py b/packages/testing/src/execution_testing/client_clis/clis/ethrex.py index d85a1e7ce86..cde6576a6d7 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/ethrex.py +++ b/packages/testing/src/execution_testing/client_clis/clis/ethrex.py @@ -17,6 +17,9 @@ class EthrexExceptionMapper(ExceptionMapper): TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED: ( "Exceeded MAX_BLOB_GAS_PER_BLOCK" ), + TransactionException.INVALID_CHAINID: ( + "Transaction has invalid chain id" + ), BlockException.INVALID_DEPOSIT_EVENT_LAYOUT: ( "Invalid deposit request layout" ), diff --git a/packages/testing/src/execution_testing/client_clis/clis/geth.py b/packages/testing/src/execution_testing/client_clis/clis/geth.py index 25f0e0244e9..1a9c46c720d 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/geth.py +++ b/packages/testing/src/execution_testing/client_clis/clis/geth.py @@ -54,6 +54,7 @@ class GethExceptionMapper(ExceptionMapper): TransactionException.PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS: ( "max priority fee per gas higher than max fee per gas" ), + TransactionException.INVALID_CHAINID: "invalid chain id for signer", TransactionException.TYPE_1_TX_PRE_FORK: ( "transaction type not supported" ), diff --git a/packages/testing/src/execution_testing/client_clis/clis/nethermind.py b/packages/testing/src/execution_testing/client_clis/clis/nethermind.py index 929fc139cbb..bb3687ef970 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/nethermind.py +++ b/packages/testing/src/execution_testing/client_clis/clis/nethermind.py @@ -413,6 +413,9 @@ class NethermindExceptionMapper(ExceptionMapper): ), TransactionException.NONCE_MISMATCH_TOO_LOW: (r"nonce too low"), TransactionException.NONCE_MISMATCH_TOO_HIGH: (r"nonce too high"), + TransactionException.INVALID_CHAINID: ( + r"InvalidTxChainId|Signature is invalid." + ), TransactionException.TYPE_3_TX_WITH_FULL_BLOBS: ( r"Transaction \d+ is not valid" ), diff --git a/packages/testing/src/execution_testing/client_clis/clis/reth.py b/packages/testing/src/execution_testing/client_clis/clis/reth.py index c2193fcb996..d3a77fdedce 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/reth.py +++ b/packages/testing/src/execution_testing/client_clis/clis/reth.py @@ -27,6 +27,7 @@ class RethExceptionMapper(ExceptionMapper): TransactionException.GASLIMIT_PRICE_PRODUCT_OVERFLOW: "overflow", TransactionException.TYPE_3_TX_CONTRACT_CREATION: "unexpected length", TransactionException.TYPE_3_TX_WITH_FULL_BLOBS: "unexpected list", + TransactionException.INVALID_CHAINID: "invalid chain ID", TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH: ( "blob version not supported" ), diff --git a/tests/london/eip1559_fee_market_change/test_tx_type.py b/tests/london/eip1559_fee_market_change/test_tx_type.py index 6a831d0203a..a9dae2eb0bc 100644 --- a/tests/london/eip1559_fee_market_change/test_tx_type.py +++ b/tests/london/eip1559_fee_market_change/test_tx_type.py @@ -1,6 +1,6 @@ """Test the tx type validation for EIP-1559.""" -from typing import Final, Generator, Sequence +from typing import Generator import pytest from execution_testing import ( @@ -12,10 +12,9 @@ StateTestFiller, Transaction, TransactionException, - TransactionType, + add_kzg_version, ) from execution_testing import Opcodes as Op -from execution_testing.base_types import Hash from .spec import ref_spec_1559 @@ -75,49 +74,21 @@ def test_eip1559_tx_validity( state_test(pre=pre, post=post, tx=tx) -TX_TYPES: Final[Sequence[object]] = [ - pytest.param(TransactionType.LEGACY, None), - pytest.param( - TransactionType.ACCESS_LIST, - None, - marks=[pytest.mark.valid_from("Berlin")], - ), - pytest.param( - TransactionType.BASE_FEE, - None, - marks=[pytest.mark.valid_from("London")], - ), - pytest.param( - TransactionType.BLOB_TRANSACTION, - [0], - marks=[pytest.mark.valid_from("Cancun")], - ), - pytest.param( - TransactionType.SET_CODE, - None, - marks=[pytest.mark.valid_from("Prague")], - ), -] - -if len(TX_TYPES) != len(TransactionType): - raise Exception("missing tx type") - - @pytest.mark.valid_from("SpuriousDragon") @pytest.mark.exception_test -@pytest.mark.parametrize(("tx_type", "blob_versioned_hashes"), TX_TYPES) +@pytest.mark.with_all_tx_types def test_invalid_chain_id( state_test: StateTestFiller, pre: Alloc, chain_config: ChainConfig, tx_type: int, - blob_versioned_hashes: None | Sequence[Hash], ) -> None: """ Test that a transaction with a different chain id is not valid. """ to = pre.fund_eoa(0xDEADBEEE) + blob_versioned_hashes = add_kzg_version([0], 1) if tx_type == 3 else None tx = Transaction( sender=pre.fund_eoa(), value=1,