From c2139c0c229e2df46b6cb24764d7caaa98b18605 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Tue, 16 Jun 2026 17:15:27 +0200 Subject: [PATCH 1/3] feat(tests): add EIP-7954 jumpdest test past the old code-size limit EIP-7954 raises `MAX_CODE_SIZE` to 64 KiB and the initcode limit to 128 KiB. Before this fork, deployed runtime code could never exceed 24 KiB, so no existing test ever executes a contract at a program counter past the old limit. The EIP-7954 max-size contracts are either deploy-only or run a short prefix that `STOP`s before their `JUMPDEST` padding, and `EXTCODECOPY`/`CODECOPY` only read the code rather than run it. A client whose jumpdest analysis or code execution is still bounded by the old limit would pass the existing suite yet break on a real 64 KiB contract. `test_max_code_size_high_jumpdest` deploys a `MAX_CODE_SIZE` contract and jumps near the new limit, far beyond the old 24 KiB code and 48 KiB initcode limits: - Valid `JUMPDEST`: The jump succeeds and the code runs at the high offset, storing a sentinel. - Non-`JUMPDEST` byte: The jump is rejected and nothing is stored. The contract is called through a caller that records the call result, so a correct client is told apart from one that truncated its jumpdest analysis (the valid jump fails) or accepts any high offset (the invalid jump wrongly succeeds). --- .../test_cases.md | 1 + .../test_max_code_size.py | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+) 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..ed0b46e0177 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,69 @@ 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 whose jump target sits 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 and the contract executes at the high offset (stores 1). + - ``invalid_high_dest``: the target byte is a ``PUSH1`` (not a + ``JUMPDEST``), so the jump must be rejected and nothing is stored. + + 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: + # Lands on a JUMPDEST and stores a sentinel at the high offset. + tail = Op.JUMPDEST + Op.SSTORE(0, 1) + Op.STOP + else: + # First byte is PUSH1 (0x60), not a JUMPDEST: jumping here is invalid. + # A client that wrongly accepts it would execute this and store 2. + tail = Op.SSTORE(0, 2) + 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 = 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, + gas_limit=fork.transaction_gas_limit_cap(), + ) + + # Valid: call succeeds (1) and the high JUMPDEST stores 1. + # Invalid: call fails (0) on the rejected jump 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) From edf07a2ef6c16578296bd318b7e026783e125ad1 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Wed, 17 Jun 2026 11:39:28 +0200 Subject: [PATCH 2/3] refactor(tests): rely on automatic transaction gas-limit The high jumpdest test is not gas sensitive, so drop the explicit `gas_limit=fork.transaction_gas_limit_cap()` and let the framework fill the transaction gas limit automatically. Co-authored-by: marioevz <11726710+marioevz@users.noreply.github.com> --- .../test_max_code_size.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 ed0b46e0177..123b440e76b 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 @@ -400,11 +400,7 @@ def test_max_code_size_high_jumpdest( Op.SSTORE(0, Op.CALL(gas=Op.GAS, address=target)) + Op.STOP ) - tx = Transaction( - sender=pre.fund_eoa(), - to=caller, - gas_limit=fork.transaction_gas_limit_cap(), - ) + tx = Transaction(sender=pre.fund_eoa(), to=caller) # Valid: call succeeds (1) and the high JUMPDEST stores 1. # Invalid: call fails (0) on the rejected jump and nothing is stored. From a9b7ed77df3fc3e8f878ede945cac0b9841021bb Mon Sep 17 00:00:00 2001 From: danceratopz Date: Wed, 17 Jun 2026 11:40:45 +0200 Subject: [PATCH 3/3] refactor(tests): simplify the high jumpdest target code Store the sentinel in the prefix, before the `JUMP`, so the jump target is a bare `JUMPDEST` (valid) or `STOP` (invalid) with no trailing code: - Valid: The jump completes, the frame returns, and the store is kept. - Invalid: The jump is rejected, the frame reverts, and the store is discarded. Also fix the stale comment: with a bare `STOP` target, a client that wrongly accepts the jump halts normally and keeps the prefix store, so it stores `1`, not `2`. Co-authored-by: marioevz <11726710+marioevz@users.noreply.github.com> --- .../test_max_code_size.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) 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 123b440e76b..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 @@ -367,31 +367,31 @@ def test_max_code_size_high_jumpdest( """ Ensure jump destination validity is enforced past the old size limits. - Deploy a `MAX_CODE_SIZE` contract whose jump target sits 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: + 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 and the contract executes at the high offset (stores 1). - - ``invalid_high_dest``: the target byte is a ``PUSH1`` (not a - ``JUMPDEST``), so the jump must be rejected and nothing is stored. + 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: - # Lands on a JUMPDEST and stores a sentinel at the high offset. - tail = Op.JUMPDEST + Op.SSTORE(0, 1) + Op.STOP + tail = Op.JUMPDEST else: - # First byte is PUSH1 (0x60), not a JUMPDEST: jumping here is invalid. - # A client that wrongly accepts it would execute this and store 2. - tail = Op.SSTORE(0, 2) + Op.STOP + # 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 = push_op(dest) + Op.JUMP + 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() @@ -402,8 +402,8 @@ def test_max_code_size_high_jumpdest( tx = Transaction(sender=pre.fund_eoa(), to=caller) - # Valid: call succeeds (1) and the high JUMPDEST stores 1. - # Invalid: call fails (0) on the rejected jump and nothing is stored. + # 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}),