From e2da957b67ea836d364ba4cf30b65484d3ec7189 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:12:01 +0000 Subject: [PATCH] fix(spec-specs, tests): clamp reserve balance threshold at zero Assert that a sender whose gas fees exceed the reserve can empty. Co-Authored-By: Claude --- .../forks/monad_eight/vm/interpreter.py | 25 ++-- .../forks/monad_next/vm/interpreter.py | 24 ++-- .../forks/monad_nine/vm/interpreter.py | 24 ++-- .../forks/monad_ten/vm/interpreter.py | 24 ++-- .../test_gas_fees_vs_reserve.py | 110 ++++++++++++++++++ 5 files changed, 179 insertions(+), 28 deletions(-) create mode 100644 tests/monad_eight/reserve_balance/test_gas_fees_vs_reserve.py diff --git a/src/ethereum/forks/monad_eight/vm/interpreter.py b/src/ethereum/forks/monad_eight/vm/interpreter.py index 99e75de1742..9d647318982 100644 --- a/src/ethereum/forks/monad_eight/vm/interpreter.py +++ b/src/ethereum/forks/monad_eight/vm/interpreter.py @@ -346,6 +346,13 @@ def process_message(message: Message) -> Evm: # aligns with Monad EVM implementation. if code == b"" or is_valid_delegation(code): original_balance = get_balance_original(snapshot, addr) + + is_exception = ( + message.tx_env.origin == addr + and not is_sender_authority(tx_state.parent, addr) + and not is_valid_delegation(code) + ) + if message.tx_env.origin == addr: # gas_fees already deducted, need to re-add if sender # to match with spec. @@ -355,16 +362,20 @@ def process_message(message: Message) -> Evm: ) original_balance += gas_fees reserve = min(RESERVE_BALANCE, original_balance) - threshold = reserve - gas_fees + assert is_exception or gas_fees <= reserve, ( + "gas fees exceed the reserve for a sender that " + "cannot empty; consensus only sequences a " + "transaction whose sender's in-flight gas fees " + "fit within the reserve" + ) + # Gas spend does not count against the reserve, + # so the gas already deducted from the balance is + # added back by lowering the threshold. Clamped + # at zero for U256. + threshold = reserve - min(reserve, gas_fees) else: threshold = RESERVE_BALANCE - is_exception = ( - message.tx_env.origin == addr - and not is_sender_authority(tx_state.parent, addr) - and not is_valid_delegation(code) - ) - if ( acc.balance < original_balance and acc.balance < threshold diff --git a/src/ethereum/forks/monad_next/vm/interpreter.py b/src/ethereum/forks/monad_next/vm/interpreter.py index 912ac3e42e8..13089bc8125 100644 --- a/src/ethereum/forks/monad_next/vm/interpreter.py +++ b/src/ethereum/forks/monad_next/vm/interpreter.py @@ -133,22 +133,32 @@ def is_reserve_balance_violated(evm: Evm) -> bool: # execution, but this aligns with Monad EVM implementation. if code == b"" or is_valid_delegation(code): original_balance = get_balance_original(snapshot, addr) + + is_exception = ( + message.tx_env.origin == addr + and not is_sender_authority(tx_state.parent, addr) + and not is_valid_delegation(code) + ) + if tx_env.origin == addr: # gas_fees already deducted, need to re-add if sender # to match with spec. gas_fees = U256(tx_env.gas_price * tx_env.tx_gas_limit) original_balance += gas_fees reserve = min(RESERVE_BALANCE, original_balance) - threshold = reserve - gas_fees + assert is_exception or gas_fees <= reserve, ( + "gas fees exceed the reserve for a sender that " + "cannot empty; consensus only sequences a " + "transaction whose sender's in-flight gas fees " + "fit within the reserve" + ) + # Gas spend does not count against the reserve, so the + # gas already deducted from the balance is added back by + # lowering the threshold. Clamped at zero for U256. + threshold = reserve - min(reserve, gas_fees) else: threshold = RESERVE_BALANCE - is_exception = ( - message.tx_env.origin == addr - and not is_sender_authority(tx_state.parent, addr) - and not is_valid_delegation(code) - ) - if ( acc.balance < original_balance and acc.balance < threshold diff --git a/src/ethereum/forks/monad_nine/vm/interpreter.py b/src/ethereum/forks/monad_nine/vm/interpreter.py index 9820e48d73e..a5545bf593f 100644 --- a/src/ethereum/forks/monad_nine/vm/interpreter.py +++ b/src/ethereum/forks/monad_nine/vm/interpreter.py @@ -133,22 +133,32 @@ def is_reserve_balance_violated(evm: Evm) -> bool: # execution, but this aligns with Monad EVM implementation. if code == b"" or is_valid_delegation(code): original_balance = get_balance_original(snapshot, addr) + + is_exception = ( + message.tx_env.origin == addr + and not is_sender_authority(tx_state.parent, addr) + and not is_valid_delegation(code) + ) + if tx_env.origin == addr: # gas_fees already deducted, need to re-add if sender # to match with spec. gas_fees = U256(tx_env.gas_price * tx_env.tx_gas_limit) original_balance += gas_fees reserve = min(RESERVE_BALANCE, original_balance) - threshold = reserve - gas_fees + assert is_exception or gas_fees <= reserve, ( + "gas fees exceed the reserve for a sender that " + "cannot empty; consensus only sequences a " + "transaction whose sender's in-flight gas fees " + "fit within the reserve" + ) + # Gas spend does not count against the reserve, so the + # gas already deducted from the balance is added back by + # lowering the threshold. Clamped at zero for U256. + threshold = reserve - min(reserve, gas_fees) else: threshold = RESERVE_BALANCE - is_exception = ( - message.tx_env.origin == addr - and not is_sender_authority(tx_state.parent, addr) - and not is_valid_delegation(code) - ) - if ( acc.balance < original_balance and acc.balance < threshold diff --git a/src/ethereum/forks/monad_ten/vm/interpreter.py b/src/ethereum/forks/monad_ten/vm/interpreter.py index 40adf562659..6449afb2f23 100644 --- a/src/ethereum/forks/monad_ten/vm/interpreter.py +++ b/src/ethereum/forks/monad_ten/vm/interpreter.py @@ -133,22 +133,32 @@ def is_reserve_balance_violated(evm: Evm) -> bool: # execution, but this aligns with Monad EVM implementation. if code == b"" or is_valid_delegation(code): original_balance = get_balance_original(snapshot, addr) + + is_exception = ( + message.tx_env.origin == addr + and not is_sender_authority(tx_state.parent, addr) + and not is_valid_delegation(code) + ) + if tx_env.origin == addr: # gas_fees already deducted, need to re-add if sender # to match with spec. gas_fees = U256(tx_env.gas_price * tx_env.tx_gas_limit) original_balance += gas_fees reserve = min(RESERVE_BALANCE, original_balance) - threshold = reserve - gas_fees + assert is_exception or gas_fees <= reserve, ( + "gas fees exceed the reserve for a sender that " + "cannot empty; consensus only sequences a " + "transaction whose sender's in-flight gas fees " + "fit within the reserve" + ) + # Gas spend does not count against the reserve, so the + # gas already deducted from the balance is added back by + # lowering the threshold. Clamped at zero for U256. + threshold = reserve - min(reserve, gas_fees) else: threshold = RESERVE_BALANCE - is_exception = ( - message.tx_env.origin == addr - and not is_sender_authority(tx_state.parent, addr) - and not is_valid_delegation(code) - ) - if ( acc.balance < original_balance and acc.balance < threshold 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 new file mode 100644 index 00000000000..183d5b77d9d --- /dev/null +++ b/tests/monad_eight/reserve_balance/test_gas_fees_vs_reserve.py @@ -0,0 +1,110 @@ +""" +Tests reserve balance when a transaction's gas fees reach the reserve. +""" + +from enum import Enum, auto, unique + +import pytest +from execution_testing import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Op, + Transaction, +) +from execution_testing.forks.helpers import Fork + +from .spec import Spec, ref_spec_7702 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7702.git_path +REFERENCE_SPEC_VERSION = ref_spec_7702.version + +slot_code_worked = 0x1 +value_code_worked = 0x1234 + +pytestmark = [ + pytest.mark.valid_from("MONAD_EIGHT"), + pytest.mark.pre_alloc_group( + "reserve_balance_tests", + reason="Tests reserve balance", + ), +] + + +@unique +class GasFees(Enum): + """Gas fees of a max-gas transaction, relative to the reserve.""" + + HALF_RESERVE = auto() + JUST_BELOW_RESERVE = auto() + JUST_ABOVE_RESERVE = auto() + DOUBLE_RESERVE = auto() + + def __str__(self) -> str: + """Return string representation.""" + return self.name.lower() + + def gas_price(self, gas_limit: int) -> int: + """ + Compute the gas price placing the fees at this point. + + The reserve is not an exact multiple of the gas limit, so the + boundary price is the largest one whose fees still fit within the + reserve, and the pair straddling it is the tightest available. + """ + boundary = Spec.RESERVE_BALANCE // gas_limit + match self: + case GasFees.HALF_RESERVE: + return boundary // 2 + case GasFees.JUST_BELOW_RESERVE: + return boundary + case GasFees.JUST_ABOVE_RESERVE: + return boundary + 1 + case GasFees.DOUBLE_RESERVE: + return 2 * boundary + + +@pytest.mark.parametrize("gas_fees", list(GasFees)) +def test_gas_fees_never_violate_reserve( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + gas_fees: GasFees, +) -> None: + """ + Test that a transaction is allowed through however large its gas fees + are, including at and above the whole 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. + """ + 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) + + 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) + + tx = Transaction( + ty=0, + to=contract_address, + gas_limit=gas_limit, + gas_price=gas_price, + sender=sender, + ) + + blockchain_test( + pre=pre, + post={ + contract_address: Account( + storage={slot_code_worked: value_code_worked} + ), + sender: Account(nonce=1), + }, + blocks=[Block(txs=[tx])], + )