diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index ecd72f554df..e5e3c06fc0b 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -1,11 +1,11 @@ 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_benchmark: evm-type: eels 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/cli/pytest_commands/plugins/execute/rpc/hive.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py index c1c25093915..eb9c0825870 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 @@ -182,7 +182,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/cli/pytest_commands/plugins/forks/forks.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/forks/forks.py index c181bfdaf6c..bef753bb50c 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 @@ -1275,7 +1275,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/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/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 12ffaff1a5a..ebf087e4aa9 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -397,7 +397,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/__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/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index 3ad3941b097..0bd73764e62 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -335,12 +335,30 @@ 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. The + EIPs of a followed fork stay out of that ordering. """ a = BaseForkMeta._maybe_transitioned(a) b = BaseForkMeta._maybe_transitioned(b) - return issubclass(a, b) + if issubclass(a, b): + return True + # The metaclass sees its instances as plain classes, so the + # 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): + 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).""" @@ -565,6 +583,17 @@ def header_bal_hash_required(cls) -> bool: """Return true if the header must contain block access list hash.""" pass + @classmethod + 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 the lists, and then fixes the field at zero, so + this follows the EIP rather than the header requirement. + """ + return cls.is_eip_enabled(7928) + @classmethod @abstractmethod def empty_block_bal_item_count(cls) -> int: @@ -1469,6 +1498,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 1681792afe9..2e30a2a99ec 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -1838,12 +1838,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, @@ -1947,3 +1941,37 @@ class Bogota( """Bogota fork.""" pass + + +class MONAD_ELEVEN( # noqa: N801 + eips.EIP7708, + eips.EIP7843, + eips.EIP7981, + eips.EIP7997, + eips.EIP8024, + eips.EIP8246, + MONAD_TEN, + deployed=False, +): + """ + 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_ELEVEN + after Amsterdam through `follows`. + """ + + @classmethod + def follows(cls) -> type[BaseFork] | None: + """MONAD_ELEVEN comes after Amsterdam without inheriting it.""" + return Amsterdam + + @classmethod + def header_bal_hash_required(cls) -> bool: + """ + 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 aaf8c2323bd..708e27add8f 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, SystemCallPhase +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_ELEVEN, + MONAD_TEN, Amsterdam, Berlin, Bogota, @@ -304,6 +307,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_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_ELEVEN > Amsterdam + assert MONAD_ELEVEN > MONAD_TEN + + assert MONAD_ELEVEN >= EIP7708 + assert MONAD_ELEVEN.is_eip_enabled(7708) + + assert not MONAD_ELEVEN >= EIP2780 + assert not MONAD_ELEVEN.is_eip_enabled(2780) + + def test_transition_fork_comparison() -> None: """ Test comparing to a transition fork. @@ -390,6 +412,39 @@ class PreAllocTransitionFork(TransitionBaseClass): pass +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_ELEVEN 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_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_ELEVEN's bases, drops these opcodes silently. + """ + adopted = {"DUPN", "SWAPN", "EXCHANGE"} + assert adopted <= opcode_names(MONAD_ELEVEN) + 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/packages/testing/src/execution_testing/specs/blockchain.py b/packages/testing/src/execution_testing/specs/blockchain.py index f6d9de153b3..aa7e29f2c91 100644 --- a/packages/testing/src/execution_testing/specs/blockchain.py +++ b/packages/testing/src/execution_testing/specs/blockchain.py @@ -1095,6 +1095,16 @@ def generate_block_data( int(env.slot_number) if env.slot_number is not None else 0 ) + # 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. + bal_hash_field["block_access_list_hash"] = Hash(0) + header = FixtureHeader( **( transition_tool_output.result.model_dump( @@ -1105,6 +1115,7 @@ def generate_block_data( exclude_none=True, exclude={"blob_gas_used", "slot_number"}, ) + | bal_hash_field ), blob_gas_used=blob_gas_used, transactions_trie=Transaction.list_root(txs), @@ -1168,7 +1179,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_eleven/__init__.py b/src/ethereum/forks/monad_eleven/__init__.py new file mode 100644 index 00000000000..30282ec017e --- /dev/null +++ b/src/ethereum/forks/monad_eleven/__init__.py @@ -0,0 +1,30 @@ +""" +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 +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 + +# TODO: just a bit after MONAD_TEN +FORK_CRITERIA: ForkCriteria = ByTimestamp(1774898553) diff --git a/src/ethereum/forks/monad_next/blocks.py b/src/ethereum/forks/monad_eleven/blocks.py similarity index 89% rename from src/ethereum/forks/monad_next/blocks.py rename to src/ethereum/forks/monad_eleven/blocks.py index f6745f79de3..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,10 +244,28 @@ 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 """ + 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_eleven.fork.validate_header + """ + + 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 @@ -272,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 @@ -281,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, ...] @@ -313,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 99% rename from src/ethereum/forks/monad_next/fork.py rename to src/ethereum/forks/monad_eleven/fork.py index 1a9c251a54e..f82cf4f1c30 100644 --- a/src/ethereum/forks/monad_next/fork.py +++ b/src/ethereum/forks/monad_eleven/fork.py @@ -58,8 +58,8 @@ BlockState, TransactionState, add_sender_authority, + clear_account_preserving_balance, create_ether, - destroy_account, extract_block_diff, forget_senders_authorities, get_account, @@ -242,6 +242,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( @@ -398,6 +399,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: @@ -967,9 +970,6 @@ 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) - # 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 @@ -989,6 +989,9 @@ def process_transaction( block_output.block_logs += tx_output.logs + for address in tx_output.accounts_to_delete: + clear_account_preserving_balance(tx_state, address) + incorporate_tx_into_block(tx_state) 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 96% rename from src/ethereum/forks/monad_next/state_tracker.py rename to src/ethereum/forks/monad_eleven/state_tracker.py index 182dfe1b9f7..05822329b3d 100644 --- a/src/ethereum/forks/monad_next/state_tracker.py +++ b/src/ethereum/forks/monad_eleven/state_tracker.py @@ -413,10 +413,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 ---------- @@ -430,6 +429,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/transactions.py b/src/ethereum/forks/monad_eleven/transactions.py similarity index 96% rename from src/ethereum/forks/monad_next/transactions.py rename to src/ethereum/forks/monad_eleven/transactions.py index ed92baf89a0..1e3a883749c 100644 --- a/src/ethereum/forks/monad_next/transactions.py +++ b/src/ethereum/forks/monad_eleven/transactions.py @@ -28,6 +28,22 @@ ) from .fork_types import Authorization, VersionedHash +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 @@ -588,10 +604,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 @@ -601,12 +613,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/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 c421f740327..026efe80389 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 85% rename from src/ethereum/forks/monad_next/vm/__init__.py rename to src/ethereum/forks/monad_eleven/vm/__init__.py index 5cff6a44f12..cc58a9c733d 100644 --- a/src/ethereum/forks/monad_next/vm/__init__.py +++ b/src/ethereum/forks/monad_eleven/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,11 @@ __all__ = ("Environment", "Evm", "Message") +TRANSFER_TOPIC = keccak256(b"Transfer(address,address,uint256)") +SYSTEM_ADDRESS = Address( + bytes.fromhex("fffffffffffffffffffffffffffffffffffffffe") +) + @final @dataclass @@ -49,6 +55,7 @@ class BlockEnvironment: prev_randao: Bytes32 excess_blob_gas: U64 parent_beacon_block_root: Hash32 + slot_number: U64 @final @@ -236,3 +243,42 @@ 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,) 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 99% rename from src/ethereum/forks/monad_next/vm/gas.py rename to src/ethereum/forks/monad_eleven/vm/gas.py index 0186e2a22d0..55ab00c444e 100644 --- a/src/ethereum/forks/monad_next/vm/gas.py +++ b/src/ethereum/forks/monad_eleven/vm/gas.py @@ -192,11 +192,15 @@ class GasCosts: OPCODE_SELFBALANCE: Final[Uint] = FAST_STEP 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 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 OPCODE_TLOAD: Final[Uint] = WARM_ACCESS OPCODE_TSTORE: Final[Uint] = WARM_ACCESS diff --git a/src/ethereum/forks/monad_next/vm/instructions/__init__.py b/src/ethereum/forks/monad_eleven/vm/instructions/__init__.py similarity index 97% rename from src/ethereum/forks/monad_next/vm/instructions/__init__.py rename to src/ethereum/forks/monad_eleven/vm/instructions/__init__.py index 0da72c8ea5c..06295ec86f1 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/__init__.py +++ b/src/ethereum/forks/monad_eleven/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 @@ -188,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 @@ -251,6 +257,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, @@ -350,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/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 73% rename from src/ethereum/forks/monad_next/vm/instructions/block.py rename to src/ethereum/forks/monad_eleven/vm/instructions/block.py index baa589c4395..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`. """ @@ -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_eleven.vm.exceptions.StackOverflowError` + If `len(stack)` is equal to `1024`. + :py:class:`~ethereum.forks.monad_eleven.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) 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 72% rename from src/ethereum/forks/monad_next/vm/instructions/stack.py rename to src/ethereum/forks/monad_eleven/vm/instructions/stack.py index eff50ac73c9..2b37f84b351 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/stack.py +++ b/src/ethereum/forks/monad_eleven/vm/instructions/stack.py @@ -14,12 +14,13 @@ 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 from ..gas import GasCosts, charge_gas from ..memory import buffer_read +from ..stack import decode_pair, decode_single def pop(evm: Evm) -> None: @@ -207,3 +208,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/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 99% rename from src/ethereum/forks/monad_next/vm/instructions/system.py rename to src/ethereum/forks/monad_eleven/vm/instructions/system.py index 38abf706b03..4a87e4eae5c 100644 --- a/src/ethereum/forks/monad_next/vm/instructions/system.py +++ b/src/ethereum/forks/monad_eleven/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, @@ -35,6 +34,7 @@ from .. import ( Evm, Message, + emit_transfer_log, incorporate_child_on_error, incorporate_child_on_success, ) @@ -577,12 +577,13 @@ def selfdestruct(evm: Evm) -> None: originator_balance, ) + # EIP-7708: Emit a transfer log for the beneficiary transfer + if 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: - # 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/src/ethereum/forks/monad_next/vm/interpreter.py b/src/ethereum/forks/monad_eleven/vm/interpreter.py similarity index 97% rename from src/ethereum/forks/monad_next/vm/interpreter.py rename to src/ethereum/forks/monad_eleven/vm/interpreter.py index 1814011826b..831d6a71612 100644 --- a/src/ethereum/forks/monad_next/vm/interpreter.py +++ b/src/ethereum/forks/monad_eleven/vm/interpreter.py @@ -55,7 +55,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, @@ -274,7 +274,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. """ @@ -334,7 +334,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 """ @@ -410,6 +410,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: 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_eleven/vm/stack.py b/src/ethereum/forks/monad_eleven/vm/stack.py new file mode 100644 index 00000000000..98ba815cb73 --- /dev/null +++ b/src/ethereum/forks/monad_eleven/vm/stack.py @@ -0,0 +1,131 @@ +""" +Ethereum Virtual Machine (EVM) Stack. + +.. contents:: Table of Contents + :backlinks: none + :local: + +Introduction +------------ + +Implementation of the stack operators for the EVM. +""" + +from typing import List, Tuple + +from ethereum_types.numeric import U8, U256 + +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: + """ + Pops the top item off of `stack`. + + Parameters + ---------- + stack : + EVM stack. + + Returns + ------- + value : `U256` + The top element on the stack. + + """ + if len(stack) == 0: + raise StackUnderflowError + + return stack.pop() + + +def push(stack: List[U256], value: U256) -> None: + """ + Pushes `value` onto `stack`. + + Parameters + ---------- + stack : + EVM stack. + + value : + Item to be pushed onto `stack`. + + """ + if len(stack) == 1024: + raise StackOverflowError + + return stack.append(value) diff --git a/src/ethereum/forks/monad_next/__init__.py b/src/ethereum/forks/monad_next/__init__.py deleted file mode 100644 index b6c71ab2450..00000000000 --- a/src/ethereum/forks/monad_next/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -MONAD_NEXT fork is a placeholder for upcoming Monad changes and is -currently identical to MONAD_TEN. -""" - -from ethereum.fork_criteria import ByTimestamp, ForkCriteria - -# TODO: just a bit after MONAD_TEN -FORK_CRITERIA: ForkCriteria = ByTimestamp(1774898553) diff --git a/src/ethereum/forks/monad_next/vm/stack.py b/src/ethereum/forks/monad_next/vm/stack.py deleted file mode 100644 index a87b0a47079..00000000000 --- a/src/ethereum/forks/monad_next/vm/stack.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Ethereum Virtual Machine (EVM) Stack. - -.. contents:: Table of Contents - :backlinks: none - :local: - -Introduction ------------- - -Implementation of the stack operators for the EVM. -""" - -from typing import List - -from ethereum_types.numeric import U256 - -from .exceptions import StackOverflowError, StackUnderflowError - - -def pop(stack: List[U256]) -> U256: - """ - Pops the top item off of `stack`. - - Parameters - ---------- - stack : - EVM stack. - - Returns - ------- - value : `U256` - The top element on the stack. - - """ - if len(stack) == 0: - raise StackUnderflowError - - return stack.pop() - - -def push(stack: List[U256], value: U256) -> None: - """ - Pushes `value` onto `stack`. - - Parameters - ---------- - stack : - EVM stack. - - value : - Item to be pushed onto `stack`. - - """ - if len(stack) == 1024: - raise StackOverflowError - - return stack.append(value) diff --git a/src/ethereum_spec_tools/loaders/fork_loader.py b/src/ethereum_spec_tools/loaders/fork_loader.py index 270ed874a08..b45378be129 100644 --- a/src/ethereum_spec_tools/loaders/fork_loader.py +++ b/src/ethereum_spec_tools/loaders/fork_loader.py @@ -159,6 +159,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/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/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/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/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..ab5b838e915 --- /dev/null +++ b/tests/amsterdam/eip7928_block_level_access_lists/conftest.py @@ -0,0 +1,32 @@ +"""Pytest (plugin) definitions local to EIP-7928 tests.""" + +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 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 0eeed2a1126..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 @@ -45,6 +45,7 @@ keccak256, ) from execution_testing import Macros as Om +from execution_testing.forks import MONAD_EIGHT from ...prague.eip7702_set_code_tx.spec import Spec as Spec7702 from .spec import ref_spec_7928 @@ -2387,6 +2388,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, @@ -2491,6 +2493,7 @@ def test_bal_multiple_balance_changes_same_account( ) +@pytest.mark.no_bal_dependency @pytest.mark.parametrize( "senders", [ @@ -2756,6 +2759,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, @@ -2897,6 +2901,7 @@ def test_bal_cross_tx_reverted_storage_reads( ) +@pytest.mark.no_bal_dependency def test_bal_cross_tx_storage_chain( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -3068,6 +3073,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, @@ -3171,6 +3177,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, @@ -3234,6 +3241,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], @@ -3242,7 +3258,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) @@ -3328,6 +3344,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, @@ -3429,6 +3446,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, @@ -3655,6 +3673,7 @@ def test_bal_cross_tx_funding_chain( ) +@pytest.mark.no_bal_dependency @pytest.mark.parametrize( "last_write", [ 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 b35d93d8399..cf306fffd35 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 @@ -270,6 +270,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 a7e7704a38a..a176424b316 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 @@ -1052,6 +1052,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 6b524255598..21f7b00bc48 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 @@ -2875,6 +2875,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, @@ -3125,6 +3126,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, @@ -3908,6 +3910,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, 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) diff --git a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py index 26069de2bb3..dcafa427689 100644 --- a/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py +++ b/tests/amsterdam/eip7976_increase_calldata_floor_cost/conftest.py @@ -377,3 +377,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/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..26dd8f7edaf 100644 --- a/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py +++ b/tests/amsterdam/eip7981_increase_access_list_cost/helpers.py @@ -1,8 +1,9 @@ """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 def calculate_access_list_data_cost( @@ -25,3 +26,31 @@ 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 + + +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 1472b254615..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 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 @@ -123,13 +128,11 @@ def test_access_list_token_calculation( == expected_data_cost ) - expected_floor_cost = ( - expected_data_cost - + gas_costs.TX_BASE + expected_floor_cost = expected_data_cost + 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 ) @@ -161,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", ), ], @@ -332,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( @@ -354,7 +365,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 + ) ), ) @@ -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 @@ -403,6 +422,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..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,6 +19,7 @@ TransactionReceipt, ) +from .helpers import billed_gas, calldata_clearing_floor 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( @@ -92,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, @@ -114,7 +132,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( @@ -125,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_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 {} ) 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 7fd9120b9fa..478871f83f4 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 @@ -114,11 +115,12 @@ def test_access_list_intrinsic_across_amsterdam_transition( surcharge = calculate_access_list_data_cost(access_list, post_fork) 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] @@ -316,10 +318,11 @@ def test_access_list_floor_across_amsterdam_transition( post_tokens = len(data) * int(post_costs.TX_DATA_TOKEN_STANDARD) expected_post = int( post_costs.TX_BASE - + post_costs.COLD_ACCOUNT_ACCESS + post_tokens * post_costs.TX_DATA_TOKEN_FLOOR + calculate_access_list_data_cost(access_list, post_fork) ) + 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/eip7981_increase_access_list_cost/test_transaction_validity.py b/tests/amsterdam/eip7981_increase_access_list_cost/test_transaction_validity.py index 810268ca6ad..f548c021c3a 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( diff --git a/tests/amsterdam/eip7997_deterministic_factory_contract/test_factory.py b/tests/amsterdam/eip7997_deterministic_factory_contract/test_factory.py index 4f5cb377fcd..fc9a207de88 100644 --- a/tests/amsterdam/eip7997_deterministic_factory_contract/test_factory.py +++ b/tests/amsterdam/eip7997_deterministic_factory_contract/test_factory.py @@ -42,6 +42,7 @@ from execution_testing import ( Macros as Om, ) +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 @@ -985,12 +986,16 @@ def test_factory_via_eip7702_delegation( state_test: StateTestFiller, pre: Alloc, forwarded_value: int, + fork: Fork, ) -> None: """ Execute delegated factory code with the authorized EOA as deployer. The delegated EOA funds the CREATE2 endowment from the forwarded value. """ + # Monad forbids the create opcodes in a delegated frame, so the + # factory halts there instead of deploying. + factory_halts = fork >= MONAD_EIGHT auth_signer = pre.fund_eoa(amount=0) auth_signer_nonce = auth_signer.nonce @@ -1032,12 +1037,14 @@ def test_factory_via_eip7702_delegation( ), post={ auth_signer: Account( - nonce=auth_signer_nonce + 2, + nonce=auth_signer_nonce + (1 if factory_halts else 2), balance=0, code=Spec7702.delegation_designation(Address(FACTORY)), ), - caller: Account(balance=0), - expected_address: Account( + caller: Account(balance=forwarded_value if factory_halts else 0), + expected_address: Account.NONEXISTENT + if factory_halts + else Account( nonce=1, balance=forwarded_value, code=bytes(runtime_code), diff --git a/tests/amsterdam/eip7997_deterministic_factory_contract/test_fork_transition.py b/tests/amsterdam/eip7997_deterministic_factory_contract/test_fork_transition.py index 9866e90dd50..faba1fbc525 100644 --- a/tests/amsterdam/eip7997_deterministic_factory_contract/test_fork_transition.py +++ b/tests/amsterdam/eip7997_deterministic_factory_contract/test_fork_transition.py @@ -30,7 +30,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]) @EIPChecklist.SystemContract.Test.ForkTransition.CallBeforeFork() @@ -108,7 +108,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 @pytest.mark.parametrize( "factory_pre_state", 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/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={}, 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_eight/reserve_balance/test_transfers.py b/tests/monad_eight/reserve_balance/test_transfers.py index 1ac29a5c2aa..56f7b1af1d3 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])], ) @@ -1310,8 +1325,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( @@ -1323,6 +1343,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, @@ -1339,7 +1360,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) ) @@ -1368,6 +1391,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={ @@ -1378,9 +1420,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)], @@ -1457,8 +1502,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( @@ -1469,6 +1519,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, @@ -1488,7 +1539,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) ) @@ -1519,15 +1572,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])], @@ -1538,8 +1602,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, @@ -1558,6 +1633,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: @@ -1584,7 +1661,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) @@ -1672,6 +1749,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={ @@ -1685,7 +1772,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, @@ -1695,7 +1782,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 @@ -1729,8 +1816,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, @@ -1741,6 +1833,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: @@ -1762,7 +1855,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( @@ -1808,6 +1905,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( @@ -1825,12 +1932,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)], @@ -1848,8 +1955,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( @@ -1860,6 +1972,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, @@ -1882,7 +1995,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( @@ -1917,6 +2034,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={ @@ -1924,12 +2051,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_eleven/__init__.py b/tests/monad_eleven/__init__.py new file mode 100644 index 00000000000..e0c66f0fa89 --- /dev/null +++ b/tests/monad_eleven/__init__.py @@ -0,0 +1 @@ +"""Tests for the Amsterdam EIPs MONAD_ELEVEN adopts.""" diff --git a/tests/monad_eleven/eip7997_delegated_create/__init__.py b/tests/monad_eleven/eip7997_delegated_create/__init__.py new file mode 100644 index 00000000000..4cd6b5b4cb1 --- /dev/null +++ b/tests/monad_eleven/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_eleven/eip7997_delegated_create/test_factory_reach.py b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py new file mode 100644 index 00000000000..f047d3431e6 --- /dev/null +++ b/tests/monad_eleven/eip7997_delegated_create/test_factory_reach.py @@ -0,0 +1,157 @@ +""" +Tests for reaching the EIP-7997 factory from a delegated frame. + +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, + Hash, + Initcode, + Op, + Storage, + Transaction, + TransactionReceipt, + compute_create2_address, +) + +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 +FACTORY = Address(Spec.FACTORY_ADDRESS) + +pytestmark = [ + 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", + ), +] + + +def test_factory_as_delegated_code( + blockchain_test: BlockchainTestFiller, + pre: Alloc, +) -> None: + """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, + data=Hash(SALT) + bytes(initcode), + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt(status=0), + ) + + blockchain_test( + pre=pre, + 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, + pre: Alloc, + call_opcode: Op, +) -> None: + """ + 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) + 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=pre.fund_eoa(), + ) + + post: dict[Address, Account | None] + if deploys: + post = { + delegated: Account(storage={slot_call_result: 1}), + compute_create2_address(FACTORY, SALT, initcode): Account( + code=Op.STOP + ), + } + else: + # STATICCALL halts the factory on the static context instead. + post = { + delegated: Account(storage={slot_call_result: 0}), + compute_create2_address( + delegated, SALT, initcode + ): Account.NONEXISTENT, + } + + blockchain_test(pre=pre, post=post, blocks=[Block(txs=[tx])]) diff --git a/tests/monad_nine/mip4_checkreservebalance/test_transfers.py b/tests/monad_nine/mip4_checkreservebalance/test_transfers.py index ec43d75dd91..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,6 +1537,22 @@ def test_contract_unrestricted_within_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 both the retained balance and the refill. + retained = balance - value if selfdestruct_to_self else 0 + selfdestructed_account = ( + Account( + nonce=0, + balance=retained + 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,12 +1582,12 @@ 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. selfdestruct_target: Account(balance=balance - value) - if selfdestruct + if selfdestruct and not selfdestruct_to_self else None, }, blocks=[Block(txs=txs)], @@ -1513,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( @@ -1525,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, @@ -1565,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( @@ -1596,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={ @@ -1606,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)],