From a2bd721647df5ca2a9a6bc56da7405d903c42cea 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 1/9] 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 d8284e126c5..31a9c9bc277 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_next/fork.py @@ -42,6 +42,7 @@ 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 @@ -1019,15 +1020,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)) @@ -1039,7 +1057,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 5738e4f0d5f..c0b5982c49a 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 @@ -236,3 +243,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 912ac3e42e8..d3a3a39d917 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, @@ -402,6 +402,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 a0a30290a8021264954117b4d4857d5aaef241bd 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 2/9] 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 31a9c9bc277..310036c2471 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_next/fork.py @@ -295,6 +295,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 c0b5982c49a..0d018d625b7 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 328caa5f7e788b59c69f014ed6ace973b58e60af 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 3/9] 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 868163a3b5928d2638f05e152dab1cc9324a108b 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 4/9] 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 5b0e8d08d79..d77dcad5adb 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1754,12 +1754,6 @@ def _calculate_sstore_gas_mip8( return gas_cost -class MONAD_NEXT(MONAD_TEN, solc_name="cancun"): # noqa: N801 - """MONAD_NEXT fork, a placeholder identical to MONAD_TEN.""" - - pass - - class BPO1( Osaka, bpo_fork=True, @@ -1847,3 +1841,69 @@ class Amsterdam( # live on mainnet. pass + + +class MONAD_NEXT(MONAD_TEN, Amsterdam, solc_name="cancun"): # 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 |= {7708, 7843, 8024} From f23593f7fde691ca838f8dd03f03ace33d4905ed Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:25:07 +0000 Subject: [PATCH 5/9] fix(test): Unhardcode gas in eip-7708 test Derive gas_limit from SSTORE.gas_cost(fork)*64 so the failed-CREATE test survives EIP-150 1/64 retention under heavier (MIP-8) SSTORE pricing. Co-Authored-By: Claude --- .../eip7708_eth_transfer_logs/test_transfer_logs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 61c216406f8..9881c5f94d2 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/test_transfer_logs.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/test_transfer_logs.py @@ -533,6 +533,7 @@ def test_failed_create_with_value_no_log( env: Environment, pre: Alloc, sender: EOA, + fork: Fork, initcode: Bytecode, ) -> None: """ @@ -547,11 +548,17 @@ def test_failed_create_with_value_no_log( ) + Op.SSTORE(0, Op.CREATE(1, 32 - initcode_len, initcode_len)) contract = pre.deploy_contract(contract_code, balance=1) + # INVALID initcode consumes all gas forwarded to the child CREATE + # frame, leaving the parent only the EIP-150 1/64 retained gas for + # the trailing SSTORE. Budget 64x that SSTORE's cost so the retained + # 1/64 covers it on forks with a heavier SSTORE schedule (MIP-8). + gas_limit = 100_000 + Op.SSTORE(new_value=0).gas_cost(fork) * 64 + tx = Transaction( sender=sender, to=contract, value=1, - gas_limit=500_000, + gas_limit=gas_limit, expected_receipt=TransactionReceipt( logs=[transfer_log(sender, contract, 1)] ), From 2ff028d0f519a30daa9c3987887cc281105f9124 Mon Sep 17 00:00:00 2001 From: pdobacz <5735525+pdobacz@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:14:28 +0000 Subject: [PATCH 6/9] fix(test): make eip-7708 finalization tests pass under monad reserve/fee model Deploy factory runtime code (or fund above reserve for the gas-exact test) to avoid the empty-code reserve revert, and compute the priority fee on the full gas limit on Monad forks. Co-Authored-By: Claude --- .../test_burn_logs.py | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/amsterdam/eip7708_eth_transfer_logs/test_burn_logs.py b/tests/amsterdam/eip7708_eth_transfer_logs/test_burn_logs.py index f8f8c5eeeff..c67af24ee03 100644 --- a/tests/amsterdam/eip7708_eth_transfer_logs/test_burn_logs.py +++ b/tests/amsterdam/eip7708_eth_transfer_logs/test_burn_logs.py @@ -30,6 +30,7 @@ from execution_testing import ( Macros as Om, ) +from execution_testing.forks import MONAD_EIGHT from .spec import burn_log, ref_spec_7708, transfer_log @@ -39,6 +40,26 @@ pytestmark = pytest.mark.valid_from("EIP7708") +def _factory_with_runtime(factory_code: Bytecode) -> Bytecode: + """ + Append a minimal STOP-byte runtime deployment to a factory body. + + A factory created via a ``to=None`` transaction runs its body as + initcode; returning nothing deploys empty code. On Monad forks an + empty-code account that ends below the reserve balance is treated as + an EOA and reverts the whole transaction. Returning a single STOP + byte gives the factory non-empty runtime code so it is exempt from + the reserve-balance check. Inert on forks without a reserve balance. + """ + return factory_code + Op.MSTORE8(0, 0x00) + Op.RETURN(0, 1) + + +# Monad reserve balance (10 MON). Keeps a self-draining factory above the +# reserve in the gas-exact priority-fee test, where deploying runtime code +# would perturb the gas accounting. Inert on forks without a reserve balance. +MONAD_RESERVE_BALANCE = 10 * 10**18 + + def test_selfdestruct_to_self_pre_existing_no_log( state_test: StateTestFiller, env: Environment, @@ -431,6 +452,7 @@ def test_finalization_burn_logs( + Op.MSTORE(0, reverse_sorted[2]) + Op.CALL(gas=100_000, address=p3, args_offset=0, args_size=32) ) + factory_code = _factory_with_runtime(factory_code) factory_balance = 1000 + 2000 + 3000 pre.fund_address(factory_address, factory_balance) @@ -595,6 +617,7 @@ def test_finalization_burn_logs_multi_account_ordering( args_offset=0, args_size=32, ) + factory_code = _factory_with_runtime(factory_code) execution_logs = [ transfer_log(factory_address, addr, create_balances[i]) @@ -705,6 +728,7 @@ def test_finalization_burn_log_single_account_multiple_transfers( args_size=32, ), ) + factory_code = _factory_with_runtime(factory_code) execution_logs = [ transfer_log(factory_address, x, create_balance), @@ -837,7 +861,12 @@ def test_selfdestruct_finalization_after_priority_fee( Op.CALLDATALOAD(0), address_warm=True, account_new=False ).gas_cost(fork) - pre.fund_address(factory_address, contract_balance) + # Fund the factory above the reserve balance so it does not revert on + # Monad forks once it drains `contract_balance` into the created + # contract. Deploying runtime code instead would perturb this test's + # exact gas accounting, so funding is used here. Inert on forks without + # a reserve balance (the factory keeps the headroom, which is unasserted). + pre.fund_address(factory_address, contract_balance + MONAD_RESERVE_BALANCE) # prio fee calc gas_price = 10 @@ -872,7 +901,18 @@ def test_selfdestruct_finalization_after_priority_fee( gas_refunds, gas_used // 5, # max discount EIP-3529 ) - priority_fee = priority_fee_per_gas * (gas_used - discount) + + gas_limit = 500_000 + if fork.is_eip_enabled(8037): + gas_limit = 2_000_000 + + if fork >= MONAD_EIGHT: + # Monad charges the miner fee on the full gas limit (gas is not + # refunded to the sender), so the coinbase receives the priority + # fee on tx.gas rather than on the gas actually used. + priority_fee = priority_fee_per_gas * gas_limit + else: + priority_fee = priority_fee_per_gas * (gas_used - discount) # Finalization burn log proves coinbase received priority fee before log finalization_balance: int | None = funding_amount + priority_fee @@ -896,9 +936,6 @@ def test_selfdestruct_finalization_after_priority_fee( ) expected_logs.append(burn_log(created_address, finalization_balance)) - gas_limit = 500_000 - if fork.is_eip_enabled(8037): - gas_limit = 2_000_000 tx = Transaction( sender=sender, From 31a294c0f35372b7a3f41f4abf93a230c28ec0f9 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 7/9] 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 8f70e6e5e6a..fc611e7e53d 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -14,3 +14,7 @@ monad_runloop: evm-type: eels # Like `monad`, but `--monad-runloop` and eestnet chain id `30143` fill-params: --suppress-no-test-exit-code -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: --suppress-no-test-exit-code -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 9c6953ba7e37364077f5e29c9c6bc3165da8d3bf 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 8/9] 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 ++++++++++ tests/frontier/validation/test_header.py | 3 +++ 9 files changed, 65 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 fc611e7e53d..2c6e6743b63 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -17,4 +17,4 @@ monad_runloop: monad_amsterdam: evm-type: eels - fill-params: --suppress-no-test-exit-code -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: --suppress-no-test-exit-code -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 78de4e79df2..12e875869c8 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) + ) diff --git a/tests/frontier/validation/test_header.py b/tests/frontier/validation/test_header.py index f53ef2e43be..6297b3a0db3 100644 --- a/tests/frontier/validation/test_header.py +++ b/tests/frontier/validation/test_header.py @@ -24,6 +24,9 @@ 5000, marks=[ pytest.mark.valid_from("EIP7928"), + pytest.mark.not_valid_for( + "MONAD_EIGHT", subsequent_forks=True + ), pytest.mark.exception_test, ], ), From 1b655befdf3fdabf60721888f8e786c96f5d4ce6 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 9/9] 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 --- .../execution_testing/fixtures/blockchain.py | 6 +++- .../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 ++ .../evm_tools/loaders/fork_loader.py | 14 ++++++++++ src/ethereum_spec_tools/evm_tools/t8n/env.py | 2 +- 10 files changed, 84 insertions(+), 18 deletions(-) diff --git a/packages/testing/src/execution_testing/fixtures/blockchain.py b/packages/testing/src/execution_testing/fixtures/blockchain.py index ff908059ba8..5101d84a3eb 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -376,7 +376,11 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> Self: if fork.header_requests_required() else None, "block_access_list_hash": ( - BlockAccessList().rlp_hash + ( + BlockAccessList().rlp_hash + if fork.supports_block_access_lists() + else Hash(0) + ) if fork.header_bal_hash_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 f6d5edb2bf9..b50203843b0 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -430,6 +430,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 d4b1b9a6e17..b2657f99e2f 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 d77dcad5adb..f4180943e3d 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -928,6 +928,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.""" @@ -1849,7 +1854,9 @@ class MONAD_NEXT(MONAD_TEN, Amsterdam, solc_name="cancun"): # 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 @@ -1885,9 +1892,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 d3c86e001ef..123282109f3 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -910,17 +910,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=( @@ -983,7 +989,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 310036c2471..697017ac12c 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_next/fork.py @@ -457,6 +457,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/evm_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py index 9e769dd61c0..c370844def6 100644 --- a/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py @@ -171,6 +171,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.""" diff --git a/src/ethereum_spec_tools/evm_tools/t8n/env.py b/src/ethereum_spec_tools/evm_tools/t8n/env.py index edf3763d573..3994ef3007e 100644 --- a/src/ethereum_spec_tools/evm_tools/t8n/env.py +++ b/src/ethereum_spec_tools/evm_tools/t8n/env.py @@ -148,7 +148,7 @@ def read_excess_blob_gas(self, data: Any, t8n: "T8N") -> None: if t8n.fork.has_compute_requests_hash: arguments["requests_hash"] = Hash32(b"\0" * 32) - if t8n.fork.has_hash_block_access_list: + if t8n.fork.has_block_access_list_hash_header: arguments["block_access_list_hash"] = Hash32(b"\0" * 32) if t8n.fork.has_slot_number: arguments["slot_number"] = U64(0)