diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7954.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7954.py new file mode 100644 index 00000000000..b5c23ce2752 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7954.py @@ -0,0 +1,24 @@ +""" +EIP-7954: Increase Maximum Contract Size. + +Raise the maximum contract code size from 24KiB to 32KiB and initcode size from +48KiB to 64KiB. + +https://eips.ethereum.org/EIPS/eip-7954 +""" + +from ....base_fork import BaseFork + + +class EIP7954(BaseFork): + """EIP-7954 class.""" + + @classmethod + def max_code_size(cls) -> int: + """Max contract code size is 32 KiB.""" + return 32 * 1024 + + @classmethod + def max_initcode_size(cls) -> int: + """Max initcode size is 64 KiB.""" + return 64 * 1024 diff --git a/src/ethereum/forks/amsterdam/__init__.py b/src/ethereum/forks/amsterdam/__init__.py index e6f3e9476a0..e47bb43c1a3 100644 --- a/src/ethereum/forks/amsterdam/__init__.py +++ b/src/ethereum/forks/amsterdam/__init__.py @@ -4,11 +4,13 @@ ### Changes - [EIP-7928: Block-Level Access Lists][EIP-7928] +- [EIP-7954: Increase Maximum Contract Size][EIP-7954] ### Releases [EIP-7773]: https://eips.ethereum.org/EIPS/eip-7773 [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 +[EIP-7954]: https://eips.ethereum.org/EIPS/eip-7954 """ from ethereum.fork_criteria import ForkCriteria, Unscheduled diff --git a/src/ethereum/forks/amsterdam/vm/interpreter.py b/src/ethereum/forks/amsterdam/vm/interpreter.py index 80b30bd7ab2..f709d603770 100644 --- a/src/ethereum/forks/amsterdam/vm/interpreter.py +++ b/src/ethereum/forks/amsterdam/vm/interpreter.py @@ -62,7 +62,7 @@ from .runtime import get_valid_jump_destinations STACK_DEPTH_LIMIT = Uint(1024) -MAX_CODE_SIZE = 0x6000 +MAX_CODE_SIZE = 0x8000 MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/__init__.py b/tests/amsterdam/eip7954_increase_max_contract_size/__init__.py new file mode 100644 index 00000000000..7ea8f8276b3 --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/__init__.py @@ -0,0 +1 @@ +"""Tests for [EIP-7954: Increase Maximum Contract Size](https://eips.ethereum.org/EIPS/eip-7954).""" diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py b/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py new file mode 100644 index 00000000000..78cb66ed14d --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for the EIP-7954 max contract size tests.""" + +import pytest +from execution_testing import Address, Alloc, Bytecode, Fork, Op + + +@pytest.fixture +def max_code_size_contract( + pre: Alloc, + fork: Fork, +) -> tuple[Address, Bytecode]: + """ + Deploy a max-size self-checking contract deterministically. + + The contract uses its own ADDRESS to query EXTCODESIZE, EXTCODEHASH, + and EXTCODECOPY on itself, storing results in storage slots 0-2. + Padded with JUMPDESTs to reach the fork's max code size. + """ + logic = ( + Op.SSTORE(0, Op.EXTCODESIZE(Op.ADDRESS)) + + Op.SSTORE(1, Op.EXTCODEHASH(Op.ADDRESS)) + + Op.EXTCODECOPY(Op.ADDRESS, 0, 0, Op.EXTCODESIZE(Op.ADDRESS)) + + Op.SSTORE(2, Op.SHA3(0, Op.EXTCODESIZE(Op.ADDRESS))) + + Op.STOP + ) + target_code = logic + Op.JUMPDEST * (fork.max_code_size() - len(logic)) + target = pre.deterministic_deploy_contract(deploy_code=target_code) + return target, target_code diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/spec.py b/tests/amsterdam/eip7954_increase_max_contract_size/spec.py new file mode 100644 index 00000000000..7dac2d83625 --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/spec.py @@ -0,0 +1,17 @@ +"""Reference spec for [EIP-7954: Increase Maximum Contract Size](https://eips.ethereum.org/EIPS/eip-7954).""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Reference specification.""" + + git_path: str + version: str + + +ref_spec_7954 = ReferenceSpec( + git_path="EIPS/eip-7954.md", + version="b1f5bf8f70ba9306400f5e13313f781c35acc860", +) diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/test_cases.md b/tests/amsterdam/eip7954_increase_max_contract_size/test_cases.md new file mode 100644 index 00000000000..68d665d0c82 --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_cases.md @@ -0,0 +1,23 @@ +# EIP-7954 Increase Maximum Contract Size Test Cases + +| Function Name | Goal | Setup | Expectation | Status | +|---------------|------|-------|-------------|--------| +| `test_max_code_size` | Enforce new `MAX_CODE_SIZE` boundary for contract creation transactions | Alice deploys contracts with runtime code at the new max and one byte over. | New max: contract deployed. Over max: deployment fails. | ✅ Completed | +| `test_max_code_size_via_create` | Enforce new `MAX_CODE_SIZE` boundary via CREATE/CREATE2 opcodes | Same as above but deployment is done through a factory contract using CREATE and CREATE2. | New max: child contract deployed. Over max: child contract does not exist. | ✅ Completed | +| `test_max_initcode_size` | Enforce new `MAX_INITCODE_SIZE` boundary for contract creation transactions | Alice sends creation transactions with initcode at the new max and one byte over. | New max: transaction accepted, contract deployed. Over max: transaction rejected. | ✅ Completed | +| `test_max_initcode_size_via_create` | Enforce new `MAX_INITCODE_SIZE` boundary via CREATE/CREATE2 opcodes | Same as above but initcode is passed through a factory contract using CREATE and CREATE2. | New max: child contract deployed. Over max: CREATE returns 0, child contract does not exist. | ✅ Completed | +| `test_max_initcode_size_gas_metering` | Verify initcode gas metering at the new max (transaction level) | Alice sends a creation transaction with max-size initcode. Gas limit set to exact intrinsic cost, then one short. | Exact gas: contract deployed. One short: transaction rejected. | ✅ Completed | +| `test_max_initcode_size_gas_metering_via_create` | Verify initcode gas metering at the new max (opcode level) | Caller forwards computed exact gas to a factory that runs CREATE with max-size initcode. Tested with exact gas and one short. | Exact gas: CREATE succeeds, contract deployed. One short: factory runs out of gas. | ✅ Completed | +| `test_max_code_size_deposit_gas` | Verify code deposit gas is charged correctly at the new max | Alice deploys a contract with exactly `MAX_CODE_SIZE` bytes. Gas set to exact deposit cost, then one short. | Exact gas: contract deployed. One short: deployment fails (out of gas during code deposit). | ✅ Completed | +| `test_max_code_size_external_opcodes` | Verify external code opcodes work with max-size contracts | Deterministically pre-deploy a max-size self-checking contract. Call it to run EXTCODESIZE, EXTCODEHASH, and EXTCODECOPY on itself via ADDRESS. | Each opcode returns the correct value for the max-size contract. | ✅ Completed | +| `test_max_code_size_self_opcodes` | Verify self code opcodes work with max-size contracts | Pre-deploy a max-size contract with CODESIZE and CODECOPY checker logic. Call via DELEGATECALL so opcodes operate on the large contract's own code. | CODESIZE returns the correct length, CODECOPY produces the correct hash. | ✅ Completed | +| `test_max_code_size_with_max_initcode` | Deploy max-size code when initcode is also at max size | Alice deploys a contract with `MAX_CODE_SIZE` bytes of runtime code using initcode padded to `MAX_INITCODE_SIZE`. | Contract deployed with the full max-size runtime code. | ✅ Completed | +| `test_max_code_size_fork_transition` | New `MAX_CODE_SIZE` activates exactly at the fork boundary | Before the fork, deploy a contract with the new `MAX_CODE_SIZE` bytes of runtime code. After the fork, attempt the same deployment. | Pre-fork: deployment fails (exceeds old limit). Post-fork: deployment succeeds. | ✅ Completed | +| `test_max_code_size_via_create_fork_transition` | New `MAX_CODE_SIZE` activates at the fork boundary via CREATE/CREATE2 opcodes | Same as above but deployment is done through a factory contract using CREATE and CREATE2. | Pre-fork: child contract does not exist. Post-fork: child contract deployed. | ✅ Completed | +| `test_max_initcode_size_fork_transition` | New `MAX_INITCODE_SIZE` activates exactly at the fork boundary for transactions | Before the fork, send a creation transaction with the new `MAX_INITCODE_SIZE` bytes of initcode. After the fork, send the same transaction. | Pre-fork: block rejected (initcode exceeds old limit). Post-fork: transaction accepted, contract deployed. | ✅ Completed | +| `test_max_initcode_size_via_create_fork_transition` | New `MAX_INITCODE_SIZE` activates at the fork boundary via CREATE/CREATE2 opcodes | Same as above but initcode is passed through a factory contract using CREATE and CREATE2. | Pre-fork: CREATE fails (initcode exceeds old limit). Post-fork: child contract deployed. | ✅ Completed | +| `test_max_code_size_with_max_initcode_fork_transition` | Both new limits activate together at the fork boundary | Before the fork, deploy max code with max initcode. After the fork, attempt the same deployment. | Pre-fork: block rejected (initcode exceeds old limit). Post-fork: contract deployed with max-size runtime code. | ✅ Completed | +| `test_parent_max_code_size_across_fork` | Old `MAX_CODE_SIZE` still works on both sides of the transition | Before and after the fork, deploy a contract with the old `MAX_CODE_SIZE` bytes of runtime code. | Both deployments succeed. The old limit remains valid after the fork. | ✅ Completed | +| `test_over_max_code_size_mainnet` | Deploying above the new limit fails on mainnet | Alice deploys a contract with `MAX_CODE_SIZE + 1` bytes of runtime code. | Contract does not exist (deployment fails during code deposit). | ✅ Completed | +| `test_over_max_initcode_size_mainnet` | Oversized initcode creation is rejected on mainnet | Alice sends a creation transaction with `MAX_INITCODE_SIZE + 1` bytes of initcode. | Transaction rejected. No contract deployed. | ✅ Completed | +| `test_max_code_size_with_max_initcode_mainnet` | Verify opcodes on a max-size contract on mainnet | Call the deterministic max-size self-checking contract. It queries EXTCODESIZE, EXTCODEHASH, and EXTCODECOPY on itself via ADDRESS. | Each opcode returns the correct value for the max-size contract. | ✅ Completed | diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/test_eip_mainnet.py b/tests/amsterdam/eip7954_increase_max_contract_size/test_eip_mainnet.py new file mode 100644 index 00000000000..7d0ff0e0a36 --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_eip_mainnet.py @@ -0,0 +1,119 @@ +""" +Mainnet tests for +[EIP-7954: Increase Maximum Contract Size](https://eips.ethereum.org/EIPS/eip-7954). +""" + +from typing import Any + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + Initcode, + Op, + StateTestFiller, + Transaction, + TransactionException, + compute_create_address, + keccak256, +) + +from .spec import ref_spec_7954 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7954.git_path +REFERENCE_SPEC_VERSION = ref_spec_7954.version + +pytestmark = [pytest.mark.valid_at("EIP7954"), pytest.mark.mainnet] + + +def test_over_max_code_size_mainnet( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """Verify deployment above the new limit is rejected on mainnet.""" + deploy_code = Op.JUMPDEST * (fork.max_code_size() + 1) + initcode = Initcode(deploy_code=deploy_code) + + alice = pre.fund_eoa() + create_address = compute_create_address(address=alice, nonce=0) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + post: dict[Any, Account | None] = { + create_address: Account.NONEXISTENT, + } + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.exception_test +def test_over_max_initcode_size_mainnet( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """Verify a CREATE transaction over the new initcode limit is rejected.""" + initcode = Initcode( + deploy_code=Op.STOP, + initcode_length=fork.max_initcode_size() + 1, + ) + + alice = pre.fund_eoa() + create_address = compute_create_address(address=alice, nonce=0) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transaction_gas_limit_cap(), + error=TransactionException.INITCODE_SIZE_EXCEEDED, + ) + + post: dict[Any, Account | None] = { + create_address: Account.NONEXISTENT, + } + + state_test(pre=pre, tx=tx, post=post) + + +def test_max_code_size_with_max_initcode_mainnet( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + max_code_size_contract: tuple, +) -> None: + """ + Verify max-size contract works on mainnet. + + Calls the deterministic max-size contract which checks EXTCODESIZE, + EXTCODEHASH, and EXTCODECOPY on itself. The contract bytecode is + the same used for deployment tests, padded to max code size. + """ + target, target_code = max_code_size_contract + + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=target, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + post = { + target: Account( + storage={ + 0: len(target_code), + 1: keccak256(bytes(target_code)), + 2: keccak256(bytes(target_code)), + } + ) + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/test_fork_transition.py b/tests/amsterdam/eip7954_increase_max_contract_size/test_fork_transition.py new file mode 100644 index 00000000000..4000ed7df4a --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_fork_transition.py @@ -0,0 +1,410 @@ +""" +Fork transition tests for +[EIP-7954: Increase Maximum Contract Size](https://eips.ethereum.org/EIPS/eip-7954). + +Tests that the new max code size and initcode size limits activate +exactly at the EIP7954 fork boundary (timestamp 15,000). +""" + +from typing import Any + +import pytest +from execution_testing import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Initcode, + Op, + Transaction, + TransactionException, + TransitionFork, + compute_create_address, +) + +from .spec import ref_spec_7954 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7954.git_path +REFERENCE_SPEC_VERSION = ref_spec_7954.version + +pytestmark = pytest.mark.valid_at_transition_to("EIP7954") + +CREATE2_SALT = 0xC0FFEE + + +def test_max_code_size_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, +) -> None: + """Ensure the new max code size limit activates at the fork boundary.""" + code_size = fork.transitions_to().max_code_size() + deploy_code = Op.JUMPDEST * code_size + initcode = Initcode(deploy_code=deploy_code) + + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + create_address_pre = compute_create_address(address=alice, nonce=0) + create_address_post = compute_create_address(address=bob, nonce=0) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transitions_from().transaction_gas_limit_cap(), + ) + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + sender=bob, + to=None, + data=initcode, + gas_limit=fork.transitions_to().transaction_gas_limit_cap(), + ) + ], + ), + ] + + post: dict[Any, Account | None] = { + create_address_pre: Account.NONEXISTENT, + create_address_post: Account(code=deploy_code), + } + + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2]) +def test_max_code_size_via_create_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, + create_opcode: Op, +) -> None: + """Ensure the new max code size limit activates at the fork via opcodes.""" + code_size = fork.transitions_to().max_code_size() + deploy_code = Op.JUMPDEST * code_size + initcode = Initcode(deploy_code=deploy_code) + initcode_bytes = bytes(initcode) + + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + create_call = ( + create_opcode( + value=0, offset=0, size=Op.CALLDATASIZE, salt=CREATE2_SALT + ) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=0, size=Op.CALLDATASIZE) + ) + + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(0, create_call) + + Op.STOP + ) + + factory_pre = pre.deploy_contract(factory_code) + factory_post = pre.deploy_contract(factory_code) + + create_address_pre = compute_create_address( + address=factory_pre, + nonce=1, + salt=CREATE2_SALT, + initcode=initcode, + opcode=create_opcode, + ) + create_address_post = compute_create_address( + address=factory_post, + nonce=1, + salt=CREATE2_SALT, + initcode=initcode, + opcode=create_opcode, + ) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + sender=alice, + to=factory_pre, + data=initcode_bytes, + gas_limit=fork.transitions_from().transaction_gas_limit_cap(), + ) + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + sender=bob, + to=factory_post, + data=initcode_bytes, + gas_limit=fork.transitions_to().transaction_gas_limit_cap(), + ) + ], + ), + ] + + post: dict[Any, Account | None] = { + create_address_pre: Account.NONEXISTENT, + create_address_post: Account(code=deploy_code), + } + + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@pytest.mark.exception_test +def test_max_initcode_size_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, +) -> None: + """Ensure the new max initcode size limit activates exactly at the fork.""" + initcode = Initcode( + deploy_code=Op.STOP, + initcode_length=fork.transitions_to().max_initcode_size(), + ) + + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + create_address_post = compute_create_address(address=bob, nonce=0) + + initcode_too_large = TransactionException.INITCODE_SIZE_EXCEEDED + + blocks = [ + # Pre-fork: initcode at the new max exceeds the parent fork's limit, + # so the tx is rejected and the block is invalid. + Block( + timestamp=14_999, + txs=[ + Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transitions_from().transaction_gas_limit_cap(), + error=initcode_too_large, + ) + ], + exception=initcode_too_large, + ), + # Post-fork: the new limit is in effect, tx succeeds. + Block( + timestamp=15_000, + txs=[ + Transaction( + sender=bob, + to=None, + data=initcode, + gas_limit=fork.transitions_to().transaction_gas_limit_cap(), + ) + ], + ), + ] + + post: dict[Any, Account | None] = { + create_address_post: Account(code=Op.STOP), + } + + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2]) +def test_max_initcode_size_via_create_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, + create_opcode: Op, +) -> None: + """Ensure the new max initcode size limit activates at fork via opcodes.""" + initcode = Initcode( + deploy_code=Op.STOP, + initcode_length=fork.transitions_to().max_initcode_size(), + ) + initcode_bytes = bytes(initcode) + + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + create_call = ( + create_opcode( + value=0, offset=0, size=Op.CALLDATASIZE, salt=CREATE2_SALT + ) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=0, size=Op.CALLDATASIZE) + ) + + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(0, create_call) + + Op.STOP + ) + + factory_pre = pre.deploy_contract(factory_code) + factory_post = pre.deploy_contract(factory_code) + + create_address_pre = compute_create_address( + address=factory_pre, + nonce=1, + salt=CREATE2_SALT, + initcode=initcode, + opcode=create_opcode, + ) + create_address_post = compute_create_address( + address=factory_post, + nonce=1, + salt=CREATE2_SALT, + initcode=initcode, + opcode=create_opcode, + ) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + sender=alice, + to=factory_pre, + data=initcode_bytes, + gas_limit=fork.transitions_from().transaction_gas_limit_cap(), + ) + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + sender=bob, + to=factory_post, + data=initcode_bytes, + gas_limit=fork.transitions_to().transaction_gas_limit_cap(), + ) + ], + ), + ] + + # Pre-fork: CREATE returns 0 (initcode exceeds parent fork limit) + # Post-fork: CREATE succeeds + post: dict[Any, Account | None] = { + factory_pre: Account(storage={0: 0}), + create_address_pre: Account.NONEXISTENT, + factory_post: Account(storage={0: create_address_post}), + create_address_post: Account(code=Op.STOP), + } + + blockchain_test(pre=pre, blocks=blocks, post=post) + + +@pytest.mark.exception_test +def test_max_code_size_with_max_initcode_fork_transition( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, +) -> None: + """Ensure max code + max initcode activates at the fork boundary.""" + deploy_code = Op.JUMPDEST * fork.transitions_to().max_code_size() + initcode = Initcode( + deploy_code=deploy_code, + initcode_length=fork.transitions_to().max_initcode_size(), + ) + + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + create_address_post = compute_create_address(address=bob, nonce=0) + + initcode_too_large = TransactionException.INITCODE_SIZE_EXCEEDED + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transitions_from().transaction_gas_limit_cap(), + error=initcode_too_large, + ) + ], + exception=initcode_too_large, + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + sender=bob, + to=None, + data=initcode, + gas_limit=fork.transitions_to().transaction_gas_limit_cap(), + ) + ], + ), + ] + + post: dict[Any, Account | None] = { + create_address_post: Account(code=deploy_code), + } + + blockchain_test(pre=pre, blocks=blocks, post=post) + + +def test_parent_max_code_size_across_fork( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: TransitionFork, +) -> None: + """Ensure previous max code size works after transition.""" + parent = fork.transitions_from() + assert parent is not None, "Parent fork must be defined for this test" + + code_size = parent.max_code_size() + deploy_code = Op.JUMPDEST * code_size + initcode = Initcode(deploy_code=deploy_code) + + alice = pre.fund_eoa() + bob = pre.fund_eoa() + + create_address_pre = compute_create_address(address=alice, nonce=0) + create_address_post = compute_create_address(address=bob, nonce=0) + + blocks = [ + Block( + timestamp=14_999, + txs=[ + Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transitions_from().transaction_gas_limit_cap(), + ) + ], + ), + Block( + timestamp=15_000, + txs=[ + Transaction( + sender=bob, + to=None, + data=initcode, + gas_limit=fork.transitions_to().transaction_gas_limit_cap(), + ) + ], + ), + ] + + post: dict[Any, Account | None] = { + create_address_pre: Account(code=deploy_code), + create_address_post: Account(code=deploy_code), + } + + blockchain_test(pre=pre, blocks=blocks, post=post) 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 new file mode 100644 index 00000000000..0eb46b61ae4 --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_code_size.py @@ -0,0 +1,272 @@ +""" +Test [EIP-7954: Increase Maximum Contract Size](https://eips.ethereum.org/EIPS/eip-7954). +""" + +from typing import Any, Callable + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + Initcode, + Op, + StateTestFiller, + Transaction, + compute_create_address, + keccak256, +) + +from .spec import ref_spec_7954 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7954.git_path +REFERENCE_SPEC_VERSION = ref_spec_7954.version + +pytestmark = pytest.mark.valid_from("EIP7954") + +CREATE2_SALT = 0xC0FFEE + +DEPLOY_CODE_SIZE_PARAMS = [ + pytest.param(lambda f: f.max_code_size(), id="at_max"), + pytest.param(lambda f: f.max_code_size() + 1, id="over_max"), +] + + +@pytest.mark.parametrize("deploy_code_size", DEPLOY_CODE_SIZE_PARAMS) +def test_max_code_size( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + deploy_code_size: Callable[[Fork], int], +) -> None: + """Ensure the new max code size boundary is enforced.""" + code_size = deploy_code_size(fork) + deploy_code = Op.JUMPDEST * code_size + + alice = pre.fund_eoa() + initcode = Initcode(deploy_code=deploy_code) + create_address = compute_create_address(address=alice, nonce=0) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + post: dict[Any, Account | None] = {} + if code_size <= fork.max_code_size(): + post[create_address] = Account(code=deploy_code) + else: + post[create_address] = Account.NONEXISTENT + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize("deploy_code_size", DEPLOY_CODE_SIZE_PARAMS) +@pytest.mark.with_all_create_opcodes() +def test_max_code_size_via_create( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + deploy_code_size: Callable[[Fork], int], + create_opcode: Op, +) -> None: + """Ensure the new max code size boundary is enforced via create opcodes.""" + code_size = deploy_code_size(fork) + deploy_code = Op.JUMPDEST * code_size + initcode = Initcode(deploy_code=deploy_code) + initcode_bytes = bytes(initcode) + + alice = pre.fund_eoa() + + create_call = ( + create_opcode( + value=0, offset=0, size=Op.CALLDATASIZE, salt=CREATE2_SALT + ) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=0, size=Op.CALLDATASIZE) + ) + + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(0, create_call) + + Op.STOP + ) + + factory = pre.deploy_contract(factory_code) + + create_address = compute_create_address( + address=factory, + nonce=1, + salt=CREATE2_SALT, + initcode=initcode, + opcode=create_opcode, + ) + + tx = Transaction( + sender=alice, + to=factory, + data=initcode_bytes, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + created = code_size <= fork.max_code_size() + post: dict[Any, Account | None] = { + factory: Account(storage={0: create_address if created else 0}), + } + if created: + post[create_address] = Account(code=deploy_code) + else: + post[create_address] = Account.NONEXISTENT + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "gas_shortfall", + [ + pytest.param(0, id="exact_gas"), + pytest.param(1, id="short_one_gas"), + ], +) +def test_max_code_size_deposit_gas( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + gas_shortfall: int, +) -> None: + """Ensure code deposit gas is charged correctly at the new max.""" + deploy_code = Op.JUMPDEST * fork.max_code_size() + initcode = Initcode(deploy_code=deploy_code) + + alice = pre.fund_eoa() + create_address = compute_create_address(address=alice, nonce=0) + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + return_cost_deducted_prior_execution=True, + ) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=( + intrinsic_gas + + initcode.execution_gas(fork) + + initcode.deployment_gas(fork) + - gas_shortfall + ), + ) + # With shortfall, code deposit OOGs: tx succeeds but + # contract is not deployed + post = { + create_address: Account(code=deploy_code) + if not gas_shortfall + else Account.NONEXISTENT, + } + + state_test(pre=pre, tx=tx, post=post) + + +def test_max_code_size_with_max_initcode( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """Ensure max-size code deploys when initcode is also at max size.""" + deploy_code = Op.JUMPDEST * fork.max_code_size() + initcode = Initcode( + deploy_code=deploy_code, + initcode_length=fork.max_initcode_size(), + ) + + alice = pre.fund_eoa() + create_address = compute_create_address(address=alice, nonce=0) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + post = {create_address: Account(code=deploy_code)} + + state_test(pre=pre, tx=tx, post=post) + + +def test_max_code_size_external_opcodes( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + max_code_size_contract: tuple, +) -> None: + """Ensure external code opcodes work with the new max contract size.""" + target, target_code = max_code_size_contract + + alice = pre.fund_eoa() + + tx = Transaction( + sender=alice, + to=target, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + post = { + target: Account( + storage={ + 0: len(target_code), + 1: keccak256(bytes(target_code)), + 2: keccak256(bytes(target_code)), + } + ) + } + + state_test(pre=pre, tx=tx, post=post) + + +def test_max_code_size_self_opcodes( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, +) -> None: + """ + Ensure self code opcodes work with the new max contract size. + + Tested via DELEGATECALL so opcodes operate on the large + contract's own code while writing results to the caller's + storage. + """ + logic = ( + Op.SSTORE(0, Op.CODESIZE) + + Op.CODECOPY(0, 0, Op.CODESIZE) + + Op.SSTORE(1, Op.SHA3(0, Op.CODESIZE)) + + Op.STOP + ) + target_code = logic + Op.JUMPDEST * (fork.max_code_size() - len(logic)) + target = pre.deterministic_deploy_contract(deploy_code=target_code) + + alice = pre.fund_eoa() + oracle = pre.deploy_contract( + code=Op.DELEGATECALL(gas=Op.GAS, address=target) + ) + + tx = Transaction( + sender=alice, + to=oracle, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + post = { + oracle: Account( + storage={ + 0: len(target_code), + 1: keccak256(bytes(target_code)), + } + ) + } + + state_test(pre=pre, tx=tx, post=post) 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 new file mode 100644 index 00000000000..f3a469c43a5 --- /dev/null +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_max_initcode_size.py @@ -0,0 +1,187 @@ +""" +Test [EIP-7954: Increase Maximum Contract Size](https://eips.ethereum.org/EIPS/eip-7954). + +Tests for the increased maximum initcode size (64 KiB). +""" + +from typing import Any, Callable + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + Initcode, + Op, + StateTestFiller, + Transaction, + TransactionException, + compute_create_address, +) + +from .spec import ref_spec_7954 + +REFERENCE_SPEC_GIT_PATH = ref_spec_7954.git_path +REFERENCE_SPEC_VERSION = ref_spec_7954.version + +pytestmark = pytest.mark.valid_from("EIP7954") + +CREATE2_SALT = 0xC0FFEE + +INITCODE_SIZE_PARAMS = [ + pytest.param(lambda f: f.max_initcode_size(), id="at_max"), + pytest.param(lambda f: f.max_initcode_size() + 1, id="over_max"), +] + +TX_INITCODE_SIZE_PARAMS = [ + pytest.param(lambda f: f.max_initcode_size(), id="at_max"), + pytest.param( + lambda f: f.max_initcode_size() + 1, + id="over_max", + marks=pytest.mark.exception_test, + ), +] + + +@pytest.mark.parametrize("initcode_size", TX_INITCODE_SIZE_PARAMS) +def test_max_initcode_size( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + initcode_size: Callable[[Fork], int], +) -> None: + """Ensure the new max initcode size is enforced for transactions.""" + size = initcode_size(fork) + initcode = Initcode( + deploy_code=Op.STOP, + initcode_length=size, + ) + + alice = pre.fund_eoa() + create_address = compute_create_address(address=alice, nonce=0) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + post: dict[Any, Account | None] = {} + if size <= fork.max_initcode_size(): + post[create_address] = Account(code=Op.STOP) + else: + tx.error = TransactionException.INITCODE_SIZE_EXCEEDED + post[create_address] = Account.NONEXISTENT + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize("initcode_size", INITCODE_SIZE_PARAMS) +@pytest.mark.with_all_create_opcodes() +def test_max_initcode_size_via_create( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + initcode_size: Callable[[Fork], int], + create_opcode: Op, +) -> None: + """Ensure the new max initcode size is enforced via create opcodes.""" + size = initcode_size(fork) + initcode = Initcode( + deploy_code=Op.STOP, + initcode_length=size, + ) + initcode_bytes = bytes(initcode) + + alice = pre.fund_eoa() + + create_call = ( + create_opcode( + value=0, offset=0, size=Op.CALLDATASIZE, salt=CREATE2_SALT + ) + if create_opcode == Op.CREATE2 + else create_opcode(value=0, offset=0, size=Op.CALLDATASIZE) + ) + + factory_code = ( + Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE) + + Op.SSTORE(0, create_call) + + Op.STOP + ) + + factory = pre.deploy_contract(factory_code) + + create_address = compute_create_address( + address=factory, + nonce=1, + salt=CREATE2_SALT, + initcode=initcode, + opcode=create_opcode, + ) + + tx = Transaction( + sender=alice, + to=factory, + data=initcode_bytes, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + # Opcode-level: oversized initcode causes OutOfGasError + # (tx succeeds, CREATE returns 0) + created = size <= fork.max_initcode_size() + post: dict[Any, Account | None] = { + factory: Account(storage={0: create_address if created else 0}), + } + if created: + post[create_address] = Account(code=Op.STOP) + else: + post[create_address] = Account.NONEXISTENT + + state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "gas_shortfall", + [ + pytest.param(0, id="exact_gas"), + pytest.param( + 1, + id="short_one_gas", + marks=pytest.mark.exception_test, + ), + ], +) +def test_max_initcode_size_gas_metering( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + gas_shortfall: int, +) -> None: + """Verify initcode gas metering at the new max initcode size.""" + initcode = Initcode( + deploy_code=Op.STOP, initcode_length=fork.max_initcode_size() + ) + alice = pre.fund_eoa() + + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, contract_creation=True + ) + + tx = Transaction( + sender=alice, + to=None, + data=initcode, + gas_limit=intrinsic_gas - gas_shortfall, + error=TransactionException.INTRINSIC_GAS_TOO_LOW + if gas_shortfall + else None, + ) + + post = { + compute_create_address(address=alice, nonce=0): Account.NONEXISTENT + if gas_shortfall + else Account(code=Op.STOP), + } + + state_test(pre=pre, tx=tx, post=post) diff --git a/tests/shanghai/eip3860_initcode/test_initcode.py b/tests/shanghai/eip3860_initcode/test_initcode.py index 08e56b48c48..c3196ebb128 100644 --- a/tests/shanghai/eip3860_initcode/test_initcode.py +++ b/tests/shanghai/eip3860_initcode/test_initcode.py @@ -238,7 +238,7 @@ def tx_access_list(self) -> List[AccessList]: """ return [ AccessList(address=Address(i), storage_keys=[]) - for i in range(1, 478) + for i in range(1, 642) ] @pytest.fixture