From b01952b54d92dd9f586d9e1b4e1d28ce42ef2922 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:34:32 +0000 Subject: [PATCH 01/44] feat(monad_next): implement EIP-7708 ETH transfer/burn logs Emit LOG3/LOG2 from SYSTEM_ADDRESS for value transfers, selfdestruct beneficiary transfers/burns, and finalization burns of deleted accounts. Co-Authored-By: Claude --- src/ethereum/forks/monad_next/fork.py | 29 ++++++- src/ethereum/forks/monad_next/vm/__init__.py | 82 ++++++++++++++++++- .../monad_next/vm/instructions/system.py | 11 +++ .../forks/monad_next/vm/interpreter.py | 6 +- 4 files changed, 122 insertions(+), 6 deletions(-) diff --git a/src/ethereum/forks/monad_next/fork.py b/src/ethereum/forks/monad_next/fork.py index 653f05d030f..ebe71312adf 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_next/fork.py @@ -33,6 +33,7 @@ ) from ethereum.state import EMPTY_CODE_HASH, Address from ethereum.state_paged import State, apply_changes_to_state +from ethereum.utils.byte import left_pad_zero_bytes from . import vm from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt @@ -973,15 +974,32 @@ def process_transaction( # transfer miner fees create_ether(tx_state, block_env.coinbase, U256(transaction_fee)) - for address in tx_output.accounts_to_delete: - destroy_account(tx_state, address) + # EIP-7708: Emit burn logs for balances held by accounts marked for + # deletion AFTER miner fee transfer. + finalization_logs: List[Log] = [] + for address in sorted(tx_output.accounts_to_delete): + balance = get_account(tx_state, address).balance + if balance > U256(0): + padded_address = left_pad_zero_bytes(address, 32) + finalization_logs.append( + Log( + address=vm.SYSTEM_ADDRESS, + topics=( + vm.BURN_TOPIC, + Hash32(padded_address), + ), + data=balance.to_be_bytes32(), + ) + ) + + all_logs = tx_output.logs + tuple(finalization_logs) # block_output.block_gas_used += tx_gas_used_after_refund block_output.block_gas_used += tx.gas block_output.blob_gas_used += tx_blob_gas_used receipt = make_receipt( - tx, tx_output.error, block_output.block_gas_used, tx_output.logs + tx, tx_output.error, block_output.block_gas_used, all_logs ) receipt_key = rlp.encode(Uint(index)) @@ -993,7 +1011,10 @@ def process_transaction( receipt, ) - block_output.block_logs += tx_output.logs + block_output.block_logs += all_logs + + for address in tx_output.accounts_to_delete: + destroy_account(tx_state, address) incorporate_tx_into_block(tx_state) diff --git a/src/ethereum/forks/monad_next/vm/__init__.py b/src/ethereum/forks/monad_next/vm/__init__.py index 15c53bc75b6..9ebf92dd9f5 100644 --- a/src/ethereum/forks/monad_next/vm/__init__.py +++ b/src/ethereum/forks/monad_next/vm/__init__.py @@ -18,10 +18,11 @@ from ethereum_types.bytes import Bytes, Bytes0, Bytes32 from ethereum_types.numeric import U64, U256, Uint -from ethereum.crypto.hash import Hash32 +from ethereum.crypto.hash import Hash32, keccak256 from ethereum.exceptions import EthereumException from ethereum.merkle_patricia_trie import Trie from ethereum.state import Address +from ethereum.utils.byte import left_pad_zero_bytes from ..blocks import Log, Receipt, Withdrawal from ..fork_types import Authorization, VersionedHash @@ -30,6 +31,12 @@ __all__ = ("Environment", "Evm", "Message") +TRANSFER_TOPIC = keccak256(b"Transfer(address,address,uint256)") +BURN_TOPIC = keccak256(b"Burn(address,uint256)") +SYSTEM_ADDRESS = Address( + bytes.fromhex("fffffffffffffffffffffffffffffffffffffffe") +) + @final @dataclass @@ -237,3 +244,76 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None: # NOTE: absence of `evm.memory`, in particular of its high watermark # is intended for memory to deallocate on call frame exit. + + +def emit_transfer_log( + evm: Evm, + sender: Address, + recipient: Address, + transfer_amount: U256, +) -> None: + """ + Emit a LOG3 for all ETH transfers satisfying EIP-7708. + + Parameters + ---------- + evm : + The state of the ethereum virtual machine + sender : + The account address sending the transfer + recipient : + The account address receiving the transfer + transfer_amount : + The amount of ETH transacted + + """ + if transfer_amount == 0: + return + + padded_sender = left_pad_zero_bytes(sender, 32) + padded_recipient = left_pad_zero_bytes(recipient, 32) + log_entry = Log( + address=SYSTEM_ADDRESS, + topics=( + TRANSFER_TOPIC, + Hash32(padded_sender), + Hash32(padded_recipient), + ), + data=transfer_amount.to_be_bytes32(), + ) + + evm.logs = evm.logs + (log_entry,) + + +def emit_burn_log( + evm: Evm, + account: Address, + amount: U256, +) -> None: + """ + Emit a LOG2 for ETH burn per EIP-7708. + + Parameters + ---------- + evm : + The state of the ethereum virtual machine + account : + The account address whose ETH is being burned + amount : + The amount of ETH being burned + + """ + if amount == 0: + return + + padded_account = left_pad_zero_bytes(account, 32) + log_entry = Log( + address=SYSTEM_ADDRESS, + topics=( + BURN_TOPIC, + Hash32(padded_account), + ), + data=amount.to_be_bytes32(), + ) + + evm.logs = evm.logs + (log_entry,) diff --git a/src/ethereum/forks/monad_next/vm/instructions/system.py b/src/ethereum/forks/monad_next/vm/instructions/system.py index 7b8634c5d7e..99e31fc1a1b 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/system.py +++ b/src/ethereum/forks/monad_next/vm/instructions/system.py @@ -35,6 +35,8 @@ from .. import ( Evm, Message, + emit_burn_log, + emit_transfer_log, incorporate_child_on_error, incorporate_child_on_success, ) @@ -584,6 +586,15 @@ def selfdestruct(evm: Evm) -> None: originator_balance, ) + # EIP-7708: Emit transfer or burn log for the beneficiary transfer + if ( + originator in evm.message.tx_env.state.created_accounts + and beneficiary == originator + ): + emit_burn_log(evm, originator, originator_balance) + elif beneficiary != originator: + emit_transfer_log(evm, originator, beneficiary, originator_balance) + # register account for deletion only if it was created # in the same transaction if originator in evm.message.tx_env.state.created_accounts: diff --git a/src/ethereum/forks/monad_next/vm/interpreter.py b/src/ethereum/forks/monad_next/vm/interpreter.py index 13089bc8125..f72394c5745 100644 --- a/src/ethereum/forks/monad_next/vm/interpreter.py +++ b/src/ethereum/forks/monad_next/vm/interpreter.py @@ -56,7 +56,7 @@ from ..vm.gas import GasCosts, charge_gas, page_index from ..vm.precompiled_contracts import MONAD_PRECOMPILE_ADDRESSES from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS -from . import Evm, EvmMemory +from . import Evm, EvmMemory, emit_transfer_log from .exceptions import ( AddressCollision, ExceptionalHalt, @@ -412,6 +412,10 @@ def process_message(message: Message) -> Evm: message.current_target, message.value, ) + if message.caller != message.current_target: + emit_transfer_log( + evm, message.caller, message.current_target, message.value + ) try: if evm.message.code_address in PRE_COMPILED_CONTRACTS: From 57b3fbc9c5bfea1858ce213ddfac3dbcfcdd76ec Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:36:37 +0000 Subject: [PATCH 02/44] feat(monad_next): implement EIP-7843 SLOTNUM opcode Add SLOTNUM (0x4b, gas BASE) pushing the block slot number, a slot_number U64 field on the header and block environment, and fork.py plumbing. Co-Authored-By: Claude --- src/ethereum/forks/monad_next/blocks.py | 8 +++++ src/ethereum/forks/monad_next/fork.py | 1 + src/ethereum/forks/monad_next/vm/__init__.py | 1 + src/ethereum/forks/monad_next/vm/gas.py | 1 + .../monad_next/vm/instructions/__init__.py | 2 ++ .../forks/monad_next/vm/instructions/block.py | 33 +++++++++++++++++++ 6 files changed, 46 insertions(+) diff --git a/src/ethereum/forks/monad_next/blocks.py b/src/ethereum/forks/monad_next/blocks.py index f6745f79de3..4d83d39df9e 100644 --- a/src/ethereum/forks/monad_next/blocks.py +++ b/src/ethereum/forks/monad_next/blocks.py @@ -248,6 +248,14 @@ class Header: [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 """ + slot_number: U64 + """ + The slot number of this block as provided by the consensus layer. + Introduced in [EIP-7843]. + + [EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843 + """ + @final @slotted_freezable diff --git a/src/ethereum/forks/monad_next/fork.py b/src/ethereum/forks/monad_next/fork.py index ebe71312adf..5c4bb5c1fc2 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_next/fork.py @@ -247,6 +247,7 @@ def state_transition(chain: BlockChain, block: Block) -> None: prev_randao=block.header.prev_randao, excess_blob_gas=block.header.excess_blob_gas, parent_beacon_block_root=block.header.parent_beacon_block_root, + slot_number=block.header.slot_number, ) block_output = apply_body( diff --git a/src/ethereum/forks/monad_next/vm/__init__.py b/src/ethereum/forks/monad_next/vm/__init__.py index 9ebf92dd9f5..381db45cad9 100644 --- a/src/ethereum/forks/monad_next/vm/__init__.py +++ b/src/ethereum/forks/monad_next/vm/__init__.py @@ -56,6 +56,7 @@ class BlockEnvironment: prev_randao: Bytes32 excess_blob_gas: U64 parent_beacon_block_root: Hash32 + slot_number: U64 @final diff --git a/src/ethereum/forks/monad_next/vm/gas.py b/src/ethereum/forks/monad_next/vm/gas.py index 870cee592ac..708ec866fb0 100644 --- a/src/ethereum/forks/monad_next/vm/gas.py +++ b/src/ethereum/forks/monad_next/vm/gas.py @@ -189,6 +189,7 @@ class GasCosts: OPCODE_CHAINID: Final[Uint] = BASE OPCODE_BASEFEE: Final[Uint] = BASE OPCODE_BLOBBASEFEE: Final[Uint] = BASE + OPCODE_SLOTNUM: Final[Uint] = BASE OPCODE_BLOBHASH: Final[Uint] = Uint(3) OPCODE_PUSH: Final[Uint] = VERY_LOW OPCODE_PUSH0: Final[Uint] = BASE diff --git a/src/ethereum/forks/monad_next/vm/instructions/__init__.py b/src/ethereum/forks/monad_next/vm/instructions/__init__.py index 0da72c8ea5c..d858b5053f0 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/__init__.py +++ b/src/ethereum/forks/monad_next/vm/instructions/__init__.py @@ -99,6 +99,7 @@ class Ops(enum.Enum): BASEFEE = 0x48 BLOBHASH = 0x49 BLOBBASEFEE = 0x4A + SLOTNUM = 0x4B # Control Flow Ops STOP = 0x00 @@ -251,6 +252,7 @@ class Ops(enum.Enum): Ops.PREVRANDAO: block_instructions.prev_randao, Ops.GASLIMIT: block_instructions.gas_limit, Ops.CHAINID: block_instructions.chain_id, + Ops.SLOTNUM: block_instructions.slot_number, Ops.MLOAD: memory_instructions.mload, Ops.MSTORE: memory_instructions.mstore, Ops.MSTORE8: memory_instructions.mstore8, diff --git a/src/ethereum/forks/monad_next/vm/instructions/block.py b/src/ethereum/forks/monad_next/vm/instructions/block.py index baa589c4395..4f9f9e5d5c3 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/block.py +++ b/src/ethereum/forks/monad_next/vm/instructions/block.py @@ -259,3 +259,36 @@ def chain_id(evm: Evm) -> None: # PROGRAM COUNTER evm.pc += Uint(1) + + +def slot_number(evm: Evm) -> None: + """ + Push the current slot number onto the stack. + + The slot number is provided by the consensus layer and passed to the + execution layer through the engine API. + + Parameters + ---------- + evm : + The current EVM frame. + + Raises + ------ + :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + If `evm.gas_left` is less than `2`. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_SLOTNUM) + + # OPERATION + push(evm.stack, U256(evm.message.block_env.slot_number)) + + # PROGRAM COUNTER + evm.pc += Uint(1) From 24e4bb825137f9f26c03dbbdd09ec25184ee9763 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:39:01 +0000 Subject: [PATCH 03/44] feat(monad_next): implement EIP-8024 SWAPN, DUPN, EXCHANGE Add DUPN (0xe6), SWAPN (0xe7), EXCHANGE (0xe8) stack instructions with a 1-byte immediate, gas VERY_LOW, plus immediate-aware jumpdest analysis. Co-Authored-By: Claude --- src/ethereum/forks/monad_next/vm/gas.py | 3 + .../monad_next/vm/instructions/__init__.py | 8 ++ .../forks/monad_next/vm/instructions/stack.py | 107 +++++++++++++++++- src/ethereum/forks/monad_next/vm/runtime.py | 26 +++++ src/ethereum/forks/monad_next/vm/stack.py | 79 ++++++++++++- 5 files changed, 219 insertions(+), 4 deletions(-) diff --git a/src/ethereum/forks/monad_next/vm/gas.py b/src/ethereum/forks/monad_next/vm/gas.py index 708ec866fb0..461f0f79a27 100644 --- a/src/ethereum/forks/monad_next/vm/gas.py +++ b/src/ethereum/forks/monad_next/vm/gas.py @@ -195,6 +195,9 @@ class GasCosts: OPCODE_PUSH0: Final[Uint] = BASE OPCODE_DUP: Final[Uint] = VERY_LOW OPCODE_SWAP: Final[Uint] = VERY_LOW + OPCODE_DUPN: Final[Uint] = VERY_LOW + OPCODE_SWAPN: Final[Uint] = VERY_LOW + OPCODE_EXCHANGE: Final[Uint] = VERY_LOW # Dynamic Opcodes OPCODE_RETURNDATACOPY_BASE: Final[Uint] = VERY_LOW diff --git a/src/ethereum/forks/monad_next/vm/instructions/__init__.py b/src/ethereum/forks/monad_next/vm/instructions/__init__.py index d858b5053f0..06295ec86f1 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/__init__.py +++ b/src/ethereum/forks/monad_next/vm/instructions/__init__.py @@ -189,6 +189,11 @@ class Ops(enum.Enum): SWAP15 = 0x9E SWAP16 = 0x9F + # EIP-8024: Stack access instructions + DUPN = 0xE6 + SWAPN = 0xE7 + EXCHANGE = 0xE8 + # Memory Operations MLOAD = 0x51 MSTORE = 0x52 @@ -352,6 +357,9 @@ class Ops(enum.Enum): Ops.SWAP14: stack_instructions.swap14, Ops.SWAP15: stack_instructions.swap15, Ops.SWAP16: stack_instructions.swap16, + Ops.DUPN: stack_instructions.dupn, + Ops.SWAPN: stack_instructions.swapn, + Ops.EXCHANGE: stack_instructions.exchange, Ops.LOG0: log_instructions.log0, Ops.LOG1: log_instructions.log1, Ops.LOG2: log_instructions.log2, diff --git a/src/ethereum/forks/monad_next/vm/instructions/stack.py b/src/ethereum/forks/monad_next/vm/instructions/stack.py index ce94af6ce8e..0e72bd01f31 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/stack.py +++ b/src/ethereum/forks/monad_next/vm/instructions/stack.py @@ -14,7 +14,7 @@ from functools import partial from typing import Callable -from ethereum_types.numeric import U256, Uint +from ethereum_types.numeric import U8, U256, Uint from .. import Evm, stack from ..exceptions import StackUnderflowError @@ -23,6 +23,7 @@ charge_gas, ) from ..memory import buffer_read +from ..stack import decode_pair, decode_single def pop(evm: Evm) -> None: @@ -210,3 +211,107 @@ def swap_n(evm: Evm, item_number: int) -> None: swap14: Callable[[Evm], None] = partial(swap_n, item_number=14) swap15: Callable[[Evm], None] = partial(swap_n, item_number=15) swap16: Callable[[Evm], None] = partial(swap_n, item_number=16) + + +def dupn(evm: Evm) -> None: + """ + Duplicate the Nth stack item (from top of the stack) to the top of stack. + The item number is read from the immediate byte following the opcode and + decoded using the EIP-8024 index shifting rules. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_DUPN) + + # OPERATION + immediate_data = U8( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(1))[0] + ) + item_number = decode_single(immediate_data) + if int(item_number) > len(evm.stack): + raise StackUnderflowError + data_to_duplicate = evm.stack[-item_number] + stack.push(evm.stack, data_to_duplicate) + + # PROGRAM COUNTER + evm.pc += Uint(2) + + +def swapn(evm: Evm) -> None: + """ + Swap the top stack item with the Nth stack item. + The value N is read from the immediate byte following the opcode and + decoded using the EIP-8024 index shifting rules. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_SWAPN) + + # OPERATION + immediate_data = U8( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(1))[0] + ) + item_number = decode_single(immediate_data) + # SWAPN with decoded value n swaps top (position 1) with position (n+1) + if int(item_number) + 1 > len(evm.stack): + raise StackUnderflowError + # stack[-1] is top (position 1), stack[-(item_number+1)] is position (n+1) + evm.stack[-1], evm.stack[-(item_number + U8(1))] = ( + evm.stack[-(item_number + U8(1))], + evm.stack[-1], + ) + + # PROGRAM COUNTER + evm.pc += Uint(2) + + +def exchange(evm: Evm) -> None: + """ + Exchange the Nth stack item with the Mth stack item. + The values N and M are decoded from the immediate byte using the + EIP-8024 index shifting rules. + + Parameters + ---------- + evm : + The current EVM frame. + + """ + # STACK + pass + + # GAS + charge_gas(evm, GasCosts.OPCODE_EXCHANGE) + + # OPERATION + immediate_data = U8( + buffer_read(evm.code, U256(evm.pc + Uint(1)), U256(1))[0] + ) + n, m = decode_pair(immediate_data) + # EXCHANGE swaps position (n+1) with position (m+1) + depth = max(n, m) + U8(1) + if int(depth) > len(evm.stack): + raise StackUnderflowError + evm.stack[-(n + U8(1))], evm.stack[-(m + U8(1))] = ( + evm.stack[-(m + U8(1))], + evm.stack[-(n + U8(1))], + ) + + # PROGRAM COUNTER + evm.pc += Uint(2) diff --git a/src/ethereum/forks/monad_next/vm/runtime.py b/src/ethereum/forks/monad_next/vm/runtime.py index 0aa5ddd5e20..60fd42b52c9 100644 --- a/src/ethereum/forks/monad_next/vm/runtime.py +++ b/src/ethereum/forks/monad_next/vm/runtime.py @@ -28,6 +28,8 @@ def get_valid_jump_destinations(code: Bytes) -> Set[Uint]: * The jump destination should have the `JUMPDEST` opcode (0x5B). * The jump destination shouldn't be part of the data corresponding to `PUSH-N` opcodes. + * The jump destination shouldn't be part of the immediate byte + corresponding to `DUPN`, `SWAPN`, or `EXCHANGE` opcodes (EIP-8024). Note - Jump destinations are 0-indexed. @@ -63,6 +65,30 @@ def get_valid_jump_destinations(code: Bytes) -> Set[Uint]: # opcodes. push_data_size = current_opcode.value - Ops.PUSH1.value + 1 pc += Uint(push_data_size) + elif current_opcode in (Ops.DUPN, Ops.SWAPN): + # EIP-8024: DUPN/SWAPN invalid immediate range is + # 90 < x < 128, i.e. 0x5B (91) to 0x7F (127). + # Invalid immediates are not skipped so the byte + # remains at an instruction boundary. + if ( + pc + Uint(1) < ulen(code) + and 0x5B <= code[pc + Uint(1)] <= 0x7F + ): + pass + else: + pc += Uint(1) + elif current_opcode == Ops.EXCHANGE: + # EIP-8024: EXCHANGE invalid immediate range is + # 81 < x < 128, i.e. 0x52 (82) to 0x7F (127). + # Invalid immediates are not skipped so the byte + # remains at an instruction boundary. + if ( + pc + Uint(1) < ulen(code) + and 0x52 <= code[pc + Uint(1)] <= 0x7F + ): + pass + else: + pc += Uint(1) pc += Uint(1) diff --git a/src/ethereum/forks/monad_next/vm/stack.py b/src/ethereum/forks/monad_next/vm/stack.py index a87b0a47079..98ba815cb73 100644 --- a/src/ethereum/forks/monad_next/vm/stack.py +++ b/src/ethereum/forks/monad_next/vm/stack.py @@ -11,11 +11,84 @@ Implementation of the stack operators for the EVM. """ -from typing import List +from typing import List, Tuple -from ethereum_types.numeric import U256 +from ethereum_types.numeric import U8, U256 -from .exceptions import StackOverflowError, StackUnderflowError +from .exceptions import ( + InvalidParameter, + StackOverflowError, + StackUnderflowError, +) + + +def decode_single(x: U8) -> U8: + """ + Decode the immediate byte for DUPN/SWAPN to get the stack index. + + Return n with 17 <= n <= 235. + + Parameters + ---------- + x : int + The immediate byte value (0-90 or 128-255). + + Returns + ------- + int + The stack index n, where 17 <= n <= 235. + + Raises + ------ + InvalidParameter + If x is in the forbidden range (90 < x < 128 or x > 255). + + """ + if not (U8(0) <= x <= U8(90) or U8(128) <= x <= U8(255)): + raise InvalidParameter( + f"DUPN/SWAPN immediate byte {x} is out of range. " + "Valid range: 0 <= x <= 90 or 128 <= x <= 255" + ) + + return U8((int(x) + 145) % 256) + + +def decode_pair(x: U8) -> Tuple[U8, U8]: + """ + Decode the immediate byte for EXCHANGE to get two stack indices. + + Return (n, m) with 1 <= n <= 14 and n < m <= 30 - n. + + Parameters + ---------- + x : int + The immediate byte value (0-81 or 128-255). + + Returns + ------- + Tuple[int, int] + The two stack indices (n, m), where + 1 <= n <= 14 and n < m <= 30 - n. + + Raises + ------ + InvalidParameter + If x is in the forbidden range (81 < x < 128 or x > 255). + + """ + if not (U8(0) <= x <= U8(81) or U8(128) <= x <= U8(255)): + raise InvalidParameter( + f"EXCHANGE immediate byte {x} is in the forbidden " + "range 82 <= x <= 127\n" + "Valid range: 0 <= x <= 81 or 128 <= x <= 255" + ) + + k = U8(int(x) ^ 143) + q, r = divmod(k, U8(16)) + if q < r: + return q + U8(1), r + U8(1) + else: + return r + U8(1), U8(29) - q def pop(stack: List[U256]) -> U256: From fe8abdac47720ed3c67ea8bec134e23b420e4dde Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:12:36 +0000 Subject: [PATCH 04/44] feat(forks): make MONAD_NEXT an Amsterdam fork inheriting 3 EIPs Inherit MONAD_NEXT from Amsterdam, taking only EIP-7708/7843/8024 changes (opcodes, slot number header) and pinning EIP-7928/7954/7976/7981 members back to MONAD_TEN. Relocated after Amsterdam to resolve the base reference. Co-Authored-By: Claude --- .../execution_testing/forks/forks/forks.py | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index ecf3b02f3ae..7fab30a6306 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1805,12 +1805,6 @@ def _calculate_sstore_gas_mip8( return gas_cost -class MONAD_NEXT(MONAD_TEN): # noqa: N801 - """MONAD_NEXT fork, a placeholder identical to MONAD_TEN.""" - - pass - - class BPO1( Osaka, bpo_fork=True, @@ -1904,3 +1898,69 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: limit. """ return True + + +class MONAD_NEXT(MONAD_TEN, Amsterdam): # noqa: N801 + """ + MONAD_NEXT fork. + + Amsterdam-based successor to MONAD_TEN. Only the EIP-7708, EIP-7843 + and EIP-8024 changes are inherited from Amsterdam; every other + Amsterdam change is pinned back to the MONAD_TEN parent. + """ + + @classmethod + def valid_opcodes(cls) -> List[Opcodes]: + """ + Inherit the Amsterdam opcode set: SLOTNUM (EIP-7843) and SWAPN, + DUPN, EXCHANGE (EIP-8024) on top of the MONAD_TEN opcodes. + """ + return Amsterdam.valid_opcodes() + + @classmethod + def max_code_size(cls) -> int: + """Return spec from explicit parent (skip EIP-7954).""" + return MONAD_TEN.max_code_size() + + @classmethod + def calldata_gas_calculator(cls) -> CalldataGasCalculator: + """Return spec from explicit parent (skip EIP-7976).""" + return MONAD_TEN.calldata_gas_calculator() + + @classmethod + def transaction_data_floor_cost_calculator( + cls, + ) -> TransactionDataFloorCostCalculator: + """Return spec from explicit parent (skip EIP-7981).""" + return MONAD_TEN.transaction_data_floor_cost_calculator() + + @classmethod + def transaction_intrinsic_cost_calculator( + cls, + ) -> TransactionIntrinsicCostCalculator: + """Return spec from explicit parent (skip EIP-7981).""" + return MONAD_TEN.transaction_intrinsic_cost_calculator() + + @classmethod + def header_bal_hash_required(cls) -> bool: + """Return spec from explicit parent (skip EIP-7928).""" + return MONAD_TEN.header_bal_hash_required() + + @classmethod + def empty_block_bal_item_count(cls) -> int: + """Return spec from explicit parent (skip EIP-7928).""" + return MONAD_TEN.empty_block_bal_item_count() + + @classmethod + def engine_execution_payload_block_access_list(cls) -> bool: + """Return spec from explicit parent (skip EIP-7928).""" + return MONAD_TEN.engine_execution_payload_block_access_list() + + +# MONAD_NEXT adopts EIP-7708, EIP-7843 and EIP-8024 from Amsterdam through the +# MRO rather than by inheriting their mixin classes: inheriting them would +# register MONAD_NEXT as a spurious `enabling_fork` (breaking +# `valid_at_transition_to`) and pull in EIP-7843's engine version bumps. +# Record the adopted EIP numbers on `_enabled_eips` directly so that +# `is_eip_enabled()` reports them without those side effects. +MONAD_NEXT._enabled_eips = MONAD_TEN._enabled_eips | {7708, 7843, 8024} From d4119a45f365064c8ff8c62e8fce9e4cc41bc3d1 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:02:25 +0000 Subject: [PATCH 05/44] feat(ci): add monad_amsterdam fixture release config Single-fork (--fork MONAD_NEXT) feature covering only the adopted Amsterdam EIP dirs (7708/7843/8024); released via tag tests-monad_amsterdam@v0.1.0. Co-Authored-By: Claude --- .github/configs/feature.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 7fca18fd9ef..8865d8dd458 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -11,3 +11,7 @@ monad_runloop: evm-type: eels # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=30143 --monad-runloop -k "not invalid_header" + +monad_amsterdam: + evm-type: eels + fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" tests/amsterdam/eip7708_eth_transfer_logs tests/amsterdam/eip7843_slotnum tests/amsterdam/eip8024_dupn_swapn_exchange From 1466f6aa9f641a207313f23d30babafe3e260cb8 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:46:45 +0000 Subject: [PATCH 06/44] feat(ci): fill whole test suite for monad_amsterdam release Disable collection of non-adopted Amsterdam EIP tests on Monad forks. Co-Authored-By: Claude --- .github/configs/feature.yaml | 2 +- .../conftest.py | 10 ++++++++++ .../eip7928_block_level_access_lists/conftest.py | 10 ++++++++++ .../eip7954_increase_max_contract_size/conftest.py | 7 +++++++ .../eip7976_increase_calldata_floor_cost/conftest.py | 7 +++++++ .../eip7981_increase_access_list_cost/conftest.py | 7 +++++++ .../eip7928_block_level_access_lists/conftest.py | 10 ++++++++++ .../eip7928_block_level_access_lists/conftest.py | 10 ++++++++++ 8 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py create mode 100644 tests/amsterdam/eip7928_block_level_access_lists/conftest.py create mode 100644 tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py create mode 100644 tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 8865d8dd458..38dce031d7d 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -14,4 +14,4 @@ monad_runloop: monad_amsterdam: evm-type: eels - fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" tests/amsterdam/eip7708_eth_transfer_logs tests/amsterdam/eip7843_slotnum tests/amsterdam/eip8024_dupn_swapn_exchange + fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py new file mode 100644 index 00000000000..5cbdebdc9ef --- /dev/null +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7778 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/conftest.py b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py new file mode 100644 index 00000000000..3bedcfe2651 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7928 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py b/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py index 78cb66ed14d..b4d596bdc52 100644 --- a/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py +++ b/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py @@ -4,6 +4,13 @@ from execution_testing import Address, Alloc, Bytecode, Fork, Op +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) + + @pytest.fixture def max_code_size_contract( pre: Alloc, diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py index f92a0e003a0..db99cddc93c 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py @@ -23,6 +23,13 @@ from .helpers import DataTestType, find_floor_cost_threshold +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) + + @pytest.fixture def to( request: pytest.FixtureRequest, diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py b/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py index 104a73e464a..c02d771f5d2 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py @@ -22,6 +22,13 @@ from ...cancun.eip4844_blobs.spec import Spec as EIP_4844_Spec +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) + + @pytest.fixture def to( request: pytest.FixtureRequest, diff --git a/tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py b/tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py new file mode 100644 index 00000000000..9dc5216d7ea --- /dev/null +++ b/tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7928 benchmark tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py b/tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py new file mode 100644 index 00000000000..9dc5216d7ea --- /dev/null +++ b/tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7928 benchmark tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) From a3d7fd8889e2a50acf6561a9e152b088fc08121d Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:20:29 +0000 Subject: [PATCH 07/44] feat(forks): carry zero EIP-7928 BAL hash slot in MONAD_NEXT headers Header field sits between requests_hash and slot_number and must be zero. Co-Authored-By: Claude --- .../evm_tools/t8n/block_environment.py | 2 +- .../execution_testing/fixtures/blockchain.py | 8 +++++- .../src/execution_testing/forks/base_fork.py | 12 ++++++++ .../forks/forks/eips/amsterdam/eip_7928.py | 7 +++++ .../execution_testing/forks/forks/forks.py | 13 +++++++-- .../src/execution_testing/specs/blockchain.py | 28 +++++++++++-------- src/ethereum/forks/monad_next/__init__.py | 8 ++++-- src/ethereum/forks/monad_next/blocks.py | 10 +++++++ src/ethereum/forks/monad_next/fork.py | 2 ++ .../loaders/fork_loader.py | 14 ++++++++++ 10 files changed, 86 insertions(+), 18 deletions(-) diff --git a/packages/testing/src/execution_testing/evm_tools/t8n/block_environment.py b/packages/testing/src/execution_testing/evm_tools/t8n/block_environment.py index 3a602b9c6c4..ce61b3914f5 100644 --- a/packages/testing/src/execution_testing/evm_tools/t8n/block_environment.py +++ b/packages/testing/src/execution_testing/evm_tools/t8n/block_environment.py @@ -160,7 +160,7 @@ def _resolve_excess_blob_gas( } if fork.has_compute_requests_hash: arguments["requests_hash"] = Hash32(b"\0" * 32) - if fork.has_hash_block_access_list: + if fork.has_block_access_list_hash_header: arguments["block_access_list_hash"] = Hash32(b"\0" * 32) if fork.has_slot_number: arguments["slot_number"] = U64(0) diff --git a/packages/testing/src/execution_testing/fixtures/blockchain.py b/packages/testing/src/execution_testing/fixtures/blockchain.py index a7331378786..7d4269bfd95 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -377,7 +377,13 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self: if fork.header_requests_required(): extras["requests_hash"] = Requests() if fork.header_bal_hash_required(): - extras["block_access_list_hash"] = BlockAccessList().rlp_hash + # A fork can require the header field without building block + # access lists (e.g. Monad); the field is then fixed at zero. + extras["block_access_list_hash"] = ( + BlockAccessList().rlp_hash + if fork.supports_block_access_lists() + else Hash(0) + ) if fork.header_slot_number_required(): extras["slot_number"] = ( int(env.slot_number) if env.slot_number is not None else 0 diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index f842878c42f..aae401d8b08 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -563,6 +563,18 @@ def header_bal_hash_required(cls) -> bool: """Return true if the header must contain block access list hash.""" pass + @classmethod + @abstractmethod + def supports_block_access_lists(cls) -> bool: + """ + Return true if the fork builds block access lists (EIP-7928). + + A fork can require the block access list hash header field without + building block access lists (e.g. Monad); the field is then fixed + at zero. + """ + pass + @classmethod @abstractmethod def empty_block_bal_item_count(cls) -> int: diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py index 1eacd844b21..d7c787eed92 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py @@ -29,6 +29,13 @@ def header_bal_hash_required(cls) -> bool: """ return True + @classmethod + def supports_block_access_lists(cls) -> bool: + """ + From EIP-7928, blocks build block access lists. + """ + return True + @classmethod def gas_costs(cls) -> GasCosts: """ diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 7fab30a6306..1151b29531c 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -954,6 +954,11 @@ def header_bal_hash_required(cls) -> bool: """At genesis, header must not contain block access list hash.""" return False + @classmethod + def supports_block_access_lists(cls) -> bool: + """At genesis, no block access lists are built.""" + return False + @classmethod def empty_block_bal_item_count(cls) -> int: """Pre-Amsterdam forks have no block access list.""" @@ -1906,7 +1911,9 @@ class MONAD_NEXT(MONAD_TEN, Amsterdam): # noqa: N801 Amsterdam-based successor to MONAD_TEN. Only the EIP-7708, EIP-7843 and EIP-8024 changes are inherited from Amsterdam; every other - Amsterdam change is pinned back to the MONAD_TEN parent. + Amsterdam change is pinned back to the MONAD_TEN parent. The + EIP-7928 block access list hash header field is carried, but no + block access lists are built, so the field is always zero. """ @classmethod @@ -1942,9 +1949,9 @@ def transaction_intrinsic_cost_calculator( return MONAD_TEN.transaction_intrinsic_cost_calculator() @classmethod - def header_bal_hash_required(cls) -> bool: + def supports_block_access_lists(cls) -> bool: """Return spec from explicit parent (skip EIP-7928).""" - return MONAD_TEN.header_bal_hash_required() + return MONAD_TEN.supports_block_access_lists() @classmethod def empty_block_bal_item_count(cls) -> int: diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index c7f5398ae2e..8b996720f20 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -986,17 +986,23 @@ def generate_block_data( int(env.slot_number) if env.slot_number is not None else 0 ) + header_fields = transition_tool_output.result.model_dump( + exclude_none=True, + exclude={"blob_gas_used", "transactions_trie"}, + ) | env.model_dump( + exclude_none=True, + exclude={"blob_gas_used", "slot_number"}, + ) + if fork.header_bal_hash_required() and ( + not fork.supports_block_access_lists() + ): + # Fork requires the block access list hash header field but + # doesn't build block access lists (e.g. Monad): fix value at + # zero. + header_fields.setdefault("block_access_list_hash", Hash(0)) + header = FixtureHeader( - **( - transition_tool_output.result.model_dump( - exclude_none=True, - exclude={"blob_gas_used", "transactions_trie"}, - ) - | env.model_dump( - exclude_none=True, - exclude={"blob_gas_used", "slot_number"}, - ) - ), + **header_fields, blob_gas_used=blob_gas_used, transactions_trie=Transaction.list_root(txs), extra_data=( @@ -1059,7 +1065,7 @@ def generate_block_data( if t8n_bal_rlp is not None: t8n_bal = BlockAccessList.from_rlp(t8n_bal_rlp) - if fork.header_bal_hash_required(): + if fork.supports_block_access_lists(): assert t8n_bal is not None, ( "Block access list is required for this block but was not " "provided by the transition tool" diff --git a/src/ethereum/forks/monad_next/__init__.py b/src/ethereum/forks/monad_next/__init__.py index b6c71ab2450..2e989370ce9 100644 --- a/src/ethereum/forks/monad_next/__init__.py +++ b/src/ethereum/forks/monad_next/__init__.py @@ -1,6 +1,10 @@ """ -MONAD_NEXT fork is a placeholder for upcoming Monad changes and is -currently identical to MONAD_TEN. +MONAD_NEXT fork is a placeholder for upcoming Monad changes. It builds on +MONAD_TEN, adopting EIP-7708, EIP-7843 and EIP-8024 from Amsterdam +together with the Amsterdam block header layout; the [EIP-7928] block +access list hash header slot is carried but always zero. + +[EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 """ from ethereum.fork_criteria import ByTimestamp, ForkCriteria diff --git a/src/ethereum/forks/monad_next/blocks.py b/src/ethereum/forks/monad_next/blocks.py index 4d83d39df9e..7a66282a7ab 100644 --- a/src/ethereum/forks/monad_next/blocks.py +++ b/src/ethereum/forks/monad_next/blocks.py @@ -248,6 +248,16 @@ class Header: [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 """ + block_access_list_hash: Hash32 + """ + Header slot introduced by [EIP-7928] for the hash of the Block Access + List. Monad does not build block access lists, so this field is always + zero. See [`validate_header`][vh]. + + [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 + [vh]: ref:ethereum.forks.monad_next.fork.validate_header + """ + slot_number: U64 """ The slot number of this block as provided by the consensus layer. diff --git a/src/ethereum/forks/monad_next/fork.py b/src/ethereum/forks/monad_next/fork.py index 5c4bb5c1fc2..0288944313f 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_next/fork.py @@ -404,6 +404,8 @@ def validate_header(chain: BlockChain, header: Header) -> None: raise InvalidBlock if header.ommers_hash != EMPTY_OMMER_HASH: raise InvalidBlock + if header.block_access_list_hash != Hash32(b"\x00" * 32): + raise InvalidBlock block_parent_hash = keccak256(rlp.encode(parent_header)) if header.parent_hash != block_parent_hash: diff --git a/src/ethereum_spec_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/loaders/fork_loader.py index d9e02de32f9..70467ed3b88 100644 --- a/src/ethereum_spec_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/loaders/fork_loader.py @@ -154,6 +154,20 @@ def has_hash_block_access_list(self) -> bool: return False return hasattr(module, "hash_block_access_list") + @property + def has_block_access_list_hash_header(self) -> bool: + """ + Check if the fork's header has a `block_access_list_hash` field. + + A fork can carry the header field without building block access + lists (e.g. Monad, where the field is always zero). + """ + try: + header = self._module("blocks").Header + return "block_access_list_hash" in header.__dataclass_fields__ + except (ModuleNotFoundError, AttributeError): + return False + @property def BlockAccessIndex(self) -> Any: """BlockAccessIndex type of the fork.""" From 8796fdd7ed6919710592e66c84aac37344e463ce Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:49:52 +0000 Subject: [PATCH 08/44] refactor(forks): adopt Amsterdam EIPs by mixin, order by succession Inheriting Amsterdam routed its gas schedule and unadopted EIPs into MONAD_NEXT through the MRO; `follows` keeps the fork order without the behavior. Co-Authored-By: Claude --- .../pytest_commands/plugins/forks/forks.py | 4 +- .../src/execution_testing/forks/base_fork.py | 20 ++++- .../execution_testing/forks/forks/forks.py | 73 ++++--------------- 3 files changed, 35 insertions(+), 62 deletions(-) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py index 6075481806c..29ef4dc86e3 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py @@ -1265,7 +1265,9 @@ def _process_with_marker_args( "Missing fork argument with 'valid_at_transition_to' marker." ) - if len(forks) > 1: + # A single EIP argument expands to one fork per enabling fork, so + # the limit is on the arguments rather than on the resolved forks. + if len(fork_args) > 1: raise Exception( "Too many forks specified to 'valid_at_transition_to' marker." ) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index aae401d8b08..4767d361e35 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -333,12 +333,23 @@ def _maybe_transitioned(fork_cls: "BaseForkMeta") -> "BaseForkMeta": @staticmethod def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: """ - Check if `a` is a subclass of `b`, taking fork transitions into - account. + Check if `a` is a subclass of `b`, taking fork transitions and + declared succession into account. + + A fork can follow another fork it does not inherit from, which + places it after that fork (and after everything that fork comes + after) in the fork order without adopting its behavior. """ a = BaseForkMeta._maybe_transitioned(a) b = BaseForkMeta._maybe_transitioned(b) - return issubclass(a, b) + if issubclass(a, b): + return True + followed = getattr(a, "_follows", None) + while followed is not None: + if issubclass(followed, b): + return True + followed = followed._follows + return False def __gt__(cls, other: "BaseForkMeta") -> bool: """Compare if a fork is newer than some other fork (cls > other).""" @@ -381,6 +392,7 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True + _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -396,6 +408,7 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, + follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -410,6 +423,7 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name + cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 1151b29531c..e3e4ffe4932 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1905,69 +1905,26 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: return True -class MONAD_NEXT(MONAD_TEN, Amsterdam): # noqa: N801 +class MONAD_NEXT( # noqa: N801 + eips.EIP7708, + eips.EIP7843, + eips.EIP8024, + MONAD_TEN, + follows=Amsterdam, +): """ MONAD_NEXT fork. - Amsterdam-based successor to MONAD_TEN. Only the EIP-7708, EIP-7843 - and EIP-8024 changes are inherited from Amsterdam; every other - Amsterdam change is pinned back to the MONAD_TEN parent. The - EIP-7928 block access list hash header field is carried, but no - block access lists are built, so the field is always zero. + Amsterdam-based successor to MONAD_TEN, adopting the EIP-7708, + EIP-7843 and EIP-8024 changes. The Amsterdam changes it does not + adopt stay out of the fork by not being inherited at all; the fork + order still places MONAD_NEXT after Amsterdam through `follows`. """ @classmethod - def valid_opcodes(cls) -> List[Opcodes]: + def header_bal_hash_required(cls) -> bool: """ - Inherit the Amsterdam opcode set: SLOTNUM (EIP-7843) and SWAPN, - DUPN, EXCHANGE (EIP-8024) on top of the MONAD_TEN opcodes. + MONAD_NEXT headers carry the block access list hash field, fixed + at zero, without building block access lists (EIP-7928). """ - return Amsterdam.valid_opcodes() - - @classmethod - def max_code_size(cls) -> int: - """Return spec from explicit parent (skip EIP-7954).""" - return MONAD_TEN.max_code_size() - - @classmethod - def calldata_gas_calculator(cls) -> CalldataGasCalculator: - """Return spec from explicit parent (skip EIP-7976).""" - return MONAD_TEN.calldata_gas_calculator() - - @classmethod - def transaction_data_floor_cost_calculator( - cls, - ) -> TransactionDataFloorCostCalculator: - """Return spec from explicit parent (skip EIP-7981).""" - return MONAD_TEN.transaction_data_floor_cost_calculator() - - @classmethod - def transaction_intrinsic_cost_calculator( - cls, - ) -> TransactionIntrinsicCostCalculator: - """Return spec from explicit parent (skip EIP-7981).""" - return MONAD_TEN.transaction_intrinsic_cost_calculator() - - @classmethod - def supports_block_access_lists(cls) -> bool: - """Return spec from explicit parent (skip EIP-7928).""" - return MONAD_TEN.supports_block_access_lists() - - @classmethod - def empty_block_bal_item_count(cls) -> int: - """Return spec from explicit parent (skip EIP-7928).""" - return MONAD_TEN.empty_block_bal_item_count() - - @classmethod - def engine_execution_payload_block_access_list(cls) -> bool: - """Return spec from explicit parent (skip EIP-7928).""" - return MONAD_TEN.engine_execution_payload_block_access_list() - - -# MONAD_NEXT adopts EIP-7708, EIP-7843 and EIP-8024 from Amsterdam through the -# MRO rather than by inheriting their mixin classes: inheriting them would -# register MONAD_NEXT as a spurious `enabling_fork` (breaking -# `valid_at_transition_to`) and pull in EIP-7843's engine version bumps. -# Record the adopted EIP numbers on `_enabled_eips` directly so that -# `is_eip_enabled()` reports them without those side effects. -MONAD_NEXT._enabled_eips = MONAD_TEN._enabled_eips | {7708, 7843, 8024} + return True From efe211ddebff4d5a6c0d5e623cd158a7db7dc9eb Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:54:45 +0000 Subject: [PATCH 09/44] test(amsterdam): keep the unadopted Amsterdam suites off the Monad forks MONAD_NEXT follows Amsterdam, so its `valid_from` suites select the fork; the seven EIPs it does not adopt now exclude Monad like the rest. Co-Authored-By: Claude --- .../eip2780_reduce_intrinsic_tx_gas/conftest.py | 10 ++++++++++ .../conftest.py | 10 ++++++++++ .../conftest.py | 10 ++++++++++ .../eip8038_state_access_gas_cost_increase/conftest.py | 10 ++++++++++ tests/amsterdam/eip8070_sparse_blobpool/conftest.py | 7 +++++++ .../amsterdam/eip8246_selfdestruct_no_burn/conftest.py | 10 ++++++++++ .../eip8282_builder_execution_requests/conftest.py | 9 +++++++++ 7 files changed, 66 insertions(+) create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py create mode 100644 tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py create mode 100644 tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py create mode 100644 tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py new file mode 100644 index 00000000000..de52075d396 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-2780 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py new file mode 100644 index 00000000000..de93abfcfef --- /dev/null +++ b/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7997 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..2d16c275db4 --- /dev/null +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8037 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..cd29eb50dcb --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8038 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py index cb2a757494c..dc3ee7c3ecd 100644 --- a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py +++ b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py @@ -148,3 +148,10 @@ def txs( ) txs.append(network_wrapped_tx) return txs + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py b/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py new file mode 100644 index 00000000000..4fb53dba402 --- /dev/null +++ b/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8246 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py index e17cee467fc..7a42ba76785 100644 --- a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py +++ b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py @@ -1,8 +1,17 @@ """Fixtures for the EIP-8282 builder execution request tests.""" +import pytest + from ...common.system_contract_request_fixtures import ( blocks, # noqa: F401 included_requests, # noqa: F401 system_contract_interactions_per_block_copy, # noqa: F401 timestamp, # noqa: F401 ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) From e85abeaadf84f445cb53ced2d68f3b98aaecd140 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:34:51 +0000 Subject: [PATCH 10/44] feat(monad_next): adopt EIP-8246 remove SELFDESTRUCT balance burn MONAD_NEXT clears selfdestructed accounts in place, ordered after Amsterdam by succession, and releases as the monad_eip8246 feature. Co-Authored-By: Claude --- .github/configs/feature.yaml | 4 +++ .../pytest_commands/plugins/forks/forks.py | 4 ++- .../src/execution_testing/forks/base_fork.py | 20 ++++++++++-- .../execution_testing/forks/forks/forks.py | 23 ++++++++++---- src/ethereum/forks/monad_next/fork.py | 4 +-- .../forks/monad_next/state_tracker.py | 31 ++++++++++++++++--- .../monad_next/vm/instructions/system.py | 4 --- .../conftest.py | 10 ++++++ .../eip7708_eth_transfer_logs/conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ tests/amsterdam/eip7843_slotnum/conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ .../conftest.py | 7 +++++ .../conftest.py | 10 ++++++ .../eip8024_dupn_swapn_exchange/conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ .../eip8070_sparse_blobpool/conftest.py | 7 +++++ .../conftest.py | 9 ++++++ .../test_transfers.py | 11 ++++++- 20 files changed, 193 insertions(+), 21 deletions(-) create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py create mode 100644 tests/amsterdam/eip7708_eth_transfer_logs/conftest.py create mode 100644 tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py create mode 100644 tests/amsterdam/eip7843_slotnum/conftest.py create mode 100644 tests/amsterdam/eip7928_block_level_access_lists/conftest.py create mode 100644 tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py create mode 100644 tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py create mode 100644 tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 7fca18fd9ef..36ab811fdb9 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -11,3 +11,7 @@ monad_runloop: evm-type: eels # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=30143 --monad-runloop -k "not invalid_header" + +monad_eip8246: + evm-type: eels + fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py index 6075481806c..29ef4dc86e3 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py @@ -1265,7 +1265,9 @@ def _process_with_marker_args( "Missing fork argument with 'valid_at_transition_to' marker." ) - if len(forks) > 1: + # A single EIP argument expands to one fork per enabling fork, so + # the limit is on the arguments rather than on the resolved forks. + if len(fork_args) > 1: raise Exception( "Too many forks specified to 'valid_at_transition_to' marker." ) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index f842878c42f..808d94b6045 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -333,12 +333,23 @@ def _maybe_transitioned(fork_cls: "BaseForkMeta") -> "BaseForkMeta": @staticmethod def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: """ - Check if `a` is a subclass of `b`, taking fork transitions into - account. + Check if `a` is a subclass of `b`, taking fork transitions and + declared succession into account. + + A fork can follow another fork it does not inherit from, which + places it after that fork (and after everything that fork comes + after) in the fork order without adopting its behavior. """ a = BaseForkMeta._maybe_transitioned(a) b = BaseForkMeta._maybe_transitioned(b) - return issubclass(a, b) + if issubclass(a, b): + return True + followed = getattr(a, "_follows", None) + while followed is not None: + if issubclass(followed, b): + return True + followed = followed._follows + return False def __gt__(cls, other: "BaseForkMeta") -> bool: """Compare if a fork is newer than some other fork (cls > other).""" @@ -381,6 +392,7 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True + _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -396,6 +408,7 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, + follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -410,6 +423,7 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name + cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index ecf3b02f3ae..618ee8a0000 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1805,12 +1805,6 @@ def _calculate_sstore_gas_mip8( return gas_cost -class MONAD_NEXT(MONAD_TEN): # noqa: N801 - """MONAD_NEXT fork, a placeholder identical to MONAD_TEN.""" - - pass - - class BPO1( Osaka, bpo_fork=True, @@ -1904,3 +1898,20 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: limit. """ return True + + +class MONAD_NEXT( # noqa: N801 + eips.EIP8246, + MONAD_TEN, + follows=Amsterdam, +): + """ + MONAD_NEXT fork. + + Amsterdam-based successor to MONAD_TEN, adopting the EIP-8246 + changes. The Amsterdam changes it does not adopt stay out of the + fork by not being inherited at all; the fork order still places + MONAD_NEXT after Amsterdam through `follows`. + """ + + pass diff --git a/src/ethereum/forks/monad_next/fork.py b/src/ethereum/forks/monad_next/fork.py index 653f05d030f..7b27bf2d821 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_next/fork.py @@ -61,8 +61,8 @@ BlockState, TransactionState, add_sender_authority, + clear_account_preserving_balance, create_ether, - destroy_account, extract_block_diff, forget_senders_authorities, get_account, @@ -974,7 +974,7 @@ def process_transaction( create_ether(tx_state, block_env.coinbase, U256(transaction_fee)) for address in tx_output.accounts_to_delete: - destroy_account(tx_state, address) + clear_account_preserving_balance(tx_state, address) # block_output.block_gas_used += tx_gas_used_after_refund block_output.block_gas_used += tx.gas diff --git a/src/ethereum/forks/monad_next/state_tracker.py b/src/ethereum/forks/monad_next/state_tracker.py index 5b3d4be0bd2..1d0914461d2 100644 --- a/src/ethereum/forks/monad_next/state_tracker.py +++ b/src/ethereum/forks/monad_next/state_tracker.py @@ -423,10 +423,9 @@ def destroy_account(tx_state: TransactionState, address: Address) -> None: """ Completely remove the account at ``address`` and all of its storage. - This function is made available exclusively for the ``SELFDESTRUCT`` - opcode. It is expected that ``SELFDESTRUCT`` will be disabled in a - future hardfork and this function will be removed. Only supports same - transaction destruction. + Invoked by ``modify_state`` (and the coinbase fee-credit path) to + clean up an account that has become empty (zero nonce, empty + code, and zero balance) so it does not appear in the post-state. Parameters ---------- @@ -440,6 +439,30 @@ def destroy_account(tx_state: TransactionState, address: Address) -> None: set_account(tx_state, address, None) +def clear_account_preserving_balance( + tx_state: TransactionState, address: Address +) -> None: + """ + Clear an account's nonce, code, and storage while preserving its + balance. + + Parameters + ---------- + tx_state : + The transaction state. + address : + Address of the account to modify. + + """ + + def clear_account(account: Account) -> None: + account.nonce = Uint(0) + account.code_hash = EMPTY_CODE_HASH + + destroy_storage(tx_state, address) + modify_state(tx_state, address, clear_account) + + def destroy_storage(tx_state: TransactionState, address: Address) -> None: """ Completely remove the storage at ``address``. diff --git a/src/ethereum/forks/monad_next/vm/instructions/system.py b/src/ethereum/forks/monad_next/vm/instructions/system.py index 7b8634c5d7e..764bcec38fe 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/system.py +++ b/src/ethereum/forks/monad_next/vm/instructions/system.py @@ -24,7 +24,6 @@ increment_nonce, is_account_alive, move_ether, - set_account_balance, ) from ...utils.address import ( compute_contract_address, @@ -587,9 +586,6 @@ def selfdestruct(evm: Evm) -> None: # register account for deletion only if it was created # in the same transaction if originator in evm.message.tx_env.state.created_accounts: - # If beneficiary is the same as originator, then - # the ether is burnt. - set_account_balance(evm.message.tx_env.state, originator, U256(0)) evm.accounts_to_delete.add(originator) # HALT the execution diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py new file mode 100644 index 00000000000..de52075d396 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-2780 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py b/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py new file mode 100644 index 00000000000..fb1d9465b3e --- /dev/null +++ b/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7708 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py new file mode 100644 index 00000000000..5cbdebdc9ef --- /dev/null +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7778 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7843_slotnum/conftest.py b/tests/amsterdam/eip7843_slotnum/conftest.py new file mode 100644 index 00000000000..6e43f18fb35 --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7843 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/conftest.py b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py new file mode 100644 index 00000000000..3bedcfe2651 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7928 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py index f92a0e003a0..9a252257297 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py @@ -370,3 +370,10 @@ def tx( blob_versioned_hashes=blob_versioned_hashes, error=tx_error, ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py new file mode 100644 index 00000000000..de93abfcfef --- /dev/null +++ b/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7997 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py b/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py new file mode 100644 index 00000000000..d8c781bb489 --- /dev/null +++ b/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8024 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..2d16c275db4 --- /dev/null +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8037 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..cd29eb50dcb --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8038 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py index cb2a757494c..dc3ee7c3ecd 100644 --- a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py +++ b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py @@ -148,3 +148,10 @@ def txs( ) txs.append(network_wrapped_tx) return txs + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py index e17cee467fc..7a42ba76785 100644 --- a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py +++ b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py @@ -1,8 +1,17 @@ """Fixtures for the EIP-8282 builder execution request tests.""" +import pytest + from ...common.system_contract_request_fixtures import ( blocks, # noqa: F401 included_requests, # noqa: F401 system_contract_interactions_per_block_copy, # noqa: F401 timestamp, # noqa: F401 ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/monad_nine/mip4_checkreservebalance/test_transfers.py b/tests/monad_nine/mip4_checkreservebalance/test_transfers.py index ec43d75dd91..c217fd2cd6e 100644 --- a/tests/monad_nine/mip4_checkreservebalance/test_transfers.py +++ b/tests/monad_nine/mip4_checkreservebalance/test_transfers.py @@ -1462,6 +1462,15 @@ def test_contract_unrestricted_within_initcode( new_balance = balance - value + Spec.RESERVE_BALANCE + # EIP-8246 stops the end-of-transaction cleanup from burning the + # balance, so the refill that follows SELFDESTRUCT is left behind on + # a cleared account. + selfdestructed_account = ( + Account(nonce=0, balance=Spec.RESERVE_BALANCE, code=b"", storage={}) + if fork.is_eip_enabled(8246) + else None + ) + txs = [tx_1] if new_address_pre_funded: txs.insert( @@ -1491,7 +1500,7 @@ def test_contract_unrestricted_within_initcode( balance=new_balance, code=deploy_code ) if not selfdestruct - else None, + else selfdestructed_account, target: Account(balance=value) if value != 0 else None, # SELFDESTRUCT runs during initcode (before factory # refill), so it sends balance - value only. From 08ac088efc697c52b9843427b5228b2c5fdaf04b Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:15:37 +0000 Subject: [PATCH 11/44] test(selfdestruct): expect the Burn logs a fork without EIP-8246 emits A fork can enable EIP-7708 without EIP-8246 and still burn a swept or stranded balance, which the sweep helper now accounts for. Co-Authored-By: Claude --- .../eip7708_eth_transfer_logs/spec.py | 13 ++ .../conftest.py | 7 - .../conftest.py | 7 - .../eip6780_selfdestruct/test_selfdestruct.py | 144 +++++++++++------- 4 files changed, 101 insertions(+), 70 deletions(-) diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/spec.py b/tests/amsterdam/eip7708_eth_transfer_logs/spec.py index 56a42e2850a..11ba0ebf214 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/spec.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/spec.py @@ -30,6 +30,7 @@ class Spec: TRANSFER_TOPIC: Hash = Hash( keccak256(b"Transfer(address,address,uint256)") ) + BURN_TOPIC: Hash = Hash(keccak256(b"Burn(address,uint256)")) def transfer_log( @@ -45,3 +46,15 @@ def transfer_log( ], data=Bytes(amount.to_bytes(32, "big")), ) + + +def burn_log(contract_address: Address, amount: int) -> TransactionLog: + """Create an expected Burn log for EIP-7708.""" + return TransactionLog( + address=Spec.SYSTEM_ADDRESS, + topics=[ + Spec.BURN_TOPIC, + Hash(bytes(contract_address).rjust(32, b"\x00")), + ], + data=Bytes(amount.to_bytes(32, "big")), + ) diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py b/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py index b4d596bdc52..78cb66ed14d 100644 --- a/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py +++ b/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py @@ -4,13 +4,6 @@ from execution_testing import Address, Alloc, Bytecode, Fork, Op -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) - - @pytest.fixture def max_code_size_contract( pre: Alloc, diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py b/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py index c02d771f5d2..104a73e464a 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py @@ -22,13 +22,6 @@ from ...cancun.eip4844_blobs.spec import Spec as EIP_4844_Spec -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) - - @pytest.fixture def to( request: pytest.FixtureRequest, diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py index 24a80ceca9e..21b2734529f 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py @@ -24,12 +24,16 @@ StateTestFiller, Storage, Transaction, + TransactionLog, TransactionReceipt, compute_create_address, ) from execution_testing.forks import MONAD_EIGHT, Cancun -from tests.amsterdam.eip7708_eth_transfer_logs.spec import transfer_log +from tests.amsterdam.eip7708_eth_transfer_logs.spec import ( + burn_log, + transfer_log, +) REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md" REFERENCE_SPEC_VERSION = "1b6a0e94cc47e859b9866e570391cf37dc55059a" @@ -52,6 +56,25 @@ PRE_DEPLOY_CONTRACT_3 = "pre_deploy_contract_3" +def sweep_log( + fork: Fork, + contract_address: Address, + recipient: Address, + amount: int, +) -> TransactionLog | None: + """ + Return the EIP-7708 log a SELFDESTRUCT sweep emits, if any. + + A sweep to another account transfers. A sweep to self burns the + balance, until EIP-8246 keeps it and emits nothing. + """ + if recipient != contract_address: + return transfer_log(contract_address, recipient, amount) + if fork.is_eip_enabled(8246): + return None + return burn_log(contract_address, amount) + + @pytest.fixture def eip_enabled(fork: Fork) -> bool: """Whether the EIP is enabled or not.""" @@ -324,14 +347,14 @@ def test_create_selfdestruct_same_tx( # SELFDESTRUCT emits a Transfer log to a different address, or a Burn # log when sending to self (contract was created in this tx). if selfdestruct_contract_current_balance > 0: - if sendall_recipient != selfdestruct_contract_address: - expected_logs_after_tx_value.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient, - selfdestruct_contract_current_balance, - ) - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, + ) + if sweep is not None: + expected_logs_after_tx_value.append(sweep) # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: @@ -569,13 +592,14 @@ def test_self_destructing_initcode( ) # Initcode SELFDESTRUCT sends pre-existing balance to the recipient. if selfdestruct_contract_initial_balance > 0: - expected_logs.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient_addresses[0], - selfdestruct_contract_initial_balance, - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient_addresses[0], + selfdestruct_contract_initial_balance, ) + if sweep is not None: + expected_logs.append(sweep) # CALLs to the destroyed contract transfer ETH to it. for i in range(call_times): if i > 0: @@ -584,6 +608,12 @@ def test_self_destructing_initcode( entry_code_address, selfdestruct_contract_address, i ) ) + # Whatever the calls left on the account is burned when the + # account is deleted, until EIP-8246 keeps it. + if entry_code_balance > 0 and not fork.is_eip_enabled(8246): + expected_logs.append( + burn_log(selfdestruct_contract_address, entry_code_balance) + ) tx.expected_receipt = TransactionReceipt(logs=expected_logs) state_test(pre=pre, post=post, tx=tx) @@ -657,13 +687,14 @@ def test_self_destructing_initcode_create_tx( transfer_log(sender, selfdestruct_contract_address, tx_value) ) if sendall_amount > 0: - expected_logs.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient_addresses[0], - sendall_amount, - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient_addresses[0], + sendall_amount, ) + if sweep is not None: + expected_logs.append(sweep) tx.expected_receipt = TransactionReceipt(logs=expected_logs) state_test(pre=pre, post=post, tx=tx) @@ -786,17 +817,14 @@ def test_recreate_self_destructed_contract_different_txs( # address with 0 balance (destroyed+cleared), so no log. tx_logs: list = [] if i == 0 and selfdestruct_contract_initial_balance > 0: - if ( - sendall_recipient_addresses[0] - != selfdestruct_contract_address - ): - tx_logs.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient_addresses[0], - selfdestruct_contract_initial_balance, - ) - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient_addresses[0], + selfdestruct_contract_initial_balance, + ) + if sweep is not None: + tx_logs.append(sweep) expected_receipt = TransactionReceipt(logs=tx_logs) txs.append( Transaction( @@ -988,13 +1016,14 @@ def test_selfdestruct_pre_existing( sendall_recipient != selfdestruct_contract_address and selfdestruct_contract_current_balance > 0 ): - expected_logs_after_tx_value.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient, - selfdestruct_contract_current_balance, - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, ) + if sweep is not None: + expected_logs_after_tx_value.append(sweep) # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: @@ -1192,13 +1221,14 @@ def test_selfdestruct_created_same_block_different_tx( ) running_balance += i if running_balance > 0: - tx2_logs.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient_addresses[0], - running_balance, - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient_addresses[0], + running_balance, ) + if sweep is not None: + tx2_logs.append(sweep) running_balance = 0 tx2_receipt = TransactionReceipt(logs=tx2_logs) @@ -1384,13 +1414,14 @@ def test_calling_from_new_contract_to_pre_existing_contract( ) running_balance += i if running_balance > 0: - expected_logs.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient_addresses[0], - running_balance, - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient_addresses[0], + running_balance, ) + if sweep is not None: + expected_logs.append(sweep) running_balance = 0 tx.expected_receipt = TransactionReceipt(logs=expected_logs) @@ -1728,13 +1759,14 @@ def test_create_selfdestruct_same_tx_increased_nonce( # (SELF_ADDRESS is not parametrized here), so a Transfer log is # emitted whenever the contract has a nonzero balance. if selfdestruct_contract_current_balance > 0: - expected_logs_after_tx_value.append( - transfer_log( - selfdestruct_contract_address, - sendall_recipient, - selfdestruct_contract_current_balance, - ) + sweep = sweep_log( + fork, + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, ) + if sweep is not None: + expected_logs_after_tx_value.append(sweep) # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: From ea4b01d22c5329072bf2dea2be6e18a77aa8c60f Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:58:59 +0000 Subject: [PATCH 12/44] feat(monad_next): adopt EIP-7981 increase access list cost MONAD_NEXT charges access list bytes at its own floor token rate, ordered after Amsterdam by succession, and releases as the monad_eip7981 feature. Co-Authored-By: Claude --- .github/configs/feature.yaml | 4 +++ .../pytest_commands/plugins/forks/forks.py | 4 ++- .../src/execution_testing/forks/base_fork.py | 20 +++++++++-- .../execution_testing/forks/forks/forks.py | 23 ++++++++---- src/ethereum/forks/monad_next/transactions.py | 36 ++++++++++++++++--- .../conftest.py | 10 ++++++ .../eip7708_eth_transfer_logs/conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ tests/amsterdam/eip7843_slotnum/conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ .../conftest.py | 7 ++++ .../test_access_list_cost.py | 5 +-- .../test_fork_transition.py | 16 +++++---- .../conftest.py | 10 ++++++ .../eip8024_dupn_swapn_exchange/conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ .../conftest.py | 10 ++++++ .../eip8070_sparse_blobpool/conftest.py | 7 ++++ .../eip8246_selfdestruct_no_burn/conftest.py | 10 ++++++ .../conftest.py | 9 +++++ 20 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py create mode 100644 tests/amsterdam/eip7708_eth_transfer_logs/conftest.py create mode 100644 tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py create mode 100644 tests/amsterdam/eip7843_slotnum/conftest.py create mode 100644 tests/amsterdam/eip7928_block_level_access_lists/conftest.py create mode 100644 tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py create mode 100644 tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py create mode 100644 tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py create mode 100644 tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 7fca18fd9ef..7849a5e116a 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -11,3 +11,7 @@ monad_runloop: evm-type: eels # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=30143 --monad-runloop -k "not invalid_header" + +monad_eip7981: + evm-type: eels + fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py index 6075481806c..29ef4dc86e3 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py @@ -1265,7 +1265,9 @@ def _process_with_marker_args( "Missing fork argument with 'valid_at_transition_to' marker." ) - if len(forks) > 1: + # A single EIP argument expands to one fork per enabling fork, so + # the limit is on the arguments rather than on the resolved forks. + if len(fork_args) > 1: raise Exception( "Too many forks specified to 'valid_at_transition_to' marker." ) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index f842878c42f..808d94b6045 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -333,12 +333,23 @@ def _maybe_transitioned(fork_cls: "BaseForkMeta") -> "BaseForkMeta": @staticmethod def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: """ - Check if `a` is a subclass of `b`, taking fork transitions into - account. + Check if `a` is a subclass of `b`, taking fork transitions and + declared succession into account. + + A fork can follow another fork it does not inherit from, which + places it after that fork (and after everything that fork comes + after) in the fork order without adopting its behavior. """ a = BaseForkMeta._maybe_transitioned(a) b = BaseForkMeta._maybe_transitioned(b) - return issubclass(a, b) + if issubclass(a, b): + return True + followed = getattr(a, "_follows", None) + while followed is not None: + if issubclass(followed, b): + return True + followed = followed._follows + return False def __gt__(cls, other: "BaseForkMeta") -> bool: """Compare if a fork is newer than some other fork (cls > other).""" @@ -381,6 +392,7 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True + _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -396,6 +408,7 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, + follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -410,6 +423,7 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name + cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index ecf3b02f3ae..04fde59cc0c 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1805,12 +1805,6 @@ def _calculate_sstore_gas_mip8( return gas_cost -class MONAD_NEXT(MONAD_TEN): # noqa: N801 - """MONAD_NEXT fork, a placeholder identical to MONAD_TEN.""" - - pass - - class BPO1( Osaka, bpo_fork=True, @@ -1904,3 +1898,20 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: limit. """ return True + + +class MONAD_NEXT( # noqa: N801 + eips.EIP7981, + MONAD_TEN, + follows=Amsterdam, +): + """ + MONAD_NEXT fork. + + Amsterdam-based successor to MONAD_TEN, adopting the EIP-7981 + changes. The Amsterdam changes it does not adopt stay out of the + fork by not being inherited at all; the fork order still places + MONAD_NEXT after Amsterdam through `follows`. + """ + + pass diff --git a/src/ethereum/forks/monad_next/transactions.py b/src/ethereum/forks/monad_next/transactions.py index 24bb4880a74..71bce9409f3 100644 --- a/src/ethereum/forks/monad_next/transactions.py +++ b/src/ethereum/forks/monad_next/transactions.py @@ -30,6 +30,22 @@ TX_MAX_GAS_LIMIT = Uint(30_000_000) +ACCESS_LIST_ADDRESS_FLOOR_TOKENS = Uint(80) +""" +Floor data tokens contributed by a single access list address per +[EIP-7981]. + +[EIP-7981]: https://eips.ethereum.org/EIPS/eip-7981 +""" + +ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS = Uint(128) +""" +Floor data tokens contributed by a single access list storage key per +[EIP-7981]. + +[EIP-7981]: https://eips.ethereum.org/EIPS/eip-7981 +""" + @final @slotted_freezable @@ -587,10 +603,6 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: num_non_zeros = ulen(tx.data) - num_zeros tokens_in_calldata = num_zeros + num_non_zeros * Uint(4) - # EIP-7623 floor price (note: no EVM costs) - calldata_floor_gas_cost = ( - tokens_in_calldata * GasCosts.TX_DATA_TOKEN_FLOOR + GasCosts.TX_BASE - ) data_cost = tokens_in_calldata * GasCosts.TX_DATA_TOKEN_STANDARD @@ -600,12 +612,28 @@ def calculate_intrinsic_cost(tx: Transaction) -> Tuple[Uint, Uint]: create_cost = Uint(0) access_list_cost = Uint(0) + tokens_in_access_list = Uint(0) if has_access_list(tx): for access in tx.access_list: access_list_cost += GasCosts.TX_ACCESS_LIST_ADDRESS access_list_cost += ( ulen(access.slots) * GasCosts.TX_ACCESS_LIST_STORAGE_KEY ) + tokens_in_access_list += ACCESS_LIST_ADDRESS_FLOOR_TOKENS + tokens_in_access_list += ( + ulen(access.slots) * ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS + ) + + # Data token floor cost for access list bytes. + access_list_cost += tokens_in_access_list * GasCosts.TX_DATA_TOKEN_FLOOR + + # Total floor tokens. + total_floor_tokens = tokens_in_calldata + tokens_in_access_list + + # EIP-7623 floor price (note: no EVM costs) + calldata_floor_gas_cost = ( + total_floor_tokens * GasCosts.TX_DATA_TOKEN_FLOOR + GasCosts.TX_BASE + ) auth_cost = Uint(0) if isinstance(tx, SetCodeTransaction): diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py new file mode 100644 index 00000000000..de52075d396 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-2780 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py b/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py new file mode 100644 index 00000000000..fb1d9465b3e --- /dev/null +++ b/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7708 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py new file mode 100644 index 00000000000..5cbdebdc9ef --- /dev/null +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7778 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7843_slotnum/conftest.py b/tests/amsterdam/eip7843_slotnum/conftest.py new file mode 100644 index 00000000000..6e43f18fb35 --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7843 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/conftest.py b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py new file mode 100644 index 00000000000..3bedcfe2651 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7928 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py index f92a0e003a0..9a252257297 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py @@ -370,3 +370,10 @@ def tx( blob_versioned_hashes=blob_versioned_hashes, error=tx_error, ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py index 2d093e6d0bb..981617c0ec0 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py @@ -124,10 +124,11 @@ def test_access_list_token_calculation( expected_floor_cost = ( expected_floor_tokens * gas_costs.TX_DATA_TOKEN_FLOOR + gas_costs.TX_BASE + ) + if fork.is_eip_enabled(2780): # EIP-2780 anchors the floor on the decomposed intrinsic base; the # tx targets a non-self account, adding the recipient-access charge. - + gas_costs.COLD_ACCOUNT_ACCESS - ) + expected_floor_cost += gas_costs.COLD_ACCOUNT_ACCESS actual_floor_cost = fork.transaction_data_floor_cost_calculator()( data=b"", access_list=access_list ) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py index 62a899b346a..92b02d7def7 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_fork_transition.py @@ -7,12 +7,13 @@ transition timestamp) and pin the per-transaction gas paid on each side, plus the validity flip for gas limits inside the uplift gap. -The post-fork intrinsic composes three repricings; the hand-derived +The post-fork intrinsic composes up to three repricings; the hand-derived expectations below keep each term explicit so the EIP-7981 surcharge is individually visible: -- EIP-2780 decomposes the flat pre-fork `TX_BASE` into the lowered base - plus the `COLD_ACCOUNT_ACCESS` recipient charge. +- EIP-2780, on the forks that enable it, decomposes the flat pre-fork + `TX_BASE` into the lowered base plus the `COLD_ACCOUNT_ACCESS` + recipient charge. - EIP-8038 reprices the per-address and per-storage-key access list charges to the fork's cold access costs. - EIP-7981 adds four floor tokens per access list byte, charged at @@ -116,11 +117,12 @@ def test_access_list_intrinsic_across_amsterdam_transition( ) expected_post = ( post_costs.TX_BASE - + post_costs.COLD_ACCOUNT_ACCESS + addresses * post_costs.TX_ACCESS_LIST_ADDRESS + total_keys * post_costs.TX_ACCESS_LIST_STORAGE_KEY + surcharge ) + if post_fork.is_eip_enabled(2780): + expected_post += post_costs.COLD_ACCOUNT_ACCESS timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] expected_intrinsics = [expected_pre, expected_post] @@ -322,10 +324,10 @@ def test_access_list_floor_across_amsterdam_transition( post_costs.TX_DATA_TOKEN_STANDARD ) + calculate_access_list_floor_tokens(access_list) expected_post = int( - post_costs.TX_BASE - + post_costs.COLD_ACCOUNT_ACCESS - + post_tokens * post_costs.TX_DATA_TOKEN_FLOOR + post_costs.TX_BASE + post_tokens * post_costs.TX_DATA_TOKEN_FLOOR ) + if post_fork.is_eip_enabled(2780): + expected_post += int(post_costs.COLD_ACCOUNT_ACCESS) timestamps = [PRE_FORK_TIMESTAMP, POST_FORK_TIMESTAMP] expected_floors = [expected_pre, expected_post] diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py new file mode 100644 index 00000000000..de93abfcfef --- /dev/null +++ b/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7997 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py b/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py new file mode 100644 index 00000000000..d8c781bb489 --- /dev/null +++ b/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8024 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..2d16c275db4 --- /dev/null +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8037 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..cd29eb50dcb --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8038 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py index cb2a757494c..dc3ee7c3ecd 100644 --- a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py +++ b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py @@ -148,3 +148,10 @@ def txs( ) txs.append(network_wrapped_tx) return txs + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py b/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py new file mode 100644 index 00000000000..4fb53dba402 --- /dev/null +++ b/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8246 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py index e17cee467fc..7a42ba76785 100644 --- a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py +++ b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py @@ -1,8 +1,17 @@ """Fixtures for the EIP-8282 builder execution request tests.""" +import pytest + from ...common.system_contract_request_fixtures import ( blocks, # noqa: F401 included_requests, # noqa: F401 system_contract_interactions_per_block_copy, # noqa: F401 timestamp, # noqa: F401 ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) From d90d9f4e856c92b9024cca27d28616fa2e03bdfa Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:17:13 +0000 Subject: [PATCH 13/44] feat(monad_next): adopt EIP-7997 deterministic factory predeploy MONAD_NEXT carries the factory in its pre-allocation, ordered after Amsterdam by succession, and releases as the monad_eip7997 feature. Co-Authored-By: Claude --- .github/configs/feature.yaml | 4 ++++ .../pytest_commands/plugins/forks/forks.py | 4 +++- .../src/execution_testing/forks/base_fork.py | 20 +++++++++++++--- .../execution_testing/forks/forks/forks.py | 23 ++++++++++++++----- .../conftest.py | 10 ++++++++ .../eip7708_eth_transfer_logs/conftest.py | 10 ++++++++ .../conftest.py | 10 ++++++++ tests/amsterdam/eip7843_slotnum/conftest.py | 10 ++++++++ .../conftest.py | 10 ++++++++ .../conftest.py | 7 ++++++ .../test_factory.py | 12 ++++++++-- .../eip8024_dupn_swapn_exchange/conftest.py | 10 ++++++++ .../conftest.py | 10 ++++++++ .../conftest.py | 10 ++++++++ .../eip8070_sparse_blobpool/conftest.py | 7 ++++++ .../eip8246_selfdestruct_no_burn/conftest.py | 10 ++++++++ .../conftest.py | 9 ++++++++ 17 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py create mode 100644 tests/amsterdam/eip7708_eth_transfer_logs/conftest.py create mode 100644 tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py create mode 100644 tests/amsterdam/eip7843_slotnum/conftest.py create mode 100644 tests/amsterdam/eip7928_block_level_access_lists/conftest.py create mode 100644 tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py create mode 100644 tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py create mode 100644 tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py create mode 100644 tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 7fca18fd9ef..ceb478c56b3 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -11,3 +11,7 @@ monad_runloop: evm-type: eels # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=30143 --monad-runloop -k "not invalid_header" + +monad_eip7997: + evm-type: eels + fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py index 6075481806c..29ef4dc86e3 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py @@ -1265,7 +1265,9 @@ def _process_with_marker_args( "Missing fork argument with 'valid_at_transition_to' marker." ) - if len(forks) > 1: + # A single EIP argument expands to one fork per enabling fork, so + # the limit is on the arguments rather than on the resolved forks. + if len(fork_args) > 1: raise Exception( "Too many forks specified to 'valid_at_transition_to' marker." ) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index f842878c42f..808d94b6045 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -333,12 +333,23 @@ def _maybe_transitioned(fork_cls: "BaseForkMeta") -> "BaseForkMeta": @staticmethod def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: """ - Check if `a` is a subclass of `b`, taking fork transitions into - account. + Check if `a` is a subclass of `b`, taking fork transitions and + declared succession into account. + + A fork can follow another fork it does not inherit from, which + places it after that fork (and after everything that fork comes + after) in the fork order without adopting its behavior. """ a = BaseForkMeta._maybe_transitioned(a) b = BaseForkMeta._maybe_transitioned(b) - return issubclass(a, b) + if issubclass(a, b): + return True + followed = getattr(a, "_follows", None) + while followed is not None: + if issubclass(followed, b): + return True + followed = followed._follows + return False def __gt__(cls, other: "BaseForkMeta") -> bool: """Compare if a fork is newer than some other fork (cls > other).""" @@ -381,6 +392,7 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True + _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -396,6 +408,7 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, + follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -410,6 +423,7 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name + cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index ecf3b02f3ae..55e96539cd7 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1805,12 +1805,6 @@ def _calculate_sstore_gas_mip8( return gas_cost -class MONAD_NEXT(MONAD_TEN): # noqa: N801 - """MONAD_NEXT fork, a placeholder identical to MONAD_TEN.""" - - pass - - class BPO1( Osaka, bpo_fork=True, @@ -1904,3 +1898,20 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: limit. """ return True + + +class MONAD_NEXT( # noqa: N801 + eips.EIP7997, + MONAD_TEN, + follows=Amsterdam, +): + """ + MONAD_NEXT fork. + + Amsterdam-based successor to MONAD_TEN, adopting the EIP-7997 + changes. The Amsterdam changes it does not adopt stay out of the + fork by not being inherited at all; the fork order still places + MONAD_NEXT after Amsterdam through `follows`. + """ + + pass diff --git a/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py new file mode 100644 index 00000000000..de52075d396 --- /dev/null +++ b/tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-2780 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py b/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py new file mode 100644 index 00000000000..fb1d9465b3e --- /dev/null +++ b/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7708 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py new file mode 100644 index 00000000000..5cbdebdc9ef --- /dev/null +++ b/tests/amsterdam/eip7778_block_gas_accounting_without_refunds/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7778 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7843_slotnum/conftest.py b/tests/amsterdam/eip7843_slotnum/conftest.py new file mode 100644 index 00000000000..6e43f18fb35 --- /dev/null +++ b/tests/amsterdam/eip7843_slotnum/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7843 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/conftest.py b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py new file mode 100644 index 00000000000..3bedcfe2651 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-7928 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py index f92a0e003a0..9a252257297 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py @@ -370,3 +370,10 @@ def tx( blob_versioned_hashes=blob_versioned_hashes, error=tx_error, ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_factory.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_factory.py index bd4f9c3031d..9bb208616c2 100644 --- a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_factory.py +++ b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_factory.py @@ -30,6 +30,7 @@ compute_create2_address, keccak256, ) +from execution_testing.forks import MONAD_EIGHT from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 from .spec import Spec, ref_spec_7997 @@ -538,6 +539,7 @@ def test_factory_receives_balance_via_selfdestruct( def test_factory_via_eip7702_delegation( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """ An EOA delegates its code to the factory via an EIP-7702 @@ -549,6 +551,10 @@ def test_factory_via_eip7702_delegation( auth_signer = pre.fund_eoa() auth_signer_nonce = auth_signer.nonce + # Monad forbids the `CREATE2` of a delegated account's context, so + # the factory reverts there instead of deploying. + factory_reverts = fork >= MONAD_EIGHT + salt = 0x42 runtime_code = Op.PUSH1(0x01) + Op.PUSH1(0x00) + Op.RETURN initcode = Initcode(deploy_code=runtime_code) @@ -586,10 +592,12 @@ def test_factory_via_eip7702_delegation( ), post={ auth_signer: Account( - nonce=auth_signer_nonce + 2, + nonce=auth_signer_nonce + (1 if factory_reverts else 2), code=Spec7702.delegation_designation(Address(FACTORY)), ), - expected_address: Account(nonce=1, code=bytes(runtime_code)), + expected_address: Account.NONEXISTENT + if factory_reverts + else Account(nonce=1, code=bytes(runtime_code)), FACTORY: Account( nonce=1, balance=0, diff --git a/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py b/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py new file mode 100644 index 00000000000..d8c781bb489 --- /dev/null +++ b/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8024 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..2d16c275db4 --- /dev/null +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8037 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py new file mode 100644 index 00000000000..cd29eb50dcb --- /dev/null +++ b/tests/amsterdam/eip8038_state_access_gas_cost_increase/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8038 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py index cb2a757494c..dc3ee7c3ecd 100644 --- a/tests/amsterdam/eip8070_sparse_blobpool/conftest.py +++ b/tests/amsterdam/eip8070_sparse_blobpool/conftest.py @@ -148,3 +148,10 @@ def txs( ) txs.append(network_wrapped_tx) return txs + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py b/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py new file mode 100644 index 00000000000..4fb53dba402 --- /dev/null +++ b/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py @@ -0,0 +1,10 @@ +"""Pytest (plugin) definitions local to EIP-8246 tests.""" + +import pytest + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) diff --git a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py index e17cee467fc..7a42ba76785 100644 --- a/tests/amsterdam/eip8282_builder_execution_requests/conftest.py +++ b/tests/amsterdam/eip8282_builder_execution_requests/conftest.py @@ -1,8 +1,17 @@ """Fixtures for the EIP-8282 builder execution request tests.""" +import pytest + from ...common.system_contract_request_fixtures import ( blocks, # noqa: F401 included_requests, # noqa: F401 system_contract_interactions_per_block_copy, # noqa: F401 timestamp, # noqa: F401 ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + """Mark all tests in this subdir as not valid for Monad forks.""" + metafunc.definition.add_marker( + pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) + ) From bc45cc4ea190dc8a444327d47c98b83e2354688d Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:49:27 +0000 Subject: [PATCH 14/44] refactor(forks): declare fork succession as a trait, not a keyword `follows()` sits with the other fork traits, so `__init_subclass__` is untouched and a successor fork inherits the order without restating it. Co-Authored-By: Claude --- .../src/execution_testing/forks/base_fork.py | 20 ++++++++++++++----- .../execution_testing/forks/forks/forks.py | 6 ++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 808d94b6045..38dea7ca65c 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -344,11 +344,13 @@ def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: b = BaseForkMeta._maybe_transitioned(b) if issubclass(a, b): return True - followed = getattr(a, "_follows", None) + # The metaclass sees its instances as plain classes, so the + # trait is reached through a cast, as elsewhere in this class. + followed = cast(Type["BaseFork"], a).follows() while followed is not None: if issubclass(followed, b): return True - followed = followed._follows + followed = followed.follows() return False def __gt__(cls, other: "BaseForkMeta") -> bool: @@ -392,7 +394,6 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True - _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -408,7 +409,6 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, - follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -423,7 +423,6 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name - cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name @@ -1421,6 +1420,17 @@ def enabling_forks(cls) -> Set[Type["BaseFork"]]: raise Exception(f"Class {cls.__name__} is not an EIP.") return cls._enabling_forks + @classmethod + def follows(cls) -> Type["BaseFork"] | None: + """ + Return the fork this one comes after without inheriting it. + + A fork that reuses another lineage's ordering overrides this; + comparisons then place it after that fork, and after everything + that fork comes after, while its behavior stays its own. + """ + return None + @classmethod def parent(cls) -> Type["BaseFork"] | None: """Return the parent fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 618ee8a0000..52c47fcb45b 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1903,7 +1903,6 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: class MONAD_NEXT( # noqa: N801 eips.EIP8246, MONAD_TEN, - follows=Amsterdam, ): """ MONAD_NEXT fork. @@ -1914,4 +1913,7 @@ class MONAD_NEXT( # noqa: N801 MONAD_NEXT after Amsterdam through `follows`. """ - pass + @classmethod + def follows(cls) -> type[BaseFork] | None: + """MONAD_NEXT comes after Amsterdam without inheriting it.""" + return Amsterdam From 4909497420c98e20708d6f610f727df209181c82 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:50:59 +0000 Subject: [PATCH 15/44] refactor(forks): declare fork succession as a trait, not a keyword `follows()` sits with the other fork traits, so `__init_subclass__` is untouched and a successor fork inherits the order without restating it. Co-Authored-By: Claude --- .../src/execution_testing/forks/base_fork.py | 20 ++++++++++++++----- .../execution_testing/forks/forks/forks.py | 6 +++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 4767d361e35..30d58136385 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -344,11 +344,13 @@ def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: b = BaseForkMeta._maybe_transitioned(b) if issubclass(a, b): return True - followed = getattr(a, "_follows", None) + # The metaclass sees its instances as plain classes, so the + # trait is reached through a cast, as elsewhere in this class. + followed = cast(Type["BaseFork"], a).follows() while followed is not None: if issubclass(followed, b): return True - followed = followed._follows + followed = followed.follows() return False def __gt__(cls, other: "BaseForkMeta") -> bool: @@ -392,7 +394,6 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True - _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -408,7 +409,6 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, - follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -423,7 +423,6 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name - cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name @@ -1433,6 +1432,17 @@ def enabling_forks(cls) -> Set[Type["BaseFork"]]: raise Exception(f"Class {cls.__name__} is not an EIP.") return cls._enabling_forks + @classmethod + def follows(cls) -> Type["BaseFork"] | None: + """ + Return the fork this one comes after without inheriting it. + + A fork that reuses another lineage's ordering overrides this; + comparisons then place it after that fork, and after everything + that fork comes after, while its behavior stays its own. + """ + return None + @classmethod def parent(cls) -> Type["BaseFork"] | None: """Return the parent fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index e3e4ffe4932..974cd31b395 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1910,7 +1910,6 @@ class MONAD_NEXT( # noqa: N801 eips.EIP7843, eips.EIP8024, MONAD_TEN, - follows=Amsterdam, ): """ MONAD_NEXT fork. @@ -1921,6 +1920,11 @@ class MONAD_NEXT( # noqa: N801 order still places MONAD_NEXT after Amsterdam through `follows`. """ + @classmethod + def follows(cls) -> type[BaseFork] | None: + """MONAD_NEXT comes after Amsterdam without inheriting it.""" + return Amsterdam + @classmethod def header_bal_hash_required(cls) -> bool: """ From 1faef38e4baab1bd12fc049f4334962c7fba4b41 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:51:08 +0000 Subject: [PATCH 16/44] refactor(forks): declare fork succession as a trait, not a keyword `follows()` sits with the other fork traits, so `__init_subclass__` is untouched and a successor fork inherits the order without restating it. Co-Authored-By: Claude --- .../src/execution_testing/forks/base_fork.py | 20 ++++++++++++++----- .../execution_testing/forks/forks/forks.py | 6 +++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 808d94b6045..38dea7ca65c 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -344,11 +344,13 @@ def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: b = BaseForkMeta._maybe_transitioned(b) if issubclass(a, b): return True - followed = getattr(a, "_follows", None) + # The metaclass sees its instances as plain classes, so the + # trait is reached through a cast, as elsewhere in this class. + followed = cast(Type["BaseFork"], a).follows() while followed is not None: if issubclass(followed, b): return True - followed = followed._follows + followed = followed.follows() return False def __gt__(cls, other: "BaseForkMeta") -> bool: @@ -392,7 +394,6 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True - _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -408,7 +409,6 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, - follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -423,7 +423,6 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name - cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name @@ -1421,6 +1420,17 @@ def enabling_forks(cls) -> Set[Type["BaseFork"]]: raise Exception(f"Class {cls.__name__} is not an EIP.") return cls._enabling_forks + @classmethod + def follows(cls) -> Type["BaseFork"] | None: + """ + Return the fork this one comes after without inheriting it. + + A fork that reuses another lineage's ordering overrides this; + comparisons then place it after that fork, and after everything + that fork comes after, while its behavior stays its own. + """ + return None + @classmethod def parent(cls) -> Type["BaseFork"] | None: """Return the parent fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 04fde59cc0c..a4a8218052b 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1903,7 +1903,6 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: class MONAD_NEXT( # noqa: N801 eips.EIP7981, MONAD_TEN, - follows=Amsterdam, ): """ MONAD_NEXT fork. @@ -1914,4 +1913,9 @@ class MONAD_NEXT( # noqa: N801 MONAD_NEXT after Amsterdam through `follows`. """ + @classmethod + def follows(cls) -> type[BaseFork] | None: + """MONAD_NEXT comes after Amsterdam without inheriting it.""" + return Amsterdam + pass From 7916fb731a5a9020489a7536733ae275cac93103 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:51:08 +0000 Subject: [PATCH 17/44] refactor(forks): declare fork succession as a trait, not a keyword `follows()` sits with the other fork traits, so `__init_subclass__` is untouched and a successor fork inherits the order without restating it. Co-Authored-By: Claude --- .../src/execution_testing/forks/base_fork.py | 20 ++++++++++++++----- .../execution_testing/forks/forks/forks.py | 6 +++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 808d94b6045..38dea7ca65c 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -344,11 +344,13 @@ def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: b = BaseForkMeta._maybe_transitioned(b) if issubclass(a, b): return True - followed = getattr(a, "_follows", None) + # The metaclass sees its instances as plain classes, so the + # trait is reached through a cast, as elsewhere in this class. + followed = cast(Type["BaseFork"], a).follows() while followed is not None: if issubclass(followed, b): return True - followed = followed._follows + followed = followed.follows() return False def __gt__(cls, other: "BaseForkMeta") -> bool: @@ -392,7 +394,6 @@ class BaseFork(ForkOpcodeInterface, metaclass=BaseForkMeta): _fork_by_timestamp: ClassVar[bool] = False _blob_constants: ClassVar[Dict[str, int]] = {} _deployed: ClassVar[bool] = True - _follows: ClassVar[Optional[Type["BaseFork"]]] = None _enabled_eips: ClassVar[Set[int]] = set() _enabling_forks: ClassVar[Set[Type["BaseFork"]]] = set() @@ -408,7 +409,6 @@ def __init_subclass__( transition_tool_name: Optional[str] = None, ignore: bool = False, bpo_fork: bool = False, - follows: Optional[Type["BaseFork"]] = None, ruleset_name: Optional[str] = None, fork_by_timestamp: Optional[bool] = None, deployed: Optional[bool] = None, @@ -423,7 +423,6 @@ def __init_subclass__( forks. """ cls._transition_tool_name = transition_tool_name - cls._follows = follows cls._ignore = ignore cls._bpo_fork = bpo_fork cls._ruleset_name = ruleset_name @@ -1421,6 +1420,17 @@ def enabling_forks(cls) -> Set[Type["BaseFork"]]: raise Exception(f"Class {cls.__name__} is not an EIP.") return cls._enabling_forks + @classmethod + def follows(cls) -> Type["BaseFork"] | None: + """ + Return the fork this one comes after without inheriting it. + + A fork that reuses another lineage's ordering overrides this; + comparisons then place it after that fork, and after everything + that fork comes after, while its behavior stays its own. + """ + return None + @classmethod def parent(cls) -> Type["BaseFork"] | None: """Return the parent fork.""" diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 55e96539cd7..10e722b2f37 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1903,7 +1903,6 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: class MONAD_NEXT( # noqa: N801 eips.EIP7997, MONAD_TEN, - follows=Amsterdam, ): """ MONAD_NEXT fork. @@ -1914,4 +1913,9 @@ class MONAD_NEXT( # noqa: N801 MONAD_NEXT after Amsterdam through `follows`. """ + @classmethod + def follows(cls) -> type[BaseFork] | None: + """MONAD_NEXT comes after Amsterdam without inheriting it.""" + return Amsterdam + pass From cfa37f31a6a9f76e52dbe9a20b0285a5a121ff36 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:52:20 +0000 Subject: [PATCH 18/44] refactor(forks): derive block access list support from the EIP A fork builds the lists when EIP-7928 is enabled, so the trait needs no abstract declaration, no genesis default and no mixin override. Co-Authored-By: Claude --- .../testing/src/execution_testing/forks/base_fork.py | 9 ++++----- .../forks/forks/eips/amsterdam/eip_7928.py | 7 ------- .../testing/src/execution_testing/forks/forks/forks.py | 5 ----- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 30d58136385..21801a25162 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -577,16 +577,15 @@ def header_bal_hash_required(cls) -> bool: pass @classmethod - @abstractmethod def supports_block_access_lists(cls) -> bool: """ Return true if the fork builds block access lists (EIP-7928). - A fork can require the block access list hash header field without - building block access lists (e.g. Monad); the field is then fixed - at zero. + A fork can require the block access list hash header field + without building the lists, and then fixes the field at zero, so + this follows the EIP rather than the header requirement. """ - pass + return cls.is_eip_enabled(7928) @classmethod @abstractmethod diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py index d7c787eed92..1eacd844b21 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py @@ -29,13 +29,6 @@ def header_bal_hash_required(cls) -> bool: """ return True - @classmethod - def supports_block_access_lists(cls) -> bool: - """ - From EIP-7928, blocks build block access lists. - """ - return True - @classmethod def gas_costs(cls) -> GasCosts: """ diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 974cd31b395..348901ef328 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -954,11 +954,6 @@ def header_bal_hash_required(cls) -> bool: """At genesis, header must not contain block access list hash.""" return False - @classmethod - def supports_block_access_lists(cls) -> bool: - """At genesis, no block access lists are built.""" - return False - @classmethod def empty_block_bal_item_count(cls) -> int: """Pre-Amsterdam forks have no block access list.""" From 1a09d2443470df05dd84fac8108c02c1f748b61e Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:08:25 +0000 Subject: [PATCH 19/44] feat(ci): rehearse the monad fixture releases before they are cut Collect and fill each feature on PRs, `from-upstream` and `eips/**`. Co-Authored-By: Claude --- .github/actions/build-fixtures/action.yaml | 6 +- .github/configs/feature.yaml | 5 - .github/scripts/check_release_matrix.py | 124 +++++++++++++++ .github/workflows/check_release.yaml | 174 +++++++++++++++++++++ 4 files changed, 303 insertions(+), 6 deletions(-) create mode 100755 .github/scripts/check_release_matrix.py create mode 100644 .github/workflows/check_release.yaml diff --git a/.github/actions/build-fixtures/action.yaml b/.github/actions/build-fixtures/action.yaml index 36fff598e02..98148e97246 100644 --- a/.github/actions/build-fixtures/action.yaml +++ b/.github/actions/build-fixtures/action.yaml @@ -25,6 +25,9 @@ inputs: evm_ref: description: "Override the t8n tool branch / tag / commit" default: "" + upload: + description: "Upload the filled fixtures. Set to false to rehearse a fill and discard its output." + default: "true" runs: using: "composite" steps: @@ -77,11 +80,12 @@ runs: exit "$EXIT_CODE" fi - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: inputs.upload == 'true' with: name: fixtures_${{ inputs.release_name }} path: fixtures_${{ inputs.release_name }}.tar.gz - name: Upload fixture directory (split) - if: inputs.split_label != '' + if: inputs.upload == 'true' && inputs.split_label != '' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: fixtures__${{ inputs.split_label }} diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 36ab811fdb9..91eed36ca8a 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -1,8 +1,3 @@ -# Unless filling for special features, all features should fill for previous forks (starting from Frontier) too -mainnet: - evm-type: eels - fill-params: --until=BPO4 --generate-all-formats - monad: evm-type: eels fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=143 -k "not invalid_header" diff --git a/.github/scripts/check_release_matrix.py b/.github/scripts/check_release_matrix.py new file mode 100755 index 00000000000..556d5b92bf7 --- /dev/null +++ b/.github/scripts/check_release_matrix.py @@ -0,0 +1,124 @@ +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "pyyaml", +# ] +# /// +""" +Build the job matrix for a fixture release rehearsal. + +Usage: `check_release_matrix.py [features] [branch]`, where `features` +is an optional comma- or space-separated subset of the feature names in +`.github/configs/feature.yaml`. + +With no `features`, an EIP branch rehearses the feature it releases for +its own EIP and nothing else, and every other branch rehearses every +feature. Either way a branch that adds a feature is covered without +touching the workflow. + +Reuse `generate_build_matrix.py` so a rehearsal fills exactly what the +release fills, then flatten the per-feature matrices into the single +`fill_matrix` a `strategy.matrix` consumes. +""" + +import json +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from generate_build_matrix import ( # noqa: E402 + FEATURE_CONFIG, + FORK_RANGES_CONFIG, + build_matrix, + fail, + load_config, +) + +# EIP branches follow `eips//eip-`, with `+` joining the EIP +# numbers a combined branch carries, e.g. `eips/amsterdam/eip-2345+3456`. +EIP_BRANCH_RE = re.compile(r"^eips/[^/]+/eip-([0-9]+(?:\+[0-9]+)*)$") + + +def eip_features(defined: list[str], branch: str) -> list[str]: + """ + Return the features *branch* releases for its own EIPs. + + An EIP branch names its feature after the EIPs it carries, so + `eips/monad_next/eip-7997` releases `monad_eip7997` and + `eips/amsterdam/eip-2345+3456` releases `monad_eip2345+3456`. The + numbers must match in full: a combined branch does not claim the + feature of either EIP on its own, and neither claims the combined + one. Return an empty list for any other branch, and for an EIP + branch that has not declared a feature of its own yet. + """ + match = EIP_BRANCH_RE.match(branch) + if not match: + return [] + numbers = re.compile(rf"eip{re.escape(match.group(1))}(?![0-9+])") + return [name for name in defined if numbers.search(name)] + + +def defined_features(config: dict) -> list[str]: + """Return every feature name in `feature.yaml`, in config order.""" + return [ + name for name, feature in config.items() if isinstance(feature, dict) + ] + + +def select_features(config: dict, requested: str, branch: str) -> list[str]: + """ + Narrow the rehearsal to the requested features. + + An empty request falls back to the features *branch* releases for + its own EIPs, then to every feature: this fork releases all of + them. An unknown name fails the run rather than silently + rehearsing less than was asked for. + """ + defined = defined_features(config) + names = [name for name in requested.replace(",", " ").split() if name] + if names: + unknown = [name for name in names if name not in defined] + if unknown: + fail( + f"unknown feature(s) {', '.join(unknown)}; " + f"{FEATURE_CONFIG} defines {', '.join(defined)}" + ) + return names + from_branch = eip_features(defined, branch) + if from_branch: + print( + f"Branch '{branch}' releases {', '.join(from_branch)}; " + "rehearsing only that.", + file=sys.stderr, + ) + return from_branch + return defined + + +def main() -> None: + """Print the rehearsal's feature list and fill matrix to stdout.""" + requested = sys.argv[1] if len(sys.argv) > 1 else "" + branch = sys.argv[2] if len(sys.argv) > 2 else "" + + config = load_config(FEATURE_CONFIG) + fork_ranges = load_config(FORK_RANGES_CONFIG) or [] + + features = select_features(config, requested, branch) + if not features: + fail(f"{FEATURE_CONFIG} defines no feature") + + matrix: list[dict] = [] + for name in features: + entries, _ = build_matrix(config[name], name, fork_ranges) + matrix.extend(entries) + + print(f"features={json.dumps(features)}") + print(f"fill_matrix={json.dumps(matrix)}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/check_release.yaml b/.github/workflows/check_release.yaml new file mode 100644 index 00000000000..8b1f7447b6f --- /dev/null +++ b/.github/workflows/check_release.yaml @@ -0,0 +1,174 @@ +name: Check Fixture Release + +run-name: ${{ github.event_name == 'workflow_dispatch' && format('Check Fixture Release ({0}) {1}', inputs.depth, inputs.features) || 'Check Fixture Release' }} + +# Rehearse the fixture releases this fork ships, so a fill that a change +# breaks surfaces on the branch that broke it rather than on the next +# release attempt. +# +# Two tiers, both running on every pull request and on pushes to the +# branches that release in parallel with the fork branch, +# `from-upstream` and `eips/**`. +# +# `collect` is cheap: it reads the features the branch releases out of +# `.github/configs/feature.yaml`, builds the release's job matrix +# from it and collects its tests, catching malformed feature entries, +# import errors and parametrization errors in a few minutes. It gates +# `fill`, which is comprehensive: `fill` runs the very fill the release +# runs, via the release's own action, and is the only tier that catches +# a test that fills wrong. A manual dispatch can ask for `collect` +# alone, and can narrow the run to a subset of the features. + +on: + push: + branches: + - from-upstream + - "eips/**" + paths-ignore: + - "**.md" + - "LICENSE*" + - ".gitignore" + - ".vscode/**" + - "whitelist.txt" + - "docs/**" + - "mkdocs.yml" + pull_request: + paths-ignore: + - "**.md" + - "LICENSE*" + - ".gitignore" + - ".vscode/**" + - "whitelist.txt" + - "docs/**" + - "mkdocs.yml" + workflow_dispatch: + inputs: + depth: + description: "full = fill every feature (hours); collect = validate the config and collect the tests (minutes)" + required: true + type: choice + options: [full, collect] + default: full + features: + description: "Features to rehearse, e.g. monad_runloop. Empty = an EIP branch's own feature, or every feature elsewhere." + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} + +permissions: + contents: read + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + features: ${{ steps.matrix.outputs.features }} + fill_matrix: ${{ steps.matrix.outputs.fill_matrix }} + fill: ${{ steps.depth.outputs.fill }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: false + - uses: ./.github/actions/setup-uv + + - name: Select the features to rehearse + id: matrix + shell: bash + env: + INPUT_FEATURES: ${{ inputs.features }} + # The head branch, so a pull request from an EIP branch + # rehearses that branch's feature; `ref_name` is the merge ref + # on a pull request and the branch everywhere else. + BRANCH: ${{ github.head_ref || github.ref_name }} + run: | + # The feature selection and the per-feature release matrix live + # in (and are shared with the release workflow via) + # check_release_matrix.py. + uv run -q .github/scripts/check_release_matrix.py \ + "$INPUT_FEATURES" "$BRANCH" | tee -a "$GITHUB_OUTPUT" + + - name: Decide whether to fill + id: depth + shell: bash + env: + EVENT: ${{ github.event_name }} + DEPTH: ${{ inputs.depth }} + run: | + # Every automatic trigger fills; only a dispatch can ask for + # the cheap tier alone. + if [ "$EVENT" = "workflow_dispatch" ] && [ "$DEPTH" != "full" ] + then + fill=false + else + fill=true + fi + echo "Fill: $fill" + echo "fill=$fill" >> "$GITHUB_OUTPUT" + + collect: + name: collect (${{ matrix.feature }}) + needs: setup + runs-on: ubuntu-24.04 + timeout-minutes: 30 + strategy: + # A rehearsal wants every feature's verdict, not the first failure. + fail-fast: false + matrix: + feature: ${{ fromJson(needs.setup.outputs.features) }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true + - uses: ./.github/actions/setup-uv + - name: Install EEST + run: uv sync --no-progress + + - name: Extract fixture release properties from config + id: properties + run: | + uv run -q .github/scripts/get_release_props.py ${{ matrix.feature }} >> "$GITHUB_OUTPUT" + + - name: Collect the release's tests + shell: bash + run: | + # `fill-params` is interpolated rather than passed through a + # variable so its quoting survives, as in `build-fixtures`. + # Collecting nothing means an empty release, so pytest's exit + # code 5 fails here where a release's fork-range split + # tolerates it. + status=0 + uv run fill --collect-only -q \ + ${{ steps.properties.outputs.fill-params }} > collected.txt \ + || status=$? + tail -n 20 collected.txt + exit "$status" + + fill: + name: fill (${{ matrix.label || matrix.feature }}) + needs: [setup, collect] + if: needs.setup.outputs.fill == 'true' + runs-on: ubuntu-24.04 + # Hosted runners cap a job at six hours regardless; the explicit + # timeout keeps a wedged fill from burning the whole budget. + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.setup.outputs.fill_matrix) }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true + + - uses: ./.github/actions/build-fixtures + with: + release_name: ${{ matrix.feature }} + from_fork: ${{ matrix.from_fork }} + until_fork: ${{ matrix.until_fork }} + split_label: ${{ matrix.label }} + # A rehearsal only reports whether the fill succeeds; keeping + # its fixtures would cost storage for output nothing consumes. + upload: "false" From 74a642d65853689704726d46c5f82fee9b33d6e7 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:48:10 +0000 Subject: [PATCH 20/44] test(amsterdam): select the suites for the EIPs MONAD_NEXT now adopts The merged fork adopts EIP-7708, EIP-7843, EIP-7997, EIP-8024 and EIP-8246, so their suites lose the Monad exclusion; the EIP-7976 hook lands once. Co-Authored-By: Claude --- tests/amsterdam/eip7708_eth_transfer_logs/conftest.py | 10 ---------- tests/amsterdam/eip7843_slotnum/conftest.py | 10 ---------- .../eip7976_increase_calldata_floor_cost/conftest.py | 7 ------- .../conftest.py | 10 ---------- .../amsterdam/eip8024_dupn_swapn_exchange/conftest.py | 10 ---------- .../amsterdam/eip8246_selfdestruct_no_burn/conftest.py | 10 ---------- 6 files changed, 57 deletions(-) delete mode 100644 tests/amsterdam/eip7708_eth_transfer_logs/conftest.py delete mode 100644 tests/amsterdam/eip7843_slotnum/conftest.py delete mode 100644 tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py delete mode 100644 tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py delete mode 100644 tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py b/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py deleted file mode 100644 index fb1d9465b3e..00000000000 --- a/tests/amsterdam/eip7708_eth_transfer_logs/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Pytest (plugin) definitions local to EIP-7708 tests.""" - -import pytest - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) diff --git a/tests/amsterdam/eip7843_slotnum/conftest.py b/tests/amsterdam/eip7843_slotnum/conftest.py deleted file mode 100644 index 6e43f18fb35..00000000000 --- a/tests/amsterdam/eip7843_slotnum/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Pytest (plugin) definitions local to EIP-7843 tests.""" - -import pytest - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py index 279a69479d5..9a252257297 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py @@ -23,13 +23,6 @@ from .helpers import DataTestType, find_floor_cost_threshold -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) - - @pytest.fixture def to( request: pytest.FixtureRequest, diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py deleted file mode 100644 index de93abfcfef..00000000000 --- a/tests/amsterdam/eip7997_deterministic_factory_predeploy/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Pytest (plugin) definitions local to EIP-7997 tests.""" - -import pytest - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) diff --git a/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py b/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py deleted file mode 100644 index d8c781bb489..00000000000 --- a/tests/amsterdam/eip8024_dupn_swapn_exchange/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Pytest (plugin) definitions local to EIP-8024 tests.""" - -import pytest - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) diff --git a/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py b/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py deleted file mode 100644 index 4fb53dba402..00000000000 --- a/tests/amsterdam/eip8246_selfdestruct_no_burn/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Pytest (plugin) definitions local to EIP-8246 tests.""" - -import pytest - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) From d0a9612636276d1a605f6ad3c2e34edb08837850 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:48:18 +0000 Subject: [PATCH 21/44] test(selfdestruct): expect no Burn log now that EIP-8246 joins EIP-7708 EIP-8246 keeps the balance a swept account holds, so the sweep emits a Transfer log or nothing at all. Co-Authored-By: Claude --- .../eip7708_eth_transfer_logs/spec.py | 13 -- .../eip6780_selfdestruct/test_selfdestruct.py | 144 +++++++----------- 2 files changed, 56 insertions(+), 101 deletions(-) diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/spec.py b/tests/amsterdam/eip7708_eth_transfer_logs/spec.py index 11ba0ebf214..56a42e2850a 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/spec.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/spec.py @@ -30,7 +30,6 @@ class Spec: TRANSFER_TOPIC: Hash = Hash( keccak256(b"Transfer(address,address,uint256)") ) - BURN_TOPIC: Hash = Hash(keccak256(b"Burn(address,uint256)")) def transfer_log( @@ -46,15 +45,3 @@ def transfer_log( ], data=Bytes(amount.to_bytes(32, "big")), ) - - -def burn_log(contract_address: Address, amount: int) -> TransactionLog: - """Create an expected Burn log for EIP-7708.""" - return TransactionLog( - address=Spec.SYSTEM_ADDRESS, - topics=[ - Spec.BURN_TOPIC, - Hash(bytes(contract_address).rjust(32, b"\x00")), - ], - data=Bytes(amount.to_bytes(32, "big")), - ) diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py index 21b2734529f..24a80ceca9e 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py @@ -24,16 +24,12 @@ StateTestFiller, Storage, Transaction, - TransactionLog, TransactionReceipt, compute_create_address, ) from execution_testing.forks import MONAD_EIGHT, Cancun -from tests.amsterdam.eip7708_eth_transfer_logs.spec import ( - burn_log, - transfer_log, -) +from tests.amsterdam.eip7708_eth_transfer_logs.spec import transfer_log REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md" REFERENCE_SPEC_VERSION = "1b6a0e94cc47e859b9866e570391cf37dc55059a" @@ -56,25 +52,6 @@ PRE_DEPLOY_CONTRACT_3 = "pre_deploy_contract_3" -def sweep_log( - fork: Fork, - contract_address: Address, - recipient: Address, - amount: int, -) -> TransactionLog | None: - """ - Return the EIP-7708 log a SELFDESTRUCT sweep emits, if any. - - A sweep to another account transfers. A sweep to self burns the - balance, until EIP-8246 keeps it and emits nothing. - """ - if recipient != contract_address: - return transfer_log(contract_address, recipient, amount) - if fork.is_eip_enabled(8246): - return None - return burn_log(contract_address, amount) - - @pytest.fixture def eip_enabled(fork: Fork) -> bool: """Whether the EIP is enabled or not.""" @@ -347,14 +324,14 @@ def test_create_selfdestruct_same_tx( # SELFDESTRUCT emits a Transfer log to a different address, or a Burn # log when sending to self (contract was created in this tx). if selfdestruct_contract_current_balance > 0: - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient, - selfdestruct_contract_current_balance, - ) - if sweep is not None: - expected_logs_after_tx_value.append(sweep) + if sendall_recipient != selfdestruct_contract_address: + expected_logs_after_tx_value.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, + ) + ) # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: @@ -592,14 +569,13 @@ def test_self_destructing_initcode( ) # Initcode SELFDESTRUCT sends pre-existing balance to the recipient. if selfdestruct_contract_initial_balance > 0: - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient_addresses[0], - selfdestruct_contract_initial_balance, + expected_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + selfdestruct_contract_initial_balance, + ) ) - if sweep is not None: - expected_logs.append(sweep) # CALLs to the destroyed contract transfer ETH to it. for i in range(call_times): if i > 0: @@ -608,12 +584,6 @@ def test_self_destructing_initcode( entry_code_address, selfdestruct_contract_address, i ) ) - # Whatever the calls left on the account is burned when the - # account is deleted, until EIP-8246 keeps it. - if entry_code_balance > 0 and not fork.is_eip_enabled(8246): - expected_logs.append( - burn_log(selfdestruct_contract_address, entry_code_balance) - ) tx.expected_receipt = TransactionReceipt(logs=expected_logs) state_test(pre=pre, post=post, tx=tx) @@ -687,14 +657,13 @@ def test_self_destructing_initcode_create_tx( transfer_log(sender, selfdestruct_contract_address, tx_value) ) if sendall_amount > 0: - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient_addresses[0], - sendall_amount, + expected_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + sendall_amount, + ) ) - if sweep is not None: - expected_logs.append(sweep) tx.expected_receipt = TransactionReceipt(logs=expected_logs) state_test(pre=pre, post=post, tx=tx) @@ -817,14 +786,17 @@ def test_recreate_self_destructed_contract_different_txs( # address with 0 balance (destroyed+cleared), so no log. tx_logs: list = [] if i == 0 and selfdestruct_contract_initial_balance > 0: - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient_addresses[0], - selfdestruct_contract_initial_balance, - ) - if sweep is not None: - tx_logs.append(sweep) + if ( + sendall_recipient_addresses[0] + != selfdestruct_contract_address + ): + tx_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + selfdestruct_contract_initial_balance, + ) + ) expected_receipt = TransactionReceipt(logs=tx_logs) txs.append( Transaction( @@ -1016,14 +988,13 @@ def test_selfdestruct_pre_existing( sendall_recipient != selfdestruct_contract_address and selfdestruct_contract_current_balance > 0 ): - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient, - selfdestruct_contract_current_balance, + expected_logs_after_tx_value.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, + ) ) - if sweep is not None: - expected_logs_after_tx_value.append(sweep) # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: @@ -1221,14 +1192,13 @@ def test_selfdestruct_created_same_block_different_tx( ) running_balance += i if running_balance > 0: - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient_addresses[0], - running_balance, + tx2_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + running_balance, + ) ) - if sweep is not None: - tx2_logs.append(sweep) running_balance = 0 tx2_receipt = TransactionReceipt(logs=tx2_logs) @@ -1414,14 +1384,13 @@ def test_calling_from_new_contract_to_pre_existing_contract( ) running_balance += i if running_balance > 0: - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient_addresses[0], - running_balance, + expected_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + running_balance, + ) ) - if sweep is not None: - expected_logs.append(sweep) running_balance = 0 tx.expected_receipt = TransactionReceipt(logs=expected_logs) @@ -1759,14 +1728,13 @@ def test_create_selfdestruct_same_tx_increased_nonce( # (SELF_ADDRESS is not parametrized here), so a Transfer log is # emitted whenever the contract has a nonzero balance. if selfdestruct_contract_current_balance > 0: - sweep = sweep_log( - fork, - selfdestruct_contract_address, - sendall_recipient, - selfdestruct_contract_current_balance, + expected_logs_after_tx_value.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, + ) ) - if sweep is not None: - expected_logs_after_tx_value.append(sweep) # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: From b39d2900d14379e14d2a0020da0bcdad5b6ecd9a Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:48:27 +0000 Subject: [PATCH 22/44] feat(ci): release the merged MONAD_NEXT fixtures under the EIP bundle Name the six adopted EIPs in the fork docstring and the release feature. Co-Authored-By: Claude --- .github/configs/feature.yaml | 14 +------------ src/ethereum/forks/monad_next/__init__.py | 25 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 78bb44a95bc..8ec546059a8 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -7,18 +7,6 @@ monad_runloop: # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=30143 --monad-runloop -k "not invalid_header" -monad_amsterdam: - evm-type: eels - fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" - -monad_eip7981: - evm-type: eels - fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" - -monad_eip7997: - evm-type: eels - fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" - -monad_eip8246: +monad_eip7708+7843+7981+7997+8024+8246: evm-type: eels fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" diff --git a/src/ethereum/forks/monad_next/__init__.py b/src/ethereum/forks/monad_next/__init__.py index 2e989370ce9..fe79f2c61c8 100644 --- a/src/ethereum/forks/monad_next/__init__.py +++ b/src/ethereum/forks/monad_next/__init__.py @@ -1,10 +1,27 @@ """ -MONAD_NEXT fork is a placeholder for upcoming Monad changes. It builds on -MONAD_TEN, adopting EIP-7708, EIP-7843 and EIP-8024 from Amsterdam -together with the Amsterdam block header layout; the [EIP-7928] block -access list hash header slot is carried but always zero. +MONAD_NEXT fork builds on MONAD_TEN, adopting part of Amsterdam. +The Amsterdam changes it does not adopt stay out of the fork entirely. +The [EIP-7928] block access list hash header slot is carried but always +zero, so the header layout matches Amsterdam while no block access list +is built. + +### Changes + +- [EIP-7708: ETH transfers emit a log][EIP-7708] +- [EIP-7843: SLOTNUM][EIP-7843] +- [EIP-7981: Increase Access List Cost][EIP-7981] +- [EIP-7997: Deterministic Factory Predeploy][EIP-7997] +- [EIP-8024: Stack Access Instructions][EIP-8024] +- [EIP-8246: Remove SELFDESTRUCT balance burn][EIP-8246] + +[EIP-7708]: https://eips.ethereum.org/EIPS/eip-7708 +[EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843 [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 +[EIP-7981]: https://eips.ethereum.org/EIPS/eip-7981 +[EIP-7997]: https://eips.ethereum.org/EIPS/eip-7997 +[EIP-8024]: https://eips.ethereum.org/EIPS/eip-8024 +[EIP-8246]: https://eips.ethereum.org/EIPS/eip-8246 """ from ethereum.fork_criteria import ByTimestamp, ForkCriteria From 2ba48ddfba97b3ddb5bf8d69e48eb5d839b75a41 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:24:32 +0000 Subject: [PATCH 23/44] docs(skills): add the EIP branch merge skill Record how the single-EIP adoption branches consolidate onto one fork. Co-Authored-By: Claude --- .claude/commands/merge-eip-branches.md | 150 +++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 .claude/commands/merge-eip-branches.md diff --git a/.claude/commands/merge-eip-branches.md b/.claude/commands/merge-eip-branches.md new file mode 100644 index 00000000000..05c4f068975 --- /dev/null +++ b/.claude/commands/merge-eip-branches.md @@ -0,0 +1,150 @@ +# Merge EIP Adoption Branches + +Consolidate several single-EIP adoption branches into one fork that adopts +all of them. Run this skill before starting such a merge. Each source +branch was built by `/adopt-upstream-eip`, alone, against its own upstream +EIP branch, so no source branch knows what the combined fork does. + +## Inputs + +- The source branches, and `` they all target. +- ``, the branch they merge into. Example: `forks/monad_nine`. +- `` and its upstream integration branch, which already has every + EIP composed. Example: `upstream/forks/amsterdam`. + +## Read First + +`/adopt-upstream-eip` defines the fork shape, the shared-file cost rules, +the step-12 cleanup list and the definition of done. This skill only covers +what the merge adds. + +## 1. Prepare + +```bash +git worktree add -b origin/ +git branch --unset-upstream +``` + +Merge into a fresh worktree, and never check out or rewrite a source +branch. Note which commits already landed on `` after the +source branches were cut: a branch carrying its own copy of one of them +conflicts, and ``'s version wins. + +```bash +git log --oneline $(git merge-base origin/ )..origin/ +``` + +## 2. Merge + +Merge the largest branch first, then the rest, with real merge commits. +Squashing or cherry-picking hides which branch each conflict came from. + +```bash +git merge # once per source branch +``` + +## 3. Resolve the Conflicts Git Reports + +Expect these, in rising order of thought needed: + +- **`.github/configs/feature.yaml`** — every branch appends its own entry + at the same point. Keep all entries for now; step 5 collapses them. +- **`forks.py`, the `` declaration** — each branch declares the + class with one mixin. The merged class lists every mixin ahead of the + parent fork, in ascending EIP order. Drop any dead `pass` left after + `follows()`. +- **CI files already on ``** — keep the base branch's version. +- **`src/ethereum/forks//`** — two EIPs changing the same file. + Resolve against the upstream integration branch, not against either + branch alone. + +What must merge silently: the `follows()` trait, the +`BaseForkMeta._is_subclass_of` walk, the `ValidAtTransitionTo` guard and the +exclusion conftests. They are byte-identical across branches by design. +Check afterwards that one copy of each survived. + +## 4. Find the Silent Merges + +This is the main risk. Two branches editing the same file at different +lines merge cleanly and produce behavior neither branch had. Git reports +nothing. + +Find the candidates from the upstream spec changes: + +```bash +for n in ; do + base=$(git merge-base upstream/eips//eip-$n) + echo "### EIP-$n" + git diff --stat $base..upstream/eips//eip-$n -- src/ethereum/forks/ +done +``` + +Any file listed under two EIPs needs reading in full on the merge branch. +Where one EIP removes what another adds, the merged file must match the +upstream integration branch, which already has both. + +Two shapes seen in practice: + +- One EIP adds a log for a state change a later EIP removes. Git keeps + both: a log for something that no longer happens. +- Two branches add the same conftest hook at different offsets, one after + the imports and one appended. Git keeps both hook definitions in one + module. + +Also read every file the merge touched outside `src/ethereum/forks/monad*` +and `tests/monad*`, whether or not git flagged it. + +## 5. Clean Up the Scaffolding + +Follow `/adopt-upstream-eip` step 12, driven by what the merged fork now +adopts: + +- Delete the exclusion conftest for each adopted EIP's suite. Those suites + must now be selected and pass. +- Collapse the per-branch feature entries into one. When the branch name + follows `eips//eip-+`, the feature name must carry the same + numbers in the same order: `check_release_matrix.py` matches the sequence + as a literal string. Verify it: + + ```bash + python3 .github/scripts/check_release_matrix.py "" + ``` + +- Revert expectation scaffolding written for an intermediate fork shape. A + test edit that only made a fork with EIP-A but without EIP-B green is + scaffolding once B joins. + +To test whether an edit was scaffolding, revert it and diff against the +base branch: + +```bash +git revert --no-commit && git diff origin/ -- +``` + +An empty diff means the edit existed only for the intermediate shape. +Reverting cleanly needs the source branch to have kept scaffolding in its +own commit; a commit mixing scaffolding with permanent change has to be +undone by hand. + +Keep gates on a capability (`fork.is_eip_enabled()`, `fork >= +MONAD_EIGHT`) that stay true on the merged fork. They are permanent. + +## 6. Validate + +Run `/adopt-upstream-eip`'s step-3 shape check, its step-5 `--collect-only` +selection check, then the full-suite fill for the fork. The full suite +matters: the merge changes which suites select the fork. + +Measure any exclusion that survived the cleanup, one suite at a time, by +stripping its hook and filling that suite. State which ones you did not get +to measure. + +## Definition of Done + +- Every source branch merged, with the merge commits kept. +- Each file two EIPs touched read in full and matched against the upstream + integration branch. +- One copy of each shared addition; no duplicated hook or trait. +- Adopted suites selected and passing; their exclusions deleted. +- One feature entry, its name matching the branch's EIP sequence. +- Full-suite fill green, with any unmeasured exclusion named. From 02880af26a882d0d1e8a536ba07c671aaa16fc02 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:31:17 +0000 Subject: [PATCH 24/44] test(eip7997): select the transition tests by EIP, not by Amsterdam The fork-named marker skipped every fork that adopts EIP-7997 on its own. Co-Authored-By: Claude --- .../test_fork_transition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py index 8f3d20b983b..2a92c2de067 100644 --- a/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py +++ b/tests/amsterdam/eip7997_deterministic_factory_predeploy/test_fork_transition.py @@ -32,7 +32,7 @@ FORK_TIMESTAMP = 15_000 -@pytest.mark.valid_at_transition_to("Amsterdam") +@pytest.mark.valid_at_transition_to("EIP7997") @pytest.mark.pre_alloc_mutable @pytest.mark.parametrize("pre_fork_nonce", [1, 2, 32]) def test_factory_deploys_across_transition( @@ -106,7 +106,7 @@ def test_factory_deploys_across_transition( ) -@pytest.mark.valid_at_transition_to("Amsterdam") +@pytest.mark.valid_at_transition_to("EIP7997") @pytest.mark.pre_alloc_mutable def test_factory_absent_across_transition( blockchain_test: BlockchainTestFiller, From a1f25523034dc67d5bf4615f086cd51b489c5e6c Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:27:02 +0000 Subject: [PATCH 25/44] test(benchmark): drop the inert EIP-7928 exclusions Filling without a path never reaches tests/benchmark, so neither hook was load-bearing. Co-Authored-By: Claude --- .../eip7928_block_level_access_lists/conftest.py | 10 ---------- .../eip7928_block_level_access_lists/conftest.py | 10 ---------- 2 files changed, 20 deletions(-) delete mode 100644 tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py delete mode 100644 tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py diff --git a/tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py b/tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py deleted file mode 100644 index 9dc5216d7ea..00000000000 --- a/tests/benchmark/compute/eip7928_block_level_access_lists/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Pytest (plugin) definitions local to EIP-7928 benchmark tests.""" - -import pytest - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) diff --git a/tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py b/tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py deleted file mode 100644 index 9dc5216d7ea..00000000000 --- a/tests/benchmark/stateful/eip7928_block_level_access_lists/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Pytest (plugin) definitions local to EIP-7928 benchmark tests.""" - -import pytest - - -def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" - metafunc.definition.add_marker( - pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) - ) From 2eaeb1ddfd5bf54967768b1fb34da7524ff019b9 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:27:02 +0000 Subject: [PATCH 26/44] refactor(specs): append the fixed block access list hash field Keep the header call as it stands and merge the field in as a mapping. Co-Authored-By: Claude --- .../src/execution_testing/specs/blockchain.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index 8b996720f20..cd2f85cf333 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -986,23 +986,28 @@ def generate_block_data( int(env.slot_number) if env.slot_number is not None else 0 ) - header_fields = transition_tool_output.result.model_dump( - exclude_none=True, - exclude={"blob_gas_used", "transactions_trie"}, - ) | env.model_dump( - exclude_none=True, - exclude={"blob_gas_used", "slot_number"}, - ) + # Prepare block_access_list_hash for header initialization + bal_hash_field: Dict[str, Any] = {} if fork.header_bal_hash_required() and ( not fork.supports_block_access_lists() ): # Fork requires the block access list hash header field but # doesn't build block access lists (e.g. Monad): fix value at # zero. - header_fields.setdefault("block_access_list_hash", Hash(0)) + bal_hash_field["block_access_list_hash"] = Hash(0) header = FixtureHeader( - **header_fields, + **( + transition_tool_output.result.model_dump( + exclude_none=True, + exclude={"blob_gas_used", "transactions_trie"}, + ) + | env.model_dump( + exclude_none=True, + exclude={"blob_gas_used", "slot_number"}, + ) + | bal_hash_field + ), blob_gas_used=blob_gas_used, transactions_trie=Transaction.list_root(txs), extra_data=( From b89a7f548570853ad4e0850872c81c097fa7f6b7 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:51:03 +0000 Subject: [PATCH 27/44] test(monad_next): pin the Amsterdam EIP interactions with Monad features Cover the reserve balance sweep against a SELFDESTRUCT to the destructing account, keep MONAD_NEXT out of the deployed fork range, and stop fork succession from implying membership of the followed fork's EIPs. Co-Authored-By: Claude --- .../plugins/execute/rpc/hive.py | 6 +- .../src/execution_testing/forks/base_fork.py | 9 +- .../execution_testing/forks/forks/forks.py | 1 + .../forks/tests/test_forks.py | 55 ++++++ tests/monad_amsterdam/__init__.py | 1 + .../eip7997_delegated_create/__init__.py | 1 + .../test_factory_reach.py | 122 ++++++++++++ .../reserve_balance/test_transfers.py | 187 +++++++++++++++--- .../test_transfers.py | 148 +++++++++++--- 9 files changed, 472 insertions(+), 58 deletions(-) create mode 100644 tests/monad_amsterdam/__init__.py create mode 100644 tests/monad_amsterdam/eip7997_delegated_create/__init__.py create mode 100644 tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py index 58ce18f480f..bbd287900a2 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py @@ -183,7 +183,11 @@ def build_genesis_header( requests_hash=Requests() if genesis_fork.header_requests_required() else None, - block_access_list_hash=BlockAccessList().rlp_hash + block_access_list_hash=( + BlockAccessList().rlp_hash + if genesis_fork.supports_block_access_lists() + else Hash(0) + ) if genesis_fork.header_bal_hash_required() else None, slot_number=0 if genesis_fork.header_slot_number_required() else None, diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 21801a25162..e480bce1e79 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -338,14 +338,19 @@ def _is_subclass_of(a: "BaseForkMeta", b: "BaseForkMeta") -> bool: A fork can follow another fork it does not inherit from, which places it after that fork (and after everything that fork comes - after) in the fork order without adopting its behavior. + after) in the fork order without adopting its behavior. The + EIPs of a followed fork stay out of that ordering. """ a = BaseForkMeta._maybe_transitioned(a) b = BaseForkMeta._maybe_transitioned(b) if issubclass(a, b): return True # The metaclass sees its instances as plain classes, so the - # trait is reached through a cast, as elsewhere in this class. + # traits are reached through a cast, as elsewhere in this class. + # Succession places a fork after another fork, not after that + # fork's EIPs; an adopted EIP is reached by inheritance above. + if cast(Type["BaseFork"], b).is_eip(): + return False followed = cast(Type["BaseFork"], a).follows() while followed is not None: if issubclass(followed, b): diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index dbbc09be2b2..05b17c4d4fd 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1908,6 +1908,7 @@ class MONAD_NEXT( # noqa: N801 eips.EIP8024, eips.EIP8246, MONAD_TEN, + deployed=False, ): """ MONAD_NEXT fork. diff --git a/packages/testing/src/execution_testing/forks/tests/test_forks.py b/packages/testing/src/execution_testing/forks/tests/test_forks.py index ea747d480f2..592cca401a9 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_forks.py +++ b/packages/testing/src/execution_testing/forks/tests/test_forks.py @@ -10,6 +10,7 @@ from execution_testing.vm import Opcodes from ..base_fork import BaseFork, BaseForkMeta +from ..forks.eips import EIP2780, EIP7708, EIP8024 from ..forks.eips.paris.eip_3675 import EIP3675 from ..forks.forks import ( BPO1, @@ -18,6 +19,8 @@ BPO4, BPO5, MONAD_EIGHT, + MONAD_NEXT, + MONAD_TEN, Amsterdam, Berlin, Cancun, @@ -296,6 +299,25 @@ def test_fork_comparison() -> None: assert fork == Berlin +def test_succession_does_not_imply_eip_membership() -> None: + """ + A fork placed after another by `follows` does not gain its EIPs. + + Succession orders MONAD_NEXT after Amsterdam so the Amsterdam + suites can select it, while the EIPs it declines must still compare + as absent. An adopted EIP is reached through inheritance instead, so + it keeps comparing as present. + """ + assert MONAD_NEXT > Amsterdam + assert MONAD_NEXT > MONAD_TEN + + assert MONAD_NEXT >= EIP7708 + assert MONAD_NEXT.is_eip_enabled(7708) + + assert not MONAD_NEXT >= EIP2780 + assert not MONAD_NEXT.is_eip_enabled(2780) + + def test_transition_fork_comparison() -> None: """ Test comparing to a transition fork. @@ -382,6 +404,39 @@ class PreAllocTransitionFork(TransitionBaseClass): pass +def test_monad_next_is_not_deployed() -> None: + """ + A fork under development must stay out of the deployed fork range. + """ + deployed_forks = get_deployed_forks() + assert MONAD_TEN in deployed_forks + assert MONAD_NEXT not in deployed_forks + + +def opcode_names(fork: Type[BaseFork]) -> set[str]: + """Return the names of the opcodes a fork declares valid.""" + return {str(opcode) for opcode in fork.valid_opcodes()} + + +def test_adopted_opcodes_depend_on_mixin_order() -> None: + """ + The adopted opcodes reach MONAD_NEXT through its mixin order. + + MONAD_NINE returning Osaka's list directly is what ends the chain + there, which is what lets a mixin placed ahead of the Monad forks + contribute on the way out. Making that call cooperative, or + reordering MONAD_NEXT's bases, drops these opcodes silently. + """ + adopted = {"DUPN", "SWAPN", "EXCHANGE"} + assert adopted <= opcode_names(MONAD_NEXT) + assert not adopted & opcode_names(MONAD_TEN) + + class MixinAfterFork(MONAD_TEN, EIP8024): # noqa: N801 + """Dummy fork ordering an EIP mixin after the Monad fork.""" + + assert not adopted & opcode_names(MixinAfterFork) + + def test_pre_alloc() -> None: # noqa: D103 assert PrePreAllocFork.pre_allocation() == {"test": "test"} assert PreAllocFork.pre_allocation() == {"test": "test", "test2": "test2"} diff --git a/tests/monad_amsterdam/__init__.py b/tests/monad_amsterdam/__init__.py new file mode 100644 index 00000000000..b37e6faa138 --- /dev/null +++ b/tests/monad_amsterdam/__init__.py @@ -0,0 +1 @@ +"""Tests for the Amsterdam EIPs MONAD_NEXT adopts.""" diff --git a/tests/monad_amsterdam/eip7997_delegated_create/__init__.py b/tests/monad_amsterdam/eip7997_delegated_create/__init__.py new file mode 100644 index 00000000000..4cd6b5b4cb1 --- /dev/null +++ b/tests/monad_amsterdam/eip7997_delegated_create/__init__.py @@ -0,0 +1 @@ +"""Tests for the EIP-7997 factory against Monad's CREATE ban in 7702 frames.""" diff --git a/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py b/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py new file mode 100644 index 00000000000..f0c3382e86c --- /dev/null +++ b/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py @@ -0,0 +1,122 @@ +""" +Tests for reaching the EIP-7997 factory from a delegated frame. + +Monad forbids the create opcodes inside a frame executing an EOA's +delegated code. The ban is callee-scoped: `access_delegation` sets it on +the frame it builds, and a child frame the delegated code calls does not +inherit it. EIP-7997 makes a CREATE2 factory reachable at a fixed +address on every chain, so these tests pin both halves of that +boundary. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Hash, + Op, + Transaction, +) +from execution_testing.test_types.helpers import compute_create2_address +from execution_testing.tools.tools_code.generators import Initcode + +from ...amsterdam.eip7997_deterministic_factory_predeploy.spec import ( + Spec, + ref_spec_7997, +) + +REFERENCE_SPEC_GIT_PATH = ref_spec_7997.git_path +REFERENCE_SPEC_VERSION = ref_spec_7997.version + +SALT = 0x42 + +slot_code_worked = 0x1 +value_code_worked = 0x1234 +slot_call_result = 0x2 + +pytestmark = [ + pytest.mark.valid_from("MONAD_NEXT"), + pytest.mark.pre_alloc_group( + "eip7997_delegated_create_tests", + reason="Tests the EIP-7997 factory reached from a delegated frame", + ), +] + + +@pytest.mark.parametrize("create_op", [Op.CREATE, Op.CREATE2]) +def test_create_halts_in_delegated_frame( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + create_op: Op, +) -> None: + """ + A create opcode executed as an EOA's delegated code halts the frame. + + The halt is the only coverage of `CreateIn7702Context`; nothing is + deployed and the storage write that follows never lands. + """ + # The create halts before its initcode is read, so the empty + # memory the size refers to is immaterial. + delegate = pre.deploy_contract( + create_op(value=0, size=1) + + Op.SSTORE(slot_code_worked, value_code_worked) + ) + sender = pre.fund_eoa() + delegated = pre.fund_eoa(delegation=delegate) + + tx = Transaction( + to=delegated, + sender=sender, + ) + + blockchain_test( + pre=pre, + post={delegated: Account(storage={})}, + blocks=[Block(txs=[tx])], + ) + + +def test_factory_deploys_from_delegated_frame( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """ + A delegated frame reaches CREATE2 through the EIP-7997 factory. + + The ban applies to the frame running the delegated code, not to the + factory frame it calls, so the deployment succeeds. + """ + initcode = Initcode(deploy_code=Op.STOP) + deployed = compute_create2_address(Spec.FACTORY_ADDRESS, SALT, initcode) + + delegate = pre.deploy_contract( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE( + slot_call_result, + Op.CALL( + gas=Op.GAS, + address=Spec.FACTORY_ADDRESS, + args_offset=0, + args_size=Op.CALLDATASIZE, + ), + ) + ) + sender = pre.fund_eoa() + delegated = pre.fund_eoa(delegation=delegate) + + tx = Transaction( + to=delegated, + data=Hash(SALT) + bytes(initcode), + sender=sender, + ) + + blockchain_test( + pre=pre, + post={ + delegated: Account(storage={slot_call_result: 1}), + deployed: Account(code=Op.STOP), + }, + blocks=[Block(txs=[tx])], + ) diff --git a/tests/monad_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 5a06d5b19ce..6fabc7b20e1 100644 --- a/tests/monad_eight/reserve_balance/test_transfers.py +++ b/tests/monad_eight/reserve_balance/test_transfers.py @@ -572,6 +572,7 @@ def test_sc_wallet_send_value_with_selfdestruct( @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("delegate", [True, False]) @pytest.mark.parametrize("undelegate", [True, False]) +@pytest.mark.parametrize("selfdestruct_to_self", [True, False]) def test_sc_wallet_selfdestruct( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -581,16 +582,22 @@ def test_sc_wallet_selfdestruct( pre_delegated: bool, delegate: bool, undelegate: bool, + selfdestruct_to_self: bool, fork: Fork, ) -> None: """ Test reserve balance violations for a delegated EOA whose wallet code SELFDESTRUCTs on behalf of the EOA. - NOTE: this is different from test_sc_wallet_send_value_with_selfdestruct - in that SELFDESTRUCT is not used to send value. + Naming the EOA itself moves no value, so the reserve balance is only + at stake when the beneficiary is another account. """ - wallet_address = pre.deploy_contract(code=Op.SELFDESTRUCT(Op.ADDRESS)) + selfdestruct_target = Address(0x5656) + wallet_address = pre.deploy_contract( + code=Op.SELFDESTRUCT( + Op.ADDRESS if selfdestruct_to_self else selfdestruct_target + ) + ) if pre_delegated: sender = pre.fund_eoa(balance, delegation=wallet_address) @@ -629,7 +636,14 @@ def test_sc_wallet_selfdestruct( ) any_delegation = pre_delegated or delegate or undelegate - reverted = violation and any_delegation + # The authorizations apply before execution, so an undelegation + # leaves nothing for the call to run. + wallet_runs = (pre_delegated or delegate) and not undelegate + # Sweeping the sender to another account empties a delegated EOA, + # which no emptying transaction exception covers. + reverted = (violation and any_delegation) or ( + wallet_runs and not selfdestruct_to_self + ) storage = {} if reverted else {slot_code_worked: value_code_worked} blockchain_test( @@ -637,7 +651,8 @@ def test_sc_wallet_selfdestruct( post={ contract_address: Account( storage=storage, balance=value if not reverted else 0 - ) + ), + selfdestruct_target: None, }, blocks=[Block(txs=[tx_1])], ) @@ -1258,8 +1273,13 @@ def test_access_lists( @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("new_address_pre_funded", [True, False]) @pytest.mark.parametrize( - "selfdestruct,deploy_code", - [(True, None), (False, Bytecode()), (False, Op.STOP)], + "selfdestruct,selfdestruct_to_self,deploy_code", + [ + (True, False, None), + (True, True, None), + (False, False, Bytecode()), + (False, False, Op.STOP), + ], ) @pytest.mark.with_all_contract_creating_tx_types def test_creation_tx( @@ -1271,6 +1291,7 @@ def test_creation_tx( pre_delegated: bool, new_address_pre_funded: bool, selfdestruct: bool, + selfdestruct_to_self: bool, deploy_code: Bytecode | None, tx_type: int, fork: Fork, @@ -1287,7 +1308,9 @@ def test_creation_tx( selfdestruct_target = Address(0x5656) initcode = ( - Op.SELFDESTRUCT(address=selfdestruct_target) + Op.SELFDESTRUCT( + address=Op.ADDRESS if selfdestruct_to_self else selfdestruct_target + ) if selfdestruct else Initcode(deploy_code=deploy_code) ) @@ -1316,6 +1339,25 @@ def test_creation_tx( fork == MONAD_EIGHT and selfdestruct and new_address_pre_funded ) + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. A zero balance still + # leaves the account empty, and so pruned. + selfdestructed_account = ( + Account( + nonce=0, + balance=value + pre_fund_value, + code=b"", + storage={}, + ) + if selfdestruct_to_self + and fork.is_eip_enabled(8246) + and not reverted + and value + pre_fund_value != 0 + else None + ) + blockchain_test( pre=pre, post={ @@ -1326,9 +1368,12 @@ def test_creation_tx( if not reverted and not selfdestruct else Account(balance=pre_fund_value) if new_address_pre_funded and reverted - else None, + else selfdestructed_account, selfdestruct_target: Account(balance=value + pre_fund_value) - if selfdestruct and not reverted and value + pre_fund_value != 0 + if selfdestruct + and not selfdestruct_to_self + and not reverted + and value + pre_fund_value != 0 else None, }, blocks=[Block(txs=txs)], @@ -1405,8 +1450,13 @@ def test_contract_unrestricted( @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("pre_funded", [True, False]) @pytest.mark.parametrize( - "selfdestruct,deploy_code", - [(True, None), (False, Bytecode()), (False, Op.STOP)], + "selfdestruct,selfdestruct_to_self,deploy_code", + [ + (True, False, None), + (True, True, None), + (False, False, Bytecode()), + (False, False, Op.STOP), + ], ) @pytest.mark.with_all_create_opcodes def test_contract_unrestricted_with_create( @@ -1417,6 +1467,7 @@ def test_contract_unrestricted_with_create( pre_delegated: bool, pre_funded: bool, selfdestruct: bool, + selfdestruct_to_self: bool, deploy_code: Bytecode | None, create_opcode: Op, fork: Fork, @@ -1436,7 +1487,9 @@ def test_contract_unrestricted_with_create( selfdestruct_target = Address(0x5656) initcode = ( - Op.SELFDESTRUCT(address=selfdestruct_target) + Op.SELFDESTRUCT( + address=Op.ADDRESS if selfdestruct_to_self else selfdestruct_target + ) if selfdestruct else Initcode(deploy_code=deploy_code) ) @@ -1467,15 +1520,26 @@ def test_contract_unrestricted_with_create( ) storage = {slot_code_worked: value_code_worked} + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. A zero balance still + # leaves the account empty, and so pruned. + selfdestructed_account = ( + Account(nonce=0, balance=value, code=b"", storage={}) + if selfdestruct_to_self and fork.is_eip_enabled(8246) and value != 0 + else None + ) + blockchain_test( pre=pre, post={ factory_address: Account(storage=storage, balance=balance - value), new_contract_address: Account(balance=value, code=deploy_code) if not selfdestruct - else None, + else selfdestructed_account, selfdestruct_target: Account(balance=value) - if selfdestruct and value != 0 + if selfdestruct and value != 0 and not selfdestruct_to_self else None, }, blocks=[Block(txs=[tx_1])], @@ -1486,8 +1550,19 @@ def test_contract_unrestricted_with_create( @pytest.mark.parametrize("create_balance", [0, Spec.RESERVE_BALANCE // 2]) @pytest.mark.parametrize("call_balance", [0, Spec.RESERVE_BALANCE // 2]) @pytest.mark.parametrize("pull_balance", [0, Spec.RESERVE_BALANCE // 2]) -@pytest.mark.parametrize("same_tx", [True, False]) -@pytest.mark.parametrize("through_delegation", [True, False]) +@pytest.mark.parametrize( + "same_tx,through_delegation,selfdestruct_to_self", + [ + # Only a same-transaction creation reaches EIP-8246's branch, and + # a delegated frame destructs the delegating account rather than + # the created one, so the self target varies in one case alone. + (True, False, False), + (True, False, True), + (True, True, False), + (False, False, False), + (False, True, False), + ], +) @pytest.mark.with_all_create_opcodes def test_contract_unrestricted_with_selfdestruct( blockchain_test: BlockchainTestFiller, @@ -1506,6 +1581,8 @@ def test_contract_unrestricted_with_selfdestruct( # Whether the SELFDESTRUCT should be called on behalf of # a delegating account through_delegation: bool, + # Whether SELFDESTRUCT names the destructing account itself + selfdestruct_to_self: bool, create_opcode: Op, fork: Fork, ) -> None: @@ -1532,7 +1609,7 @@ def test_contract_unrestricted_with_selfdestruct( Op.SELFDESTRUCT(address=Op.CALLER), balance=pull_balance ) deploy_code = Op.CALL(address=pull_funder_address) + Op.SELFDESTRUCT( - address=selfdestruct_target + address=Op.ADDRESS if selfdestruct_to_self else selfdestruct_target ) initcode = Initcode(deploy_code=deploy_code) @@ -1620,6 +1697,16 @@ def test_contract_unrestricted_with_selfdestruct( storage = {slot_code_worked: value_code_worked} reverted = through_delegation and value > 0 and prefund_balance > 0 + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. + selfdestructed_account = ( + Account(nonce=0, balance=value, code=b"", storage={}) + if selfdestruct_to_self and fork.is_eip_enabled(8246) and value != 0 + else None + ) + blockchain_test( pre=pre, post={ @@ -1633,7 +1720,7 @@ def test_contract_unrestricted_with_selfdestruct( code=deploy_code, ) if not same_tx or through_delegation - else None, + else selfdestructed_account, # Delegated account is deleted if there is no delegation delegated_address: Account( balance=0, @@ -1643,7 +1730,7 @@ def test_contract_unrestricted_with_selfdestruct( else None, # SELFDESTRUCT target is deleted if source was empty selfdestruct_target: Account(balance=value) - if value != 0 + if value != 0 and not selfdestruct_to_self else None, } if not reverted @@ -1677,8 +1764,13 @@ def test_contract_unrestricted_with_selfdestruct( @pytest.mark.with_all_create_opcodes @pytest.mark.parametrize("new_address_pre_funded", [True, False]) @pytest.mark.parametrize( - "selfdestruct,deploy_code", - [(True, None), (False, Bytecode()), (False, Op.STOP)], + "selfdestruct,selfdestruct_to_self,deploy_code", + [ + (True, False, None), + (True, True, None), + (False, False, Bytecode()), + (False, False, Op.STOP), + ], ) def test_contract_unrestricted_within_initcode( blockchain_test: BlockchainTestFiller, @@ -1689,6 +1781,7 @@ def test_contract_unrestricted_within_initcode( create_opcode: Op, new_address_pre_funded: bool, selfdestruct: bool, + selfdestruct_to_self: bool, deploy_code: Bytecode | None, fork: Fork, ) -> None: @@ -1710,7 +1803,11 @@ def test_contract_unrestricted_within_initcode( initcode = ( ( Op.CALL(value=value, address=target) - + Op.SELFDESTRUCT(address=selfdestruct_target) + + Op.SELFDESTRUCT( + address=Op.ADDRESS + if selfdestruct_to_self + else selfdestruct_target + ) ) if selfdestruct else Initcode( @@ -1756,6 +1853,16 @@ def test_contract_unrestricted_within_initcode( ) storage = {} if reverted else {slot_code_worked: value_code_worked} + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. + selfdestructed_account = ( + Account(nonce=0, balance=balance - value, code=b"", storage={}) + if selfdestruct_to_self and fork.is_eip_enabled(8246) and not reverted + else None + ) + txs = [tx_1] if new_address_pre_funded: txs.insert( @@ -1773,12 +1880,12 @@ def test_contract_unrestricted_within_initcode( if reverted and new_address_pre_funded else Account(balance=balance - value, code=deploy_code) if not selfdestruct - else None, + else selfdestructed_account, target: Account(balance=value) if value != 0 and not reverted else None, selfdestruct_target: Account(balance=balance - value) - if selfdestruct and not reverted + if selfdestruct and not reverted and not selfdestruct_to_self else None, }, blocks=[Block(txs=txs)], @@ -1796,8 +1903,13 @@ def test_contract_unrestricted_within_initcode( @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("new_address_pre_funded", [True, False]) @pytest.mark.parametrize( - "selfdestruct,deploy_code", - [(True, None), (False, Bytecode()), (False, Op.STOP)], + "selfdestruct,selfdestruct_to_self,deploy_code", + [ + (True, False, None), + (True, True, None), + (False, False, Bytecode()), + (False, False, Op.STOP), + ], ) @pytest.mark.with_all_contract_creating_tx_types def test_unrestricted_in_creation_tx_initcode( @@ -1808,6 +1920,7 @@ def test_unrestricted_in_creation_tx_initcode( pre_delegated: bool, new_address_pre_funded: bool, selfdestruct: bool, + selfdestruct_to_self: bool, deploy_code: Bytecode | None, tx_type: int, fork: Fork, @@ -1830,7 +1943,11 @@ def test_unrestricted_in_creation_tx_initcode( initcode = ( ( Op.CALL(value=value, address=target) - + Op.SELFDESTRUCT(address=selfdestruct_target) + + Op.SELFDESTRUCT( + address=Op.ADDRESS + if selfdestruct_to_self + else selfdestruct_target + ) ) if selfdestruct else Initcode( @@ -1865,6 +1982,16 @@ def test_unrestricted_in_creation_tx_initcode( and balance - value < Spec.RESERVE_BALANCE ) + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. + selfdestructed_account = ( + Account(nonce=0, balance=balance - value, code=b"", storage={}) + if selfdestruct_to_self and fork.is_eip_enabled(8246) and not reverted + else None + ) + blockchain_test( pre=pre, post={ @@ -1872,12 +1999,12 @@ def test_unrestricted_in_creation_tx_initcode( if reverted and new_address_pre_funded else Account(code=deploy_code, balance=balance - value) if not selfdestruct - else None, + else selfdestructed_account, target: Account(balance=value) if value != 0 and not reverted else None, selfdestruct_target: Account(balance=balance - value) - if selfdestruct and not reverted + if selfdestruct and not reverted and not selfdestruct_to_self else None, }, blocks=[Block(txs=txs)], diff --git a/tests/monad_nine/mip4_checkreservebalance/test_transfers.py b/tests/monad_nine/mip4_checkreservebalance/test_transfers.py index c217fd2cd6e..ec4d6cdf523 100644 --- a/tests/monad_nine/mip4_checkreservebalance/test_transfers.py +++ b/tests/monad_nine/mip4_checkreservebalance/test_transfers.py @@ -417,6 +417,7 @@ def test_sc_wallet_send_value( @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("delegate", [True, False]) @pytest.mark.parametrize("undelegate", [True, False]) +@pytest.mark.parametrize("selfdestruct_to_self", [True, False]) def test_sc_wallet_selfdestruct( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -427,14 +428,23 @@ def test_sc_wallet_selfdestruct( pre_delegated: bool, delegate: bool, undelegate: bool, + selfdestruct_to_self: bool, fork: Fork, ) -> None: """ Test dippedIntoReserve() for a delegated EOA whose wallet code SELFDESTRUCTs on behalf of the EOA. + + Naming the EOA itself moves no value, so the reserve balance is only + at stake when the beneficiary is another account. """ refill_call = refill_factory() - wallet_address = pre.deploy_contract(code=Op.SELFDESTRUCT(Op.ADDRESS)) + selfdestruct_target = Address(0x5656) + wallet_address = pre.deploy_contract( + code=Op.SELFDESTRUCT( + Op.ADDRESS if selfdestruct_to_self else selfdestruct_target + ) + ) if pre_delegated: sender = pre.fund_eoa(balance, delegation=wallet_address) @@ -475,7 +485,17 @@ def test_sc_wallet_selfdestruct( ) any_delegation = pre_delegated or delegate or undelegate - expected_violation = 1 if (violation and any_delegation) else 0 + # The authorizations apply before execution, so an undelegation + # leaves nothing for the call to run. + wallet_runs = (pre_delegated or delegate) and not undelegate + # Sweeping the sender to another account empties a delegated EOA, + # which the probe reports whatever the transaction's own value does. + expected_violation = ( + 1 + if (violation and any_delegation) + or (wallet_runs and not selfdestruct_to_self) + else 0 + ) storage = { slot_violation_result: expected_violation, @@ -484,7 +504,14 @@ def test_sc_wallet_selfdestruct( blockchain_test( pre=pre, - post={contract_address: Account(storage=storage, balance=value)}, + post={ + contract_address: Account(storage=storage, balance=value), + # What the sweep sends depends on the gas billed, so only + # its presence is pinned. + selfdestruct_target: Account() + if wallet_runs and not selfdestruct_to_self + else None, + }, blocks=[Block(txs=[tx_1])], ) @@ -1078,7 +1105,10 @@ def test_contract_unrestricted( ) @pytest.mark.parametrize("pre_delegated", [True, False]) @pytest.mark.parametrize("pre_funded", [True, False]) -@pytest.mark.parametrize("selfdestruct", [True, False]) +@pytest.mark.parametrize( + "selfdestruct,selfdestruct_to_self", + [(True, False), (True, True), (False, False)], +) @pytest.mark.with_all_create_opcodes def test_contract_unrestricted_with_create( blockchain_test: BlockchainTestFiller, @@ -1088,6 +1118,7 @@ def test_contract_unrestricted_with_create( pre_delegated: bool, pre_funded: bool, selfdestruct: bool, + selfdestruct_to_self: bool, create_opcode: Op, fork: Fork, ) -> None: @@ -1105,7 +1136,9 @@ def test_contract_unrestricted_with_create( selfdestruct_target = Address(0x5656) initcode = ( - Op.SELFDESTRUCT(address=selfdestruct_target) + Op.SELFDESTRUCT( + address=Op.ADDRESS if selfdestruct_to_self else selfdestruct_target + ) if selfdestruct else Initcode(deploy_code=Op.STOP) ) @@ -1136,15 +1169,26 @@ def test_contract_unrestricted_with_create( ) storage = {slot_code_worked: value_code_worked, slot_violation_result: 0} + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. A zero balance still + # leaves the account empty, and so pruned. + selfdestructed_account = ( + Account(nonce=0, balance=value, code=b"", storage={}) + if selfdestruct_to_self and fork.is_eip_enabled(8246) and value != 0 + else None + ) + blockchain_test( pre=pre, post={ factory_address: Account(storage=storage, balance=balance - value), new_contract_address: Account(balance=value, code=Op.STOP) if not selfdestruct - else None, + else selfdestructed_account, selfdestruct_target: Account(balance=value) - if selfdestruct and value != 0 + if selfdestruct and value != 0 and not selfdestruct_to_self else None, }, blocks=[Block(txs=[tx_1])], @@ -1155,8 +1199,19 @@ def test_contract_unrestricted_with_create( @pytest.mark.parametrize("create_balance", [0, Spec.RESERVE_BALANCE // 2]) @pytest.mark.parametrize("call_balance", [0, Spec.RESERVE_BALANCE // 2]) @pytest.mark.parametrize("pull_balance", [0, Spec.RESERVE_BALANCE // 2]) -@pytest.mark.parametrize("same_tx", [True, False]) -@pytest.mark.parametrize("through_delegation", [True, False]) +@pytest.mark.parametrize( + "same_tx,through_delegation,selfdestruct_to_self", + [ + # Only a same-transaction creation reaches EIP-8246's branch, and + # a delegated frame destructs the delegating account rather than + # the created one, so the self target varies in one case alone. + (True, False, False), + (True, False, True), + (True, True, False), + (False, False, False), + (False, True, False), + ], +) @pytest.mark.with_all_create_opcodes def test_contract_unrestricted_with_selfdestruct( blockchain_test: BlockchainTestFiller, @@ -1176,6 +1231,8 @@ def test_contract_unrestricted_with_selfdestruct( # Whether the SELFDESTRUCT should be called on behalf of # a delegating account through_delegation: bool, + # Whether SELFDESTRUCT names the destructing account itself + selfdestruct_to_self: bool, create_opcode: Op, fork: Fork, ) -> None: @@ -1205,7 +1262,7 @@ def test_contract_unrestricted_with_selfdestruct( ) deploy_code = Op.CALL(address=pull_funder_address) + Op.SELFDESTRUCT( - address=selfdestruct_target + address=Op.ADDRESS if selfdestruct_to_self else selfdestruct_target ) initcode = Initcode(deploy_code=deploy_code) @@ -1309,6 +1366,16 @@ def test_contract_unrestricted_with_selfdestruct( if same_tx: factory_storage[slot_violation_result] = expected_violation + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. + selfdestructed_account = ( + Account(nonce=0, balance=value, code=b"", storage={}) + if selfdestruct_to_self and fork.is_eip_enabled(8246) and value != 0 + else None + ) + post = { # Factory is the caller to store result if same_tx # Factory is always left with no balance. @@ -1327,9 +1394,11 @@ def test_contract_unrestricted_with_selfdestruct( code=deploy_code, ) if not same_tx or through_delegation - else None, + else selfdestructed_account, # SELFDESTRUCT target is deleted if source was empty - selfdestruct_target: Account(balance=value) if value != 0 else None, + selfdestruct_target: Account(balance=value) + if value != 0 and not selfdestruct_to_self + else None, } blockchain_test( @@ -1350,8 +1419,13 @@ def test_contract_unrestricted_with_selfdestruct( @pytest.mark.with_all_create_opcodes @pytest.mark.parametrize("new_address_pre_funded", [True, False]) @pytest.mark.parametrize( - "selfdestruct,deploy_code", - [(True, None), (False, Bytecode()), (False, Op.STOP)], + "selfdestruct,selfdestruct_to_self,deploy_code", + [ + (True, False, None), + (True, True, None), + (False, False, Bytecode()), + (False, False, Op.STOP), + ], ) def test_contract_unrestricted_within_initcode( blockchain_test: BlockchainTestFiller, @@ -1362,6 +1436,7 @@ def test_contract_unrestricted_within_initcode( create_opcode: Op, new_address_pre_funded: bool, selfdestruct: bool, + selfdestruct_to_self: bool, deploy_code: Bytecode | None, fork: Fork, ) -> None: @@ -1392,7 +1467,7 @@ def test_contract_unrestricted_within_initcode( ) if selfdestruct: initcode = common_initcode + Op.SELFDESTRUCT( - address=selfdestruct_target + address=Op.ADDRESS if selfdestruct_to_self else selfdestruct_target ) else: initcode = Initcode( @@ -1462,11 +1537,18 @@ def test_contract_unrestricted_within_initcode( new_balance = balance - value + Spec.RESERVE_BALANCE - # EIP-8246 stops the end-of-transaction cleanup from burning the - # balance, so the refill that follows SELFDESTRUCT is left behind on - # a cleared account. + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps both the retained balance and the refill. + retained = balance - value if selfdestruct_to_self else 0 selfdestructed_account = ( - Account(nonce=0, balance=Spec.RESERVE_BALANCE, code=b"", storage={}) + Account( + nonce=0, + balance=retained + Spec.RESERVE_BALANCE, + code=b"", + storage={}, + ) if fork.is_eip_enabled(8246) else None ) @@ -1505,7 +1587,7 @@ def test_contract_unrestricted_within_initcode( # SELFDESTRUCT runs during initcode (before factory # refill), so it sends balance - value only. selfdestruct_target: Account(balance=balance - value) - if selfdestruct + if selfdestruct and not selfdestruct_to_self else None, }, blocks=[Block(txs=txs)], @@ -1522,8 +1604,13 @@ def test_contract_unrestricted_within_initcode( ) @pytest.mark.parametrize("new_address_pre_funded", [True, False]) @pytest.mark.parametrize( - "selfdestruct,deploy_code", - [(True, None), (False, Bytecode()), (False, Op.STOP)], + "selfdestruct,selfdestruct_to_self,deploy_code", + [ + (True, False, None), + (True, True, None), + (False, False, Bytecode()), + (False, False, Op.STOP), + ], ) @pytest.mark.with_all_contract_creating_tx_types def test_unrestricted_in_creation_tx_initcode( @@ -1534,6 +1621,7 @@ def test_unrestricted_in_creation_tx_initcode( balance: int, new_address_pre_funded: bool, selfdestruct: bool, + selfdestruct_to_self: bool, deploy_code: Bytecode | None, tx_type: int, fork: Fork, @@ -1574,7 +1662,7 @@ def test_unrestricted_in_creation_tx_initcode( ) if selfdestruct: initcode = common_initcode + Op.SELFDESTRUCT( - address=selfdestruct_target + address=Op.ADDRESS if selfdestruct_to_self else selfdestruct_target ) else: initcode = Initcode( @@ -1605,6 +1693,16 @@ def test_unrestricted_in_creation_tx_initcode( new_balance = balance - value + Spec.RESERVE_BALANCE + # A SELFDESTRUCT to the destructing account itself moves nothing, so + # what it leaves behind is what EIP-8246 changes: before it, the + # balance is burnt and the account destroyed; after it, the account + # is cleared and keeps the balance it retained. + selfdestructed_account = ( + Account(nonce=0, balance=new_balance, code=b"", storage={}) + if selfdestruct_to_self and fork.is_eip_enabled(8246) + else None + ) + blockchain_test( pre=pre, post={ @@ -1615,10 +1713,10 @@ def test_unrestricted_in_creation_tx_initcode( ), new_address: Account(code=deploy_code, balance=new_balance) if not selfdestruct - else None, + else selfdestructed_account, target: Account(balance=value) if value != 0 else None, selfdestruct_target: Account(balance=new_balance) - if selfdestruct + if selfdestruct and not selfdestruct_to_self else None, }, blocks=[Block(txs=txs)], From 6d0cadc2c3824de9861e3bf40d3f30560a102c12 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:51:03 +0000 Subject: [PATCH 28/44] fix(ci): rehearse a fixture release once per push A push to a branch with an open pull request fired both triggers, and the two events land in different concurrency groups, so neither run cancelled the other. Co-Authored-By: Claude --- .github/workflows/check_release.yaml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/check_release.yaml b/.github/workflows/check_release.yaml index 8b1f7447b6f..d26999766bf 100644 --- a/.github/workflows/check_release.yaml +++ b/.github/workflows/check_release.yaml @@ -6,9 +6,10 @@ run-name: ${{ github.event_name == 'workflow_dispatch' && format('Check Fixture # breaks surfaces on the branch that broke it rather than on the next # release attempt. # -# Two tiers, both running on every pull request and on pushes to the -# branches that release in parallel with the fork branch, -# `from-upstream` and `eips/**`. +# Two tiers, both running on pushes to the branches that release in +# parallel with the fork branch, `from-upstream` and `eips/**`. A pull +# request adds no trigger of its own: its pushes already run here, and +# an event per push and per synchronize rehearsed each push twice. # # `collect` is cheap: it reads the features the branch releases out of # `.github/configs/feature.yaml`, builds the release's job matrix @@ -32,15 +33,6 @@ on: - "whitelist.txt" - "docs/**" - "mkdocs.yml" - pull_request: - paths-ignore: - - "**.md" - - "LICENSE*" - - ".gitignore" - - ".vscode/**" - - "whitelist.txt" - - "docs/**" - - "mkdocs.yml" workflow_dispatch: inputs: depth: @@ -79,10 +71,7 @@ jobs: shell: bash env: INPUT_FEATURES: ${{ inputs.features }} - # The head branch, so a pull request from an EIP branch - # rehearses that branch's feature; `ref_name` is the merge ref - # on a pull request and the branch everywhere else. - BRANCH: ${{ github.head_ref || github.ref_name }} + BRANCH: ${{ github.ref_name }} run: | # The feature selection and the per-feature release matrix live # in (and are shared with the release workflow via) From 12fa2f609a9c0d5bbf1f1d84c82748e5ef9a1ebb Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:18:33 +0000 Subject: [PATCH 29/44] docs(skills): require a new fork to arrive as a replica of its parent A fork added in the same commit as its EIPs hides which lines are the adoption and which are the copy. Co-Authored-By: Claude --- .claude/commands/adopt-upstream-eip.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.claude/commands/adopt-upstream-eip.md b/.claude/commands/adopt-upstream-eip.md index 24ffd4878f2..c4a53518b4b 100644 --- a/.claude/commands/adopt-upstream-eip.md +++ b/.claude/commands/adopt-upstream-eip.md @@ -168,6 +168,12 @@ stay independent until a deliberate merge. ## 3. Adopt the EIP in the framework +Create the fork before adopting anything into it. Where `` +has no directory under `src/ethereum/forks/` yet, or no class in the +testing package, add them as exact replicas of `` in a +separate commit. The adoption then shows the EIPs as the only deviations +from the parent Monad fork, which is what the diff has to make visible. + In `packages/testing/src/execution_testing/forks/forks/forks.py`, declare `` **after** `` (so the name resolves) with the EIP mixins first: From 44deaed359b41787c5f9e20a9521488a961e48ae Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:34:44 +0000 Subject: [PATCH 30/44] feat(tests): run the EIP-7928 state visibility tests on monad forks Narrow the monad exclusion to the tests whose assertions need a block access list, and share the block gas limit across the CREATE-chain transactions on monad. Co-Authored-By: Claude --- .../conftest.py | 24 ++++++++++++++++++- .../test_block_access_lists.py | 21 +++++++++++++++- .../test_block_access_lists_cross_index.py | 1 + .../test_block_access_lists_eip7702.py | 1 + .../test_block_access_lists_opcodes.py | 3 +++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/conftest.py b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py index 3bedcfe2651..ab5b838e915 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/conftest.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py @@ -2,9 +2,31 @@ import pytest +NO_BAL_DEPENDENCY_MARKER = "no_bal_dependency" + + +def pytest_configure(config: pytest.Config) -> None: + """Register the marker that keeps a test valid on Monad forks.""" + config.addinivalue_line( + "markers", + f"{NO_BAL_DEPENDENCY_MARKER}: the test's post-state assertions hold " + "without a block access list, so it also runs on forks that do not " + "implement EIP-7928", + ) + def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: - """Mark all tests in this subdir as not valid for Monad forks.""" + """ + Mark the tests in this subdir as not valid for Monad forks. + + Monad forks carry the block access list hash header field fixed at + zero and build no list, so the transition tool returns none and every + `expected_block_access_list` is dropped unverified. Tests marked + `no_bal_dependency` assert intra-block state visibility through the + post-state instead, and stay valid. + """ + if metafunc.definition.get_closest_marker(NO_BAL_DEPENDENCY_MARKER): + return metafunc.definition.add_marker( pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) ) diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index eb728087828..4f9fde97fa7 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -37,6 +37,7 @@ compute_create_address, ) from execution_testing import Macros as Om +from execution_testing.forks import MONAD_EIGHT from .spec import ref_spec_7928 @@ -2092,6 +2093,7 @@ def test_bal_nonexistent_account_access_value_transfer( ) +@pytest.mark.no_bal_dependency def test_bal_multiple_balance_changes_same_account( pre: Alloc, fork: Fork, @@ -2196,6 +2198,7 @@ def test_bal_multiple_balance_changes_same_account( ) +@pytest.mark.no_bal_dependency def test_bal_multiple_storage_writes_same_slot( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -2420,6 +2423,7 @@ def test_bal_create_transaction_empty_code( pytest.param(0xABCD, id="tx2_rewrites_same_value"), ], ) +@pytest.mark.no_bal_dependency def test_bal_cross_tx_storage_write( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -2490,6 +2494,7 @@ def test_bal_cross_tx_storage_write( ) +@pytest.mark.no_bal_dependency def test_bal_cross_tx_storage_chain( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -2661,6 +2666,7 @@ def test_bal_many_storage_writes_single_account( @pytest.mark.with_all_create_opcodes +@pytest.mark.no_bal_dependency def test_bal_cross_tx_deploy_then_call( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -2761,6 +2767,7 @@ def test_bal_cross_tx_deploy_then_call( ], ) @pytest.mark.pre_alloc_mutable() +@pytest.mark.no_bal_dependency def test_bal_cross_tx_factory_nonce_create_chain( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -2824,6 +2831,15 @@ def test_bal_cross_tx_factory_nonce_create_chain( intrinsic = fork.transaction_intrinsic_cost_calculator()( calldata=bytes(initcode), contract_creation=False, access_list=[] ) + tx_gas_limit = fork.transaction_gas_limit_cap() + if fork >= MONAD_EIGHT: + # Monad charges the block for each transaction's whole gas limit + # rather than its gas used, so the chain only fits the block when + # every transaction takes an equal share of the block gas limit. + assert tx_gas_limit is not None, "monad forks cap transaction gas" + tx_gas_limit = min( + tx_gas_limit, int(Environment().gas_limit) // chain_length + ) txs = [ Transaction( sender=senders[i], @@ -2832,7 +2848,7 @@ def test_bal_cross_tx_factory_nonce_create_chain( gas_limit=( intrinsic + 1 if failure_mode == "oog" and i == failure_index - else fork.transaction_gas_limit_cap() + else tx_gas_limit ), ) for i in range(chain_length) @@ -2918,6 +2934,7 @@ def test_bal_cross_tx_factory_nonce_create_chain( "funding_method", ["direct_call", "selfdestruct"], ) +@pytest.mark.no_bal_dependency def test_bal_cross_tx_balance_dependency( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -3019,6 +3036,7 @@ def test_bal_cross_tx_balance_dependency( ), ], ) +@pytest.mark.no_bal_dependency def test_bal_cross_tx_funding_chain( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -3245,6 +3263,7 @@ def test_bal_cross_tx_funding_chain( ) +@pytest.mark.no_bal_dependency def test_bal_cross_block_ripemd160_state_leak( pre: Alloc, blockchain_test: BlockchainTestFiller, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py index 5ece8d98570..1671a4e176f 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py @@ -263,6 +263,7 @@ def test_bal_noop_write_filtering( ) +@pytest.mark.no_bal_dependency def test_bal_intra_tx_round_trip_after_prior_tx_write( pre: Alloc, blockchain_test: BlockchainTestFiller, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py index cc0f6fa9aaa..b9beed1061f 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7702.py @@ -997,6 +997,7 @@ def test_bal_7702_multi_hop_delegation_chain( ) +@pytest.mark.no_bal_dependency def test_bal_7702_cross_tx_delegation_then_call( pre: Alloc, blockchain_test: BlockchainTestFiller, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py index da11a3d19cd..cb12a8b68e6 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py @@ -2908,6 +2908,7 @@ def test_bal_create_selfdestruct_to_self_with_call( ["collision_only", "then_nonce_change", "then_storage_change"], ) @pytest.mark.pre_alloc_mutable() +@pytest.mark.no_bal_dependency def test_bal_create_collision( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -3158,6 +3159,7 @@ def test_bal_transient_storage_not_tracked( ) +@pytest.mark.no_bal_dependency def test_bal_create2_deploy_then_collision( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -3811,6 +3813,7 @@ def test_bal_create_storage_op_then_selfdestruct_same_tx( [0, 100], ids=["no_balance", "with_balance"], ) +@pytest.mark.no_bal_dependency def test_bal_create2_selfdestruct_then_recreate_same_block( pre: Alloc, blockchain_test: BlockchainTestFiller, From a793d6aeb6d9e192f0618aec810b74ceaad32091 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:27:31 +0000 Subject: [PATCH 31/44] test(monad): exclude type 3 transactions by fork, not by skip A skip drops the case on every fork; `not_valid_for` drops it only on the forks without blob transactions, so the non-Monad forks keep the coverage. Co-Authored-By: Claude --- .../test_transaction_validity.py | 3 +++ tests/istanbul/eip1344_chainid/test_chainid.py | 4 ++-- .../eip7623_increase_calldata_cost/test_execution_gas.py | 8 ++++++-- .../test_transaction_validity.py | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py index 440e844c868..fffbc62d278 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py @@ -258,6 +258,9 @@ def test_mixed_zero_nonzero_bytes_floor_cost( 3, [], id="type_3_empty_access_list", + marks=pytest.mark.not_valid_for( + "MONAD_EIGHT", subsequent_forks=True + ), ), pytest.param( 4, diff --git a/tests/istanbul/eip1344_chainid/test_chainid.py b/tests/istanbul/eip1344_chainid/test_chainid.py index 8379b55cc9e..78d48d2153e 100644 --- a/tests/istanbul/eip1344_chainid/test_chainid.py +++ b/tests/istanbul/eip1344_chainid/test_chainid.py @@ -17,8 +17,8 @@ @pytest.mark.with_all_typed_transactions( - marks=lambda tx_type: pytest.mark.skip( - reason="type 3 transactions aren't supported in Monad" + marks=lambda tx_type: pytest.mark.not_valid_for( + "MONAD_EIGHT", subsequent_forks=True ) if tx_type == 3 else None diff --git a/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py index e0722edae48..4a6f5de5403 100644 --- a/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py +++ b/tests/prague/eip7623_increase_calldata_cost/test_execution_gas.py @@ -69,7 +69,9 @@ def to( True, None, id="type_3", - marks=pytest.mark.skip(reason="Not supported in Monad"), + marks=pytest.mark.not_valid_for( + "MONAD_EIGHT", subsequent_forks=True + ), ), pytest.param(4, True, [Address(1)], id="type_4"), ], @@ -153,7 +155,9 @@ def to( True, None, id="type_3", - marks=pytest.mark.skip(reason="Not supported in Monad"), + marks=pytest.mark.not_valid_for( + "MONAD_EIGHT", subsequent_forks=True + ), ), pytest.param(4, True, [Address(1)], id="type_4"), ], diff --git a/tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py b/tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py index 792c8e8fc98..d20f3bb8a07 100644 --- a/tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py +++ b/tests/prague/eip7623_increase_calldata_cost/test_transaction_validity.py @@ -179,7 +179,7 @@ def test_transaction_validity_type_1_type_2( ) -@pytest.mark.skip(reason="Not supported in Monad") +@pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) @pytest.mark.parametrize( "access_list", [ From 404411deff5f062e38ab90676c1439648576ded6 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:36:27 +0000 Subject: [PATCH 32/44] feat(monad): reject blob transactions on the Monad forks The forks advertise no blob schedule and fix the blob header fields at zero, so the type EIP-4844 assigns is unknown to them rather than merely unused. The type is checked ahead of the generic transaction validation, so that a transaction failing both reports the type. Co-Authored-By: Claude --- .../client_clis/clis/execution_specs.py | 1 + .../forks/monad_eight/transactions.py | 8 ++- src/ethereum/forks/monad_next/transactions.py | 8 ++- src/ethereum/forks/monad_nine/transactions.py | 8 ++- src/ethereum/forks/monad_ten/transactions.py | 8 ++- .../typed_transactions/__init__.py | 1 + .../test_blob_transaction.py | 65 +++++++++++++++++++ 7 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 tests/monad_eight/typed_transactions/__init__.py create mode 100644 tests/monad_eight/typed_transactions/test_blob_transaction.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 7d007cca02a..cb6acfb75e3 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 @@ -197,6 +197,7 @@ class ExecutionSpecsExceptionMapper(ExceptionMapper): """ mapping_substring: ClassVar[Dict[ExceptionBase, str]] = { + TransactionException.TYPE_NOT_SUPPORTED: "TransactionTypeError", TransactionException.TYPE_4_EMPTY_AUTHORIZATION_LIST: ( "EmptyAuthorizationListError" ), diff --git a/src/ethereum/forks/monad_eight/transactions.py b/src/ethereum/forks/monad_eight/transactions.py index 24bb4880a74..2073ae50a11 100644 --- a/src/ethereum/forks/monad_eight/transactions.py +++ b/src/ethereum/forks/monad_eight/transactions.py @@ -495,15 +495,14 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: Needed because non-legacy transactions aren't RLP. Legacy transactions are returned as-is, while other transaction types - are decoded based on their type identifier prefix. + are decoded based on their type identifier prefix. Blob transactions + are not accepted, so their type byte is unknown here. """ if isinstance(tx, Bytes): if tx[0] == 1: return rlp.decode_to(AccessListTransaction, tx[1:]) elif tx[0] == 2: return rlp.decode_to(FeeMarketTransaction, tx[1:]) - elif tx[0] == 3: - return rlp.decode_to(BlobTransaction, tx[1:]) elif tx[0] == 4: return rlp.decode_to(SetCodeTransaction, tx[1:]) else: @@ -541,6 +540,9 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 """ + if isinstance(tx, BlobTransaction): + raise TransactionTypeError(3) + from .vm.interpreter import MAX_INIT_CODE_SIZE intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) diff --git a/src/ethereum/forks/monad_next/transactions.py b/src/ethereum/forks/monad_next/transactions.py index 71bce9409f3..c3920f417b8 100644 --- a/src/ethereum/forks/monad_next/transactions.py +++ b/src/ethereum/forks/monad_next/transactions.py @@ -511,15 +511,14 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: Needed because non-legacy transactions aren't RLP. Legacy transactions are returned as-is, while other transaction types - are decoded based on their type identifier prefix. + are decoded based on their type identifier prefix. Blob transactions + are not accepted, so their type byte is unknown here. """ if isinstance(tx, Bytes): if tx[0] == 1: return rlp.decode_to(AccessListTransaction, tx[1:]) elif tx[0] == 2: return rlp.decode_to(FeeMarketTransaction, tx[1:]) - elif tx[0] == 3: - return rlp.decode_to(BlobTransaction, tx[1:]) elif tx[0] == 4: return rlp.decode_to(SetCodeTransaction, tx[1:]) else: @@ -557,6 +556,9 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 """ + if isinstance(tx, BlobTransaction): + raise TransactionTypeError(3) + from .vm.interpreter import MAX_INIT_CODE_SIZE intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) diff --git a/src/ethereum/forks/monad_nine/transactions.py b/src/ethereum/forks/monad_nine/transactions.py index 24bb4880a74..2073ae50a11 100644 --- a/src/ethereum/forks/monad_nine/transactions.py +++ b/src/ethereum/forks/monad_nine/transactions.py @@ -495,15 +495,14 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: Needed because non-legacy transactions aren't RLP. Legacy transactions are returned as-is, while other transaction types - are decoded based on their type identifier prefix. + are decoded based on their type identifier prefix. Blob transactions + are not accepted, so their type byte is unknown here. """ if isinstance(tx, Bytes): if tx[0] == 1: return rlp.decode_to(AccessListTransaction, tx[1:]) elif tx[0] == 2: return rlp.decode_to(FeeMarketTransaction, tx[1:]) - elif tx[0] == 3: - return rlp.decode_to(BlobTransaction, tx[1:]) elif tx[0] == 4: return rlp.decode_to(SetCodeTransaction, tx[1:]) else: @@ -541,6 +540,9 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 """ + if isinstance(tx, BlobTransaction): + raise TransactionTypeError(3) + from .vm.interpreter import MAX_INIT_CODE_SIZE intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) diff --git a/src/ethereum/forks/monad_ten/transactions.py b/src/ethereum/forks/monad_ten/transactions.py index 24bb4880a74..2073ae50a11 100644 --- a/src/ethereum/forks/monad_ten/transactions.py +++ b/src/ethereum/forks/monad_ten/transactions.py @@ -495,15 +495,14 @@ def decode_transaction(tx: LegacyTransaction | Bytes) -> Transaction: Needed because non-legacy transactions aren't RLP. Legacy transactions are returned as-is, while other transaction types - are decoded based on their type identifier prefix. + are decoded based on their type identifier prefix. Blob transactions + are not accepted, so their type byte is unknown here. """ if isinstance(tx, Bytes): if tx[0] == 1: return rlp.decode_to(AccessListTransaction, tx[1:]) elif tx[0] == 2: return rlp.decode_to(FeeMarketTransaction, tx[1:]) - elif tx[0] == 3: - return rlp.decode_to(BlobTransaction, tx[1:]) elif tx[0] == 4: return rlp.decode_to(SetCodeTransaction, tx[1:]) else: @@ -541,6 +540,9 @@ def validate_transaction(tx: Transaction) -> Tuple[Uint, Uint]: [EIP-2681]: https://eips.ethereum.org/EIPS/eip-2681 [EIP-7623]: https://eips.ethereum.org/EIPS/eip-7623 """ + if isinstance(tx, BlobTransaction): + raise TransactionTypeError(3) + from .vm.interpreter import MAX_INIT_CODE_SIZE intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx) diff --git a/tests/monad_eight/typed_transactions/__init__.py b/tests/monad_eight/typed_transactions/__init__.py new file mode 100644 index 00000000000..d21a75bb780 --- /dev/null +++ b/tests/monad_eight/typed_transactions/__init__.py @@ -0,0 +1 @@ +"""Tests for the transaction types Monad forks accept.""" diff --git a/tests/monad_eight/typed_transactions/test_blob_transaction.py b/tests/monad_eight/typed_transactions/test_blob_transaction.py new file mode 100644 index 00000000000..93b5b4ebb58 --- /dev/null +++ b/tests/monad_eight/typed_transactions/test_blob_transaction.py @@ -0,0 +1,65 @@ +""" +Tests that Monad forks reject blob transactions. + +Monad advertises no blob schedule and carries the blob header fields +fixed at zero, so the type byte EIP-4844 assigns is unknown to the fork +and a transaction carrying it is invalid rather than merely unused. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Hash, + Transaction, + TransactionException, + add_kzg_version, +) +from execution_testing.forks.helpers import Fork + +BLOB_COMMITMENT_VERSION_KZG = 1 + +pytestmark = [ + pytest.mark.valid_from("MONAD_EIGHT"), + pytest.mark.exception_test, +] + + +@pytest.mark.parametrize( + "gas_limit", + [ + pytest.param(100_000, id="sufficient_gas"), + # Below the intrinsic cost, so the transaction would fail generic + # validation too; the unknown type is reported ahead of it. + pytest.param(1_000, id="below_intrinsic_gas"), + ], +) +def test_blob_transaction_is_rejected( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + gas_limit: int, +) -> None: + """A blob transaction is not a transaction type the fork knows.""" + assert 3 not in fork.tx_types() + + sender = pre.fund_eoa() + tx = Transaction( + ty=3, + to=pre.fund_eoa(amount=0), + gas_limit=gas_limit, + max_fee_per_blob_gas=1, + blob_versioned_hashes=add_kzg_version( + [Hash(1)], BLOB_COMMITMENT_VERSION_KZG + ), + sender=sender, + error=TransactionException.TYPE_NOT_SUPPORTED, + ) + + blockchain_test( + pre=pre, + post={sender: Account(nonce=0)}, + blocks=[Block(txs=[tx], exception=tx.error)], + ) From 215356034cc1775d240944783be1a9cecc882a14 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:52:41 +0000 Subject: [PATCH 33/44] test(eip7981): pin the receipt to the gas limit on Monad Monad bills the whole limit, with no refund and no floor comparison. Co-Authored-By: Claude --- .../eip7981_increase_access_list_cost/conftest.py | 7 +++++++ .../eip7981_increase_access_list_cost/helpers.py | 10 ++++++++++ .../test_access_list_cost.py | 11 ++++++++--- .../test_execution_gas.py | 13 +++++++++++-- .../test_floor_boundary_exact_balance.py | 9 +++++++-- 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py b/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py index 9d44df9ec7b..94651deb78d 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/conftest.py @@ -19,6 +19,7 @@ TransactionReceipt, add_kzg_version, ) +from execution_testing.forks import MONAD_EIGHT from ...cancun.eip4844_blobs.spec import Spec as EIP_4844_Spec @@ -275,12 +276,18 @@ def tx_error( @pytest.fixture def tx_expected_receipt( + fork: Fork, tx_error: TransactionException | None, tx_expected_gas_used: int, + tx_gas_limit: int, ) -> TransactionReceipt | None: """Expect success and the exact gas used from a valid transaction.""" if tx_error is not None: return None + # Monad bills the whole gas limit, so the receipt pins the limit + # rather than the gas the execution would otherwise have used. + if fork >= MONAD_EIGHT: + return TransactionReceipt(status=1, gas_used=tx_gas_limit) return TransactionReceipt(status=1, gas_used=tx_expected_gas_used) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py b/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py index ae927b0f5d3..997e77605f2 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py @@ -3,6 +3,7 @@ from typing import List from execution_testing import AccessList, Fork +from execution_testing.forks import MONAD_EIGHT def calculate_access_list_data_cost( @@ -25,3 +26,12 @@ def calculate_access_list_data_cost( total_bytes += len(slot) return total_bytes * 4 * fork.gas_costs().TX_DATA_TOKEN_FLOOR + + +def billed_gas(fork: Fork, gas_limit: int, gas_used: int) -> int: + """ + Return the gas a successful transaction's receipt reports. + + Monad bills the whole limit, with no refund and no floor comparison. + """ + return gas_limit if fork >= MONAD_EIGHT else gas_used diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py index d30a4471dba..7a5406a3d94 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py @@ -18,7 +18,7 @@ TransactionReceipt, ) -from .helpers import calculate_access_list_data_cost +from .helpers import billed_gas, calculate_access_list_data_cost from .spec import ref_spec_7981 REFERENCE_SPEC_GIT_PATH = ref_spec_7981.git_path @@ -352,7 +352,9 @@ def test_access_list_data_cost_with_execution( # a gas limit that happens to equal the expected receipt value. gas_limit=expected_gas_used + 1000, expected_receipt=TransactionReceipt( - cumulative_gas_used=expected_gas_used + cumulative_gas_used=billed_gas( + fork, expected_gas_used + 1000, expected_gas_used + ) ), ) @@ -401,6 +403,9 @@ def test_access_list_surcharge_with_recipient_costs( data=data, access_list=access_list, gas_limit=expected_gas + 1000, - expected_receipt=TransactionReceipt(status=1, gas_used=expected_gas), + expected_receipt=TransactionReceipt( + status=1, + gas_used=billed_gas(fork, expected_gas + 1000, expected_gas), + ), ) state_test(pre=pre, post={}, tx=tx) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py index 14411724576..5a86c314d18 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py @@ -19,6 +19,7 @@ TransactionReceipt, ) +from .helpers import billed_gas from .spec import ref_spec_7981 REFERENCE_SPEC_GIT_PATH = ref_spec_7981.git_path @@ -63,7 +64,11 @@ def test_execution_gas_after_access_list_surcharge( gas_limit=required_gas + gas_delta, expected_receipt=TransactionReceipt( status=int(succeeds), - gas_used=required_gas if succeeds else required_gas - 1, + gas_used=billed_gas( + fork, + required_gas + gas_delta, + required_gas if succeeds else required_gas - 1, + ), ), ) state_test( @@ -114,7 +119,11 @@ def test_access_list_surcharge_with_refund( gas_limit=max(before_refund, floor) + 1000, expected_receipt=TransactionReceipt( status=0 if reverts else 1, - gas_used=max(before_refund - refund, floor), + gas_used=billed_gas( + fork, + max(before_refund, floor) + 1000, + max(before_refund - refund, floor), + ), ), ) state_test( diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py index 78b686fcff8..8d4a9325328 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_floor_boundary_exact_balance.py @@ -19,6 +19,7 @@ TransactionReceipt, ) +from .helpers import billed_gas from .spec import ref_spec_7981 REFERENCE_SPEC_GIT_PATH = ref_spec_7981.git_path @@ -101,15 +102,19 @@ def test_access_list_floor_with_exact_balance( else None ), expected_receipt=( - TransactionReceipt(status=1, gas_used=amsterdam_floor) + TransactionReceipt( + status=1, + gas_used=billed_gas(fork, gas_limit, amsterdam_floor), + ) if gas_delta >= 0 else None ), **fee_args, ) + unspent = gas_limit - billed_gas(fork, gas_limit, amsterdam_floor) post = ( - {sender: Account(nonce=1, balance=gas_delta * gas_price)} + {sender: Account(nonce=1, balance=unspent * gas_price)} if gas_delta >= 0 else {} ) From c6f42a442696d08bbb1afcb5a70459735ab0e9d3 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:45:00 +0000 Subject: [PATCH 34/44] test(eip7981): size the gas regimes from the fork The access list entry charges outgrow Monad's floor rate, so a fixed calldata length lands the cases in the wrong regime. Co-Authored-By: Claude --- .../helpers.py | 21 ++++++- .../test_access_list_cost.py | 55 +++++++++++++------ .../test_execution_gas.py | 30 +++++++--- .../test_transaction_validity.py | 2 +- 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py b/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py index 997e77605f2..26dd8f7edaf 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py @@ -1,6 +1,6 @@ """Helpers for testing EIP-7981.""" -from typing import List +from typing import Callable, List, Tuple from execution_testing import AccessList, Fork from execution_testing.forks import MONAD_EIGHT @@ -35,3 +35,22 @@ def billed_gas(fork: Fork, gas_limit: int, gas_used: int) -> int: Monad bills the whole limit, with no refund and no floor comparison. """ return gas_limit if fork >= MONAD_EIGHT else gas_used + + +def calldata_clearing_floor( + bill: Callable[[bytes], Tuple[int, int]], +) -> bytes: + """ + Return zero calldata grown in words until the floor clears the bill. + + `bill` returns the floor and the cost it competes with for the + calldata it is given. Each byte adds more to the floor than to the + intrinsic, so the search terminates on any fork. + """ + size = 0 + while True: + data = b"\x00" * size + floor, competing = bill(data) + if floor > competing: + return data + size += 32 diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py index 7a5406a3d94..0034073e7a1 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_access_list_cost.py @@ -17,8 +17,13 @@ Transaction, TransactionReceipt, ) +from execution_testing.forks import MONAD_EIGHT -from .helpers import billed_gas, calculate_access_list_data_cost +from .helpers import ( + billed_gas, + calculate_access_list_data_cost, + calldata_clearing_floor, +) from .spec import ref_spec_7981 REFERENCE_SPEC_GIT_PATH = ref_spec_7981.git_path @@ -159,7 +164,7 @@ def test_access_list_token_calculation( storage_keys=[Hash(i) for i in range(10)], ) ], - Bytes(b"\x00" * 500 + b"\x01" * 500), + Bytes(b"\x00" * 1800 + b"\x01" * 1800), id="large_access_list_mixed_calldata", ), ], @@ -330,16 +335,24 @@ def test_access_list_data_cost_with_execution( # cost so each case lands exactly where the delta places it. jumpdest_gas = Op.JUMPDEST.gas_cost(fork) assert execution_budget % jumpdest_gas == 0 + # Monad's access list entry charges outgrow the floor, leaving a + # negative budget that no program can be sized from. + program_sized_from_budget = not fork >= MONAD_EIGHT jumpdests = execution_budget // jumpdest_gas + execution_gas_delta - assert jumpdests >= 0 + if program_sized_from_budget: + assert jumpdests >= 0 + else: + jumpdests = max(jumpdests, 0) code = Op.JUMPDEST * jumpdests contract = pre.deploy_contract(code + Op.STOP) execution_cost = ( intrinsic_without_access_list + entry_charges + code.gas_cost(fork) ) - assert ( - execution_cost - calldata_floor == execution_gas_delta * jumpdest_gas - ) + if program_sized_from_budget: + assert ( + execution_cost - calldata_floor + == execution_gas_delta * jumpdest_gas + ) expected_gas_used = max(execution_cost, calldata_floor) + surcharge tx = Transaction( @@ -380,18 +393,24 @@ def test_access_list_surcharge_with_recipient_costs( target = sender if self_transfer else pre.fund_eoa(amount=1) access_list = [AccessList(address=target, storage_keys=[])] recipient_type = RecipientType.SELF if self_transfer else RecipientType.EOA - data = b"\x00" * (400 if floor_dominates else 0) - intrinsic = fork.transaction_intrinsic_cost_calculator()( - calldata=data, - sends_value=value > 0, - recipient_type=recipient_type, - return_cost_deducted_prior_execution=True, - ) - floor = fork.transaction_data_floor_cost_calculator()( - data=data, sends_value=value > 0, recipient_type=recipient_type - ) - costs = fork.gas_costs() - execution_cost = intrinsic + costs.TX_ACCESS_LIST_ADDRESS + intrinsic_calculator = fork.transaction_intrinsic_cost_calculator() + floor_calculator = fork.transaction_data_floor_cost_calculator() + + def bill(data: bytes) -> tuple[int, int]: + """Return the floor and the execution bill for `data`.""" + intrinsic = intrinsic_calculator( + calldata=data, + sends_value=value > 0, + recipient_type=recipient_type, + return_cost_deducted_prior_execution=True, + ) + floor = floor_calculator( + data=data, sends_value=value > 0, recipient_type=recipient_type + ) + return floor, intrinsic + fork.gas_costs().TX_ACCESS_LIST_ADDRESS + + data = calldata_clearing_floor(bill) if floor_dominates else b"" + floor, execution_cost = bill(data) surcharge = calculate_access_list_data_cost(access_list, fork) assert (floor > execution_cost) == floor_dominates expected_gas = max(execution_cost, floor) + surcharge diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py index 5a86c314d18..dfc3416403e 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_execution_gas.py @@ -19,7 +19,7 @@ TransactionReceipt, ) -from .helpers import billed_gas +from .helpers import billed_gas, calldata_clearing_floor from .spec import ref_spec_7981 REFERENCE_SPEC_GIT_PATH = ref_spec_7981.git_path @@ -97,18 +97,31 @@ def test_access_list_surcharge_with_refund( code = clear + (Op.REVERT(0, 0) if reverts else Op.STOP) contract = pre.deploy_contract(code=code, storage={0: 1}) access_list = [AccessList(address=contract, storage_keys=[Hash(0)])] - data = b"\x00" * (1000 if floor_dominates else 0) - intrinsic = fork.transaction_intrinsic_cost_calculator()( + intrinsic_calculator = fork.transaction_intrinsic_cost_calculator() + floor_calculator = fork.transaction_data_floor_cost_calculator() + refund_counter = 0 if reverts else clear.refund(fork) + + def bill(data: bytes) -> tuple[int, int]: + """Return the floor and the post-refund bill for `data`.""" + intrinsic = intrinsic_calculator( + calldata=data, + access_list=access_list, + return_cost_deducted_prior_execution=True, + ) + before = intrinsic + code.gas_cost(fork) + given = min(refund_counter, before // fork.max_refund_quotient()) + floor = floor_calculator(data=data, access_list=access_list) + return floor, before - given + + data = calldata_clearing_floor(bill) if floor_dominates else b"" + intrinsic = intrinsic_calculator( calldata=data, access_list=access_list, return_cost_deducted_prior_execution=True, ) before_refund = intrinsic + code.gas_cost(fork) - refund_counter = 0 if reverts else clear.refund(fork) refund = min(refund_counter, before_refund // fork.max_refund_quotient()) - floor = fork.transaction_data_floor_cost_calculator()( - data=data, access_list=access_list - ) + floor, _ = bill(data) assert (floor > before_refund - refund) == floor_dominates tx = Transaction( ty=tx_type, @@ -134,6 +147,9 @@ def test_access_list_surcharge_with_refund( @EIPChecklist.GasCostChanges.Test.GasUpdatesMeasurement() +# The state gas the reservoir funds arrives with EIP-8037, which the +# Monad forks do not adopt, leaving nothing to distribute here. +@pytest.mark.not_valid_for("MONAD_EIGHT", subsequent_forks=True) @pytest.mark.with_all_tx_types(selector=lambda tx_type: tx_type in (1, 2)) @pytest.mark.parametrize( "gas_dominance", diff --git a/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py index f931571b93d..12b446a65be 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py @@ -192,7 +192,7 @@ def test_valid_gas_limits_with_access_list( [ pytest.param( [AccessList(address=Address(0), storage_keys=[Hash(0)] * 10)], - Bytes(b"\x00" * 3000), + Bytes(b"\x00" * 4000), id="zero_heavy_data_and_access_list", ), pytest.param( From d20bef1c64f79c557dff39f696f7670cfa9893bc Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:18:19 +0000 Subject: [PATCH 35/44] test(eip7954): pin the receipts to Monad's gas billing MONAD_NEXT holds the exact deposit fit under its cap with no reservoir. Co-Authored-By: Claude --- .../helpers.py | 13 +++++++++ .../test_max_code_size.py | 27 ++++++++++++------- .../test_max_initcode_size.py | 5 +++- 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 tests/amsterdam/eip7954_increase_max_contract_size/helpers.py diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/helpers.py b/tests/amsterdam/eip7954_increase_max_contract_size/helpers.py new file mode 100644 index 00000000000..3a3cfd87081 --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/helpers.py @@ -0,0 +1,13 @@ +"""Helpers for testing EIP-7954.""" + +from execution_testing import Fork +from execution_testing.forks import MONAD_EIGHT + + +def billed_gas(fork: Fork, gas_limit: int, gas_used: int) -> int: + """ + Return the gas a successful transaction's receipt reports. + + Monad bills the whole limit, with no refund and no floor comparison. + """ + return gas_limit if fork >= MONAD_EIGHT else gas_used diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py index e6bfda7853f..e35f380ac5d 100644 --- a/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py @@ -25,6 +25,7 @@ from execution_testing.forks import Osaka from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 +from .helpers import billed_gas from .spec import ref_spec_7954 REFERENCE_SPEC_GIT_PATH = ref_spec_7954.git_path @@ -196,18 +197,24 @@ def test_max_code_size_deposit_gas( gas_limit=exact_gas - gas_shortfall, ) - # The receipt pin below reads the cap, which only holds while the exact - # fit exceeds it and the deposit is funded from the reservoir. + # The shortfall receipt reads the cap while the exact fit exceeds it + # and the deposit is funded from the reservoir. Without the reservoir + # the cap holds the exact fit and the shortfall burns the whole limit. gas_limit_cap = fork.transaction_gas_limit_cap() assert gas_limit_cap is not None - assert exact_gas > gas_limit_cap + if fork.state_gas_reservoir_enabled(): + assert exact_gas > gas_limit_cap + shortfall_gas_used = gas_limit_cap + else: + assert exact_gas <= gas_limit_cap + shortfall_gas_used = tx.gas_limit post: dict[Any, Account | None] = {} if gas_shortfall: - # The deposit halts the frame, burning the whole execution gas - # allowance while the state gas reservoir is handed back. + # The deposit halts the frame, burning the execution gas allowance + # while any state gas reservoir is handed back. tx.expected_receipt = TransactionReceipt( - cumulative_gas_used=gas_limit_cap + cumulative_gas_used=shortfall_gas_used ) post[create_address] = Account.NONEXISTENT else: @@ -382,9 +389,11 @@ def test_max_code_size_linear_execution( sender=pre.fund_eoa(), to=target, gas_limit=fork.transaction_gas_limit_cap(), - expected_receipt=TransactionReceipt( - cumulative_gas_used=intrinsic_gas + target_code.gas_cost(fork) - ), + ) + tx.expected_receipt = TransactionReceipt( + cumulative_gas_used=billed_gas( + fork, tx.gas_limit, intrinsic_gas + target_code.gas_cost(fork) + ) ) state_test(pre=pre, tx=tx, post={target: Account(code=target_code)}) diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/test_max_initcode_size.py b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_initcode_size.py index ce8f745f507..26045748da1 100644 --- a/tests/amsterdam/eip7954_increase_max_contract_size/test_max_initcode_size.py +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_initcode_size.py @@ -26,6 +26,7 @@ from execution_testing import Macros as Om from execution_testing.forks import Osaka +from .helpers import billed_gas from .spec import ref_spec_7954 REFERENCE_SPEC_GIT_PATH = ref_spec_7954.git_path @@ -224,7 +225,9 @@ def test_max_initcode_size_calldata_floor( else: # The deployment spends a fraction of the floor, so the floor is # what the sender is charged, spare gas or not. - tx.expected_receipt = TransactionReceipt(cumulative_gas_used=floor_gas) + tx.expected_receipt = TransactionReceipt( + cumulative_gas_used=billed_gas(fork, tx.gas_limit, floor_gas) + ) post[create_address] = Account(code=Op.STOP) state_test(pre=pre, tx=tx, post=post) From 7e22d08365cc821f2e009c78d0b272e92b856b74 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:00 +0000 Subject: [PATCH 36/44] chore(forks,tests): resolve the from-upstream merge conflicts Keep the Bogota fork class next to MONAD_NEXT and stack upstream's new parametrizations on the no_bal_dependency BAL tests. Co-Authored-By: Claude --- .../execution_testing/forks/forks/forks.py | 22 +++++++++---------- .../test_block_access_lists.py | 8 ------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 9776d6661fd..4e590aff412 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1933,7 +1933,16 @@ def engine_payload_attribute_target_gas_limit(cls) -> bool: return True -<<<<<<< HEAD +class Bogota( + BogotaEIPs, + Amsterdam, + deployed=False, +): + """Bogota fork.""" + + pass + + class MONAD_NEXT( # noqa: N801 eips.EIP7708, eips.EIP7843, @@ -1966,14 +1975,3 @@ def header_bal_hash_required(cls) -> bool: at zero, without building block access lists (EIP-7928). """ return True -||||||| b5f3d1cfd -======= -class Bogota( - BogotaEIPs, - Amsterdam, - deployed=False, -): - """Bogota fork.""" - - pass ->>>>>>> from-upstream diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 0129e051771..f57f9ff2491 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -2493,10 +2493,7 @@ def test_bal_multiple_balance_changes_same_account( ) -<<<<<<< HEAD @pytest.mark.no_bal_dependency -||||||| b5f3d1cfd -======= @pytest.mark.parametrize( "senders", [ @@ -2504,7 +2501,6 @@ def test_bal_multiple_balance_changes_same_account( pytest.param("distinct", id="distinct_senders"), ], ) ->>>>>>> from-upstream def test_bal_multiple_storage_writes_same_slot( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -3677,10 +3673,7 @@ def test_bal_cross_tx_funding_chain( ) -<<<<<<< HEAD @pytest.mark.no_bal_dependency -||||||| b5f3d1cfd -======= @pytest.mark.parametrize( "last_write", [ @@ -4375,7 +4368,6 @@ def test_bal_cross_tx_code_reads_across_deploy( ) ->>>>>>> from-upstream def test_bal_cross_block_ripemd160_state_leak( pre: Alloc, blockchain_test: BlockchainTestFiller, From e59c7761d6d5977fe0b461d1a43cf185651a0695 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:00 +0000 Subject: [PATCH 37/44] test(amsterdam): pin the new EIP-7708 and EIP-8246 cases to Monad's reserve and gas billing The delegated EOA keeps the reserve on top of what it sends, and the created coinbase earns the tip on the whole gas limit. Co-Authored-By: Claude --- .../test_transfer_logs.py | 12 +++++++++-- .../eip8246_selfdestruct_no_burn/helpers.py | 13 ++++++++++++ .../test_selfdestruct_no_burn.py | 20 +++++++++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 tests/amsterdam/eip8246_selfdestruct_no_burn/helpers.py diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/test_transfer_logs.py b/tests/amsterdam/eip7708_eth_transfer_logs/test_transfer_logs.py index 78ef5161676..8135e3be31c 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/test_transfer_logs.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/test_transfer_logs.py @@ -34,7 +34,9 @@ create_op, ) from execution_testing.base_types import ZeroPaddedHexNumber +from execution_testing.forks import MONAD_EIGHT +from ...monad_eight.reserve_balance.spec import Spec as ReserveSpec from .spec import Spec, ref_spec_7708, transfer_log REFERENCE_SPEC_GIT_PATH = ref_spec_7708.git_path @@ -1526,6 +1528,7 @@ def test_call_to_delegated_account_with_value( def test_delegated_eoa_sends_value( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """ Test that CALL with value from an EIP-7702 delegated EOA's code logs @@ -1535,7 +1538,12 @@ def test_delegated_eoa_sends_value( delegate = pre.deploy_contract( Op.CALL(gas=Op.GAS, address=recipient, value=ENDOWMENT) ) - delegated_eoa = pre.fund_eoa(amount=ENDOWMENT, delegation=delegate) + # Monad reverts a delegated EOA that spends below its reserve, so the + # EOA keeps the reserve on top of the endowment it sends. + reserve = ReserveSpec.RESERVE_BALANCE if fork >= MONAD_EIGHT else 0 + delegated_eoa = pre.fund_eoa( + amount=ENDOWMENT + reserve, delegation=delegate + ) tx = Transaction( sender=pre.fund_eoa(), @@ -1545,7 +1553,7 @@ def test_delegated_eoa_sends_value( ), ) post = { - delegated_eoa: Account(balance=0), + delegated_eoa: Account(balance=reserve), recipient: Account(balance=ENDOWMENT), delegate: Account(balance=0), } diff --git a/tests/amsterdam/eip8246_selfdestruct_no_burn/helpers.py b/tests/amsterdam/eip8246_selfdestruct_no_burn/helpers.py new file mode 100644 index 00000000000..2700b7ccc2b --- /dev/null +++ b/tests/amsterdam/eip8246_selfdestruct_no_burn/helpers.py @@ -0,0 +1,13 @@ +"""Helpers for testing EIP-8246.""" + +from execution_testing import Fork +from execution_testing.forks import MONAD_EIGHT + + +def billed_gas(fork: Fork, gas_limit: int, gas_used: int) -> int: + """ + Return the gas a successful transaction's receipt reports. + + Monad bills the whole limit, with no refund and no floor comparison. + """ + return gas_limit if fork >= MONAD_EIGHT else gas_used diff --git a/tests/amsterdam/eip8246_selfdestruct_no_burn/test_selfdestruct_no_burn.py b/tests/amsterdam/eip8246_selfdestruct_no_burn/test_selfdestruct_no_burn.py index 6c095e804eb..df740cab605 100644 --- a/tests/amsterdam/eip8246_selfdestruct_no_burn/test_selfdestruct_no_burn.py +++ b/tests/amsterdam/eip8246_selfdestruct_no_burn/test_selfdestruct_no_burn.py @@ -36,9 +36,11 @@ Macros as Om, ) from execution_testing.checklists import EIPChecklist +from execution_testing.forks import MONAD_EIGHT from ..eip7708_eth_transfer_logs.spec import transfer_log from ..eip7997_deterministic_factory_contract.spec import Spec +from .helpers import billed_gas from .spec import ref_spec_8246 REFERENCE_SPEC_GIT_PATH = ref_spec_8246.git_path @@ -322,18 +324,25 @@ def test_created_coinbase_initcode_selfdestruct_keeps_priority_fee( ) # No refund accrues; the sender pays the larger of use and floor. gas_used = max(executed, floor) + # Monad bills the whole gas limit, so pin it to the fork's cap there, + # which is where the implicit limit lands anyway. + gas_limit = ( + fork.transaction_gas_limit_cap() if fork >= MONAD_EIGHT else None + ) tx = Transaction( sender=sender, to=None, value=value, data=initcode, + gas_limit=gas_limit, ) state_test( env=Environment(fee_recipient=created), pre=pre, post={ created: Account( - balance_change=value + Tip(tx, gas=gas_used), + balance_change=value + + Tip(tx, gas=billed_gas(fork, tx.gas_limit, gas_used)), nonce=0, code=b"", storage={}, @@ -400,17 +409,24 @@ def test_created_coinbase_selfdestruct_keeps_priority_fee( + initcode.gas_cost(fork) + deploy_code.gas_cost(fork) ) + # Monad bills the whole gas limit, so pin it to the fork's cap there, + # which is where the implicit limit lands anyway. + gas_limit = ( + fork.transaction_gas_limit_cap() if fork >= MONAD_EIGHT else None + ) tx = Transaction( sender=sender, to=factory, value=value, + gas_limit=gas_limit, ) state_test( env=Environment(fee_recipient=created), pre=pre, post={ created: Account( - balance_change=value + Tip(tx, gas=gas_used), + balance_change=value + + Tip(tx, gas=billed_gas(fork, tx.gas_limit, gas_used)), nonce=0, code=b"", storage={}, From f05a76b16e3a2f39e9ea05479e97191f71bdd6c3 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:00 +0000 Subject: [PATCH 38/44] refactor(forks,ci): rename `monad_next` to `monad_eleven` Release MONAD_ELEVEN from the monad and monad_runloop features instead of the EIP bundle's own feature. Co-Authored-By: Claude --- .github/configs/feature.yaml | 7 ++-- .github/scripts/generate_build_matrix.py | 2 +- .../rust-harness/eest-runner/src/main.rs | 2 +- .../client_clis/clis/monad.py | 4 +-- .../src/execution_testing/forks/__init__.py | 8 ++--- .../execution_testing/forks/forks/forks.py | 10 +++--- .../forks/forks/transition.py | 10 +++--- .../forks/tests/test_forks.py | 26 +++++++-------- .../{monad_next => monad_eleven}/__init__.py | 2 +- .../{monad_next => monad_eleven}/blocks.py | 26 +++++++-------- .../{monad_next => monad_eleven}/bloom.py | 0 .../exceptions.py | 0 .../{monad_next => monad_eleven}/fork.py | 0 .../fork_types.py | 0 .../{monad_next => monad_eleven}/requests.py | 23 ++++++------- .../state_tracker.py | 0 .../transactions.py | 0 .../utils/__init__.py | 0 .../utils/address.py | 4 +-- .../utils/hexadecimal.py | 2 +- .../utils/message.py | 4 +-- .../vm/__init__.py | 0 .../vm/eoa_delegation.py | 0 .../vm/exceptions.py | 0 .../{monad_next => monad_eleven}/vm/gas.py | 0 .../vm/instructions/__init__.py | 0 .../vm/instructions/arithmetic.py | 0 .../vm/instructions/bitwise.py | 0 .../vm/instructions/block.py | 32 +++++++++---------- .../vm/instructions/comparison.py | 0 .../vm/instructions/control_flow.py | 0 .../vm/instructions/environment.py | 0 .../vm/instructions/keccak.py | 0 .../vm/instructions/log.py | 0 .../vm/instructions/memory.py | 0 .../vm/instructions/stack.py | 0 .../vm/instructions/storage.py | 0 .../vm/instructions/system.py | 0 .../vm/interpreter.py | 4 +-- .../{monad_next => monad_eleven}/vm/memory.py | 0 .../vm/precompiled_contracts/__init__.py | 0 .../vm/precompiled_contracts/alt_bn128.py | 0 .../vm/precompiled_contracts/blake2f.py | 0 .../bls12_381/__init__.py | 0 .../bls12_381/bls12_381_g1.py | 0 .../bls12_381/bls12_381_g2.py | 0 .../bls12_381/bls12_381_pairing.py | 0 .../vm/precompiled_contracts/ecrecover.py | 0 .../vm/precompiled_contracts/identity.py | 0 .../vm/precompiled_contracts/mapping.py | 0 .../vm/precompiled_contracts/modexp.py | 0 .../vm/precompiled_contracts/p256verify.py | 0 .../precompiled_contracts/point_evaluation.py | 0 .../precompiled_contracts/reserve_balance.py | 0 .../vm/precompiled_contracts/ripemd160.py | 0 .../vm/precompiled_contracts/sha256.py | 0 .../vm/runtime.py | 0 .../{monad_next => monad_eleven}/vm/stack.py | 0 src/ethereum_spec_tools/utils.py | 6 ++-- tests/monad_amsterdam/__init__.py | 2 +- .../test_factory_reach.py | 2 +- 61 files changed, 88 insertions(+), 88 deletions(-) rename src/ethereum/forks/{monad_next => monad_eleven}/__init__.py (94%) rename src/ethereum/forks/{monad_next => monad_eleven}/blocks.py (93%) rename src/ethereum/forks/{monad_next => monad_eleven}/bloom.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/exceptions.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/fork.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/fork_types.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/requests.py (92%) rename src/ethereum/forks/{monad_next => monad_eleven}/state_tracker.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/transactions.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/utils/__init__.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/utils/address.py (94%) rename src/ethereum/forks/{monad_next => monad_eleven}/utils/hexadecimal.py (98%) rename src/ethereum/forks/{monad_next => monad_eleven}/utils/message.py (95%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/__init__.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/eoa_delegation.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/exceptions.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/gas.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/__init__.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/arithmetic.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/bitwise.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/block.py (82%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/comparison.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/control_flow.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/environment.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/keccak.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/log.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/memory.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/stack.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/storage.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/instructions/system.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/interpreter.py (99%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/memory.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/__init__.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/alt_bn128.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/blake2f.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/bls12_381/__init__.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/bls12_381/bls12_381_g1.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/bls12_381/bls12_381_g2.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/ecrecover.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/identity.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/mapping.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/modexp.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/p256verify.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/point_evaluation.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/reserve_balance.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/ripemd160.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/precompiled_contracts/sha256.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/runtime.py (100%) rename src/ethereum/forks/{monad_next => monad_eleven}/vm/stack.py (100%) diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index 1e8bfe6c9e6..e5e3c06fc0b 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -1,15 +1,12 @@ monad: evm-type: eels - fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=143 -k "not invalid_header" + fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_ELEVEN --chain-id=143 -k "not invalid_header" monad_runloop: evm-type: eels # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` - fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_TEN --chain-id=30143 --monad-runloop -k "not invalid_header" + fill-params: -m blockchain_test --from=MONAD_EIGHT --until=MONAD_ELEVEN --chain-id=30143 --monad-runloop -k "not invalid_header" -monad_eip7708+7843+7981+7997+8024+8246: - evm-type: eels - fill-params: -m blockchain_test --fork=MONAD_NEXT --chain-id=143 -k "not invalid_header" monad_benchmark: evm-type: eels # The MIP-8 perf suite, which lives outside the default collection and diff --git a/.github/scripts/generate_build_matrix.py b/.github/scripts/generate_build_matrix.py index 179395ec251..5853dc99e3e 100644 --- a/.github/scripts/generate_build_matrix.py +++ b/.github/scripts/generate_build_matrix.py @@ -75,7 +75,7 @@ "MONAD_EIGHT", "MONAD_NINE", "MONAD_TEN", - "MONAD_NEXT", + "MONAD_ELEVEN", ] FORK_INDEX = {name: i for i, name in enumerate(FORK_ORDER)} diff --git a/monad-runloop/rust-harness/eest-runner/src/main.rs b/monad-runloop/rust-harness/eest-runner/src/main.rs index ef950818f5c..045ecac48ca 100644 --- a/monad-runloop/rust-harness/eest-runner/src/main.rs +++ b/monad-runloop/rust-harness/eest-runner/src/main.rs @@ -242,7 +242,7 @@ fn main() { } // The canonical state root is the page-encoded secondary timeline's - // from MONAD_NEXT (MIP-8, revision 10) on, the slot-encoded + // from MONAD_ELEVEN (MIP-8, revision 10) on, the slot-encoded // primary's before. The schedule is ascending, so the last entry is // the revision the run ends in. let final_revision = input diff --git a/packages/testing/src/execution_testing/client_clis/clis/monad.py b/packages/testing/src/execution_testing/client_clis/clis/monad.py index 5f28a0e70aa..4c7f95b9b76 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/monad.py +++ b/packages/testing/src/execution_testing/client_clis/clis/monad.py @@ -50,10 +50,10 @@ "MONAD_EIGHT": [(8, 0)], "MONAD_NINE": [(9, 0)], "MONAD_TEN": [(10, 0)], - "MONAD_NEXT": [(11, 0)], + "MONAD_ELEVEN": [(11, 0)], "MONAD_EIGHTToMONAD_NINEAtTime15k": [(8, 0), (9, 15_000)], "MONAD_NINEToMONAD_TENAtTime15k": [(9, 0), (10, 15_000)], - "MONAD_TENToMONAD_NEXTAtTime15k": [(10, 0), (11, 15_000)], + "MONAD_TENToMONAD_ELEVENAtTime15k": [(10, 0), (11, 15_000)], } diff --git a/packages/testing/src/execution_testing/forks/__init__.py b/packages/testing/src/execution_testing/forks/__init__.py index 83949a172bc..041fe32a1d8 100644 --- a/packages/testing/src/execution_testing/forks/__init__.py +++ b/packages/testing/src/execution_testing/forks/__init__.py @@ -18,7 +18,7 @@ BPO4, BPO5, MONAD_EIGHT, - MONAD_NEXT, + MONAD_ELEVEN, MONAD_NINE, MONAD_TEN, Amsterdam, @@ -52,7 +52,7 @@ CancunToPragueAtTime15k, MONAD_EIGHTToMONAD_NINEAtTime15k, MONAD_NINEToMONAD_TENAtTime15k, - MONAD_TENToMONAD_NEXTAtTime15k, + MONAD_TENToMONAD_ELEVENAtTime15k, OsakaToBPO1AtTime15k, ParisToShanghaiAtTime15k, PragueToOsakaAtTime15k, @@ -161,8 +161,8 @@ "MONAD_NINE", "MONAD_NINEToMONAD_TENAtTime15k", "MONAD_TEN", - "MONAD_TENToMONAD_NEXTAtTime15k", - "MONAD_NEXT", + "MONAD_TENToMONAD_ELEVENAtTime15k", + "MONAD_ELEVEN", "BPO1", "BPO1ToBPO2AtTime15k", "BPO2", diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 4e590aff412..2e30a2a99ec 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1943,7 +1943,7 @@ class Bogota( pass -class MONAD_NEXT( # noqa: N801 +class MONAD_ELEVEN( # noqa: N801 eips.EIP7708, eips.EIP7843, eips.EIP7981, @@ -1954,24 +1954,24 @@ class MONAD_NEXT( # noqa: N801 deployed=False, ): """ - MONAD_NEXT fork. + MONAD_ELEVEN fork. Amsterdam-based successor to MONAD_TEN, adopting the EIP-7708, EIP-7843, EIP-7981, EIP-7997, EIP-8024 and EIP-8246 changes. The Amsterdam changes it does not adopt stay out of the fork by not - being inherited at all; the fork order still places MONAD_NEXT + being inherited at all; the fork order still places MONAD_ELEVEN after Amsterdam through `follows`. """ @classmethod def follows(cls) -> type[BaseFork] | None: - """MONAD_NEXT comes after Amsterdam without inheriting it.""" + """MONAD_ELEVEN comes after Amsterdam without inheriting it.""" return Amsterdam @classmethod def header_bal_hash_required(cls) -> bool: """ - MONAD_NEXT headers carry the block access list hash field, fixed + MONAD_ELEVEN headers carry the block access list hash field, fixed at zero, without building block access lists (EIP-7928). """ return True diff --git a/packages/testing/src/execution_testing/forks/forks/transition.py b/packages/testing/src/execution_testing/forks/forks/transition.py index c7f0d7e9287..39227d304cf 100644 --- a/packages/testing/src/execution_testing/forks/forks/transition.py +++ b/packages/testing/src/execution_testing/forks/forks/transition.py @@ -7,7 +7,7 @@ BPO3, BPO4, MONAD_EIGHT, - MONAD_NEXT, + MONAD_ELEVEN, MONAD_NINE, MONAD_TEN, Amsterdam, @@ -74,9 +74,11 @@ class MONAD_NINEToMONAD_TENAtTime15k(TransitionBaseClass): # noqa: N801 pass -@transition_fork(to_fork=MONAD_NEXT, from_fork=MONAD_TEN, at_timestamp=15_000) -class MONAD_TENToMONAD_NEXTAtTime15k(TransitionBaseClass): # noqa: N801 - """MONAD_TEN to MONAD_NEXT transition at Timestamp 15k.""" +@transition_fork( + to_fork=MONAD_ELEVEN, from_fork=MONAD_TEN, at_timestamp=15_000 +) +class MONAD_TENToMONAD_ELEVENAtTime15k(TransitionBaseClass): # noqa: N801 + """MONAD_TEN to MONAD_ELEVEN transition at Timestamp 15k.""" pass diff --git a/packages/testing/src/execution_testing/forks/tests/test_forks.py b/packages/testing/src/execution_testing/forks/tests/test_forks.py index 698be106319..708e27add8f 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_forks.py +++ b/packages/testing/src/execution_testing/forks/tests/test_forks.py @@ -19,7 +19,7 @@ BPO4, BPO5, MONAD_EIGHT, - MONAD_NEXT, + MONAD_ELEVEN, MONAD_TEN, Amsterdam, Berlin, @@ -311,19 +311,19 @@ def test_succession_does_not_imply_eip_membership() -> None: """ A fork placed after another by `follows` does not gain its EIPs. - Succession orders MONAD_NEXT after Amsterdam so the Amsterdam + Succession orders MONAD_ELEVEN after Amsterdam so the Amsterdam suites can select it, while the EIPs it declines must still compare as absent. An adopted EIP is reached through inheritance instead, so it keeps comparing as present. """ - assert MONAD_NEXT > Amsterdam - assert MONAD_NEXT > MONAD_TEN + assert MONAD_ELEVEN > Amsterdam + assert MONAD_ELEVEN > MONAD_TEN - assert MONAD_NEXT >= EIP7708 - assert MONAD_NEXT.is_eip_enabled(7708) + assert MONAD_ELEVEN >= EIP7708 + assert MONAD_ELEVEN.is_eip_enabled(7708) - assert not MONAD_NEXT >= EIP2780 - assert not MONAD_NEXT.is_eip_enabled(2780) + assert not MONAD_ELEVEN >= EIP2780 + assert not MONAD_ELEVEN.is_eip_enabled(2780) def test_transition_fork_comparison() -> None: @@ -412,13 +412,13 @@ class PreAllocTransitionFork(TransitionBaseClass): pass -def test_monad_next_is_not_deployed() -> None: +def test_monad_eleven_is_not_deployed() -> None: """ A fork under development must stay out of the deployed fork range. """ deployed_forks = get_deployed_forks() assert MONAD_TEN in deployed_forks - assert MONAD_NEXT not in deployed_forks + assert MONAD_ELEVEN not in deployed_forks def opcode_names(fork: Type[BaseFork]) -> set[str]: @@ -428,15 +428,15 @@ def opcode_names(fork: Type[BaseFork]) -> set[str]: def test_adopted_opcodes_depend_on_mixin_order() -> None: """ - The adopted opcodes reach MONAD_NEXT through its mixin order. + The adopted opcodes reach MONAD_ELEVEN through its mixin order. MONAD_NINE returning Osaka's list directly is what ends the chain there, which is what lets a mixin placed ahead of the Monad forks contribute on the way out. Making that call cooperative, or - reordering MONAD_NEXT's bases, drops these opcodes silently. + reordering MONAD_ELEVEN's bases, drops these opcodes silently. """ adopted = {"DUPN", "SWAPN", "EXCHANGE"} - assert adopted <= opcode_names(MONAD_NEXT) + assert adopted <= opcode_names(MONAD_ELEVEN) assert not adopted & opcode_names(MONAD_TEN) class MixinAfterFork(MONAD_TEN, EIP8024): # noqa: N801 diff --git a/src/ethereum/forks/monad_next/__init__.py b/src/ethereum/forks/monad_eleven/__init__.py similarity index 94% rename from src/ethereum/forks/monad_next/__init__.py rename to src/ethereum/forks/monad_eleven/__init__.py index fe79f2c61c8..30282ec017e 100644 --- a/src/ethereum/forks/monad_next/__init__.py +++ b/src/ethereum/forks/monad_eleven/__init__.py @@ -1,5 +1,5 @@ """ -MONAD_NEXT fork builds on MONAD_TEN, adopting part of Amsterdam. +MONAD_ELEVEN fork builds on MONAD_TEN, adopting part of Amsterdam. The Amsterdam changes it does not adopt stay out of the fork entirely. The [EIP-7928] block access list hash header slot is carried but always diff --git a/src/ethereum/forks/monad_next/blocks.py b/src/ethereum/forks/monad_eleven/blocks.py similarity index 93% rename from src/ethereum/forks/monad_next/blocks.py rename to src/ethereum/forks/monad_eleven/blocks.py index 7a66282a7ab..168435cea9b 100644 --- a/src/ethereum/forks/monad_next/blocks.py +++ b/src/ethereum/forks/monad_eleven/blocks.py @@ -40,7 +40,7 @@ class Withdrawal: chain) to the execution layer, as validated by the consensus layer. Each withdrawal is listed in the block's list of withdrawals. See [`block`]. - [`block`]: ref:ethereum.forks.monad_next.blocks.Block.withdrawals + [`block`]: ref:ethereum.forks.monad_eleven.blocks.Block.withdrawals """ index: U64 @@ -90,7 +90,7 @@ class Header: [`keccak256`]: ref:ethereum.crypto.hash.keccak256 [RLP]: https://ethereum.github.io/ethereum-rlp/src/ethereum_rlp/rlp.py.html - [`EMPTY_OMMER_HASH`]: ref:ethereum.forks.monad_next.fork.EMPTY_OMMER_HASH + [`EMPTY_OMMER_HASH`]: ref:ethereum.forks.monad_eleven.fork.EMPTY_OMMER_HASH """ coinbase: Address @@ -146,7 +146,7 @@ class Header: Bloom filter for logs generated by transactions in this block. Constructed from all logs in the block using the [logs bloom] mechanism. - [logs bloom]: ref:ethereum.forks.monad_next.bloom.logs_bloom + [logs bloom]: ref:ethereum.forks.monad_eleven.bloom.logs_bloom """ difficulty: Uint @@ -244,7 +244,7 @@ class Header: [EIP-7685]. See [`compute_requests_hash`][crh] for more details. [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 - [crh]: ref:ethereum.forks.monad_next.requests.compute_requests_hash + [crh]: ref:ethereum.forks.monad_eleven.requests.compute_requests_hash [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 """ @@ -255,7 +255,7 @@ class Header: zero. See [`validate_header`][vh]. [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 - [vh]: ref:ethereum.forks.monad_next.fork.validate_header + [vh]: ref:ethereum.forks.monad_eleven.fork.validate_header """ slot_number: U64 @@ -290,8 +290,8 @@ class Block: Withdrawals represent ETH transfers from validators to their recipients, introduced by the consensus layer. Ommers remain deprecated and empty. - [`header`]: ref:ethereum.forks.monad_next.blocks.Header - [withdrawals]: ref:ethereum.forks.monad_next.blocks.Withdrawal + [`header`]: ref:ethereum.forks.monad_eleven.blocks.Header + [withdrawals]: ref:ethereum.forks.monad_eleven.blocks.Withdrawal """ header: Header @@ -299,7 +299,7 @@ class Block: The block header containing metadata and cryptographic commitments. Refer to [headers] for more details on the fields included in the header. - [headers]: ref:ethereum.forks.monad_next.blocks.Header + [headers]: ref:ethereum.forks.monad_eleven.blocks.Header """ transactions: Tuple[Bytes | LegacyTransaction, ...] @@ -331,11 +331,11 @@ class Log: [`LOG1`], [`LOG2`], [`LOG3`] and [`LOG4`]), which can be efficiently searched using the bloom filter in the block header. - [`LOG0`]: ref:ethereum.forks.monad_next.vm.instructions.log.log0 - [`LOG1`]: ref:ethereum.forks.monad_next.vm.instructions.log.log1 - [`LOG2`]: ref:ethereum.forks.monad_next.vm.instructions.log.log2 - [`LOG3`]: ref:ethereum.forks.monad_next.vm.instructions.log.log3 - [`LOG4`]: ref:ethereum.forks.monad_next.vm.instructions.log.log4 + [`LOG0`]: ref:ethereum.forks.monad_eleven.vm.instructions.log.log0 + [`LOG1`]: ref:ethereum.forks.monad_eleven.vm.instructions.log.log1 + [`LOG2`]: ref:ethereum.forks.monad_eleven.vm.instructions.log.log2 + [`LOG3`]: ref:ethereum.forks.monad_eleven.vm.instructions.log.log3 + [`LOG4`]: ref:ethereum.forks.monad_eleven.vm.instructions.log.log4 """ address: Address diff --git a/src/ethereum/forks/monad_next/bloom.py b/src/ethereum/forks/monad_eleven/bloom.py similarity index 100% rename from src/ethereum/forks/monad_next/bloom.py rename to src/ethereum/forks/monad_eleven/bloom.py diff --git a/src/ethereum/forks/monad_next/exceptions.py b/src/ethereum/forks/monad_eleven/exceptions.py similarity index 100% rename from src/ethereum/forks/monad_next/exceptions.py rename to src/ethereum/forks/monad_eleven/exceptions.py diff --git a/src/ethereum/forks/monad_next/fork.py b/src/ethereum/forks/monad_eleven/fork.py similarity index 100% rename from src/ethereum/forks/monad_next/fork.py rename to src/ethereum/forks/monad_eleven/fork.py diff --git a/src/ethereum/forks/monad_next/fork_types.py b/src/ethereum/forks/monad_eleven/fork_types.py similarity index 100% rename from src/ethereum/forks/monad_next/fork_types.py rename to src/ethereum/forks/monad_eleven/fork_types.py diff --git a/src/ethereum/forks/monad_next/requests.py b/src/ethereum/forks/monad_eleven/requests.py similarity index 92% rename from src/ethereum/forks/monad_next/requests.py rename to src/ethereum/forks/monad_eleven/requests.py index df2c2901ab4..82a3fbb1ebc 100644 --- a/src/ethereum/forks/monad_next/requests.py +++ b/src/ethereum/forks/monad_eleven/requests.py @@ -19,13 +19,13 @@ [EIP-4895]: https://eips.ethereum.org/EIPS/eip-4895 [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 -[rh]: ref:ethereum.forks.monad_next.blocks.Header.requests_hash -[dt]: ref:ethereum.forks.monad_next.requests.DEPOSIT_REQUEST_TYPE -[wt]: ref:ethereum.forks.monad_next.requests.WITHDRAWAL_REQUEST_TYPE -[ct]: ref:ethereum.forks.monad_next.requests.CONSOLIDATION_REQUEST_TYPE -[pd]: ref:ethereum.forks.monad_next.requests.parse_deposit_requests -[crh]: ref:ethereum.forks.monad_next.requests.compute_requests_hash -[pgpr]: ref:ethereum.forks.monad_next.fork.process_general_purpose_requests +[rh]: ref:ethereum.forks.monad_eleven.blocks.Header.requests_hash +[dt]: ref:ethereum.forks.monad_eleven.requests.DEPOSIT_REQUEST_TYPE +[wt]: ref:ethereum.forks.monad_eleven.requests.WITHDRAWAL_REQUEST_TYPE +[ct]: ref:ethereum.forks.monad_eleven.requests.CONSOLIDATION_REQUEST_TYPE +[pd]: ref:ethereum.forks.monad_eleven.requests.parse_deposit_requests +[crh]: ref:ethereum.forks.monad_eleven.requests.compute_requests_hash +[pgpr]: ref:ethereum.forks.monad_eleven.fork.process_general_purpose_requests """ from hashlib import sha256 @@ -265,9 +265,10 @@ def parse_deposit_requests(block_output: BlockOutput) -> Bytes: prefixed with [`DEPOSIT_REQUEST_TYPE`][dt] before being appended to the block's request list. - [addr]: ref:ethereum.forks.monad_next.requests.DEPOSIT_CONTRACT_ADDRESS - [sig]: ref:ethereum.forks.monad_next.requests.DEPOSIT_EVENT_SIGNATURE_HASH - [dt]: ref:ethereum.forks.monad_next.requests.DEPOSIT_REQUEST_TYPE + [addr]: ref:ethereum.forks.monad_eleven.requests.DEPOSIT_CONTRACT_ADDRESS + [sig]: + ref:ethereum.forks.monad_eleven.requests.DEPOSIT_EVENT_SIGNATURE_HASH + [dt]: ref:ethereum.forks.monad_eleven.requests.DEPOSIT_REQUEST_TYPE """ deposit_requests: Bytes = b"" for key in block_output.receipt_keys: @@ -299,7 +300,7 @@ def compute_requests_hash(requests: List[Bytes]) -> Bytes: [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 [SHA2-256]: https://en.wikipedia.org/wiki/SHA-2 - [rh]: ref:ethereum.forks.monad_next.blocks.Header.requests_hash + [rh]: ref:ethereum.forks.monad_eleven.blocks.Header.requests_hash """ m = sha256() for request in requests: diff --git a/src/ethereum/forks/monad_next/state_tracker.py b/src/ethereum/forks/monad_eleven/state_tracker.py similarity index 100% rename from src/ethereum/forks/monad_next/state_tracker.py rename to src/ethereum/forks/monad_eleven/state_tracker.py diff --git a/src/ethereum/forks/monad_next/transactions.py b/src/ethereum/forks/monad_eleven/transactions.py similarity index 100% rename from src/ethereum/forks/monad_next/transactions.py rename to src/ethereum/forks/monad_eleven/transactions.py diff --git a/src/ethereum/forks/monad_next/utils/__init__.py b/src/ethereum/forks/monad_eleven/utils/__init__.py similarity index 100% rename from src/ethereum/forks/monad_next/utils/__init__.py rename to src/ethereum/forks/monad_eleven/utils/__init__.py diff --git a/src/ethereum/forks/monad_next/utils/address.py b/src/ethereum/forks/monad_eleven/utils/address.py similarity index 94% rename from src/ethereum/forks/monad_next/utils/address.py rename to src/ethereum/forks/monad_eleven/utils/address.py index 37a4a05511b..52f0dfa9113 100644 --- a/src/ethereum/forks/monad_next/utils/address.py +++ b/src/ethereum/forks/monad_eleven/utils/address.py @@ -8,7 +8,7 @@ Introduction ------------ -Address specific functions used in this MONAD_NEXT version of +Address specific functions used in this MONAD_ELEVEN version of specification. """ @@ -81,7 +81,7 @@ def compute_create2_contract_address( Returns ------- - address: `ethereum.forks.monad_next.fork_types.Address` + address: `ethereum.forks.monad_eleven.fork_types.Address` The computed address of the new account. """ diff --git a/src/ethereum/forks/monad_next/utils/hexadecimal.py b/src/ethereum/forks/monad_eleven/utils/hexadecimal.py similarity index 98% rename from src/ethereum/forks/monad_next/utils/hexadecimal.py rename to src/ethereum/forks/monad_eleven/utils/hexadecimal.py index d647084b730..3a8c3f3e112 100644 --- a/src/ethereum/forks/monad_next/utils/hexadecimal.py +++ b/src/ethereum/forks/monad_eleven/utils/hexadecimal.py @@ -9,7 +9,7 @@ ------------ Hexadecimal utility functions used in this specification, specific to -Monad Next types. +Monad Eleven types. """ from ethereum_types.bytes import Bytes diff --git a/src/ethereum/forks/monad_next/utils/message.py b/src/ethereum/forks/monad_eleven/utils/message.py similarity index 95% rename from src/ethereum/forks/monad_next/utils/message.py rename to src/ethereum/forks/monad_eleven/utils/message.py index 7cad195ead2..a06fc698791 100644 --- a/src/ethereum/forks/monad_next/utils/message.py +++ b/src/ethereum/forks/monad_eleven/utils/message.py @@ -8,7 +8,7 @@ Introduction ------------ -Message specific functions used in this MONAD_NEXT version of +Message specific functions used in this MONAD_ELEVEN version of specification. """ @@ -43,7 +43,7 @@ def prepare_message( Returns ------- - message: `ethereum.forks.monad_next.vm.Message` + message: `ethereum.forks.monad_eleven.vm.Message` Items containing contract creation or message call specific data. """ diff --git a/src/ethereum/forks/monad_next/vm/__init__.py b/src/ethereum/forks/monad_eleven/vm/__init__.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/__init__.py rename to src/ethereum/forks/monad_eleven/vm/__init__.py diff --git a/src/ethereum/forks/monad_next/vm/eoa_delegation.py b/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/eoa_delegation.py rename to src/ethereum/forks/monad_eleven/vm/eoa_delegation.py diff --git a/src/ethereum/forks/monad_next/vm/exceptions.py b/src/ethereum/forks/monad_eleven/vm/exceptions.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/exceptions.py rename to src/ethereum/forks/monad_eleven/vm/exceptions.py diff --git a/src/ethereum/forks/monad_next/vm/gas.py b/src/ethereum/forks/monad_eleven/vm/gas.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/gas.py rename to src/ethereum/forks/monad_eleven/vm/gas.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/__init__.py b/src/ethereum/forks/monad_eleven/vm/instructions/__init__.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/__init__.py rename to src/ethereum/forks/monad_eleven/vm/instructions/__init__.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/arithmetic.py b/src/ethereum/forks/monad_eleven/vm/instructions/arithmetic.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/arithmetic.py rename to src/ethereum/forks/monad_eleven/vm/instructions/arithmetic.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/bitwise.py b/src/ethereum/forks/monad_eleven/vm/instructions/bitwise.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/bitwise.py rename to src/ethereum/forks/monad_eleven/vm/instructions/bitwise.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/block.py b/src/ethereum/forks/monad_eleven/vm/instructions/block.py similarity index 82% rename from src/ethereum/forks/monad_next/vm/instructions/block.py rename to src/ethereum/forks/monad_eleven/vm/instructions/block.py index 4f9f9e5d5c3..497d83656ec 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/block.py +++ b/src/ethereum/forks/monad_eleven/vm/instructions/block.py @@ -30,9 +30,9 @@ def block_hash(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackUnderflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackUnderflowError` If `len(stack)` is less than `1`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `20`. """ @@ -79,9 +79,9 @@ def coinbase(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackOverflowError` If `len(stack)` is equal to `1024`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `2`. """ @@ -113,9 +113,9 @@ def timestamp(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackOverflowError` If `len(stack)` is equal to `1024`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `2`. """ @@ -146,9 +146,9 @@ def number(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackOverflowError` If `len(stack)` is equal to `1024`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `2`. """ @@ -179,9 +179,9 @@ def prev_randao(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackOverflowError` If `len(stack)` is equal to `1024`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `2`. """ @@ -212,9 +212,9 @@ def gas_limit(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackOverflowError` If `len(stack)` is equal to `1024`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `2`. """ @@ -242,9 +242,9 @@ def chain_id(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackOverflowError` If `len(stack)` is equal to `1024`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `2`. """ @@ -275,9 +275,9 @@ def slot_number(evm: Evm) -> None: Raises ------ - :py:class:`~ethereum.forks.monad_next.vm.exceptions.StackOverflowError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.StackOverflowError` If `len(stack)` is equal to `1024`. - :py:class:`~ethereum.forks.monad_next.vm.exceptions.OutOfGasError` + :py:class:`~ethereum.forks.monad_eleven.vm.exceptions.OutOfGasError` If `evm.gas_left` is less than `2`. """ diff --git a/src/ethereum/forks/monad_next/vm/instructions/comparison.py b/src/ethereum/forks/monad_eleven/vm/instructions/comparison.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/comparison.py rename to src/ethereum/forks/monad_eleven/vm/instructions/comparison.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/control_flow.py b/src/ethereum/forks/monad_eleven/vm/instructions/control_flow.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/control_flow.py rename to src/ethereum/forks/monad_eleven/vm/instructions/control_flow.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/environment.py b/src/ethereum/forks/monad_eleven/vm/instructions/environment.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/environment.py rename to src/ethereum/forks/monad_eleven/vm/instructions/environment.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/keccak.py b/src/ethereum/forks/monad_eleven/vm/instructions/keccak.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/keccak.py rename to src/ethereum/forks/monad_eleven/vm/instructions/keccak.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/log.py b/src/ethereum/forks/monad_eleven/vm/instructions/log.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/log.py rename to src/ethereum/forks/monad_eleven/vm/instructions/log.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/memory.py b/src/ethereum/forks/monad_eleven/vm/instructions/memory.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/memory.py rename to src/ethereum/forks/monad_eleven/vm/instructions/memory.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/stack.py b/src/ethereum/forks/monad_eleven/vm/instructions/stack.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/stack.py rename to src/ethereum/forks/monad_eleven/vm/instructions/stack.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/storage.py b/src/ethereum/forks/monad_eleven/vm/instructions/storage.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/storage.py rename to src/ethereum/forks/monad_eleven/vm/instructions/storage.py diff --git a/src/ethereum/forks/monad_next/vm/instructions/system.py b/src/ethereum/forks/monad_eleven/vm/instructions/system.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/instructions/system.py rename to src/ethereum/forks/monad_eleven/vm/instructions/system.py diff --git a/src/ethereum/forks/monad_next/vm/interpreter.py b/src/ethereum/forks/monad_eleven/vm/interpreter.py similarity index 99% rename from src/ethereum/forks/monad_next/vm/interpreter.py rename to src/ethereum/forks/monad_eleven/vm/interpreter.py index 9363e760eee..f0ff0e40f23 100644 --- a/src/ethereum/forks/monad_next/vm/interpreter.py +++ b/src/ethereum/forks/monad_eleven/vm/interpreter.py @@ -275,7 +275,7 @@ def process_create_message(message: Message) -> Evm: Returns ------- - evm: :py:class:`~ethereum.forks.monad_next.vm.Evm` + evm: :py:class:`~ethereum.forks.monad_eleven.vm.Evm` Items containing execution specific objects. """ @@ -335,7 +335,7 @@ def process_message(message: Message) -> Evm: Returns ------- - evm: :py:class:`~ethereum.forks.monad_next.vm.Evm` + evm: :py:class:`~ethereum.forks.monad_eleven.vm.Evm` Items containing execution specific objects """ diff --git a/src/ethereum/forks/monad_next/vm/memory.py b/src/ethereum/forks/monad_eleven/vm/memory.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/memory.py rename to src/ethereum/forks/monad_eleven/vm/memory.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/__init__.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/__init__.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/__init__.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/__init__.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/alt_bn128.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/alt_bn128.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/alt_bn128.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/alt_bn128.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/blake2f.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/blake2f.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/blake2f.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/blake2f.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/__init__.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/__init__.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/__init__.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/__init__.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g1.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/bls12_381_g1.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g1.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/bls12_381_g1.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g2.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/bls12_381_g2.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_g2.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/bls12_381_g2.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/bls12_381/bls12_381_pairing.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/ecrecover.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/ecrecover.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/ecrecover.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/ecrecover.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/identity.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/identity.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/identity.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/identity.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/mapping.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/mapping.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/mapping.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/mapping.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/modexp.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/modexp.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/modexp.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/modexp.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/p256verify.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/p256verify.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/p256verify.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/p256verify.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/point_evaluation.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/point_evaluation.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/point_evaluation.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/point_evaluation.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/reserve_balance.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/reserve_balance.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/reserve_balance.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/reserve_balance.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/ripemd160.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/ripemd160.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/ripemd160.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/ripemd160.py diff --git a/src/ethereum/forks/monad_next/vm/precompiled_contracts/sha256.py b/src/ethereum/forks/monad_eleven/vm/precompiled_contracts/sha256.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/precompiled_contracts/sha256.py rename to src/ethereum/forks/monad_eleven/vm/precompiled_contracts/sha256.py diff --git a/src/ethereum/forks/monad_next/vm/runtime.py b/src/ethereum/forks/monad_eleven/vm/runtime.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/runtime.py rename to src/ethereum/forks/monad_eleven/vm/runtime.py diff --git a/src/ethereum/forks/monad_next/vm/stack.py b/src/ethereum/forks/monad_eleven/vm/stack.py similarity index 100% rename from src/ethereum/forks/monad_next/vm/stack.py rename to src/ethereum/forks/monad_eleven/vm/stack.py diff --git a/src/ethereum_spec_tools/utils.py b/src/ethereum_spec_tools/utils.py index 1ef04d2f5f2..7af3aa25f13 100644 --- a/src/ethereum_spec_tools/utils.py +++ b/src/ethereum_spec_tools/utils.py @@ -24,8 +24,8 @@ W = TypeVar("W", Uint, U64, U256) EXCEPTION_MAPS = { - "MONAD_NEXT": { - "fork_blocks": [("monad_next", 0)], + "MONAD_ELEVEN": { + "fork_blocks": [("monad_eleven", 0)], }, "MONAD_TEN": { "fork_blocks": [("monad_ten", 0)], @@ -156,7 +156,7 @@ def find_fork( "MONAD_EIGHT": "monad_eight", "MONAD_NINE": "monad_nine", "MONAD_TEN": "monad_ten", - "MONAD_NEXT": "monad_next", + "MONAD_ELEVEN": "monad_eleven", } diff --git a/tests/monad_amsterdam/__init__.py b/tests/monad_amsterdam/__init__.py index b37e6faa138..e0c66f0fa89 100644 --- a/tests/monad_amsterdam/__init__.py +++ b/tests/monad_amsterdam/__init__.py @@ -1 +1 @@ -"""Tests for the Amsterdam EIPs MONAD_NEXT adopts.""" +"""Tests for the Amsterdam EIPs MONAD_ELEVEN adopts.""" diff --git a/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py b/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py index 38db3eb8747..f327b6f40b6 100644 --- a/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py +++ b/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py @@ -37,7 +37,7 @@ slot_call_result = 0x2 pytestmark = [ - pytest.mark.valid_from("MONAD_NEXT"), + pytest.mark.valid_from("MONAD_ELEVEN"), pytest.mark.pre_alloc_group( "eip7997_delegated_create_tests", reason="Tests the EIP-7997 factory reached from a delegated frame", From 022ff949cc270c0574d628ff29d1e78ccf10c517 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:01 +0000 Subject: [PATCH 39/44] refactor(tests): rename `tests/monad_amsterdam` to `tests/monad_eleven` Co-Authored-By: Claude --- tests/{monad_amsterdam => monad_eleven}/__init__.py | 0 .../eip7997_delegated_create/__init__.py | 0 .../eip7997_delegated_create/test_factory_reach.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{monad_amsterdam => monad_eleven}/__init__.py (100%) rename tests/{monad_amsterdam => monad_eleven}/eip7997_delegated_create/__init__.py (100%) rename tests/{monad_amsterdam => monad_eleven}/eip7997_delegated_create/test_factory_reach.py (100%) diff --git a/tests/monad_amsterdam/__init__.py b/tests/monad_eleven/__init__.py similarity index 100% rename from tests/monad_amsterdam/__init__.py rename to tests/monad_eleven/__init__.py diff --git a/tests/monad_amsterdam/eip7997_delegated_create/__init__.py b/tests/monad_eleven/eip7997_delegated_create/__init__.py similarity index 100% rename from tests/monad_amsterdam/eip7997_delegated_create/__init__.py rename to tests/monad_eleven/eip7997_delegated_create/__init__.py diff --git a/tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py similarity index 100% rename from tests/monad_amsterdam/eip7997_delegated_create/test_factory_reach.py rename to tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py From b6472968c7d7a589af46d5d7b0f669ced6ea1212 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:01 +0000 Subject: [PATCH 40/44] fix(monad): ban creates by the frame's recipient, not the code address The factory reached by DELEGATECALL or CALLCODE from a delegated frame halts, as the client checks the recipient's delegation at create time. Co-Authored-By: Claude --- src/ethereum/forks/monad_eight/fork.py | 1 - .../forks/monad_eight/utils/message.py | 1 - src/ethereum/forks/monad_eight/vm/__init__.py | 1 - .../forks/monad_eight/vm/eoa_delegation.py | 12 ++-- .../monad_eight/vm/instructions/system.py | 20 +++---- .../forks/monad_eight/vm/interpreter.py | 1 - src/ethereum/forks/monad_eleven/fork.py | 1 - .../forks/monad_eleven/utils/message.py | 1 - .../forks/monad_eleven/vm/__init__.py | 1 - .../forks/monad_eleven/vm/eoa_delegation.py | 12 ++-- .../monad_eleven/vm/instructions/system.py | 20 +++---- .../forks/monad_eleven/vm/interpreter.py | 1 - src/ethereum/forks/monad_nine/fork.py | 1 - .../forks/monad_nine/utils/message.py | 1 - src/ethereum/forks/monad_nine/vm/__init__.py | 1 - .../forks/monad_nine/vm/eoa_delegation.py | 12 ++-- .../monad_nine/vm/instructions/system.py | 20 +++---- .../forks/monad_nine/vm/interpreter.py | 1 - src/ethereum/forks/monad_ten/fork.py | 1 - src/ethereum/forks/monad_ten/utils/message.py | 1 - src/ethereum/forks/monad_ten/vm/__init__.py | 1 - .../forks/monad_ten/vm/eoa_delegation.py | 12 ++-- .../forks/monad_ten/vm/instructions/system.py | 20 +++---- .../forks/monad_ten/vm/interpreter.py | 1 - .../test_factory_reach.py | 60 +++++++++++-------- 25 files changed, 87 insertions(+), 117 deletions(-) diff --git a/src/ethereum/forks/monad_eight/fork.py b/src/ethereum/forks/monad_eight/fork.py index 150b0fcf79b..210954c1b80 100644 --- a/src/ethereum/forks/monad_eight/fork.py +++ b/src/ethereum/forks/monad_eight/fork.py @@ -710,7 +710,6 @@ def process_unchecked_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) system_tx_output = process_message_call(system_tx_message) diff --git a/src/ethereum/forks/monad_eight/utils/message.py b/src/ethereum/forks/monad_eight/utils/message.py index 7b36863a32c..7ace9c15f4a 100644 --- a/src/ethereum/forks/monad_eight/utils/message.py +++ b/src/ethereum/forks/monad_eight/utils/message.py @@ -90,5 +90,4 @@ def prepare_message( accessed_storage_keys=set(tx_env.access_list_storage_keys), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) diff --git a/src/ethereum/forks/monad_eight/vm/__init__.py b/src/ethereum/forks/monad_eight/vm/__init__.py index 8041768baa7..f18ed379cb5 100644 --- a/src/ethereum/forks/monad_eight/vm/__init__.py +++ b/src/ethereum/forks/monad_eight/vm/__init__.py @@ -138,7 +138,6 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - disable_create_opcodes: bool @final diff --git a/src/ethereum/forks/monad_eight/vm/eoa_delegation.py b/src/ethereum/forks/monad_eight/vm/eoa_delegation.py index 0ba1242c487..cf3c6b678cf 100644 --- a/src/ethereum/forks/monad_eight/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_eight/vm/eoa_delegation.py @@ -122,7 +122,7 @@ def recover_authority(authorization: Authorization) -> Address: def access_delegation( evm: Evm, address: Address -) -> Tuple[bool, bool, Address, Bytes, Uint]: +) -> Tuple[bool, Address, Bytes, Uint]: """ Get the delegation address, code, and the cost of access from the address. @@ -135,15 +135,15 @@ def access_delegation( Returns ------- - delegation : `Tuple[bool, bool, Address, Bytes, Uint]` - The disable precompiles, disable creates, delegation address, code, and - access gas cost. + delegation : `Tuple[bool, Address, Bytes, Uint]` + The disable precompiles, delegation address, code, and access gas + cost. """ tx_state = evm.message.tx_env.state code = get_code(tx_state, get_account(tx_state, address).code_hash) if not is_valid_delegation(code): - return False, False, address, code, Uint(0) + return False, address, code, Uint(0) address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) if address in evm.accessed_addresses: @@ -153,7 +153,7 @@ def access_delegation( access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS code = get_code(tx_state, get_account(tx_state, address).code_hash) - return True, True, address, code, access_gas_cost + return True, address, code, access_gas_cost def set_delegation(message: Message) -> U256: diff --git a/src/ethereum/forks/monad_eight/vm/instructions/system.py b/src/ethereum/forks/monad_eight/vm/instructions/system.py index bbc03513a63..86076c0417e 100644 --- a/src/ethereum/forks/monad_eight/vm/instructions/system.py +++ b/src/ethereum/forks/monad_eight/vm/instructions/system.py @@ -20,6 +20,7 @@ from ...state_tracker import ( account_has_code_or_nonce, get_account, + get_code, increment_nonce, is_account_alive, move_ether, @@ -30,7 +31,7 @@ compute_create2_contract_address, to_address_masked, ) -from ...vm.eoa_delegation import access_delegation +from ...vm.eoa_delegation import access_delegation, is_valid_delegation from .. import ( Evm, Message, @@ -83,7 +84,11 @@ def generic_create( evm.gas_left -= create_message_gas if evm.message.is_static: raise WriteInStaticContext - if evm.message.disable_create_opcodes: + tx_state = evm.message.tx_env.state + recipient_code = get_code( + tx_state, get_account(tx_state, evm.message.current_target).code_hash + ) + if is_valid_delegation(recipient_code): raise CreateIn7702Context evm.return_data = b"" @@ -126,7 +131,6 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, - disable_create_opcodes=False, ) child_evm = process_create_message(child_message) @@ -288,7 +292,6 @@ def generic_call( memory_output_size: U256, code: Bytes, disable_precompiles: bool, - disable_create_opcodes: bool, ) -> None: """ Perform the core logic of the `CALL*` family of opcodes. @@ -324,7 +327,6 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, - disable_create_opcodes=disable_create_opcodes, ) child_evm = process_message(child_message) @@ -382,7 +384,6 @@ def call(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -427,7 +428,6 @@ def call(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -472,7 +472,6 @@ def callcode(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -514,7 +513,6 @@ def callcode(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -615,7 +613,6 @@ def delegatecall(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -648,7 +645,6 @@ def delegatecall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -691,7 +687,6 @@ def staticcall(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -724,7 +719,6 @@ def staticcall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER diff --git a/src/ethereum/forks/monad_eight/vm/interpreter.py b/src/ethereum/forks/monad_eight/vm/interpreter.py index 07d86617301..7d4f568f3a2 100644 --- a/src/ethereum/forks/monad_eight/vm/interpreter.py +++ b/src/ethereum/forks/monad_eight/vm/interpreter.py @@ -147,7 +147,6 @@ def process_message_call(message: Message) -> MessageCallOutput: get_account(tx_state, delegated_address).code_hash, ) message.code_address = delegated_address - message.disable_create_opcodes = True evm = process_message(message) diff --git a/src/ethereum/forks/monad_eleven/fork.py b/src/ethereum/forks/monad_eleven/fork.py index 61b0ba626ee..f82cf4f1c30 100644 --- a/src/ethereum/forks/monad_eleven/fork.py +++ b/src/ethereum/forks/monad_eleven/fork.py @@ -730,7 +730,6 @@ def process_unchecked_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) system_tx_output = process_message_call(system_tx_message) diff --git a/src/ethereum/forks/monad_eleven/utils/message.py b/src/ethereum/forks/monad_eleven/utils/message.py index a06fc698791..026efe80389 100644 --- a/src/ethereum/forks/monad_eleven/utils/message.py +++ b/src/ethereum/forks/monad_eleven/utils/message.py @@ -90,5 +90,4 @@ def prepare_message( accessed_storage_keys=set(tx_env.access_list_storage_keys), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) diff --git a/src/ethereum/forks/monad_eleven/vm/__init__.py b/src/ethereum/forks/monad_eleven/vm/__init__.py index e686d622a2d..cc58a9c733d 100644 --- a/src/ethereum/forks/monad_eleven/vm/__init__.py +++ b/src/ethereum/forks/monad_eleven/vm/__init__.py @@ -150,7 +150,6 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - disable_create_opcodes: bool @final diff --git a/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py b/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py index 0ba1242c487..cf3c6b678cf 100644 --- a/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_eleven/vm/eoa_delegation.py @@ -122,7 +122,7 @@ def recover_authority(authorization: Authorization) -> Address: def access_delegation( evm: Evm, address: Address -) -> Tuple[bool, bool, Address, Bytes, Uint]: +) -> Tuple[bool, Address, Bytes, Uint]: """ Get the delegation address, code, and the cost of access from the address. @@ -135,15 +135,15 @@ def access_delegation( Returns ------- - delegation : `Tuple[bool, bool, Address, Bytes, Uint]` - The disable precompiles, disable creates, delegation address, code, and - access gas cost. + delegation : `Tuple[bool, Address, Bytes, Uint]` + The disable precompiles, delegation address, code, and access gas + cost. """ tx_state = evm.message.tx_env.state code = get_code(tx_state, get_account(tx_state, address).code_hash) if not is_valid_delegation(code): - return False, False, address, code, Uint(0) + return False, address, code, Uint(0) address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) if address in evm.accessed_addresses: @@ -153,7 +153,7 @@ def access_delegation( access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS code = get_code(tx_state, get_account(tx_state, address).code_hash) - return True, True, address, code, access_gas_cost + return True, address, code, access_gas_cost def set_delegation(message: Message) -> U256: diff --git a/src/ethereum/forks/monad_eleven/vm/instructions/system.py b/src/ethereum/forks/monad_eleven/vm/instructions/system.py index 201629f7e55..4a87e4eae5c 100644 --- a/src/ethereum/forks/monad_eleven/vm/instructions/system.py +++ b/src/ethereum/forks/monad_eleven/vm/instructions/system.py @@ -20,6 +20,7 @@ from ...state_tracker import ( account_has_code_or_nonce, get_account, + get_code, increment_nonce, is_account_alive, move_ether, @@ -29,7 +30,7 @@ compute_create2_contract_address, to_address_masked, ) -from ...vm.eoa_delegation import access_delegation +from ...vm.eoa_delegation import access_delegation, is_valid_delegation from .. import ( Evm, Message, @@ -84,7 +85,11 @@ def generic_create( evm.gas_left -= create_message_gas if evm.message.is_static: raise WriteInStaticContext - if evm.message.disable_create_opcodes: + tx_state = evm.message.tx_env.state + recipient_code = get_code( + tx_state, get_account(tx_state, evm.message.current_target).code_hash + ) + if is_valid_delegation(recipient_code): raise CreateIn7702Context evm.return_data = b"" @@ -127,7 +132,6 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, - disable_create_opcodes=False, ) child_evm = process_create_message(child_message) @@ -292,7 +296,6 @@ def generic_call( memory_output_size: U256, code: Bytes, disable_precompiles: bool, - disable_create_opcodes: bool, ) -> None: """ Perform the core logic of the `CALL*` family of opcodes. @@ -328,7 +331,6 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, - disable_create_opcodes=disable_create_opcodes, ) child_evm = process_message(child_message) @@ -386,7 +388,6 @@ def call(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -439,7 +440,6 @@ def call(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -484,7 +484,6 @@ def callcode(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -528,7 +527,6 @@ def callcode(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -630,7 +628,6 @@ def delegatecall(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -665,7 +662,6 @@ def delegatecall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -708,7 +704,6 @@ def staticcall(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -743,7 +738,6 @@ def staticcall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER diff --git a/src/ethereum/forks/monad_eleven/vm/interpreter.py b/src/ethereum/forks/monad_eleven/vm/interpreter.py index f0ff0e40f23..831d6a71612 100644 --- a/src/ethereum/forks/monad_eleven/vm/interpreter.py +++ b/src/ethereum/forks/monad_eleven/vm/interpreter.py @@ -237,7 +237,6 @@ def process_message_call(message: Message) -> MessageCallOutput: get_account(tx_state, delegated_address).code_hash, ) message.code_address = delegated_address - message.disable_create_opcodes = True evm = process_message(message) diff --git a/src/ethereum/forks/monad_nine/fork.py b/src/ethereum/forks/monad_nine/fork.py index e11fd0e2046..ebf97201ec7 100644 --- a/src/ethereum/forks/monad_nine/fork.py +++ b/src/ethereum/forks/monad_nine/fork.py @@ -727,7 +727,6 @@ def process_unchecked_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) system_tx_output = process_message_call(system_tx_message) diff --git a/src/ethereum/forks/monad_nine/utils/message.py b/src/ethereum/forks/monad_nine/utils/message.py index 38683a7ad14..52b402aece8 100644 --- a/src/ethereum/forks/monad_nine/utils/message.py +++ b/src/ethereum/forks/monad_nine/utils/message.py @@ -90,5 +90,4 @@ def prepare_message( accessed_storage_keys=set(tx_env.access_list_storage_keys), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) diff --git a/src/ethereum/forks/monad_nine/vm/__init__.py b/src/ethereum/forks/monad_nine/vm/__init__.py index bac9c951854..679f04ed628 100644 --- a/src/ethereum/forks/monad_nine/vm/__init__.py +++ b/src/ethereum/forks/monad_nine/vm/__init__.py @@ -143,7 +143,6 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - disable_create_opcodes: bool @final diff --git a/src/ethereum/forks/monad_nine/vm/eoa_delegation.py b/src/ethereum/forks/monad_nine/vm/eoa_delegation.py index 0ba1242c487..cf3c6b678cf 100644 --- a/src/ethereum/forks/monad_nine/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_nine/vm/eoa_delegation.py @@ -122,7 +122,7 @@ def recover_authority(authorization: Authorization) -> Address: def access_delegation( evm: Evm, address: Address -) -> Tuple[bool, bool, Address, Bytes, Uint]: +) -> Tuple[bool, Address, Bytes, Uint]: """ Get the delegation address, code, and the cost of access from the address. @@ -135,15 +135,15 @@ def access_delegation( Returns ------- - delegation : `Tuple[bool, bool, Address, Bytes, Uint]` - The disable precompiles, disable creates, delegation address, code, and - access gas cost. + delegation : `Tuple[bool, Address, Bytes, Uint]` + The disable precompiles, delegation address, code, and access gas + cost. """ tx_state = evm.message.tx_env.state code = get_code(tx_state, get_account(tx_state, address).code_hash) if not is_valid_delegation(code): - return False, False, address, code, Uint(0) + return False, address, code, Uint(0) address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) if address in evm.accessed_addresses: @@ -153,7 +153,7 @@ def access_delegation( access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS code = get_code(tx_state, get_account(tx_state, address).code_hash) - return True, True, address, code, access_gas_cost + return True, address, code, access_gas_cost def set_delegation(message: Message) -> U256: diff --git a/src/ethereum/forks/monad_nine/vm/instructions/system.py b/src/ethereum/forks/monad_nine/vm/instructions/system.py index 2b0743f4302..38abf706b03 100644 --- a/src/ethereum/forks/monad_nine/vm/instructions/system.py +++ b/src/ethereum/forks/monad_nine/vm/instructions/system.py @@ -20,6 +20,7 @@ from ...state_tracker import ( account_has_code_or_nonce, get_account, + get_code, increment_nonce, is_account_alive, move_ether, @@ -30,7 +31,7 @@ compute_create2_contract_address, to_address_masked, ) -from ...vm.eoa_delegation import access_delegation +from ...vm.eoa_delegation import access_delegation, is_valid_delegation from .. import ( Evm, Message, @@ -84,7 +85,11 @@ def generic_create( evm.gas_left -= create_message_gas if evm.message.is_static: raise WriteInStaticContext - if evm.message.disable_create_opcodes: + tx_state = evm.message.tx_env.state + recipient_code = get_code( + tx_state, get_account(tx_state, evm.message.current_target).code_hash + ) + if is_valid_delegation(recipient_code): raise CreateIn7702Context evm.return_data = b"" @@ -127,7 +132,6 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, - disable_create_opcodes=False, ) child_evm = process_create_message(child_message) @@ -292,7 +296,6 @@ def generic_call( memory_output_size: U256, code: Bytes, disable_precompiles: bool, - disable_create_opcodes: bool, ) -> None: """ Perform the core logic of the `CALL*` family of opcodes. @@ -328,7 +331,6 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, - disable_create_opcodes=disable_create_opcodes, ) child_evm = process_message(child_message) @@ -386,7 +388,6 @@ def call(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -439,7 +440,6 @@ def call(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -484,7 +484,6 @@ def callcode(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -528,7 +527,6 @@ def callcode(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -629,7 +627,6 @@ def delegatecall(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -664,7 +661,6 @@ def delegatecall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -707,7 +703,6 @@ def staticcall(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -742,7 +737,6 @@ def staticcall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER diff --git a/src/ethereum/forks/monad_nine/vm/interpreter.py b/src/ethereum/forks/monad_nine/vm/interpreter.py index 512fb6a24f4..25d15898340 100644 --- a/src/ethereum/forks/monad_nine/vm/interpreter.py +++ b/src/ethereum/forks/monad_nine/vm/interpreter.py @@ -237,7 +237,6 @@ def process_message_call(message: Message) -> MessageCallOutput: get_account(tx_state, delegated_address).code_hash, ) message.code_address = delegated_address - message.disable_create_opcodes = True evm = process_message(message) diff --git a/src/ethereum/forks/monad_ten/fork.py b/src/ethereum/forks/monad_ten/fork.py index ed7a048ead7..1a9c251a54e 100644 --- a/src/ethereum/forks/monad_ten/fork.py +++ b/src/ethereum/forks/monad_ten/fork.py @@ -727,7 +727,6 @@ def process_unchecked_system_transaction( accessed_storage_keys=set(), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) system_tx_output = process_message_call(system_tx_message) diff --git a/src/ethereum/forks/monad_ten/utils/message.py b/src/ethereum/forks/monad_ten/utils/message.py index 4ccede9406d..5de56d23d51 100644 --- a/src/ethereum/forks/monad_ten/utils/message.py +++ b/src/ethereum/forks/monad_ten/utils/message.py @@ -90,5 +90,4 @@ def prepare_message( accessed_storage_keys=set(tx_env.access_list_storage_keys), disable_precompiles=False, parent_evm=None, - disable_create_opcodes=False, ) diff --git a/src/ethereum/forks/monad_ten/vm/__init__.py b/src/ethereum/forks/monad_ten/vm/__init__.py index 15c53bc75b6..5cff6a44f12 100644 --- a/src/ethereum/forks/monad_ten/vm/__init__.py +++ b/src/ethereum/forks/monad_ten/vm/__init__.py @@ -143,7 +143,6 @@ class Message: accessed_storage_keys: Set[Tuple[Address, Bytes32]] disable_precompiles: bool parent_evm: Optional["Evm"] - disable_create_opcodes: bool @final diff --git a/src/ethereum/forks/monad_ten/vm/eoa_delegation.py b/src/ethereum/forks/monad_ten/vm/eoa_delegation.py index 0ba1242c487..cf3c6b678cf 100644 --- a/src/ethereum/forks/monad_ten/vm/eoa_delegation.py +++ b/src/ethereum/forks/monad_ten/vm/eoa_delegation.py @@ -122,7 +122,7 @@ def recover_authority(authorization: Authorization) -> Address: def access_delegation( evm: Evm, address: Address -) -> Tuple[bool, bool, Address, Bytes, Uint]: +) -> Tuple[bool, Address, Bytes, Uint]: """ Get the delegation address, code, and the cost of access from the address. @@ -135,15 +135,15 @@ def access_delegation( Returns ------- - delegation : `Tuple[bool, bool, Address, Bytes, Uint]` - The disable precompiles, disable creates, delegation address, code, and - access gas cost. + delegation : `Tuple[bool, Address, Bytes, Uint]` + The disable precompiles, delegation address, code, and access gas + cost. """ tx_state = evm.message.tx_env.state code = get_code(tx_state, get_account(tx_state, address).code_hash) if not is_valid_delegation(code): - return False, False, address, code, Uint(0) + return False, address, code, Uint(0) address = Address(code[EOA_DELEGATION_MARKER_LENGTH:]) if address in evm.accessed_addresses: @@ -153,7 +153,7 @@ def access_delegation( access_gas_cost = GasCosts.COLD_ACCOUNT_ACCESS code = get_code(tx_state, get_account(tx_state, address).code_hash) - return True, True, address, code, access_gas_cost + return True, address, code, access_gas_cost def set_delegation(message: Message) -> U256: diff --git a/src/ethereum/forks/monad_ten/vm/instructions/system.py b/src/ethereum/forks/monad_ten/vm/instructions/system.py index 2b0743f4302..38abf706b03 100644 --- a/src/ethereum/forks/monad_ten/vm/instructions/system.py +++ b/src/ethereum/forks/monad_ten/vm/instructions/system.py @@ -20,6 +20,7 @@ from ...state_tracker import ( account_has_code_or_nonce, get_account, + get_code, increment_nonce, is_account_alive, move_ether, @@ -30,7 +31,7 @@ compute_create2_contract_address, to_address_masked, ) -from ...vm.eoa_delegation import access_delegation +from ...vm.eoa_delegation import access_delegation, is_valid_delegation from .. import ( Evm, Message, @@ -84,7 +85,11 @@ def generic_create( evm.gas_left -= create_message_gas if evm.message.is_static: raise WriteInStaticContext - if evm.message.disable_create_opcodes: + tx_state = evm.message.tx_env.state + recipient_code = get_code( + tx_state, get_account(tx_state, evm.message.current_target).code_hash + ) + if is_valid_delegation(recipient_code): raise CreateIn7702Context evm.return_data = b"" @@ -127,7 +132,6 @@ def generic_create( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=False, parent_evm=evm, - disable_create_opcodes=False, ) child_evm = process_create_message(child_message) @@ -292,7 +296,6 @@ def generic_call( memory_output_size: U256, code: Bytes, disable_precompiles: bool, - disable_create_opcodes: bool, ) -> None: """ Perform the core logic of the `CALL*` family of opcodes. @@ -328,7 +331,6 @@ def generic_call( accessed_storage_keys=evm.accessed_storage_keys.copy(), disable_precompiles=disable_precompiles, parent_evm=evm, - disable_create_opcodes=disable_create_opcodes, ) child_evm = process_message(child_message) @@ -386,7 +388,6 @@ def call(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -439,7 +440,6 @@ def call(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -484,7 +484,6 @@ def callcode(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -528,7 +527,6 @@ def callcode(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -629,7 +627,6 @@ def delegatecall(evm: Evm) -> None: ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -664,7 +661,6 @@ def delegatecall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER @@ -707,7 +703,6 @@ def staticcall(evm: Evm) -> None: code_address = to ( disable_precompiles, - disable_create_opcodes, code_address, code, delegated_access_gas_cost, @@ -742,7 +737,6 @@ def staticcall(evm: Evm) -> None: memory_output_size, code, disable_precompiles, - disable_create_opcodes, ) # PROGRAM COUNTER diff --git a/src/ethereum/forks/monad_ten/vm/interpreter.py b/src/ethereum/forks/monad_ten/vm/interpreter.py index e339ded2c7b..4b989373b55 100644 --- a/src/ethereum/forks/monad_ten/vm/interpreter.py +++ b/src/ethereum/forks/monad_ten/vm/interpreter.py @@ -237,7 +237,6 @@ def process_message_call(message: Message) -> MessageCallOutput: get_account(tx_state, delegated_address).code_hash, ) message.code_address = delegated_address - message.disable_create_opcodes = True evm = process_message(message) diff --git a/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py index f327b6f40b6..d062ffd086b 100644 --- a/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py +++ b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py @@ -1,17 +1,14 @@ """ Tests for reaching the EIP-7997 factory from a delegated frame. -Monad forbids the create opcodes inside a frame executing an EOA's -delegated code. The ban is callee-scoped: `access_delegation` sets it on -the frame it builds, and a child frame the delegated code calls does not -inherit it. EIP-7997 makes a CREATE2 factory reachable at a fixed -address on every chain, so these tests pin both halves of that -boundary. +Monad forbids the create opcodes in a frame running as a 7702-delegated +EOA. """ import pytest from execution_testing import ( Account, + Address, Alloc, Block, BlockchainTestFiller, @@ -53,12 +50,7 @@ def test_create_halts_in_delegated_frame( ) -> None: """ A create opcode executed as an EOA's delegated code halts the frame. - - The halt is the only coverage of `CreateIn7702Context`; nothing is - deployed and the storage write that follows never lands. """ - # The create halts before its initcode is read, so the empty - # memory the size refers to is immaterial. delegate = pre.deploy_contract( create_op(value=0, size=1) + Op.SSTORE(slot_code_worked, value_code_worked) @@ -78,28 +70,30 @@ def test_create_halts_in_delegated_frame( ) +@pytest.mark.with_all_call_opcodes def test_factory_deploys_from_delegated_frame( blockchain_test: BlockchainTestFiller, pre: Alloc, + call_opcode: Op, ) -> None: """ - A delegated frame reaches CREATE2 through the EIP-7997 factory. - - The ban applies to the frame running the delegated code, not to the - factory frame it calls, so the deployment succeeds. + A delegated frame reaches CREATE2 through the EIP-7997 factory only + when the factory runs at its own address. """ initcode = Initcode(deploy_code=Op.STOP) - deployed = compute_create2_address(Spec.FACTORY_ADDRESS, SALT, initcode) delegate = pre.deploy_contract( Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.SSTORE( slot_call_result, - Op.CALL( - gas=Op.GAS, - address=Spec.FACTORY_ADDRESS, - args_offset=0, - args_size=Op.CALLDATASIZE, + Op.ADD( + 1, + call_opcode( + gas=Op.GAS, + address=Spec.FACTORY_ADDRESS, + args_offset=0, + args_size=Op.CALLDATASIZE, + ), ), ) ) @@ -112,11 +106,27 @@ def test_factory_deploys_from_delegated_frame( sender=sender, ) + post: dict[Address, Account | None] + if call_opcode == Op.CALL: + post = { + delegated: Account(storage={slot_call_result: 2}), + compute_create2_address( + Spec.FACTORY_ADDRESS, SALT, initcode + ): Account(code=Op.STOP), + } + elif call_opcode in (Op.DELEGATECALL, Op.CALLCODE, Op.STATICCALL): + # STATICCALL halts the factory on the static context instead. + post = { + delegated: Account(storage={slot_call_result: 1}), + compute_create2_address( + delegated, SALT, initcode + ): Account.NONEXISTENT, + } + else: + raise ValueError(f"unexpected call opcode {call_opcode}") + blockchain_test( pre=pre, - post={ - delegated: Account(storage={slot_call_result: 1}), - deployed: Account(code=Op.STOP), - }, + post=post, blocks=[Block(txs=[tx])], ) From a9ae4708dea3f75b2b02443a189bf1068c3c05e9 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:01 +0000 Subject: [PATCH 41/44] test(monad): cover the create ban across the call shapes A delegated EOA's creating code and a creator reached from a delegated frame, each entered by every call opcode, on Monad forks and through the EIP-7997 factory alike. Co-Authored-By: Claude --- .../monad_eight/delegated_creates/__init__.py | 1 + .../monad_eight/delegated_creates/helpers.py | 26 +++ .../delegated_creates/test_create_ban.py | 155 ++++++++++++++++++ .../test_factory_reach.py | 129 +++++++++------ 4 files changed, 258 insertions(+), 53 deletions(-) create mode 100644 tests/monad_eight/delegated_creates/__init__.py create mode 100644 tests/monad_eight/delegated_creates/helpers.py create mode 100644 tests/monad_eight/delegated_creates/test_create_ban.py diff --git a/tests/monad_eight/delegated_creates/__init__.py b/tests/monad_eight/delegated_creates/__init__.py new file mode 100644 index 00000000000..c1bd1092348 --- /dev/null +++ b/tests/monad_eight/delegated_creates/__init__.py @@ -0,0 +1 @@ +"""Tests for the create opcodes around a 7702-delegated EOA.""" diff --git a/tests/monad_eight/delegated_creates/helpers.py b/tests/monad_eight/delegated_creates/helpers.py new file mode 100644 index 00000000000..05b2b627dd4 --- /dev/null +++ b/tests/monad_eight/delegated_creates/helpers.py @@ -0,0 +1,26 @@ +"""Bytecode for the delegated create tests.""" + +from execution_testing import Address, Bytecode, Op + +slot_created = 0x1 +slot_call_result = 0x2 + + +def factory_code(create_opcode: Op) -> Bytecode: + """Deploy the calldata as initcode and record the created address.""" + return Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.SSTORE( + slot_created, create_opcode(value=0, offset=0, size=Op.CALLDATASIZE) + ) + + +def forwarding_code(call_opcode: Op, address: Address) -> Bytecode: + """Forward the calldata to `address` and record the call result.""" + return Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + Op.SSTORE( + slot_call_result, + call_opcode( + gas=Op.GAS, + address=address, + args_offset=0, + args_size=Op.CALLDATASIZE, + ), + ) diff --git a/tests/monad_eight/delegated_creates/test_create_ban.py b/tests/monad_eight/delegated_creates/test_create_ban.py new file mode 100644 index 00000000000..097c02596bc --- /dev/null +++ b/tests/monad_eight/delegated_creates/test_create_ban.py @@ -0,0 +1,155 @@ +""" +Tests for the create opcodes around a 7702-delegated EOA. + +Monad forbids the create opcodes in a frame running as a 7702-delegated +EOA, whichever code the frame executes. +""" + +import pytest +from execution_testing import ( + Account, + Address, + Alloc, + Initcode, + Op, + StateTestFiller, + Storage, + Transaction, + compute_create_address, +) + +from .helpers import ( + factory_code, + forwarding_code, + slot_call_result, + slot_created, +) + +pytestmark = pytest.mark.valid_from("MONAD_EIGHT") + + +@pytest.mark.with_all_create_opcodes +def test_create_in_delegated_code( + state_test: StateTestFiller, + pre: Alloc, + create_opcode: Op, +) -> None: + """A create opcode run as a delegated EOA's code halts the frame.""" + initcode = Initcode(deploy_code=Op.STOP) + delegate = pre.deploy_contract(factory_code(create_opcode)) + delegated = pre.fund_eoa(delegation=delegate) + + tx = Transaction(to=delegated, data=initcode, sender=pre.fund_eoa()) + + state_test( + pre=pre, + tx=tx, + post={ + delegated: Account(nonce=delegated.nonce, storage={}), + compute_create_address( + address=delegated, + nonce=delegated.nonce, + initcode=initcode, + opcode=create_opcode, + ): Account.NONEXISTENT, + }, + ) + + +@pytest.mark.with_all_call_opcodes +@pytest.mark.with_all_create_opcodes +def test_contract_calls_delegated_factory( + state_test: StateTestFiller, + pre: Alloc, + call_opcode: Op, + create_opcode: Op, +) -> None: + """ + A contract reaching a delegated EOA's creating code deploys only when + that code runs at the contract's own address. + """ + initcode = Initcode(deploy_code=Op.STOP) + delegate = pre.deploy_contract(factory_code(create_opcode)) + delegated = pre.fund_eoa(delegation=delegate) + deploys = call_opcode in (Op.DELEGATECALL, Op.CALLCODE) + storage = Storage() + storage[slot_call_result] = 1 if deploys else 0 + caller = pre.deploy_contract( + forwarding_code(call_opcode, delegated), storage=storage.canary() + ) + + tx = Transaction(to=caller, data=initcode, sender=pre.fund_eoa()) + + post: dict[Address, Account | None] = { + delegated: Account(nonce=delegated.nonce, storage={}), + } + if deploys: + created = compute_create_address( + address=caller, nonce=1, initcode=initcode, opcode=create_opcode + ) + post[caller] = Account( + nonce=2, storage={slot_call_result: 1, slot_created: created} + ) + post[created] = Account(code=Op.STOP) + else: + post[caller] = Account(nonce=1, storage={slot_call_result: 0}) + post[ + compute_create_address( + address=delegated, + nonce=delegated.nonce, + initcode=initcode, + opcode=create_opcode, + ) + ] = Account.NONEXISTENT + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.with_all_call_opcodes +@pytest.mark.with_all_create_opcodes +def test_delegated_frame_calls_factory( + state_test: StateTestFiller, + pre: Alloc, + call_opcode: Op, + create_opcode: Op, +) -> None: + """ + A delegated frame reaching a factory contract deploys only when the + factory runs at its own address. + """ + initcode = Initcode(deploy_code=Op.STOP) + factory = pre.deploy_contract(factory_code(create_opcode)) + delegate = pre.deploy_contract(forwarding_code(call_opcode, factory)) + deploys = call_opcode == Op.CALL + storage = Storage() + storage[slot_call_result] = 1 if deploys else 0 + delegated = pre.fund_eoa(delegation=delegate, storage=storage.canary()) + + tx = Transaction(to=delegated, data=initcode, sender=pre.fund_eoa()) + + post: dict[Address, Account | None] + if deploys: + created = compute_create_address( + address=factory, nonce=1, initcode=initcode, opcode=create_opcode + ) + post = { + delegated: Account(storage={slot_call_result: 1}), + factory: Account(nonce=2, storage={slot_created: created}), + created: Account(code=Op.STOP), + } + else: + # STATICCALL halts the factory on the static context instead. + post = { + delegated: Account( + nonce=delegated.nonce, storage={slot_call_result: 0} + ), + factory: Account(nonce=1, storage={}), + compute_create_address( + address=delegated, + nonce=delegated.nonce, + initcode=initcode, + opcode=create_opcode, + ): Account.NONEXISTENT, + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py index d062ffd086b..3deab82f2f5 100644 --- a/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py +++ b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py @@ -13,25 +13,27 @@ Block, BlockchainTestFiller, Hash, + Initcode, Op, + Storage, Transaction, + compute_create2_address, ) -from execution_testing.test_types.helpers import compute_create2_address -from execution_testing.tools.tools_code.generators import Initcode from ...amsterdam.eip7997_deterministic_factory_contract.spec import ( Spec, ref_spec_7997, ) +from ...monad_eight.delegated_creates.helpers import ( + forwarding_code, + slot_call_result, +) REFERENCE_SPEC_GIT_PATH = ref_spec_7997.git_path REFERENCE_SPEC_VERSION = ref_spec_7997.version SALT = 0x42 - -slot_code_worked = 0x1 -value_code_worked = 0x1234 -slot_call_result = 0x2 +FACTORY = Address(Spec.FACTORY_ADDRESS) pytestmark = [ pytest.mark.valid_from("MONAD_ELEVEN"), @@ -42,34 +44,74 @@ ] -@pytest.mark.parametrize("create_op", [Op.CREATE, Op.CREATE2]) -def test_create_halts_in_delegated_frame( +def test_factory_as_delegated_code( blockchain_test: BlockchainTestFiller, pre: Alloc, - create_op: Op, ) -> None: - """ - A create opcode executed as an EOA's delegated code halts the frame. - """ - delegate = pre.deploy_contract( - create_op(value=0, size=1) - + Op.SSTORE(slot_code_worked, value_code_worked) - ) - sender = pre.fund_eoa() - delegated = pre.fund_eoa(delegation=delegate) + """The factory run as a delegated EOA's code halts the frame.""" + initcode = Initcode(deploy_code=Op.STOP) + delegated = pre.fund_eoa(delegation=FACTORY) tx = Transaction( to=delegated, - sender=sender, + data=Hash(SALT) + bytes(initcode), + sender=pre.fund_eoa(), ) blockchain_test( pre=pre, - post={delegated: Account(storage={})}, + post={ + delegated: Account(nonce=delegated.nonce), + compute_create2_address( + delegated, SALT, initcode + ): Account.NONEXISTENT, + }, blocks=[Block(txs=[tx])], ) +@pytest.mark.with_all_call_opcodes +def test_contract_calls_delegated_factory( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + call_opcode: Op, +) -> None: + """ + A contract reaching the factory as a delegated EOA's code deploys only + when the factory runs at the contract's own address. + """ + initcode = Initcode(deploy_code=Op.STOP) + delegated = pre.fund_eoa(delegation=FACTORY) + deploys = call_opcode in (Op.DELEGATECALL, Op.CALLCODE) + storage = Storage() + storage[slot_call_result] = 1 if deploys else 0 + caller = pre.deploy_contract( + forwarding_code(call_opcode, delegated), storage=storage.canary() + ) + + tx = Transaction( + to=caller, + data=Hash(SALT) + bytes(initcode), + sender=pre.fund_eoa(), + ) + + post: dict[Address, Account | None] = { + delegated: Account(nonce=delegated.nonce), + } + if deploys: + post[caller] = Account(storage={slot_call_result: 1}) + post[compute_create2_address(caller, SALT, initcode)] = Account( + code=Op.STOP + ) + else: + post[caller] = Account(storage={slot_call_result: 0}) + post[compute_create2_address(delegated, SALT, initcode)] = ( + Account.NONEXISTENT + ) + + blockchain_test(pre=pre, post=post, blocks=[Block(txs=[tx])]) + + @pytest.mark.with_all_call_opcodes def test_factory_deploys_from_delegated_frame( blockchain_test: BlockchainTestFiller, @@ -81,52 +123,33 @@ def test_factory_deploys_from_delegated_frame( when the factory runs at its own address. """ initcode = Initcode(deploy_code=Op.STOP) - - delegate = pre.deploy_contract( - Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) - + Op.SSTORE( - slot_call_result, - Op.ADD( - 1, - call_opcode( - gas=Op.GAS, - address=Spec.FACTORY_ADDRESS, - args_offset=0, - args_size=Op.CALLDATASIZE, - ), - ), - ) - ) - sender = pre.fund_eoa() - delegated = pre.fund_eoa(delegation=delegate) + delegate = pre.deploy_contract(forwarding_code(call_opcode, FACTORY)) + deploys = call_opcode == Op.CALL + storage = Storage() + storage[slot_call_result] = 1 if deploys else 0 + delegated = pre.fund_eoa(delegation=delegate, storage=storage.canary()) tx = Transaction( to=delegated, data=Hash(SALT) + bytes(initcode), - sender=sender, + sender=pre.fund_eoa(), ) post: dict[Address, Account | None] - if call_opcode == Op.CALL: + if deploys: post = { - delegated: Account(storage={slot_call_result: 2}), - compute_create2_address( - Spec.FACTORY_ADDRESS, SALT, initcode - ): Account(code=Op.STOP), + delegated: Account(storage={slot_call_result: 1}), + compute_create2_address(FACTORY, SALT, initcode): Account( + code=Op.STOP + ), } - elif call_opcode in (Op.DELEGATECALL, Op.CALLCODE, Op.STATICCALL): + else: # STATICCALL halts the factory on the static context instead. post = { - delegated: Account(storage={slot_call_result: 1}), + delegated: Account(storage={slot_call_result: 0}), compute_create2_address( delegated, SALT, initcode ): Account.NONEXISTENT, } - else: - raise ValueError(f"unexpected call opcode {call_opcode}") - blockchain_test( - pre=pre, - post=post, - blocks=[Block(txs=[tx])], - ) + blockchain_test(pre=pre, post=post, blocks=[Block(txs=[tx])]) From cdc38ca8e94eb09058973409c3530689653367b4 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:30:43 +0000 Subject: [PATCH 42/44] refactor(monad_eleven): keep JUMPDEST analysis unchanged under EIP-8024 Mirror upstream, where the skipped immediates can be neither JUMPDEST nor PUSH, so the jump destination set never changed. Co-Authored-By: Claude --- src/ethereum/forks/monad_eleven/vm/runtime.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/ethereum/forks/monad_eleven/vm/runtime.py b/src/ethereum/forks/monad_eleven/vm/runtime.py index 60fd42b52c9..0aa5ddd5e20 100644 --- a/src/ethereum/forks/monad_eleven/vm/runtime.py +++ b/src/ethereum/forks/monad_eleven/vm/runtime.py @@ -28,8 +28,6 @@ def get_valid_jump_destinations(code: Bytes) -> Set[Uint]: * The jump destination should have the `JUMPDEST` opcode (0x5B). * The jump destination shouldn't be part of the data corresponding to `PUSH-N` opcodes. - * The jump destination shouldn't be part of the immediate byte - corresponding to `DUPN`, `SWAPN`, or `EXCHANGE` opcodes (EIP-8024). Note - Jump destinations are 0-indexed. @@ -65,30 +63,6 @@ def get_valid_jump_destinations(code: Bytes) -> Set[Uint]: # opcodes. push_data_size = current_opcode.value - Ops.PUSH1.value + 1 pc += Uint(push_data_size) - elif current_opcode in (Ops.DUPN, Ops.SWAPN): - # EIP-8024: DUPN/SWAPN invalid immediate range is - # 90 < x < 128, i.e. 0x5B (91) to 0x7F (127). - # Invalid immediates are not skipped so the byte - # remains at an instruction boundary. - if ( - pc + Uint(1) < ulen(code) - and 0x5B <= code[pc + Uint(1)] <= 0x7F - ): - pass - else: - pc += Uint(1) - elif current_opcode == Ops.EXCHANGE: - # EIP-8024: EXCHANGE invalid immediate range is - # 81 < x < 128, i.e. 0x52 (82) to 0x7F (127). - # Invalid immediates are not skipped so the byte - # remains at an instruction boundary. - if ( - pc + Uint(1) < ulen(code) - and 0x52 <= code[pc + Uint(1)] <= 0x7F - ): - pass - else: - pc += Uint(1) pc += Uint(1) From 9b3982e832c40387c47591c3bc59e261d60358cb Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:55:41 +0000 Subject: [PATCH 43/44] chore(skills): drop the merge skill's copy at its old path Co-Authored-By: Claude --- .claude/commands/merge-eip-branches.md | 150 ------------------------- 1 file changed, 150 deletions(-) delete mode 100644 .claude/commands/merge-eip-branches.md diff --git a/.claude/commands/merge-eip-branches.md b/.claude/commands/merge-eip-branches.md deleted file mode 100644 index 05c4f068975..00000000000 --- a/.claude/commands/merge-eip-branches.md +++ /dev/null @@ -1,150 +0,0 @@ -# Merge EIP Adoption Branches - -Consolidate several single-EIP adoption branches into one fork that adopts -all of them. Run this skill before starting such a merge. Each source -branch was built by `/adopt-upstream-eip`, alone, against its own upstream -EIP branch, so no source branch knows what the combined fork does. - -## Inputs - -- The source branches, and `` they all target. -- ``, the branch they merge into. Example: `forks/monad_nine`. -- `` and its upstream integration branch, which already has every - EIP composed. Example: `upstream/forks/amsterdam`. - -## Read First - -`/adopt-upstream-eip` defines the fork shape, the shared-file cost rules, -the step-12 cleanup list and the definition of done. This skill only covers -what the merge adds. - -## 1. Prepare - -```bash -git worktree add -b origin/ -git branch --unset-upstream -``` - -Merge into a fresh worktree, and never check out or rewrite a source -branch. Note which commits already landed on `` after the -source branches were cut: a branch carrying its own copy of one of them -conflicts, and ``'s version wins. - -```bash -git log --oneline $(git merge-base origin/ )..origin/ -``` - -## 2. Merge - -Merge the largest branch first, then the rest, with real merge commits. -Squashing or cherry-picking hides which branch each conflict came from. - -```bash -git merge # once per source branch -``` - -## 3. Resolve the Conflicts Git Reports - -Expect these, in rising order of thought needed: - -- **`.github/configs/feature.yaml`** — every branch appends its own entry - at the same point. Keep all entries for now; step 5 collapses them. -- **`forks.py`, the `` declaration** — each branch declares the - class with one mixin. The merged class lists every mixin ahead of the - parent fork, in ascending EIP order. Drop any dead `pass` left after - `follows()`. -- **CI files already on ``** — keep the base branch's version. -- **`src/ethereum/forks//`** — two EIPs changing the same file. - Resolve against the upstream integration branch, not against either - branch alone. - -What must merge silently: the `follows()` trait, the -`BaseForkMeta._is_subclass_of` walk, the `ValidAtTransitionTo` guard and the -exclusion conftests. They are byte-identical across branches by design. -Check afterwards that one copy of each survived. - -## 4. Find the Silent Merges - -This is the main risk. Two branches editing the same file at different -lines merge cleanly and produce behavior neither branch had. Git reports -nothing. - -Find the candidates from the upstream spec changes: - -```bash -for n in ; do - base=$(git merge-base upstream/eips//eip-$n) - echo "### EIP-$n" - git diff --stat $base..upstream/eips//eip-$n -- src/ethereum/forks/ -done -``` - -Any file listed under two EIPs needs reading in full on the merge branch. -Where one EIP removes what another adds, the merged file must match the -upstream integration branch, which already has both. - -Two shapes seen in practice: - -- One EIP adds a log for a state change a later EIP removes. Git keeps - both: a log for something that no longer happens. -- Two branches add the same conftest hook at different offsets, one after - the imports and one appended. Git keeps both hook definitions in one - module. - -Also read every file the merge touched outside `src/ethereum/forks/monad*` -and `tests/monad*`, whether or not git flagged it. - -## 5. Clean Up the Scaffolding - -Follow `/adopt-upstream-eip` step 12, driven by what the merged fork now -adopts: - -- Delete the exclusion conftest for each adopted EIP's suite. Those suites - must now be selected and pass. -- Collapse the per-branch feature entries into one. When the branch name - follows `eips//eip-+`, the feature name must carry the same - numbers in the same order: `check_release_matrix.py` matches the sequence - as a literal string. Verify it: - - ```bash - python3 .github/scripts/check_release_matrix.py "" - ``` - -- Revert expectation scaffolding written for an intermediate fork shape. A - test edit that only made a fork with EIP-A but without EIP-B green is - scaffolding once B joins. - -To test whether an edit was scaffolding, revert it and diff against the -base branch: - -```bash -git revert --no-commit && git diff origin/ -- -``` - -An empty diff means the edit existed only for the intermediate shape. -Reverting cleanly needs the source branch to have kept scaffolding in its -own commit; a commit mixing scaffolding with permanent change has to be -undone by hand. - -Keep gates on a capability (`fork.is_eip_enabled()`, `fork >= -MONAD_EIGHT`) that stay true on the merged fork. They are permanent. - -## 6. Validate - -Run `/adopt-upstream-eip`'s step-3 shape check, its step-5 `--collect-only` -selection check, then the full-suite fill for the fork. The full suite -matters: the merge changes which suites select the fork. - -Measure any exclusion that survived the cleanup, one suite at a time, by -stripping its hook and filling that suite. State which ones you did not get -to measure. - -## Definition of Done - -- Every source branch merged, with the merge commits kept. -- Each file two EIPs touched read in full and matched against the upstream - integration branch. -- One copy of each shared addition; no duplicated hook or trait. -- Adopted suites selected and passing; their exclusions deleted. -- One feature entry, its name matching the branch's EIP sequence. -- Full-suite fill green, with any unmeasured exclusion named. From 3c5a096ef5f99c011a49cc5b892288f1708a0db1 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Mon, 19 Oct 2026 08:35:08 +0000 Subject: [PATCH 44/44] test(monad_eleven): expect the delegated factory transaction to fail The nonce and the absent contract also hold for a client that skips the banned CREATE2 instead of halting. Co-Authored-By: Claude --- .../monad_eleven/eip7997_delegated_create/test_factory_reach.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py index 3deab82f2f5..f047d3431e6 100644 --- a/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py +++ b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py @@ -17,6 +17,7 @@ Op, Storage, Transaction, + TransactionReceipt, compute_create2_address, ) @@ -56,6 +57,7 @@ def test_factory_as_delegated_code( to=delegated, data=Hash(SALT) + bytes(initcode), sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt(status=0), ) blockchain_test(