From e7a3dcff83a6fe2933ae47051197a276ccb95c6d Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:52:43 +0000 Subject: [PATCH 1/7] test(monad): cover a transaction value above the sender's balance The short cases fail until the spec checks only the gas fees at inclusion. Co-Authored-By: Claude --- .../reserve_balance/test_transfers.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/monad_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 56f7b1af1d3..953170a6ab3 100644 --- a/tests/monad_eight/reserve_balance/test_transfers.py +++ b/tests/monad_eight/reserve_balance/test_transfers.py @@ -188,6 +188,18 @@ def target_address( True, id="value_equal_to_balance_minus_gas", ), + pytest.param( + Spec.RESERVE_BALANCE - TX_FEE + 1, + Spec.RESERVE_BALANCE, + True, + id="value_above_balance_minus_gas", + ), + pytest.param( + 2 * Spec.RESERVE_BALANCE, + Spec.RESERVE_BALANCE, + True, + id="value_above_balance", + ), pytest.param( Spec.RESERVE_BALANCE, Spec.RESERVE_BALANCE + TX_FEE, @@ -309,7 +321,9 @@ def test_delegated_eoa_send_value( authorization_list=authorization_list or None, ) any_delegation = pre_delegated or delegate or undelegate - reverted = violation and any_delegation + # A value above the balance left after the gas fee fails the call for + # any sender. + reverted = (violation and any_delegation) or value > balance - TX_FEE storage = {} if reverted else {slot_code_worked: value_code_worked} balance = balance - (value if not reverted else 0) - TX_FEE @@ -319,7 +333,11 @@ def test_delegated_eoa_send_value( contract_address: Account( storage=storage, balance=value if not reverted else 0 ), - sender: Account(balance=balance), + sender: Account( + # Each authorization bumps the nonce even when the call fails. + nonce=sender.nonce + len(authorization_list), + balance=balance, + ), }, blocks=[Block(txs=[tx_1])], ) @@ -1320,6 +1338,12 @@ def test_access_lists( pytest.param( 1, Spec.RESERVE_BALANCE + 1, False, id="non_zero_value_good" ), + pytest.param( + 2 * Spec.RESERVE_BALANCE, + Spec.RESERVE_BALANCE, + True, + id="value_above_balance", + ), ], ) @pytest.mark.parametrize("pre_delegated", [True, False]) @@ -1387,8 +1411,11 @@ def test_creation_tx( ), ) - reverted = (violation and pre_delegated) or ( - fork == MONAD_EIGHT and selfdestruct and new_address_pre_funded + reverted = ( + (violation and pre_delegated) + # A value above the balance fails the creation for any sender. + or value > balance + or (fork == MONAD_EIGHT and selfdestruct and new_address_pre_funded) ) # A SELFDESTRUCT to the destructing account itself moves nothing, so @@ -1413,6 +1440,8 @@ def test_creation_tx( blockchain_test( pre=pre, post={ + # The creation consumes the nonce even when it fails. + sender: Account(nonce=sender.nonce), new_address: Account( code=deploy_code, balance=value + pre_fund_value, From 81680e50978384f85ec0ecfbc7b0035b4367dbd1 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:17:20 +0000 Subject: [PATCH 2/7] fix(forks): include a transaction whose value exceeds the sender's balance The client checks only the gas fees at inclusion and fails the top-level frame on the value. Co-Authored-By: Claude --- src/ethereum/forks/monad_eight/fork.py | 6 ++++-- src/ethereum/forks/monad_eight/vm/exceptions.py | 13 +++++++++++++ src/ethereum/forks/monad_eight/vm/interpreter.py | 7 +++++++ src/ethereum/forks/monad_eleven/fork.py | 6 ++++-- src/ethereum/forks/monad_eleven/vm/exceptions.py | 13 +++++++++++++ src/ethereum/forks/monad_eleven/vm/interpreter.py | 7 +++++++ src/ethereum/forks/monad_nine/fork.py | 6 ++++-- src/ethereum/forks/monad_nine/vm/exceptions.py | 13 +++++++++++++ src/ethereum/forks/monad_nine/vm/interpreter.py | 7 +++++++ src/ethereum/forks/monad_ten/fork.py | 6 ++++-- src/ethereum/forks/monad_ten/vm/exceptions.py | 13 +++++++++++++ src/ethereum/forks/monad_ten/vm/interpreter.py | 7 +++++++ 12 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/ethereum/forks/monad_eight/fork.py b/src/ethereum/forks/monad_eight/fork.py index 210954c1b80..fb9b9284255 100644 --- a/src/ethereum/forks/monad_eight/fork.py +++ b/src/ethereum/forks/monad_eight/fork.py @@ -434,7 +434,7 @@ def check_transaction( NonceMismatchError : If the nonce of the transaction is not equal to the sender's nonce. InsufficientBalanceError : - If the sender's balance is not enough to pay for the transaction. + If the sender's balance is not enough to pay for the gas fees. InvalidSenderError : If the transaction is from an address that does not exist anymore. PriorityFeeGreaterThanMaxFeeError : @@ -536,7 +536,9 @@ def check_transaction( raise NonceMismatchError("nonce too low") elif sender_account.nonce < Uint(tx.nonce): raise NonceMismatchError("nonce too high") - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + # Monad: the value is not part of the inclusion check. A value the + # sender cannot cover fails the top-level frame at execution time. + if Uint(sender_account.balance) < max_gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( diff --git a/src/ethereum/forks/monad_eight/vm/exceptions.py b/src/ethereum/forks/monad_eight/vm/exceptions.py index 7c1473772cd..643f981a5ba 100644 --- a/src/ethereum/forks/monad_eight/vm/exceptions.py +++ b/src/ethereum/forks/monad_eight/vm/exceptions.py @@ -42,6 +42,19 @@ class RevertOnReserveBalance(EthereumException): pass +class InsufficientBalanceForValue(EthereumException): + """ + Raised when the caller of the top-level frame cannot cover the + value it sends. + + Monad includes a transaction once the sender's balance covers its + gas fees, so the value is checked on entering the frame. The frame + fails without consuming gas. + """ + + pass + + class StackUnderflowError(ExceptionalHalt): """ Occurs when a pop is executed on an empty stack. diff --git a/src/ethereum/forks/monad_eight/vm/interpreter.py b/src/ethereum/forks/monad_eight/vm/interpreter.py index 7d4f568f3a2..b22db30dbc6 100644 --- a/src/ethereum/forks/monad_eight/vm/interpreter.py +++ b/src/ethereum/forks/monad_eight/vm/interpreter.py @@ -59,6 +59,7 @@ from .exceptions import ( AddressCollision, ExceptionalHalt, + InsufficientBalanceForValue, InvalidContractPrefix, InvalidOpcode, OutOfGasError, @@ -277,6 +278,12 @@ def process_message(message: Message) -> Evm: snapshot = copy_tx_state(tx_state) if message.should_transfer_value and message.value != 0: + # Only the top-level frame can be short here: the call opcodes + # check the caller's balance before entering a frame, and the + # transaction is includable once its gas fees are covered. + if get_account(tx_state, message.caller).balance < message.value: + evm.error = InsufficientBalanceForValue() + return evm move_ether( tx_state, message.caller, diff --git a/src/ethereum/forks/monad_eleven/fork.py b/src/ethereum/forks/monad_eleven/fork.py index f82cf4f1c30..2a091c6507b 100644 --- a/src/ethereum/forks/monad_eleven/fork.py +++ b/src/ethereum/forks/monad_eleven/fork.py @@ -447,7 +447,7 @@ def check_transaction( NonceMismatchError : If the nonce of the transaction is not equal to the sender's nonce. InsufficientBalanceError : - If the sender's balance is not enough to pay for the transaction. + If the sender's balance is not enough to pay for the gas fees. InvalidSenderError : If the transaction is from an address that does not exist anymore. PriorityFeeGreaterThanMaxFeeError : @@ -556,7 +556,9 @@ def check_transaction( raise NonceMismatchError("nonce too low") elif sender_account.nonce < Uint(tx.nonce): raise NonceMismatchError("nonce too high") - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + # Monad: the value is not part of the inclusion check. A value the + # sender cannot cover fails the top-level frame at execution time. + if Uint(sender_account.balance) < max_gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( diff --git a/src/ethereum/forks/monad_eleven/vm/exceptions.py b/src/ethereum/forks/monad_eleven/vm/exceptions.py index 7c1473772cd..643f981a5ba 100644 --- a/src/ethereum/forks/monad_eleven/vm/exceptions.py +++ b/src/ethereum/forks/monad_eleven/vm/exceptions.py @@ -42,6 +42,19 @@ class RevertOnReserveBalance(EthereumException): pass +class InsufficientBalanceForValue(EthereumException): + """ + Raised when the caller of the top-level frame cannot cover the + value it sends. + + Monad includes a transaction once the sender's balance covers its + gas fees, so the value is checked on entering the frame. The frame + fails without consuming gas. + """ + + pass + + class StackUnderflowError(ExceptionalHalt): """ Occurs when a pop is executed on an empty stack. diff --git a/src/ethereum/forks/monad_eleven/vm/interpreter.py b/src/ethereum/forks/monad_eleven/vm/interpreter.py index 831d6a71612..e7ef450eb09 100644 --- a/src/ethereum/forks/monad_eleven/vm/interpreter.py +++ b/src/ethereum/forks/monad_eleven/vm/interpreter.py @@ -59,6 +59,7 @@ from .exceptions import ( AddressCollision, ExceptionalHalt, + InsufficientBalanceForValue, InvalidContractPrefix, InvalidOpcode, OutOfGasError, @@ -404,6 +405,12 @@ def process_message(message: Message) -> Evm: message.tx_env.tx_snapshot = snapshot if message.should_transfer_value and message.value != 0: + # Only the top-level frame can be short here: the call opcodes + # check the caller's balance before entering a frame, and the + # transaction is includable once its gas fees are covered. + if get_account(tx_state, message.caller).balance < message.value: + evm.error = InsufficientBalanceForValue() + return evm move_ether( tx_state, message.caller, diff --git a/src/ethereum/forks/monad_nine/fork.py b/src/ethereum/forks/monad_nine/fork.py index ebf97201ec7..4478f03f4fc 100644 --- a/src/ethereum/forks/monad_nine/fork.py +++ b/src/ethereum/forks/monad_nine/fork.py @@ -444,7 +444,7 @@ def check_transaction( NonceMismatchError : If the nonce of the transaction is not equal to the sender's nonce. InsufficientBalanceError : - If the sender's balance is not enough to pay for the transaction. + If the sender's balance is not enough to pay for the gas fees. InvalidSenderError : If the transaction is from an address that does not exist anymore. PriorityFeeGreaterThanMaxFeeError : @@ -553,7 +553,9 @@ def check_transaction( raise NonceMismatchError("nonce too low") elif sender_account.nonce < Uint(tx.nonce): raise NonceMismatchError("nonce too high") - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + # Monad: the value is not part of the inclusion check. A value the + # sender cannot cover fails the top-level frame at execution time. + if Uint(sender_account.balance) < max_gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( diff --git a/src/ethereum/forks/monad_nine/vm/exceptions.py b/src/ethereum/forks/monad_nine/vm/exceptions.py index 7c1473772cd..643f981a5ba 100644 --- a/src/ethereum/forks/monad_nine/vm/exceptions.py +++ b/src/ethereum/forks/monad_nine/vm/exceptions.py @@ -42,6 +42,19 @@ class RevertOnReserveBalance(EthereumException): pass +class InsufficientBalanceForValue(EthereumException): + """ + Raised when the caller of the top-level frame cannot cover the + value it sends. + + Monad includes a transaction once the sender's balance covers its + gas fees, so the value is checked on entering the frame. The frame + fails without consuming gas. + """ + + pass + + class StackUnderflowError(ExceptionalHalt): """ Occurs when a pop is executed on an empty stack. diff --git a/src/ethereum/forks/monad_nine/vm/interpreter.py b/src/ethereum/forks/monad_nine/vm/interpreter.py index 25d15898340..fdcb2e91534 100644 --- a/src/ethereum/forks/monad_nine/vm/interpreter.py +++ b/src/ethereum/forks/monad_nine/vm/interpreter.py @@ -59,6 +59,7 @@ from .exceptions import ( AddressCollision, ExceptionalHalt, + InsufficientBalanceForValue, InvalidContractPrefix, InvalidOpcode, OutOfGasError, @@ -382,6 +383,12 @@ def process_message(message: Message) -> Evm: message.tx_env.tx_snapshot = snapshot if message.should_transfer_value and message.value != 0: + # Only the top-level frame can be short here: the call opcodes + # check the caller's balance before entering a frame, and the + # transaction is includable once its gas fees are covered. + if get_account(tx_state, message.caller).balance < message.value: + evm.error = InsufficientBalanceForValue() + return evm move_ether( tx_state, message.caller, diff --git a/src/ethereum/forks/monad_ten/fork.py b/src/ethereum/forks/monad_ten/fork.py index 1a9c251a54e..e5be947364d 100644 --- a/src/ethereum/forks/monad_ten/fork.py +++ b/src/ethereum/forks/monad_ten/fork.py @@ -444,7 +444,7 @@ def check_transaction( NonceMismatchError : If the nonce of the transaction is not equal to the sender's nonce. InsufficientBalanceError : - If the sender's balance is not enough to pay for the transaction. + If the sender's balance is not enough to pay for the gas fees. InvalidSenderError : If the transaction is from an address that does not exist anymore. PriorityFeeGreaterThanMaxFeeError : @@ -553,7 +553,9 @@ def check_transaction( raise NonceMismatchError("nonce too low") elif sender_account.nonce < Uint(tx.nonce): raise NonceMismatchError("nonce too high") - if Uint(sender_account.balance) < max_gas_fee + Uint(tx.value): + # Monad: the value is not part of the inclusion check. A value the + # sender cannot cover fails the top-level frame at execution time. + if Uint(sender_account.balance) < max_gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( diff --git a/src/ethereum/forks/monad_ten/vm/exceptions.py b/src/ethereum/forks/monad_ten/vm/exceptions.py index 7c1473772cd..643f981a5ba 100644 --- a/src/ethereum/forks/monad_ten/vm/exceptions.py +++ b/src/ethereum/forks/monad_ten/vm/exceptions.py @@ -42,6 +42,19 @@ class RevertOnReserveBalance(EthereumException): pass +class InsufficientBalanceForValue(EthereumException): + """ + Raised when the caller of the top-level frame cannot cover the + value it sends. + + Monad includes a transaction once the sender's balance covers its + gas fees, so the value is checked on entering the frame. The frame + fails without consuming gas. + """ + + pass + + class StackUnderflowError(ExceptionalHalt): """ Occurs when a pop is executed on an empty stack. diff --git a/src/ethereum/forks/monad_ten/vm/interpreter.py b/src/ethereum/forks/monad_ten/vm/interpreter.py index 4b989373b55..04dc30148da 100644 --- a/src/ethereum/forks/monad_ten/vm/interpreter.py +++ b/src/ethereum/forks/monad_ten/vm/interpreter.py @@ -59,6 +59,7 @@ from .exceptions import ( AddressCollision, ExceptionalHalt, + InsufficientBalanceForValue, InvalidContractPrefix, InvalidOpcode, OutOfGasError, @@ -404,6 +405,12 @@ def process_message(message: Message) -> Evm: message.tx_env.tx_snapshot = snapshot if message.should_transfer_value and message.value != 0: + # Only the top-level frame can be short here: the call opcodes + # check the caller's balance before entering a frame, and the + # transaction is includable once its gas fees are covered. + if get_account(tx_state, message.caller).balance < message.value: + evm.error = InsufficientBalanceForValue() + return evm move_ether( tx_state, message.caller, From dfc7dd2ec9f55d33a4013d8d1dd6bdcd6491de9d Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:53:20 +0000 Subject: [PATCH 3/7] test(monad): cover a max fee per gas above the sender's balance The fee-cap cases fail until the inclusion check uses the effective gas price. Co-Authored-By: Claude --- .../test_gas_fees_vs_reserve.py | 222 ++++++++++++++++-- 1 file changed, 208 insertions(+), 14 deletions(-) diff --git a/tests/monad_eight/reserve_balance/test_gas_fees_vs_reserve.py b/tests/monad_eight/reserve_balance/test_gas_fees_vs_reserve.py index 183d5b77d9d..545cb179ff2 100644 --- a/tests/monad_eight/reserve_balance/test_gas_fees_vs_reserve.py +++ b/tests/monad_eight/reserve_balance/test_gas_fees_vs_reserve.py @@ -1,19 +1,25 @@ """ -Tests reserve balance when a transaction's gas fees reach the reserve. +Tests reserve balance when a transaction's gas fees reach the reserve or +its fee cap exceeds its balance. """ from enum import Enum, auto, unique +from typing import Any import pytest from execution_testing import ( Account, + Address, Alloc, Block, BlockchainTestFiller, + Header, Op, Transaction, + TransactionException, ) from execution_testing.forks.helpers import Fork +from execution_testing.test_types.block_types import DEFAULT_BASE_FEE from .spec import Spec, ref_spec_7702 @@ -23,6 +29,10 @@ slot_code_worked = 0x1 value_code_worked = 0x1234 +GAS_LIMIT = 500_000 +GAS_PRICE = 10 +TX_FEE = GAS_PRICE * GAS_LIMIT + pytestmark = [ pytest.mark.valid_from("MONAD_EIGHT"), pytest.mark.pre_alloc_group( @@ -32,6 +42,27 @@ ] +def priced_transaction( + tx_type: int, gas_price: int, **fields: Any +) -> Transaction: + """ + Build a transaction of `tx_type` charged at `gas_price`. + + A type-2 transaction caps its fee at twice that price, with the tip + chosen so that the effective price stays at `gas_price`. + """ + if tx_type == 0: + return Transaction(ty=0, gas_price=gas_price, **fields) + if tx_type == 2: + return Transaction( + ty=2, + max_fee_per_gas=2 * gas_price, + max_priority_fee_per_gas=gas_price - DEFAULT_BASE_FEE, + **fields, + ) + raise ValueError(f"unsupported transaction type {tx_type}") + + @unique class GasFees(Enum): """Gas fees of a max-gas transaction, relative to the reserve.""" @@ -65,46 +96,209 @@ def gas_price(self, gas_limit: int) -> int: return 2 * boundary -@pytest.mark.parametrize("gas_fees", list(GasFees)) +@unique +class Value(Enum): + """The value sent, relative to the fee held above the reserve.""" + + ZERO = auto() + EQUAL_TO_FEE = auto() + ABOVE_FEE = auto() + + def __str__(self) -> str: + """Return string representation.""" + return self.name.lower() + + def compute(self, fee: int) -> int: + """Compute the value for a transaction charged `fee`.""" + match self: + case Value.ZERO: + return 0 + case Value.EQUAL_TO_FEE: + return fee + case Value.ABOVE_FEE: + return fee + 1 + + +@pytest.mark.parametrize("tx_type", [0, 2]) +@pytest.mark.parametrize("value", list(Value)) +@pytest.mark.parametrize( + ["gas_fees", "pre_delegated"], + [ + pytest.param(GasFees.HALF_RESERVE, False, id="half_reserve"), + pytest.param(GasFees.HALF_RESERVE, True, id="half_reserve_delegated"), + pytest.param( + GasFees.JUST_BELOW_RESERVE, False, id="just_below_reserve" + ), + pytest.param( + GasFees.JUST_BELOW_RESERVE, + True, + id="just_below_reserve_delegated", + ), + # Consensus only sequences a delegated sender whose gas fees fit + # within the reserve, so the larger fees run undelegated only. + pytest.param( + GasFees.JUST_ABOVE_RESERVE, False, id="just_above_reserve" + ), + pytest.param(GasFees.DOUBLE_RESERVE, False, id="double_reserve"), + ], +) def test_gas_fees_never_violate_reserve( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_fees: GasFees, + pre_delegated: bool, + value: Value, + tx_type: int, ) -> None: """ Test that a transaction is allowed through however large its gas fees - are, including at and above the whole reserve. + are, including at and above the whole reserve, and that only the value + it sends counts against the reserve. - The sender's violation threshold is `max(reserve - gas_fees, 0)`: gas - spend does not count against the reserve, only value spend does. The - sender is funded to sit exactly at the reserve once the gas is - debited, and sends no value, so the gas term alone decides. + The sender is funded to sit exactly at the reserve once the gas is + debited, so the fee is also the value it may send without dipping + into the reserve. """ gas_limit = fork.transaction_gas_limit_cap() assert gas_limit is not None, "fork must cap the transaction gas limit" gas_price = gas_fees.gas_price(gas_limit) + fee = gas_price * gas_limit + balance = Spec.RESERVE_BALANCE + fee + amount = value.compute(fee) contract_address = pre.deploy_contract( Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP ) - sender = pre.fund_eoa(Spec.RESERVE_BALANCE + gas_price * gas_limit) + sender = pre.fund_eoa( + balance, delegation=Address(0x0111) if pre_delegated else None + ) - tx = Transaction( - ty=0, + tx = priced_transaction( + tx_type, + gas_price, to=contract_address, gas_limit=gas_limit, - gas_price=gas_price, + value=amount, sender=sender, ) + # The value fails once the balance left after the fee cannot cover it, + # and dips into the reserve once it exceeds what the sender held above + # the reserve. + insufficient = amount > balance - fee + violation = amount > balance - min(Spec.RESERVE_BALANCE, balance) + reverted = insufficient or (violation and pre_delegated) + storage = {} if reverted else {slot_code_worked: value_code_worked} + blockchain_test( pre=pre, post={ contract_address: Account( - storage={slot_code_worked: value_code_worked} + storage=storage, balance=0 if reverted else amount + ), + sender: Account( + nonce=sender.nonce, + balance=balance - fee - (0 if reverted else amount), + ), + }, + blocks=[ + Block( + txs=[tx], + header_verify=Header(base_fee_per_gas=DEFAULT_BASE_FEE), + ) + ], + ) + + +@pytest.mark.parametrize( + ["value", "balance", "violation"], + # The sender holds less than the fee cap of `TX_FEE * 2` in every case. + [ + pytest.param( + 0, + TX_FEE - 1, + False, + id="zero_value_below_fee", + marks=pytest.mark.exception_test, + ), + pytest.param(0, TX_FEE, False, id="zero_value_spend_all_on_gas"), + pytest.param(1, TX_FEE + 1, True, id="non_zero_value"), + pytest.param( + TX_FEE - 1, + 2 * TX_FEE - 1, + True, + id="value_equal_to_balance_minus_gas", + ), + pytest.param( + TX_FEE, + 2 * TX_FEE - 1, + True, + id="value_above_balance_minus_gas", + ), + ], +) +@pytest.mark.parametrize("pre_delegated", [True, False]) +def test_max_fee_above_balance( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + value: int, + balance: int, + violation: bool, + pre_delegated: bool, +) -> None: + """ + Test that a transaction is included when the sender's balance covers + its gas fees at the effective gas price but not at the max fee per gas, + and that its value is then judged against the reserve as usual. + """ + sender = pre.fund_eoa( + balance, delegation=Address(0x0111) if pre_delegated else None + ) + sender_nonce = sender.nonce + contract_address = pre.deploy_contract( + Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP + ) + + rejected = balance < TX_FEE + exception = ( + TransactionException.INSUFFICIENT_ACCOUNT_FUNDS if rejected else None + ) + tx = priced_transaction( + 2, + GAS_PRICE, + to=contract_address, + gas_limit=GAS_LIMIT, + value=value, + sender=sender, + error=exception, + ) + + insufficient = value > balance - TX_FEE + reverted = insufficient or (violation and pre_delegated) + executed = not rejected and not reverted + storage = {slot_code_worked: value_code_worked} if executed else {} + + blockchain_test( + pre=pre, + post={ + contract_address: Account( + storage=storage, balance=value if executed else 0 + ), + sender: Account( + nonce=sender_nonce if rejected else sender_nonce + 1, + balance=balance + if rejected + else balance - TX_FEE - (value if executed else 0), ), - sender: Account(nonce=1), }, - blocks=[Block(txs=[tx])], + blocks=[ + Block( + txs=[tx], + exception=exception, + header_verify=None + if rejected + else Header(base_fee_per_gas=DEFAULT_BASE_FEE), + ) + ], ) From ec853fe07c5eb1a525c99fbad76888b2ca17ea87 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:54:19 +0000 Subject: [PATCH 4/7] fix(forks): price the inclusion check at the effective gas price The client only requires the balance to cover the gas fees it charges, not the max fee per gas. Co-Authored-By: Claude --- src/ethereum/forks/monad_eight/fork.py | 12 ++++++------ src/ethereum/forks/monad_eleven/fork.py | 12 ++++++------ src/ethereum/forks/monad_nine/fork.py | 12 ++++++------ src/ethereum/forks/monad_ten/fork.py | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/ethereum/forks/monad_eight/fork.py b/src/ethereum/forks/monad_eight/fork.py index fb9b9284255..6d54ff521e4 100644 --- a/src/ethereum/forks/monad_eight/fork.py +++ b/src/ethereum/forks/monad_eight/fork.py @@ -495,12 +495,14 @@ def check_transaction( tx.max_fee_per_gas - block_env.base_fee_per_gas, ) effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas - max_gas_fee = tx.gas * tx.max_fee_per_gas else: if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price - max_gas_fee = tx.gas * tx.gas_price + + # Monad: the sender covers the gas fees the transaction is charged, at + # the effective gas price, and not its maximum fee. + gas_fee = tx.gas * effective_gas_price if isinstance(tx, BlobTransaction): if len(tx.blob_versioned_hashes) == 0: @@ -517,9 +519,7 @@ def check_transaction( "insufficient max fee per blob gas" ) - max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( - tx.max_fee_per_blob_gas - ) + gas_fee += calculate_data_fee(block_env.excess_blob_gas, tx) blob_versioned_hashes = tx.blob_versioned_hashes else: blob_versioned_hashes = () @@ -538,7 +538,7 @@ def check_transaction( raise NonceMismatchError("nonce too high") # Monad: the value is not part of the inclusion check. A value the # sender cannot cover fails the top-level frame at execution time. - if Uint(sender_account.balance) < max_gas_fee: + if Uint(sender_account.balance) < gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( diff --git a/src/ethereum/forks/monad_eleven/fork.py b/src/ethereum/forks/monad_eleven/fork.py index 2a091c6507b..7f1df22992d 100644 --- a/src/ethereum/forks/monad_eleven/fork.py +++ b/src/ethereum/forks/monad_eleven/fork.py @@ -510,12 +510,14 @@ def check_transaction( tx.max_fee_per_gas - block_env.base_fee_per_gas, ) effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas - max_gas_fee = tx.gas * tx.max_fee_per_gas else: if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price - max_gas_fee = tx.gas * tx.gas_price + + # Monad: the sender covers the gas fees the transaction is charged, at + # the effective gas price, and not its maximum fee. + gas_fee = tx.gas * effective_gas_price if isinstance(tx, BlobTransaction): blob_count = len(tx.blob_versioned_hashes) @@ -537,9 +539,7 @@ def check_transaction( "insufficient max fee per blob gas" ) - max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( - tx.max_fee_per_blob_gas - ) + gas_fee += calculate_data_fee(block_env.excess_blob_gas, tx) blob_versioned_hashes = tx.blob_versioned_hashes else: blob_versioned_hashes = () @@ -558,7 +558,7 @@ def check_transaction( raise NonceMismatchError("nonce too high") # Monad: the value is not part of the inclusion check. A value the # sender cannot cover fails the top-level frame at execution time. - if Uint(sender_account.balance) < max_gas_fee: + if Uint(sender_account.balance) < gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( diff --git a/src/ethereum/forks/monad_nine/fork.py b/src/ethereum/forks/monad_nine/fork.py index 4478f03f4fc..40b9e3e2cb7 100644 --- a/src/ethereum/forks/monad_nine/fork.py +++ b/src/ethereum/forks/monad_nine/fork.py @@ -507,12 +507,14 @@ def check_transaction( tx.max_fee_per_gas - block_env.base_fee_per_gas, ) effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas - max_gas_fee = tx.gas * tx.max_fee_per_gas else: if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price - max_gas_fee = tx.gas * tx.gas_price + + # Monad: the sender covers the gas fees the transaction is charged, at + # the effective gas price, and not its maximum fee. + gas_fee = tx.gas * effective_gas_price if isinstance(tx, BlobTransaction): blob_count = len(tx.blob_versioned_hashes) @@ -534,9 +536,7 @@ def check_transaction( "insufficient max fee per blob gas" ) - max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( - tx.max_fee_per_blob_gas - ) + gas_fee += calculate_data_fee(block_env.excess_blob_gas, tx) blob_versioned_hashes = tx.blob_versioned_hashes else: blob_versioned_hashes = () @@ -555,7 +555,7 @@ def check_transaction( raise NonceMismatchError("nonce too high") # Monad: the value is not part of the inclusion check. A value the # sender cannot cover fails the top-level frame at execution time. - if Uint(sender_account.balance) < max_gas_fee: + if Uint(sender_account.balance) < gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( diff --git a/src/ethereum/forks/monad_ten/fork.py b/src/ethereum/forks/monad_ten/fork.py index e5be947364d..6a082060df1 100644 --- a/src/ethereum/forks/monad_ten/fork.py +++ b/src/ethereum/forks/monad_ten/fork.py @@ -507,12 +507,14 @@ def check_transaction( tx.max_fee_per_gas - block_env.base_fee_per_gas, ) effective_gas_price = priority_fee_per_gas + block_env.base_fee_per_gas - max_gas_fee = tx.gas * tx.max_fee_per_gas else: if tx.gas_price < block_env.base_fee_per_gas: raise InvalidBlock effective_gas_price = tx.gas_price - max_gas_fee = tx.gas * tx.gas_price + + # Monad: the sender covers the gas fees the transaction is charged, at + # the effective gas price, and not its maximum fee. + gas_fee = tx.gas * effective_gas_price if isinstance(tx, BlobTransaction): blob_count = len(tx.blob_versioned_hashes) @@ -534,9 +536,7 @@ def check_transaction( "insufficient max fee per blob gas" ) - max_gas_fee += Uint(calculate_total_blob_gas(tx)) * Uint( - tx.max_fee_per_blob_gas - ) + gas_fee += calculate_data_fee(block_env.excess_blob_gas, tx) blob_versioned_hashes = tx.blob_versioned_hashes else: blob_versioned_hashes = () @@ -555,7 +555,7 @@ def check_transaction( raise NonceMismatchError("nonce too high") # Monad: the value is not part of the inclusion check. A value the # sender cannot cover fails the top-level frame at execution time. - if Uint(sender_account.balance) < max_gas_fee: + if Uint(sender_account.balance) < gas_fee: raise InsufficientBalanceError("insufficient sender balance") sender_code = get_code(tx_state, sender_account.code_hash) if sender_account.code_hash != EMPTY_CODE_HASH and not is_valid_delegation( From 8ee2b607cc06e3d8d5f290553533214a674b7598 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:13:01 +0000 Subject: [PATCH 5/7] test(monad): expect a failed receipt from a create in delegated code The nonce and missing-contract checks also pass when the create is skipped instead of halting the frame. Co-Authored-By: Claude --- tests/monad_eight/delegated_creates/test_create_ban.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/monad_eight/delegated_creates/test_create_ban.py b/tests/monad_eight/delegated_creates/test_create_ban.py index 097c02596bc..b5d3af0738e 100644 --- a/tests/monad_eight/delegated_creates/test_create_ban.py +++ b/tests/monad_eight/delegated_creates/test_create_ban.py @@ -15,6 +15,7 @@ StateTestFiller, Storage, Transaction, + TransactionReceipt, compute_create_address, ) @@ -39,7 +40,12 @@ def test_create_in_delegated_code( delegate = pre.deploy_contract(factory_code(create_opcode)) delegated = pre.fund_eoa(delegation=delegate) - tx = Transaction(to=delegated, data=initcode, sender=pre.fund_eoa()) + tx = Transaction( + to=delegated, + data=initcode, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt(status=0), + ) state_test( pre=pre, From 41c5d29118a050f41e32fa04dafd1d4573319651 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:33:45 +0000 Subject: [PATCH 6/7] feat(tests): cover inert authorizations in the reserve balance window The in-window cases fail until the spec records every recovered authority. Co-Authored-By: Claude --- tests/monad_eight/reserve_balance/helpers.py | 43 +++++++- tests/monad_eight/reserve_balance/spec.py | 1 + .../reserve_balance/test_multi_block.py | 100 +++++++++++++++++- 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/tests/monad_eight/reserve_balance/helpers.py b/tests/monad_eight/reserve_balance/helpers.py index 4e3b716267b..40a3a77f874 100644 --- a/tests/monad_eight/reserve_balance/helpers.py +++ b/tests/monad_eight/reserve_balance/helpers.py @@ -5,9 +5,11 @@ from enum import Enum, auto, unique from typing import List -from execution_testing import Op +from execution_testing import EOA, Address, AuthorizationTuple, Op from execution_testing.forks.helpers import Fork +from tests.prague.eip7702_set_code_tx.spec import Spec as Spec7702 + from .spec import Spec @@ -83,3 +85,42 @@ def compute_balance(self, previous_balances: List[int]) -> int: return max_val case StageBalance.ABOVE_MAX: return max_val + 1 + + +@unique +class InertAuthorization(Enum): + """ + Ways an authorization recovers its signer yet is never applied. + """ + + WRONG_CHAIN_ID = auto() + NONCE_OVERFLOW = auto() + NONCE_MISMATCH = auto() + + def __str__(self) -> str: + """Return string representation.""" + return self.name.lower() + + def authorization( + self, signer: EOA, valid_nonce: int, chain_id: int + ) -> AuthorizationTuple: + """ + Sign a delegation for `signer` that `valid_nonce` and `chain_id` + would make valid, broken in this one way. + """ + nonce = valid_nonce + match self: + case InertAuthorization.WRONG_CHAIN_ID: + chain_id += 1 + case InertAuthorization.NONCE_OVERFLOW: + nonce = Spec7702.MAX_NONCE + case InertAuthorization.NONCE_MISMATCH: + nonce += 1 + return AuthorizationTuple( + chain_id=chain_id, + address=Address(0x1111), + nonce=nonce, + signer=signer, + writes_delegation=False, + first_write=False, + ) diff --git a/tests/monad_eight/reserve_balance/spec.py b/tests/monad_eight/reserve_balance/spec.py index ef315903a44..be4d258bd1a 100644 --- a/tests/monad_eight/reserve_balance/spec.py +++ b/tests/monad_eight/reserve_balance/spec.py @@ -26,3 +26,4 @@ class Spec: """ RESERVE_BALANCE = 10 * 10**18 + RESERVE_BALANCE_DELAY_BLOCKS = 3 diff --git a/tests/monad_eight/reserve_balance/test_multi_block.py b/tests/monad_eight/reserve_balance/test_multi_block.py index bf749b1a682..ea6093cee12 100644 --- a/tests/monad_eight/reserve_balance/test_multi_block.py +++ b/tests/monad_eight/reserve_balance/test_multi_block.py @@ -12,13 +12,14 @@ AuthorizationTuple, Block, BlockchainTestFiller, + ChainConfig, Op, Transaction, ) from execution_testing.exceptions.exceptions import TransactionException from execution_testing.forks.helpers import Fork -from .helpers import generous_gas +from .helpers import InertAuthorization, generous_gas from .spec import Spec, ref_spec_7702 REFERENCE_SPEC_GIT_PATH = ref_spec_7702.git_path @@ -171,6 +172,103 @@ def test_exception_rule( ) +@pytest.mark.parametrize("inert_authorization", InertAuthorization) +@pytest.mark.parametrize( + "auth_pos", [(0, 0), (0, 1), (1, 0), (2, 1), (3, 0), (3, 1), (3, 2)] +) +def test_exception_rule_inert_authorization( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + chain_config: ChainConfig, + inert_authorization: InertAuthorization, + auth_pos: Tuple[int, int], + fork: Fork, +) -> None: + """ + Test that signing an authorization denies the emptying exception for + the whole window even when the authorization is never applied, since + the clients record every authority a block's signatures recover. + Position (3, 2) puts the authorization on the test tx itself. + """ + nblocks = 4 + test_tx_pos = (nblocks - 1, 2) + value = 1 + balance = Spec.RESERVE_BALANCE + test_sender = pre.fund_eoa(balance) + + contract = Op.SSTORE(slot_code_worked, value_code_worked) + Op.STOP + contract_address = pre.deploy_contract(contract) + + # A self-sponsored authorization is checked against the nonce the + # transaction itself has already consumed. + valid_nonce = int(test_sender.nonce) + ( + 1 if auth_pos == test_tx_pos else 0 + ) + authorization = inert_authorization.authorization( + signer=test_sender, + valid_nonce=valid_nonce, + chain_id=chain_config.chain_id, + ) + + blocks = [] + for nblock in range(nblocks): + txs = [] + for ntx in range(2): + authorization_list = [] + pos = (nblock, ntx) + + if auth_pos == pos: + authorization_list += [authorization] + prepare_tx = Transaction( + max_fee_per_gas=GAS_PRICE, + max_priority_fee_per_gas=GAS_PRICE, + to=Address(0x7873), + sender=pre.fund_eoa(), + authorization_list=authorization_list or None, + ) + txs.append(prepare_tx) + # If this isn't the last block, we're done. + # If it is, we'll append the test tx below. + if nblock < nblocks - 1: + blocks.append(Block(txs=txs)) + del txs + + authorization_list = [] + if auth_pos == test_tx_pos: + authorization_list += [authorization] + test_tx = Transaction( + gas_limit=generous_gas(fork), + max_fee_per_gas=GAS_PRICE, + max_priority_fee_per_gas=GAS_PRICE, + to=contract_address, + value=value, + sender=test_sender, + authorization_list=authorization_list or None, + ) + txs.append(test_tx) + blocks.append(Block(txs=txs)) + + reverted = auth_pos[0] >= nblocks - Spec.RESERVE_BALANCE_DELAY_BLOCKS + gas_fee = GAS_PRICE * generous_gas(fork) + storage = {} if reverted else {slot_code_worked: value_code_worked} + + blockchain_test( + pre=pre, + post={ + contract_address: Account( + storage=storage, balance=0 if reverted else value + ), + # Only the transaction itself bumped the nonce: the inert + # authorization did not. + test_sender: Account( + nonce=1, + balance=balance - gas_fee - (0 if reverted else value), + ), + }, + blocks=blocks, + ) + + @pytest.mark.parametrize( ["value", "balance", "violation"], [ From 1b93e1d637d8d2669270433c4e186001b9f59975 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:35:57 +0000 Subject: [PATCH 7/7] fix(forks): record every recovered authority for the reserve balance window The clients mark an authority before any validity check, so a skipped one counts. Co-Authored-By: Claude --- .../forks/monad_eight/state_tracker.py | 7 ++++--- .../forks/monad_eight/vm/eoa_delegation.py | 18 +++++++++--------- .../forks/monad_eleven/state_tracker.py | 7 ++++--- .../forks/monad_eleven/vm/eoa_delegation.py | 18 +++++++++--------- src/ethereum/forks/monad_nine/state_tracker.py | 7 ++++--- .../forks/monad_nine/vm/eoa_delegation.py | 18 +++++++++--------- src/ethereum/forks/monad_ten/state_tracker.py | 7 ++++--- .../forks/monad_ten/vm/eoa_delegation.py | 18 +++++++++--------- 8 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/ethereum/forks/monad_eight/state_tracker.py b/src/ethereum/forks/monad_eight/state_tracker.py index 1a0905154a4..16e5a536df2 100644 --- a/src/ethereum/forks/monad_eight/state_tracker.py +++ b/src/ethereum/forks/monad_eight/state_tracker.py @@ -60,9 +60,10 @@ class BlockState: ) code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) storage_clears: Set[Address] = field(default_factory=set) - # Monad: per-block-number set of EOAs that sent or were authorized in - # that block. Carried across blocks via t8n; used by the reserve - # balance exception window (RESERVE_BALANCE_DELAY_BLOCKS lookback). + # Monad: per-block-number set of EOAs that sent a transaction or + # signed an authorization, applied or not, in that block. Carried + # across blocks via t8n; used by the reserve balance exception window + # (RESERVE_BALANCE_DELAY_BLOCKS lookback). _senders_authorities: Dict[Uint, Set[Address]] = field( default_factory=dict ) diff --git a/src/ethereum/forks/monad_eight/vm/eoa_delegation.py b/src/ethereum/forks/monad_eight/vm/eoa_delegation.py index cf3c6b678cf..05ae07cc1df 100644 --- a/src/ethereum/forks/monad_eight/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_eight/vm/eoa_delegation.py @@ -174,15 +174,19 @@ def set_delegation(message: Message) -> U256: tx_state = message.tx_env.state refund_counter = U256(0) for auth in message.tx_env.authorizations: - if auth.chain_id not in (message.block_env.chain_id, U256(0)): + try: + authority = recover_authority(auth) + except InvalidSignatureError: continue - if auth.nonce >= U64.MAX_VALUE: + add_sender_authority( + message.block_env.state, message.block_env.number, authority + ) + + if auth.chain_id not in (message.block_env.chain_id, U256(0)): continue - try: - authority = recover_authority(auth) - except InvalidSignatureError: + if auth.nonce >= U64.MAX_VALUE: continue message.accessed_addresses.add(authority) @@ -211,10 +215,6 @@ def set_delegation(message: Message) -> U256: increment_nonce(tx_state, authority) - add_sender_authority( - message.block_env.state, message.block_env.number, authority - ) - if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") diff --git a/src/ethereum/forks/monad_eleven/state_tracker.py b/src/ethereum/forks/monad_eleven/state_tracker.py index 05822329b3d..314492be206 100644 --- a/src/ethereum/forks/monad_eleven/state_tracker.py +++ b/src/ethereum/forks/monad_eleven/state_tracker.py @@ -60,9 +60,10 @@ class BlockState: ) code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) storage_clears: Set[Address] = field(default_factory=set) - # Monad: per-block-number set of EOAs that sent or were authorized in - # that block. Carried across blocks via t8n; used by the reserve - # balance exception window (RESERVE_BALANCE_DELAY_BLOCKS lookback). + # Monad: per-block-number set of EOAs that sent a transaction or + # signed an authorization, applied or not, in that block. Carried + # across blocks via t8n; used by the reserve balance exception window + # (RESERVE_BALANCE_DELAY_BLOCKS lookback). _senders_authorities: Dict[Uint, Set[Address]] = field( default_factory=dict ) diff --git a/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py b/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py index cf3c6b678cf..05ae07cc1df 100644 --- a/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py @@ -174,15 +174,19 @@ def set_delegation(message: Message) -> U256: tx_state = message.tx_env.state refund_counter = U256(0) for auth in message.tx_env.authorizations: - if auth.chain_id not in (message.block_env.chain_id, U256(0)): + try: + authority = recover_authority(auth) + except InvalidSignatureError: continue - if auth.nonce >= U64.MAX_VALUE: + add_sender_authority( + message.block_env.state, message.block_env.number, authority + ) + + if auth.chain_id not in (message.block_env.chain_id, U256(0)): continue - try: - authority = recover_authority(auth) - except InvalidSignatureError: + if auth.nonce >= U64.MAX_VALUE: continue message.accessed_addresses.add(authority) @@ -211,10 +215,6 @@ def set_delegation(message: Message) -> U256: increment_nonce(tx_state, authority) - add_sender_authority( - message.block_env.state, message.block_env.number, authority - ) - if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") diff --git a/src/ethereum/forks/monad_nine/state_tracker.py b/src/ethereum/forks/monad_nine/state_tracker.py index 182dfe1b9f7..0f3f86e8237 100644 --- a/src/ethereum/forks/monad_nine/state_tracker.py +++ b/src/ethereum/forks/monad_nine/state_tracker.py @@ -60,9 +60,10 @@ class BlockState: ) code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) storage_clears: Set[Address] = field(default_factory=set) - # Monad: per-block-number set of EOAs that sent or were authorized in - # that block. Carried across blocks via t8n; used by the reserve - # balance exception window (RESERVE_BALANCE_DELAY_BLOCKS lookback). + # Monad: per-block-number set of EOAs that sent a transaction or + # signed an authorization, applied or not, in that block. Carried + # across blocks via t8n; used by the reserve balance exception window + # (RESERVE_BALANCE_DELAY_BLOCKS lookback). _senders_authorities: Dict[Uint, Set[Address]] = field( default_factory=dict ) diff --git a/src/ethereum/forks/monad_nine/vm/eoa_delegation.py b/src/ethereum/forks/monad_nine/vm/eoa_delegation.py index cf3c6b678cf..05ae07cc1df 100644 --- a/src/ethereum/forks/monad_nine/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_nine/vm/eoa_delegation.py @@ -174,15 +174,19 @@ def set_delegation(message: Message) -> U256: tx_state = message.tx_env.state refund_counter = U256(0) for auth in message.tx_env.authorizations: - if auth.chain_id not in (message.block_env.chain_id, U256(0)): + try: + authority = recover_authority(auth) + except InvalidSignatureError: continue - if auth.nonce >= U64.MAX_VALUE: + add_sender_authority( + message.block_env.state, message.block_env.number, authority + ) + + if auth.chain_id not in (message.block_env.chain_id, U256(0)): continue - try: - authority = recover_authority(auth) - except InvalidSignatureError: + if auth.nonce >= U64.MAX_VALUE: continue message.accessed_addresses.add(authority) @@ -211,10 +215,6 @@ def set_delegation(message: Message) -> U256: increment_nonce(tx_state, authority) - add_sender_authority( - message.block_env.state, message.block_env.number, authority - ) - if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target") diff --git a/src/ethereum/forks/monad_ten/state_tracker.py b/src/ethereum/forks/monad_ten/state_tracker.py index 182dfe1b9f7..0f3f86e8237 100644 --- a/src/ethereum/forks/monad_ten/state_tracker.py +++ b/src/ethereum/forks/monad_ten/state_tracker.py @@ -60,9 +60,10 @@ class BlockState: ) code_writes: Dict[Hash32, Bytes] = field(default_factory=dict) storage_clears: Set[Address] = field(default_factory=set) - # Monad: per-block-number set of EOAs that sent or were authorized in - # that block. Carried across blocks via t8n; used by the reserve - # balance exception window (RESERVE_BALANCE_DELAY_BLOCKS lookback). + # Monad: per-block-number set of EOAs that sent a transaction or + # signed an authorization, applied or not, in that block. Carried + # across blocks via t8n; used by the reserve balance exception window + # (RESERVE_BALANCE_DELAY_BLOCKS lookback). _senders_authorities: Dict[Uint, Set[Address]] = field( default_factory=dict ) diff --git a/src/ethereum/forks/monad_ten/vm/eoa_delegation.py b/src/ethereum/forks/monad_ten/vm/eoa_delegation.py index cf3c6b678cf..05ae07cc1df 100644 --- a/src/ethereum/forks/monad_ten/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_ten/vm/eoa_delegation.py @@ -174,15 +174,19 @@ def set_delegation(message: Message) -> U256: tx_state = message.tx_env.state refund_counter = U256(0) for auth in message.tx_env.authorizations: - if auth.chain_id not in (message.block_env.chain_id, U256(0)): + try: + authority = recover_authority(auth) + except InvalidSignatureError: continue - if auth.nonce >= U64.MAX_VALUE: + add_sender_authority( + message.block_env.state, message.block_env.number, authority + ) + + if auth.chain_id not in (message.block_env.chain_id, U256(0)): continue - try: - authority = recover_authority(auth) - except InvalidSignatureError: + if auth.nonce >= U64.MAX_VALUE: continue message.accessed_addresses.add(authority) @@ -211,10 +215,6 @@ def set_delegation(message: Message) -> U256: increment_nonce(tx_state, authority) - add_sender_authority( - message.block_env.state, message.block_env.number, authority - ) - if message.code_address is None: raise InvalidBlock("Invalid type 4 transaction: no target")