diff --git a/tests/amsterdam/eip7954_increase_max_contract_size/test_cases.md b/tests/amsterdam/eip7954_increase_max_contract_size/test_cases.md index 27ab07655c5..bb6c676639b 100644 --- a/tests/amsterdam/eip7954_increase_max_contract_size/test_cases.md +++ b/tests/amsterdam/eip7954_increase_max_contract_size/test_cases.md @@ -10,6 +10,7 @@ | `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_high_jumpdest` | Enforce JUMP destination validity and code execution past the old size limits | Deploy a `MAX_CODE_SIZE` contract that jumps near the new limit (far beyond the old 24 KiB code and 48 KiB initcode limits), to a real `JUMPDEST` or to a `PUSH1` byte, and call it via a caller that records the call result. | Valid `JUMPDEST`: jump succeeds, the contract executes at the high offset and stores a sentinel. Non-`JUMPDEST`: jump is rejected, call fails, nothing is stored. | ✅ 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_warm_after_failed_create_over_max_code_size` | A failed CREATE/CREATE2 over max code size leaves the would-be address warm | A creator runs CREATE/CREATE2 whose initcode returns `MAX_CODE_SIZE + 1` bytes; a checker then measures the gas of a `BALANCE` on that address. | The address is warm: the post-RETURN size-check rejection still leaves it in the access list. | ✅ Completed | | `test_max_code_size_fork_transition` | New `MAX_CODE_SIZE` activates exactly at the fork boundary | Before and after the fork, deploy a contract one byte over the parent fork's max code size (valid under the new limit; its initcode stays within both forks' initcode limits). | Pre-fork: deployment fails at code deposit (exceeds old limit). Post-fork: deployment succeeds. | ✅ Completed | 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 44bcc33e84b..c1a2ff5f13f 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 @@ -349,3 +349,65 @@ def test_warm_after_failed_create_over_max_code_size( } state_test(pre=pre, tx=tx, post=post) + + +@pytest.mark.parametrize( + "valid_jumpdest", + [ + pytest.param(True, id="valid_high_jumpdest"), + pytest.param(False, id="invalid_high_dest"), + ], +) +def test_max_code_size_high_jumpdest( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + valid_jumpdest: bool, +) -> None: + """ + Ensure jump destination validity is enforced past the old size limits. + + Deploy a `MAX_CODE_SIZE` contract that stores a sentinel and then jumps + near the new limit, far beyond the old 24 KiB code and 48 KiB initcode + limits, then call it through a caller that records the call's success: + + - ``valid_high_jumpdest``: the target byte is a real ``JUMPDEST``, so the + jump succeeds, the frame returns, and the sentinel store is kept. + - ``invalid_high_dest``: the target byte is a ``STOP`` (not a + ``JUMPDEST``), so the jump is rejected, the frame reverts, and the + sentinel store is discarded. + + A client whose jumpdest analysis or code execution does not cover the + full new code range fails one of the two cases. No existing test + executes a contract at a program counter beyond the old limit. + """ + if valid_jumpdest: + tail = Op.JUMPDEST + else: + # A bare STOP, not a JUMPDEST: jumping here is invalid. A client that + # wrongly accepts it halts normally and keeps the prefix store (1). + tail = Op.STOP + + dest = fork.max_code_size() - len(tail) + push_size = (dest.bit_length() + 7) // 8 + push_op = getattr(Op, f"PUSH{push_size}") + prefix = Op.SSTORE(0, 1) + push_op(dest) + Op.JUMP + target_code = prefix + Op.INVALID * (dest - len(prefix)) + tail + assert len(target_code) == fork.max_code_size() + + target = pre.deploy_contract(target_code) + caller = pre.deploy_contract( + Op.SSTORE(0, Op.CALL(gas=Op.GAS, address=target)) + Op.STOP + ) + + tx = Transaction(sender=pre.fund_eoa(), to=caller) + + # Valid: jump completes, call succeeds (1), and the store is kept. + # Invalid: jump reverts, call fails (0), and nothing is stored. + stored = 1 if valid_jumpdest else 0 + post = { + caller: Account(storage={0: stored}), + target: Account(storage={0: stored}), + } + + state_test(pre=pre, tx=tx, post=post)