From 80489401b3f9dc4b5557f14b3e6ce811139e73ec Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 12:55:01 +0200 Subject: [PATCH 01/25] fix(tests): un-skip Amsterdam gas_cost SSTORE case via fork-derived cost --- tests/ported_static/amsterdam_skip_list.txt | 6 +--- .../test_gas_cost.py | 29 +++++++++++++++++-- .../test_gas_cost_berlin.py | 21 ++++++++++++-- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 7aba6f38bb0..f6bcee6f9f5 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 153 +# Total entries: 151 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -126,10 +126,6 @@ stEIP150Specific/test_transaction64_rule_d64e0.py::test_transaction64_rule_d64e0 stEIP150Specific/test_transaction64_rule_d64m1.py::test_transaction64_rule_d64m1[fork_Amsterdam] stEIP150Specific/test_transaction64_rule_d64p1.py::test_transaction64_rule_d64p1[fork_Amsterdam] -# stEIP150singleCodeGasPrices (2) -stEIP150singleCodeGasPrices/test_gas_cost.py::test_gas_cost[fork_Amsterdam-d40] -stEIP150singleCodeGasPrices/test_gas_cost_berlin.py::test_gas_cost_berlin[fork_Amsterdam-d40] - # stEIP158Specific (1) stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py index d84fd092041..f1d08b2f849 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost.py @@ -1,11 +1,15 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the gas cost of each opcode via a crafted one-opcode contract +(by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stEIP150singleCodeGasPrices/gasCostFiller.yml @manually-enhanced: Do not overwrite. This crafts a one-opcode contract, CALLs it, and stores the opcode's measured gas via `Op.GAS`. +The SSTORE case (d40) derives its cost from the fork — EIP-8037 moves +the bulk into state gas — and the crafted CALL forwards effectively +all gas (0xFFFFFF, same PUSH3 width) so that case cannot OOG. EIP-8038 reprices state access, so four opcodes shift: `BALANCE` and `SELFDESTRUCT` (cold account, `COLD_ACCOUNT_ACCESS` 2600 -> 3000, +400), `EXTCODESIZE` (cold account plus the extra `WARM_ACCESS` charged for @@ -734,6 +738,12 @@ def test_gas_cost( code_read_delta = cold_account_delta + ( gas_costs.WARM_ACCESS if fork.is_eip_enabled(8037) else 0 ) + # EIP-8037 moves the bulk of a zero->nonzero SSTORE into state gas; + # the explicit gas limit equals the cap (zero reservoir), so the + # crafted contract's GAS delta observes the full cost. + sstore_new_slot_cost = Op.SSTORE( + key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = EOA( key=0x40AC0FC28C27E961EE46EC43355A094DE205856EDBD4654CF2577C2608D4EC1E @@ -837,7 +847,12 @@ def test_gas_cost( + Op.MSTORE(offset=0x300, value=Op.GAS) + Op.POP( Op.CALL( - gas=0x10000, + # Effectively "all gas": EIP-8037 repriced the SSTORE case + # (d40) past the ported 0x10000 budget, OOGing the callee. + # 0xFFFFFF keeps the same PUSH3 width, so the hand-coded + # JUMP targets and every measurement stay unchanged + # (unused gas returns to the caller). + gas=0xFFFFFF, address=Op.MLOAD(offset=0x280), value=0x0, args_offset=0x0, @@ -1102,10 +1117,18 @@ def test_gas_cost( }, }, { + # SSTORE zero->nonzero to a fresh cold slot. Stored value = + # actual cost minus the 0x4E20 expected-cost operand in the + # data word minus the file-wide 0x258 baseline; 1500 before + # EIP-8037, dominated by state gas after. "indexes": {"data": [40], "gas": -1, "value": -1}, "network": [">=Cancun"], "result": { - addr: Account(storage=_storage_with_any({0: 1500}, [1])) + addr: Account( + storage=_storage_with_any( + {0: sstore_new_slot_cost - 0x4E20 - 0x258}, [1] + ) + ) }, }, { diff --git a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py index 5a92e62ca28..b5f361384c2 100644 --- a/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py +++ b/tests/ported_static/stEIP150singleCodeGasPrices/test_gas_cost_berlin.py @@ -1,5 +1,6 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the gas cost of each opcode via a crafted one-opcode contract +(by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stEIP150singleCodeGasPrices/gasCostBerlinFiller.yml @@ -7,6 +8,9 @@ @manually-enhanced: Do not overwrite. This crafts a one-opcode contract, CALLs it, and stores the opcode's measured gas minus the data's hardcoded Cancun-era expected cost (so the net is normally 0). +The SSTORE case (d40) derives its net from the fork — EIP-8037 moves +the bulk into state gas — and the crafted CALL forwards effectively +all gas (0xFFFFFF, same PUSH3 width) so that case cannot OOG. EIP-8038 reprices state access, so four opcodes now exceed their old expected cost by a fork-derived delta: `BALANCE` and `SELFDESTRUCT` (cold account, `COLD_ACCOUNT_ACCESS` 2600 -> 3000, +400), `EXTCODESIZE` @@ -733,10 +737,18 @@ def test_gas_cost_berlin( # Each measured opcode subtracts its Cancun-era expected cost, so the # net is the (Amsterdam - Cancun) repricing of the one state access # it performs (cold address 0 / cold fresh slot), keyed by data index. + # EIP-8037 moves the bulk of a zero->nonzero SSTORE into state gas; + # the explicit gas limit equals the cap (zero reservoir), so the + # crafted contract's GAS delta observes the full cost. The data word + # encodes the 0x5654 (22100) pre-8037 cost, so the net is 0 there. + sstore_new_slot_cost = Op.SSTORE( + key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) measured_delta = { 23: cold_account_delta, # BALANCE 31: code_read_delta, # EXTCODESIZE 39: cold_storage_delta, # SLOAD + 40: sstore_new_slot_cost - 0x5654, # SSTORE zero->nonzero 45: cold_account_delta, # SELFDESTRUCT }.get(d, 0) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) @@ -839,7 +851,12 @@ def test_gas_cost_berlin( + Op.MSTORE(offset=0x300, value=Op.GAS) + Op.POP( Op.CALL( - gas=0x10000, + # Effectively "all gas": EIP-8037 repriced the SSTORE case + # (d40) past the ported 0x10000 budget, OOGing the callee. + # 0xFFFFFF keeps the same PUSH3 width, so the hand-coded + # JUMP targets and every measurement stay unchanged + # (unused gas returns to the caller). + gas=0xFFFFFF, address=Op.MLOAD(offset=0x280), value=0x0, args_offset=0x0, From 59fda688c66b0550e9eb70a460b8b386983897eb Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:04:39 +0200 Subject: [PATCH 02/25] fix(tests): un-skip Amsterdam stBadOpcode CREATE gas searches via fork-derived thresholds --- tests/ported_static/amsterdam_skip_list.txt | 8 +- .../stBadOpcode/test_measure_gas.py | 77 +++++++++++++------ .../stBadOpcode/test_operation_diff_gas.py | 62 +++++++++++++-- 3 files changed, 112 insertions(+), 35 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index f6bcee6f9f5..cf5ff638048 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,17 +8,11 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 151 +# Total entries: 147 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] -# stBadOpcode (4) -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE] - # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] diff --git a/tests/ported_static/stBadOpcode/test_measure_gas.py b/tests/ported_static/stBadOpcode/test_measure_gas.py index 5f9d16570a6..59832975c4a 100644 --- a/tests/ported_static/stBadOpcode/test_measure_gas.py +++ b/tests/ported_static/stBadOpcode/test_measure_gas.py @@ -1,17 +1,21 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the minimum gas each opcode needs to succeed via a binary +search (by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stBadOpcode/measureGasFiller.yml @manually-enhanced: Do not overwrite. A binary search measures the gas -an opcode needs to succeed. Only the EXTCODE case shifts: it runs a -warm `EXTCODESIZE` plus a warm `EXTCODECOPY` (the target is warmed by -earlier search iterations), and EIP-8038 adds a flat +100 to each warm -extcode access. The stored threshold therefore grows by the sum of the -two opcodes' warm `(Amsterdam - Cancun)` cost deltas, derived from the -fork's own gas model so it is exactly 0 before EIP-8038; do not -hardcode the Amsterdam number. +an opcode needs to succeed. The EXTCODE case runs a warm `EXTCODESIZE` +plus a warm `EXTCODECOPY` (the target is warmed by earlier search +iterations), and EIP-8038 adds a flat +100 to each warm extcode +access; its threshold grows by the two opcodes' warm cost deltas. The +CREATE/CREATE2 thresholds equal the probe bytecode's own +`gas_cost(fork)` (EIP-8037 adds new-account state gas), and the search +bound is supplied via calldata (same 3-byte width as the ported PUSH2 +60000, keeping JUMP targets and the CODESIZE trick intact) so those +cases cannot saturate. All values derive from the fork's own gas +model; do not hardcode them. """ import pytest @@ -240,7 +244,12 @@ def test_measure_gas( # sstore(0, max) # } contract_12 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH2[0xEA60] + # The search's upper bound comes from calldata (word at 0x24): + # EIP-8037's state gas pushes the CREATE/CREATE2 thresholds past + # the ported PUSH2 60000 bound, and CALLDATALOAD keeps the same + # 3-byte width so the hand-coded JUMP targets and the CODESIZE + # constant trick below are unaffected. + code=Op.CALLDATALOAD(offset=0x24) + Op.ADD(Op.CALLDATALOAD(offset=0x4), 0xC0DE00) + Op.PUSH1[0x0] + Op.JUMPDEST @@ -393,16 +402,36 @@ def test_measure_gas( - 103 ) + # The measured threshold for the CREATE/CREATE2 probes is exactly the + # probe bytecode's own cost (operand pushes + opcode); mirroring the + # deployed code in the metadata keeps the expectation fork-derived — + # EIP-8037 adds the new-account state gas and reprices the base. + create_probe_cost = Op.CREATE( + value=Op.DUP1, + offset=0x0, + size=0x200, + new_memory_size=0x200, + init_code_size=0x200, + ).gas_cost(fork) + create2_probe_cost = Op.CREATE2( + value=Op.DUP1, + offset=0x0, + size=0x200, + salt=Op.ADD(0x5A17, Op.GAS), + new_memory_size=0x200, + init_code_size=0x200, + ).gas_cost(fork) + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 32089})}, + "result": {contract_12: Account(storage={0: create_probe_cost})}, }, { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 32193})}, + "result": {contract_12: Account(storage={0: create2_probe_cost})}, }, { "indexes": {"data": [2, 3], "gas": -1, "value": -1}, @@ -440,18 +469,22 @@ def test_measure_gas( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Second calldata word: the binary search's upper bound. The bisection + # boundary is independent of the starting bound, so one generous value + # (covering EIP-8037's ~216k CREATE threshold) works on every fork. + search_max = Hash(0x100000) tx_data = [ - Bytes("693c6139") + Hash(0xF0), - Bytes("693c6139") + Hash(0xF5), - Bytes("693c6139") + Hash(0xF1), - Bytes("693c6139") + Hash(0xF2), - Bytes("693c6139") + Hash(0xF4), - Bytes("693c6139") + Hash(0xFA), - Bytes("693c6139") + Hash(0x51), - Bytes("693c6139") + Hash(0x52), - Bytes("693c6139") + Hash(0x53), - Bytes("693c6139") + Hash(0x20), - Bytes("693c6139") + Hash(0x3B), + Bytes("693c6139") + Hash(0xF0) + search_max, + Bytes("693c6139") + Hash(0xF5) + search_max, + Bytes("693c6139") + Hash(0xF1) + search_max, + Bytes("693c6139") + Hash(0xF2) + search_max, + Bytes("693c6139") + Hash(0xF4) + search_max, + Bytes("693c6139") + Hash(0xFA) + search_max, + Bytes("693c6139") + Hash(0x51) + search_max, + Bytes("693c6139") + Hash(0x52) + search_max, + Bytes("693c6139") + Hash(0x53) + search_max, + Bytes("693c6139") + Hash(0x20) + search_max, + Bytes("693c6139") + Hash(0x3B) + search_max, ] tx_gas = [16777216] diff --git a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py index bb80de6c8d9..241f8d4a4a5 100644 --- a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py +++ b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py @@ -1,11 +1,17 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the minimum gas each opcode needs to succeed via a linear +search in 100-gas steps (by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stBadOpcode/operationDiffGasFiller.yml @manually-enhanced: Do not overwrite. A search measures the gas an -opcode needs to succeed. Two access classes shift under EIP-8038: the +opcode needs to succeed. The CREATE/CREATE2 thresholds equal the probe +bytecode's own `gas_cost(fork)` rounded up to the search step — +EIP-8037 adds new-account and storage-set state gas — and their search +start is supplied via calldata a few steps below the threshold so the +linear probe loop cannot exhaust the transaction's gas on Amsterdam. +Two access classes also shift under EIP-8038: the CALL-family probes (`CALL`/`CALLCODE`/`DELEGATECALL`/`STATICCALL`) make one cold account access to the callee, repricing by `COLD_ACCOUNT_ACCESS - 2600`; the EXTCODE probe runs a cold @@ -378,6 +384,44 @@ def test_operation_diff_gas( # memory) so only the account-access component varies across forks. gas_costs = fork.gas_costs() cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + # The CREATE/CREATE2 probes wrap the create in a cold zero->nonzero + # SSTORE of the returned address (cost depends only on the + # transition, so new_value=1 stands in for the address). The stored + # threshold is the first search step (multiples of GAS_DIFF) at or + # above the probe bytecode's own fork-derived cost; EIP-8037 adds + # the new-account and storage-set state gas. The search starts a few + # steps below the expected threshold (via calldata) so the linear + # probe loop cannot exhaust the transaction's gas on Amsterdam. + gas_diff = 0x64 + create_probe_cost = Op.SSTORE( + key=0x0, + value=Op.CREATE( + value=Op.DUP1, + offset=0x0, + size=0x200, + new_memory_size=0x200, + init_code_size=0x200, + ), + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + create2_probe_cost = Op.SSTORE( + key=0x0, + value=Op.CREATE2( + value=Op.DUP1, + offset=0x0, + size=0x200, + salt=0x5A17, + new_memory_size=0x200, + init_code_size=0x200, + ), + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + create_threshold = -(-create_probe_cost // gas_diff) * gas_diff + create2_threshold = -(-create2_probe_cost // gas_diff) * gas_diff extcode_probe_delta = ( Op.EXTCODESIZE.with_metadata(address_warm=False).gas_cost(fork) - 2600 ) + ( @@ -394,12 +438,12 @@ def test_operation_diff_gas( { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 54200})}, + "result": {contract_12: Account(storage={0: create_threshold})}, }, { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 54300})}, + "result": {contract_12: Account(storage={0: create2_threshold})}, }, { "indexes": {"data": [2, 3, 4, 5], "gas": -1, "value": -1}, @@ -430,8 +474,14 @@ def test_operation_diff_gas( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) tx_data = [ - Bytes("048071d3") + Hash(0xF0) + Hash(0x0) + Hash(0x64), - Bytes("048071d3") + Hash(0xF5) + Hash(0x0) + Hash(0x64), + Bytes("048071d3") + + Hash(0xF0) + + Hash(create_threshold - 5 * gas_diff) + + Hash(gas_diff), + Bytes("048071d3") + + Hash(0xF5) + + Hash(create2_threshold - 5 * gas_diff) + + Hash(gas_diff), Bytes("048071d3") + Hash(0xF1) + Hash(0x0) + Hash(0x64), Bytes("048071d3") + Hash(0xF2) + Hash(0x0) + Hash(0x64), Bytes("048071d3") + Hash(0xF4) + Hash(0x0) + Hash(0x64), From 1f05a5ab9bc854a422bfd9e964f093ab43f634e2 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:09:39 +0200 Subject: [PATCH 03/25] fix(tests): consolidate & un-skip CREATE empty-contract-with-storage ported tests --- tests/ported_static/amsterdam_skip_list.txt | 7 +- ...test_create_empty_contract_with_storage.py | 220 +++++++++++++----- ..._contract_with_storage_and_call_it_0wei.py | 112 --------- ..._contract_with_storage_and_call_it_1wei.py | 115 --------- 4 files changed, 163 insertions(+), 291 deletions(-) delete mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py delete mode 100644 tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index cf5ff638048..e4b71ac326c 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 147 +# Total entries: 144 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -67,7 +67,7 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] -# stCreateTest (36) +# stCreateTest (33) stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-0xef-v1] stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-contructor-revert-v1] stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-high-nonce-v1] @@ -84,9 +84,6 @@ stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_af stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage.py::test_create_empty_contract_with_storage[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py::test_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py::test_create_empty_contract_with_storage_and_call_it_1wei[fork_Amsterdam] stCreateTest/test_create_oo_gafter_init_code_returndata_size.py::test_create_oo_gafter_init_code_returndata_size[fork_Amsterdam] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py index 9ca299282ef..1f406840b63 100644 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py +++ b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py @@ -1,18 +1,29 @@ """ -Test_create_empty_contract_with_storage. +Measure CREATE of a codeless-but-storage-writing contract, and optionally +a following CALL to it, via CodeGasMeasure. + +The init code writes the created account's own storage and calls a +storage-writer contract, then deposits no code: the result is an "empty" +(codeless) account with storage and nonce 1. Ported from: state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json +state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json +state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json + +@manually-enhanced: Do not overwrite. Three fillers folded into one +parametrize; the init code is composed (not hex blobs) so the measured +CREATE/CALL expectations derive from the same bytecode; the init code's +inner CALL forwards all gas (the ported 0xEA60 budget OOGs under +EIP-8037); the CALL success flag stays inside the measured window. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -22,78 +33,169 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0x1 +CREATE_GAS_SLOT = 0x2 +CALL_FLAG_SLOT = 0x3 +CALL_GAS_SLOT = 0x64 +STORED_VALUE = 0xC + +FORWARDED_GAS = 0xEA60 + @pytest.mark.ported_from( - ["state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json"], + [ + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json", + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json", # noqa: E501 + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "call_created, call_value", + [ + pytest.param(False, 0, id="with_storage"), + pytest.param(True, 0, id="and_call_it_0wei"), + pytest.param(True, 1, id="and_call_it_1wei"), + ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable def test_create_empty_contract_with_storage( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + call_created: bool, + call_value: int, ) -> None: - """Test_create_empty_contract_with_storage.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Measure CREATE (and optionally CALL) gas for a storage-only account.""" + # Called by the init code below; writes one cold fresh slot. + writer_store = Op.SSTORE( + key=0x1, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The init code writes the created account's own slot 0 and calls the + # writer, then runs off its end (STOP) so no code is deposited. The + # inner CALL forwards all remaining gas (default Op.GAS operand). + initcode = Op.SSTORE( + key=0x0, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + Op.CALL( + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x40, "init code must fit two MSTORE words" - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Memory is populated (and expanded to 0x40) before the measured + # window, so the CREATE itself expands nothing. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes( + initcode_bytes[:0x20].ljust(0x20, b"\x00"), "big" + ), + ) + Op.MSTORE( + offset=0x20, + value=int.from_bytes( + initcode_bytes[0x20:].ljust(0x20, b"\x00"), "big" + ), + ) + + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # The created address is stored inside the measured window (as in the + # ported filler) so the optional CALL can target it at runtime. + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, ) + # The created account exists (nonce 1) and is warm (CREATE accessed + # it); the CALL success flag is stored inside the measured window — a + # wrongly failed call would otherwise be unobservable for the 0wei arm. + call_code = Op.CALL( + gas=FORWARDED_GAS, + address=Op.SLOAD(key=ADDRESS_SLOT, key_warm=True), + value=call_value, + address_warm=True, + value_transfer=call_value > 0, + account_new=False, + ) + call_store = Op.SSTORE( + key=CALL_FLAG_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=1, + ) + + code = setup + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=CREATE_GAS_SLOT, + ) + if call_created: + code += CodeGasMeasure( + code=call_store, + extra_stack_items=0, + sstore_key=CALL_GAS_SLOT, + ) + contract = pre.deploy_contract(code=code, balance=call_value) + tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + # The measured CREATE includes the child's work: the init code's own + # consumption plus the writer's store it calls. + measured_create = ( + create_store.gas_cost(fork) + + initcode.gas_cost(fork) + + writer_store.gas_cost(fork) ) + # A value-bearing CALL whose codeless callee consumes nothing measures + # gas_cost minus the stipend (forwarded then returned unused). + stipend = fork.gas_costs().CALL_STIPEND if call_value else 0 + measured_call = call_store.gas_cost(fork) - stipend + + created = compute_create_address(address=contract, nonce=1) + contract_storage: dict = { + ADDRESS_SLOT: created, + CREATE_GAS_SLOT: measured_create, + } + if call_created: + contract_storage[CALL_FLAG_SLOT] = 1 + contract_storage[CALL_GAS_SLOT] = measured_call post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 100: 0x6F4F0, - }, + contract: Account(storage=contract_storage, balance=0), + # Codeless, but with storage and (for the 1wei arm) the value the + # measured CALL transferred — proving both the init code and the + # CALL executed. + created: Account( + nonce=1, + balance=call_value if call_created else 0, + storage={0: STORED_VALUE}, ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), + writer: Account(storage={1: STORED_VALUE}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py deleted file mode 100644 index d7940716427..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Test_create_empty_contract_with_storage_and_call_it_0wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_with_storage_and_call_it_0wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_with_storage_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (CALL 60000 (SLOAD 1) 0 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x64763, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py deleted file mode 100644 index cbd15afeba3..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Test_create_empty_contract_with_storage_and_call_it_1wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_with_storage_and_call_it_1wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_with_storage_and_call_it_1wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (CALL 60000 (SLOAD 1) 1 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x62D37, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 12}, balance=1 - ), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) From 25a6c4b6bd71d37c03c6e6718d932440fb117bf3 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:15:04 +0200 Subject: [PATCH 04/25] fix(tests): un-skip Amsterdam stStaticCall create/ask ported tests --- .claude/commands/enhance-ported-test.md | 13 + tests/ported_static/amsterdam_skip_list.txt | 7 +- ..._create_empty_contract_and_call_it_0wei.py | 224 ++++++++++++------ ..._contract_with_storage_and_call_it_0wei.py | 124 ---------- ..._that_ask_fore_gas_then_trabsaction_has.py | 205 +++++++--------- 5 files changed, 259 insertions(+), 314 deletions(-) delete mode 100644 tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index f6d3f6852e5..2e6a14f9635 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -402,6 +402,19 @@ transition, not the magnitude — which also breaks the `forward_gas`/`new_value circularity.) Set `state_gas_reservoir=0` so the state gas is captured. Validated on `test_raw_call_gas`. +**An expensive store after a callee that eats all forwarded gas — pre-write +the slot.** When a frame must SSTORE a result *after* a subcall that +deliberately consumes its whole 63/64 grant (an OOG-probe callee), the frame +retains only 1/64 — under EIP-8037 that cannot afford a cold zero→nonzero +store (~111k), and pre-8037 it often couldn't afford the cold 2.2k either +(making the ported `{slot: 0}` expectation vacuous: caller-OOG and +callee-failure were indistinguishable). Fix: write a sentinel to the slot +*before* the call (paying cold + state with the full budget), then store +`BASE + result` after it — now a dirty-warm write (100 gas) the retention +always covers, and the three outcomes (success `BASE+1`, failure `BASE`, +caller OOG `sentinel`) are all distinct. Validated on +`test_static_execute_call_that_ask_fore_gas_then_trabsaction_has`. + **Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** Ported fillers probe "how much gas does a subcall receive when it asks for more than is available" by pinning an absolute forwarded amount — fork-fragile, diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index e4b71ac326c..a19a104c584 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 144 +# Total entries: 141 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -176,11 +176,6 @@ stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contrac stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] stSolidityTest/test_test_contract_suicide.py::test_test_contract_suicide[fork_Amsterdam] -# stStaticCall (3) -stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py::test_static_create_empty_contract_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py::test_static_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py::test_static_execute_call_that_ask_fore_gas_then_trabsaction_has[fork_Amsterdam-d0] - # stSystemOperationsTest (5) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] diff --git a/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py b/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py index d426d271f6e..95c600b91ce 100644 --- a/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py +++ b/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py @@ -1,106 +1,198 @@ """ -Test_static_create_empty_contract_and_call_it_0wei. +Measure CREATE of a codeless contract (optionally writing storage in its +init code) followed by a STATICCALL to it, via CodeGasMeasure. Ported from: state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json +state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json + +@manually-enhanced: Do not overwrite. Two fillers folded into one +parametrize; the storage-writing init code is composed (not hex blobs) so +the measured CREATE/STATICCALL expectations derive from the same bytecode; +the init code's inner CALL forwards all gas (the ported 0xEA60 budget OOGs +under EIP-8037); the STATICCALL success flag stays inside the measured +window. Replaces the prior EIP-8037 expect-any band-aid with fork-derived +gas assertions. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, + CodeGasMeasure, + Fork, StateTestFiller, - Storage, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0x1 +CREATE_GAS_SLOT = 0x2 +STATICCALL_FLAG_SLOT = 0x3 +STATICCALL_GAS_SLOT = 0x64 +STORED_VALUE = 0xC + +FORWARDED_GAS = 0xEA60 + @pytest.mark.ported_from( [ - "state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json" # noqa: E501 + "state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json", # noqa: E501 + "state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "with_storage", + [ + pytest.param(False, id="empty_contract"), + pytest.param(True, id="with_storage"), ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable def test_static_create_empty_contract_and_call_it_0wei( state_test: StateTestFiller, pre: Alloc, fork: Fork, + with_storage: bool, ) -> None: - """Test_static_create_empty_contract_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """Measure CREATE and STATICCALL gas for a created codeless account.""" + if with_storage: + # Called by the init code below; writes one cold fresh slot. + writer_store = Op.SSTORE( + key=0x1, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) + + # The init code writes the created account's own slot 0 and calls + # the writer, then runs off its end (STOP) so no code is deposited. + # The inner CALL forwards all remaining gas (default Op.GAS). + initcode = Op.SSTORE( + key=0x0, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + Op.CALL( + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, + ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x40, "init code must fit two words" - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]] (STATICCALL 60000 (SLOAD 1) 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.STATICCALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + # Memory is populated (and expanded to 0x40) before the measured + # window, so the CREATE itself expands nothing. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes( + initcode_bytes[:0x20].ljust(0x20, b"\x00"), "big" ), + ) + Op.MSTORE( + offset=0x20, + value=int.from_bytes( + initcode_bytes[0x20:].ljust(0x20, b"\x00"), "big" + ), + ) + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # The measured CREATE includes the child's work: the init code's + # own consumption plus the writer's store it calls. + child_cost = initcode.gas_cost(fork) + writer_store.gas_cost(fork) + else: + # CREATE over never-written memory runs 32 zero bytes as init code + # (STOP on the first byte), depositing no code and consuming + # nothing; the memory expansion happens inside the window. + setup = Bytecode() + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, + ) + child_cost = 0 + + # The created address is stored inside the measured window (as in the + # ported filler) so the STATICCALL can target it at runtime. + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, ) - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + # The created account exists (nonce 1) and is warm (CREATE accessed + # it); the success flag is stored inside the measured window — a + # wrongly failed STATICCALL would otherwise be unobservable. + staticcall_code = Op.STATICCALL( + gas=FORWARDED_GAS, + address=Op.SLOAD(key=ADDRESS_SLOT, key_warm=True), + address_warm=True, + ) + staticcall_store = Op.SSTORE( + key=STATICCALL_FLAG_SLOT, + value=staticcall_code, + key_warm=False, + original_value=0, + new_value=1, ) - if fork.is_eip_enabled(8037): - contract_0_storage = Storage.model_validate( - {1: compute_create_address(address=contract_0, nonce=0), 3: 1} - ) - contract_0_storage.set_expect_any(0) - contract_0_storage.set_expect_any(2) - contract_0_storage.set_expect_any(100) - else: - contract_0_storage = Storage.model_validate( - { - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6FE6E, - } + contract = pre.deploy_contract( + code=setup + + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=CREATE_GAS_SLOT, ) + + CodeGasMeasure( + code=staticcall_store, + extra_stack_items=0, + sstore_key=STATICCALL_GAS_SLOT, + ), + ) + + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + measured_create = create_store.gas_cost(fork) + child_cost + measured_staticcall = staticcall_store.gas_cost(fork) + + created = compute_create_address(address=contract, nonce=1) post = { - contract_0: Account(storage=contract_0_storage), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), + contract: Account( + storage={ + ADDRESS_SLOT: created, + CREATE_GAS_SLOT: measured_create, + STATICCALL_FLAG_SLOT: 1, + STATICCALL_GAS_SLOT: measured_staticcall, + }, + ), + created: Account( + nonce=1, + storage={0: STORED_VALUE} if with_storage else {}, + ), } + if with_storage: + post[writer] = Account(storage={1: STORED_VALUE}) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py b/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py deleted file mode 100644 index 91623e4efe9..00000000000 --- a/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -Test_static_create_empty_contract_with_storage_and_call_it_0wei. - -Ported from: -state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Storage, - Transaction, - compute_create_address, -) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable -def test_static_create_empty_contract_with_storage_and_call_it_0wei( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, -) -> None: - """Test_static_create_empty_contract_with_storage_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (STATICCALL 60000 (SLOAD 1) 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.STATICCALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - if fork.is_eip_enabled(8037): - contract_0_storage = Storage.model_validate( - {1: compute_create_address(address=contract_0, nonce=0), 3: 1} - ) - contract_0_storage.set_expect_any(0) - contract_0_storage.set_expect_any(2) - contract_0_storage.set_expect_any(100) - else: - contract_0_storage = Storage.model_validate( - { - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x64766, - } - ) - post = { - contract_0: Account(storage=contract_0_storage), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py b/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py index b5965a5da21..3ea3f5959b4 100644 --- a/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py +++ b/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py @@ -1,162 +1,131 @@ """ -Test_static_execute_call_that_ask_fore_gas_then_trabsaction_has. +Verify a STATICCALL that asks for more gas than is available is clamped to +63/64 of the remaining gas (EIP-150), across callees that succeed, out-of-gas, +and violate the static context. Ported from: state_tests/stStaticCall/static_ExecuteCallThatAskForeGasThenTrabsactionHasFiller.json + +@manually-enhanced: Do not overwrite. An outer call caps the caller frame so +the callee budgets are fork-independent; the flag slot is pre-written so the +post-call store is a cheap dirty-warm write affordable from the 1/64 +retention even under EIP-8037; distinct flag values discriminate success, +callee failure, and caller OOG (the ported {1: 0} expectation could not). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, - Hash, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x1 +# Pre-written sentinel: if the caller frame dies after the call, the slot +# keeps this value instead of reverting to an ambiguous zero. +FLAG_PREWRITE = 0xFF +# Stored flag = 0x10 + STATICCALL result: 0x11 success, 0x10 failure. +FLAG_BASE = 0x10 + +# Far larger than any gas the caller frame can hold, so the EIP-150 clamp +# (not the operand) decides what the callee receives. +OVERSIZED_GAS_ASK = 2**61 +# The outer call pins the caller frame's budget: large enough to cover the +# caller's own cold flag store (~111k under EIP-8037) and the successful +# callee, small enough that the looping callee (~6.5M) still runs out. +CALLER_GAS = 1_000_000 + @pytest.mark.ported_from( [ "state_tests/stStaticCall/static_ExecuteCallThatAskForeGasThenTrabsactionHasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow +@pytest.mark.valid_from("Byzantium") @pytest.mark.parametrize( - "d, g, v", + "callee_kind, callee_succeeds", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), + pytest.param("mstore", True, id="d0"), + pytest.param("extcodesize_loop", False, id="d1"), + pytest.param("sstore_static_violation", False, id="d2"), ], ) -@pytest.mark.pre_alloc_mutable def test_static_execute_call_that_ask_fore_gas_then_trabsaction_has( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + callee_kind: str, + callee_succeeds: bool, ) -> None: - """Test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x989680) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """A STATICCALL asking for more gas than available gets 63/64 of it.""" + if callee_kind == "mstore": + # Trivial: succeeds well within the forwarded gas. + callee = pre.deploy_contract( + code=Op.MSTORE(offset=0x1, value=0x1) + Op.STOP + ) + elif callee_kind == "extcodesize_loop": + # 50000 EXTCODESIZE iterations (~6.5M gas): must exhaust the + # clamped forwarded gas, proving the callee did not receive the + # oversized ask. + callee = pre.deploy_contract( + code=Op.JUMPDEST + + Op.JUMPI( + pc=0x1C, + condition=Op.ISZERO(Op.LT(Op.MLOAD(offset=0x80), 0xC350)), + ) + + Op.POP(Op.EXTCODESIZE(address=0x1)) + + Op.MSTORE(offset=0x80, value=Op.ADD(Op.MLOAD(offset=0x80), 0x1)) + + Op.JUMP(pc=0x0) + + Op.JUMPDEST + + Op.STOP, + ) + else: + # SSTORE inside a static context: exceptional halt regardless of + # gas. + callee = pre.deploy_contract( + code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP + ) - # Source: lll - # { [[1]] (STATICCALL 600000 (CALLDATALOAD 0) 0 0 0 0) } - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x1, - value=Op.STATICCALL( - gas=0x927C0, - address=Op.CALLDATALOAD(offset=0x0), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + # The flag slot is written twice: the pre-write pays the cold/state + # cost with the full budget, so the post-call store is a dirty-warm + # write the 1/64 retention can always afford. + caller = pre.deploy_contract( + code=Op.SSTORE(key=FLAG_SLOT, value=FLAG_PREWRITE) + + Op.SSTORE( + key=FLAG_SLOT, + value=Op.ADD( + FLAG_BASE, + Op.STATICCALL(gas=OVERSIZED_GAS_ASK, address=callee), ), ) + Op.STOP, - nonce=0, - address=Address(0xA256EBCC5536CDA56E04C39FE9584ECC7594A438), # noqa: E501 - ) - # Source: lll - # { (MSTORE 1 1) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x3DC16A13CF554533F380CC938A2C1AB04DAC534F), # noqa: E501 ) - # Source: lll - # { (def 'i 0x80) (for {} (< @i 50000) [i](+ @i 1) (EXTCODESIZE 1)) } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.JUMPDEST - + Op.JUMPI( - pc=0x1C, condition=Op.ISZERO(Op.LT(Op.MLOAD(offset=0x80), 0xC350)) - ) - + Op.POP(Op.EXTCODESIZE(address=0x1)) - + Op.MSTORE(offset=0x80, value=Op.ADD(Op.MLOAD(offset=0x80), 0x1)) - + Op.JUMP(pc=0x0) - + Op.JUMPDEST + + # The outer call pins the caller frame's gas so the callee budgets do + # not depend on the tx gas limit; the clamp must always bite. + assert CALLER_GAS < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=caller)) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x73EF1878A0F2C9629DEDC1B1E9BE8D77DCF93688), # noqa: E501 - ) - # Source: lll - # { (SSTORE 1 1) } - addr_3 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xCE4CCBFFAF450AE2126EB96DCD7C891F37764F20), # noqa: E501 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [1, 2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={1: 0})}, - }, - { - "indexes": {"data": [0], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={1: 1})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(addr, left_padding=True), - Hash(addr_2, left_padding=True), - Hash(addr_3, left_padding=True), - ] - tx_gas = [100000] - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + entry: Account(storage={0: 1}), + caller: Account( + storage={FLAG_SLOT: FLAG_BASE + (1 if callee_succeeds else 0)}, + ), + } + + state_test(pre=pre, post=post, tx=tx) From da5f86bedc0d12be1e0fdb3719034a963e7064e3 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 13:20:34 +0200 Subject: [PATCH 05/25] fix(tests): un-skip Amsterdam stSStoreTest via metadata-derived costs and warm stipend boundary --- tests/ported_static/amsterdam_skip_list.txt | 8 +- .../stSStoreTest/test_sstore_gas.py | 65 ++- .../stSStoreTest/test_sstore_gas_left.py | 464 +++--------------- 3 files changed, 125 insertions(+), 412 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index a19a104c584..b91b0416e98 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 141 +# Total entries: 137 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -165,12 +165,6 @@ stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_rever stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] -# stSStoreTest (4) -stSStoreTest/test_sstore_gas.py::test_sstore_gas[fork_Amsterdam] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d2] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d5] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d8] - # stSolidityTest (3) stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contracts[fork_Amsterdam] stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] diff --git a/tests/ported_static/stSStoreTest/test_sstore_gas.py b/tests/ported_static/stSStoreTest/test_sstore_gas.py index 975d3765c56..06e840689ad 100644 --- a/tests/ported_static/stSStoreTest/test_sstore_gas.py +++ b/tests/ported_static/stSStoreTest/test_sstore_gas.py @@ -1,8 +1,16 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the gas cost of every SSTORE transition class (cold/warm x +original/current/new value combinations) via inline GAS deltas (by Ori +Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stSStoreTest/sstoreGasFiller.yml + +@manually-enhanced: Do not overwrite. The nine measured transition costs +are derived from SSTORE opcode metadata instead of pinned numbers, so +EIP-8037's state-gas repricing (and any future one) is tracked +automatically; the explicit gas limit equals the EIP-7825 cap, so the +state gas spills into the measured deltas. """ import pytest @@ -12,6 +20,7 @@ Alloc, Bytes, Environment, + Fork, StateTestFiller, Transaction, ) @@ -29,8 +38,9 @@ def test_sstore_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Ori Pomerantz qbzzt1@gmail.""" + """Measure each SSTORE transition's gas against opcode metadata.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xBA1A9CE0BA1A9CE, nonce=1) @@ -171,18 +181,51 @@ def test_sstore_gas( nonce=1, ) + # The measured transitions, in bytecode order (slots 0 and 1 start at + # 24743; slots 2 and 3 start empty). Each stored delta is the pure + # SSTORE cost (the contract subtracts its own 8-gas overhead). + transitions = [ + # slot 0: cold, original nonzero -> different nonzero + dict(key_warm=False, original_value=24743, new_value=0xBEEF), + # slot 0: warm dirty, nonzero -> nonzero + dict( + key_warm=True, + original_value=24743, + current_value=0xBEEF, + new_value=0xDEADBEEF, + ), + # slot 0: warm dirty, nonzero -> zero + dict( + key_warm=True, + original_value=24743, + current_value=0xDEADBEEF, + new_value=0, + ), + # slot 0: warm dirty, zero -> zero + dict( + key_warm=True, original_value=24743, current_value=0, new_value=0 + ), + # slot 0: warm dirty, zero -> nonzero + dict( + key_warm=True, + original_value=24743, + current_value=0, + new_value=0x1234, + ), + # slot 1: cold, original nonzero -> zero + dict(key_warm=False, original_value=24743, new_value=0), + # slot 2: cold fresh, zero -> nonzero + dict(key_warm=False, original_value=0, new_value=0x60A7), + # slot 3: cold fresh, zero -> zero + dict(key_warm=False, original_value=0, new_value=0), + # slot 3: warm fresh, zero -> nonzero + dict(key_warm=True, original_value=0, new_value=0x60A7), + ] post = { target: Account( storage={ - 4096: 5000, - 4097: 100, - 4098: 100, - 4099: 100, - 4100: 100, - 4101: 5000, - 4102: 22100, - 4103: 2200, - 4104: 20000, + 0x1000 + i: Op.SSTORE.with_metadata(**md).gas_cost(fork) + for i, md in enumerate(transitions) }, ), } diff --git a/tests/ported_static/stSStoreTest/test_sstore_gas_left.py b/tests/ported_static/stSStoreTest/test_sstore_gas_left.py index 30686397044..34fefa79a0c 100644 --- a/tests/ported_static/stSStoreTest/test_sstore_gas_left.py +++ b/tests/ported_static/stSStoreTest/test_sstore_gas_left.py @@ -1,440 +1,116 @@ """ -Checks EIP-1706/EIP-2200 out of gas requirement for non-mutating SSTOREs. +Verify the EIP-2200 (EIP-1706) minimum-gas rule for SSTORE: a non-mutating +store fails unless the gas left exceeds the call stipend, across CALL, +CALLCODE and DELEGATECALL entry into the storing frame. Ported from: state_tests/stSStoreTest/sstore_gasLeftFiller.json -@manually-enhanced: Do not overwrite. Gas budget refactored to be -fork-aware (`tx_gas = [intrinsic + tx_data[d].gas_cost(fork)]`), and -each `Op.CALL` annotated with `inner_call_cost=` metadata so -`Bytecode.gas_cost(fork)` covers the forwarded inner-frame gas. -Required for the test to fill correctly under EIP-8037's two- -dimensional gas model. Hex `gas=` literals also converted to -human-readable decimals. +@manually-enhanced: Do not overwrite. The stored-to slot is warmed before +the boundary call so the stipend check (not the cold-access charge, which +EIP-8037/8038 reprice) is the binding constraint on every fork; the +boundary gas is derived as stipend + push cost +/- 1; the success +indicator forwards gas via `flag * INDICATOR_GAS` instead of the ported +hardcoded-pc JUMPI; the tx gas is maxed so the indicator's storage write +is not budget-bound. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas forwarded to the success indicator; only needs to cover its regular +# costs (state gas rides on the transaction's implicit reservoir). +INDICATOR_GAS = 30_000 + @pytest.mark.ported_from( ["state_tests/stSStoreTest/sstore_gasLeftFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Istanbul") +@pytest.mark.parametrize( + "opcode", + [ + pytest.param(Op.CALL, id="call"), + pytest.param(Op.CALLCODE, id="callcode"), + pytest.param(Op.DELEGATECALL, id="delegatecall"), + ], +) @pytest.mark.parametrize( - "d, g, v", + "gas_offset, store_succeeds", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), - pytest.param( - 3, - 0, - 0, - id="d3", - ), - pytest.param( - 4, - 0, - 0, - id="d4", - ), - pytest.param( - 5, - 0, - 0, - id="d5", - ), - pytest.param( - 6, - 0, - 0, - id="d6", - ), - pytest.param( - 7, - 0, - 0, - id="d7", - ), - pytest.param( - 8, - 0, - 0, - id="d8", - ), + pytest.param(-1, False, id="below_boundary"), + pytest.param(0, False, id="at_boundary"), + pytest.param(1, True, id="above_boundary"), ], ) -@pytest.mark.pre_alloc_mutable def test_sstore_gas_left( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + opcode: Op, + gas_offset: int, + store_succeeds: bool, ) -> None: - """Checks EIP-1706/EIP-2200 out of gas requirement for non-mutating...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) + """A non-mutating SSTORE needs gas left above the call stipend.""" + gas_costs = fork.gas_costs() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[1]] 1 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - storage={1: 1}, - nonce=0, - address=Address(0xB0409D84AB61455CB8BEC14B94F635146AB55613), # noqa: E501 - ) - # Source: lll - # { [[1]] 1 } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x4092B3905CFEA2485EA53222F41EB26E67587802), # noqa: E501 - ) + # The storing contract: a no-op SSTORE (slot 1 already holds 1 for the + # CALL arm; the CALLCODE/DELEGATECALL arms pre-set the caller's own + # slot 1). At the SSTORE, gas left = forwarded - two pushes; EIP-2200 + # requires it to exceed the stipend. + store_code = Op.SSTORE(key=0x1, value=0x1) + storer = pre.deploy_contract(code=store_code + Op.STOP, storage={1: 1}) + push_cost = 2 * gas_costs.VERY_LOW + boundary_gas = gas_costs.CALL_STIPEND + push_cost + gas_offset - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 1, 3, 4, 6, 7], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {addr_2: Account(storage={1: 0})}, - }, - { - "indexes": {"data": [8, 2, 5], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {addr_2: Account(storage={1: 1})}, - }, - ] + # Written by the success indicator call. + indicator = pre.deploy_contract(code=Op.SSTORE(key=0x1, value=0x1)) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + if opcode == Op.CALL: + # Warm the storer's slot (and pre-write it back to 1) with an + # unbounded call, so the boundary call's SSTORE is a warm no-op + # and only the stipend check can fail it. + prelude = Op.POP(Op.CALL(address=storer)) + boundary_call = opcode(gas=boundary_gas, address=storer) + else: + # CALLCODE/DELEGATECALL store into the caller's own slot 1: the + # pre-write makes the boundary store a warm no-op. + prelude = Op.SSTORE(key=0x1, value=0x1) + if opcode == Op.CALLCODE: + boundary_call = opcode(gas=boundary_gas, address=storer, value=0) + else: + boundary_call = opcode(gas=boundary_gas, address=storer) - tx_data = [ - Op.JUMPI( - pc=0x4B, - condition=Op.ISZERO( - Op.CALL( - gas=2305, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2305, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.JUMPI( - pc=0x4B, - condition=Op.ISZERO( - Op.CALL( - gas=2306, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2306, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.JUMPI( - pc=0x4B, - condition=Op.ISZERO( - Op.CALL( - gas=2307, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2307, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x50, - condition=Op.ISZERO( - Op.CALLCODE( - gas=2305, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2305, - ) - ), - ) + # The indicator receives gas only if the boundary call succeeded + # (flag * INDICATOR_GAS), so no jump destinations are needed. + caller = pre.deploy_contract( + code=prelude + Op.POP( Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, + gas=Op.MUL(INDICATOR_GAS, boundary_call), + address=indicator, ) ) - + Op.JUMPDEST + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x50, - condition=Op.ISZERO( - Op.CALLCODE( - gas=2306, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2306, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x50, - condition=Op.ISZERO( - Op.CALLCODE( - gas=2307, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=2307, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x4E, - condition=Op.ISZERO( - Op.DELEGATECALL( - gas=2305, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x4E, - condition=Op.ISZERO( - Op.DELEGATECALL( - gas=2306, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.JUMPI( - pc=0x4E, - condition=Op.ISZERO( - Op.DELEGATECALL( - gas=2307, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ), - ) - + Op.POP( - Op.CALL( - gas=30_000, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - inner_call_cost=30_000, - ) - ) - + Op.JUMPDEST - + Op.STOP, - ] - # Fork-aware gas budget: contract-creation intrinsic from the - # fork's calculator, plus the bytecode's own gas cost (which - # already includes the gas forwarded to inner CALLs via opcode - # metadata). Any future fork-cost change is automatically - # respected. - intrinsic = fork.transaction_intrinsic_cost_calculator()( - calldata=tx_data[d], - contract_creation=True, ) - tx_gas = [intrinsic + tx_data[d].gas_cost(fork)] - tx_value = [1] tx = Transaction( - sender=sender, - to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + sender=pre.fund_eoa(), + to=caller, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + indicator: Account(storage={1: 1 if store_succeeds else 0}), + } + + state_test(pre=pre, post=post, tx=tx) From 4977d6edc20f9ea44b899ee43e6235318be5d980 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:13:41 +0200 Subject: [PATCH 06/25] fix(tests): un-skip Amsterdam 63/64 ask-family ported tests via observed-gas returns --- .claude/commands/enhance-ported-test.md | 17 ++ tests/ported_static/amsterdam_skip_list.txt | 15 +- ..._ask_more_gas_then_transaction_provided.py | 193 ++++++++---------- ...more_gas_on_depth2_then_transaction_has.py | 134 ++++++------ .../test_transaction64_rule.py | 109 ++++++++++ .../test_transaction64_rule_d64e0.py | 84 -------- .../test_transaction64_rule_d64m1.py | 84 -------- .../test_transaction64_rule_d64p1.py | 84 -------- ...ransaction_has_with_mem_expanding_calls.py | 156 ++++++++------ 9 files changed, 381 insertions(+), 495 deletions(-) create mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule.py delete mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py delete mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py delete mode 100644 tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 2e6a14f9635..37b4980601b 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -414,6 +414,23 @@ callee-failure were indistinguishable). Fix: write a sentinel to the slot always covers, and the three outcomes (success `BASE+1`, failure `BASE`, caller OOG `sentinel`) are all distinct. Validated on `test_static_execute_call_that_ask_fore_gas_then_trabsaction_has`. +**Caveat — EIP-2200's stipend rule caps this trick.** Any SSTORE (even a +100-gas dirty-warm one) exceptionally halts unless `gas_left > 2300` +(Istanbul+), so the 1/64 retention must exceed ~2400, i.e. the pre-call +budget must exceed ~154k. When the scenario *requires* a smaller budget +(e.g. a starved arm whose forwarded gas must undercut the callee's cost), +no post-call SSTORE is possible at all: write the sentinel *before* the +call and put nothing but a `POP` after it — frame completion (the account +persists with the sentinel) plus the callee-side observable already +separate the outcomes. Validated on +`test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. + +**A creation transaction's top frame pays new-account state gas +(EIP-8037).** When deriving a create-tx budget, the intrinsic calculator +does not include the created account's state gas — add +`fork.transaction_top_frame_state_gas(contract_creation=True)` (183,600 on +Amsterdam, 0 before) or the whole creation silently OOGs only on the +future fork. **Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** Ported fillers probe "how much gas does a subcall receive when it asks for more diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index b91b0416e98..a3fe6dc3556 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 137 +# Total entries: 130 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -18,15 +18,13 @@ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] -# stCallCreateCallCodeTest (11) +# stCallCreateCallCodeTest (9) stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g3] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py::test_create_name_registrator_pre_store1_not_enough_gas[fork_Amsterdam] @@ -108,14 +106,10 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP150Specific (7) -stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py::test_call_ask_more_gas_on_depth2_then_transaction_has[fork_Amsterdam] +# stEIP150Specific (3) stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64e0.py::test_transaction64_rule_d64e0[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64m1.py::test_transaction64_rule_d64m1[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64p1.py::test_transaction64_rule_d64p1[fork_Amsterdam] # stEIP158Specific (1) stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] @@ -132,8 +126,7 @@ stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_p stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g1] stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g2] -# stMemExpandingEIP150Calls (4) -stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py::test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls[fork_Amsterdam] +# stMemExpandingEIP150Calls (3) stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py b/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py index 281a5b080b4..5e3075039ea 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py @@ -1,152 +1,133 @@ """ -Test_contract_creation_make_call_that_ask_more_gas_then_transaction_prov... +Verify a CALL made inside a contract-creation transaction's init code that +asks for more gas than the transaction provided: the EIP-150 clamp decides +what the callee receives, and the transaction budget decides whether that +grant covers the callee's work. Ported from: state_tests/stCallCreateCallCodeTest/contractCreationMakeCallThatAskMoreGasThenTransactionProvidedFiller.json + +@manually-enhanced: Do not overwrite. The ask is explicitly oversized (the +ported 50000 was schedule-sized); both transaction budgets are derived from +the fork so the clamped grant lands above/below the callee's cost on every +fork; the init code writes a canary before the call (nothing after it needs +more than a POP — the 1/64 retention cannot afford an SSTORE, whose +EIP-2200 stipend rule would kill the creation), so a failed call and a +failed creation stay distinguishable. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CANARY_SLOT = 0x2 +CANARY = 0xFF + +# Far larger than any gas the init frame can hold: the clamp always +# applies, which is the scenario the ported filler names. +OVERSIZED_GAS_ASK = 2**61 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/contractCreationMakeCallThatAskMoreGasThenTransactionProvidedFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "call_covered", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(True, id="enough_gas"), + pytest.param(False, id="not_enough_gas"), ], ) -@pytest.mark.pre_alloc_mutable def test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided( # noqa: E501 state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_covered: bool, ) -> None: - """Test_contract_creation_make_call_that_ask_more_gas_then_transaction...""" # noqa: E501 - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """An init-code CALL asking above the tx budget gets the 63/64 clamp.""" + # Success indicator: writes one cold fresh slot when called. + writer_store = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The init code writes a completion canary before the call (a failed + # creation persists nothing, so the canary distinguishes it from a + # failed call), makes the oversized ask, and deposits no code. Only a + # POP runs after the call: the 1/64 retention on the starved arm is + # far below the EIP-2200 stipend an SSTORE would require. + canary_store = Op.SSTORE( + key=CANARY_SLOT, + value=CANARY, + key_warm=False, + original_value=0, + new_value=CANARY, ) - - pre[sender] = Account(balance=0x10C8E0) - # Source: lll - # {(SSTORE 1 1)} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 + ask_call = Op.CALL( + gas=OVERSIZED_GAS_ASK, + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, ) - # Source: lll - # {(CALL 50000 0x1000000000000000000000000000000000000001 0 0 64 0 64)} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, + initcode = canary_store + Op.POP(ask_call) + Op.STOP + + # Derive the two budgets around the callee's fork-priced cost: the + # clamped grant (63/64 of the base left after the charges made before + # the forward point) lands above it on one arm and below it on the + # other. The post-call flag write runs on the 1/64 retention. + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, ) - + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # EIP-8037 charges the created account's state gas to the + # creation transaction's top frame (zero before Amsterdam). + + fork.transaction_top_frame_state_gas(contract_creation=True) + + canary_store.gas_cost(fork) + + ask_call.gas_cost(fork) ) + callee_needed = writer_store.gas_cost(fork) + if call_covered: + base = -(-callee_needed * 64 // 63) + 2_000 + else: + base = callee_needed // 2 + assert base < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + gas_limit = overhead + base - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": [0], "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - balance=0 - ), - contract_1: Account(storage={1: 1}), - }, - }, - { - "indexes": {"data": -1, "gas": [1], "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - balance=0 - ), - contract_1: Account(storage={1: 0}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, - ), - ] - tx_gas = [96000, 60000] - + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=initcode, + gas_limit=gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + post = { + created: Account( + nonce=1, + code=b"", + storage={CANARY_SLOT: CANARY}, + ), + writer: Account(storage={1: 1 if call_covered else 0}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py b/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py index 61fd1052dec..20371a9ce34 100644 --- a/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py +++ b/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py @@ -1,17 +1,23 @@ """ -Test_call_ask_more_gas_on_depth2_then_transaction_has. +Verify the EIP-150 63/64 clamp at call depth 2: a first-level call receives +its exact (affordable) ask, and its own oversized ask is clamped to 63/64 +of what remains in that frame. Ported from: state_tests/stEIP150Specific/CallAskMoreGasOnDepth2ThenTransactionHasFiller.json + +@manually-enhanced: Do not overwrite. The lower frames return their +observed GAS up the stack instead of SSTORE-ing it (the ported lower-frame +gas snapshots are EIP-8037 state-gas traps), and both expectations are +derived from the fork: the depth-1 frame sees exactly its asked budget, +the depth-2 frame sees `base - base // 64` of the depth-1 remainder. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,86 +26,86 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x0 +DEPTH2_GAS_SLOT = 0x1 +DEPTH1_GAS_SLOT = 0x2 + +# The ported depth-1 budget: affordable, so it is forwarded exactly. +CALLER_GAS = 0x30D40 +# The ported depth-2 ask: above anything the depth-1 frame can hold, so +# the 63/64 clamp decides what the depth-2 frame receives. +ASK_GAS = 0x927C0 + @pytest.mark.ported_from( [ "state_tests/stEIP150Specific/CallAskMoreGasOnDepth2ThenTransactionHasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_ask_more_gas_on_depth2_then_transaction_has( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_ask_more_gas_on_depth2_then_transaction_has.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A depth-2 call asking above the frame budget gets 63/64 of it.""" + # Depth 2: returns the gas it observed on entry. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), ) - # Source: lll - # { (SSTORE 8 (GAS))} - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + Op.STOP, - nonce=0, + # Depth 1: records its own entry gas, then asks depth 2 for more gas + # than this frame holds; both observations return to the top frame. + entry_snapshot = Op.MSTORE(0x20, Op.GAS, new_memory_size=0x40) + depth2_call = Op.CALL( + gas=ASK_GAS, + address=gas_return_contract, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=0x40, + old_memory_size=0x40, ) - # Source: lll - # { (SSTORE 8 (GAS)) (SSTORE 9 (CALL 600000 0 0 0 0 0)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - nonce=0, + caller = pre.deploy_contract( + code=entry_snapshot + depth2_call + Op.RETURN(0, 0x40), ) - # Source: lll - # { (SSTORE 8 (GAS)) (SSTORE 9 (CALL 200000 0 0 0 0 0)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x30D40, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + # Top frame: forwards the exact (affordable) depth-1 budget and stores + # the success flag plus both returned observations. + entry = pre.deploy_contract( + code=Op.SSTORE( + key=FLAG_SLOT, + value=Op.CALL(gas=CALLER_GAS, address=caller, ret_size=0x40), ) - + Op.STOP, - nonce=0, + + Op.SSTORE(key=DEPTH2_GAS_SLOT, value=Op.MLOAD(0)) + + Op.SSTORE(key=DEPTH1_GAS_SLOT, value=Op.MLOAD(0x20)), ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, + ) + + # Depth 1 received exactly CALLER_GAS; its snapshot reads it minus the + # GAS opcode itself. The depth-2 base is what remains after the + # snapshot and the call's own costs, clamped by EIP-150. + depth1_observed = CALLER_GAS - Op.GAS.gas_cost(fork) + base = ( + CALLER_GAS - entry_snapshot.gas_cost(fork) - depth2_call.gas_cost(fork) ) + assert 0 < base < ASK_GAS, "the 63/64 clamp must apply at depth 2" + forwarded = base - base // 64 + depth2_observed = forwarded - Op.GAS.gas_cost(fork) post = { - addr: Account(storage={8: 0x30D3E, 9: 1}), - addr_2: Account(storage={8: 0x2A1F6}), + entry: Account( + storage={ + FLAG_SLOT: 1, + DEPTH2_GAS_SLOT: depth2_observed, + DEPTH1_GAS_SLOT: depth1_observed, + }, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule.py new file mode 100644 index 00000000000..5930e8a26c2 --- /dev/null +++ b/tests/ported_static/stEIP150Specific/test_transaction64_rule.py @@ -0,0 +1,109 @@ +""" +Verify the EIP-150 "all but one 64th" rounding at the transaction level: the +gas available when a subcall asks for more than the transaction provided is +floored as `base - base // 64`, probed with the base exactly divisible by +64 and one gas below/above it. + +Ported from: +state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json +state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json +state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json + +@manually-enhanced: Do not overwrite. Three fillers folded into one +parametrize; the callee reports its observed GAS so the exact forwarded +amount is asserted (`base - base // 64` differs from `base * 63 // 64` by +one whenever the base is not a multiple of 64 — the ported posts could not +see that difference); the tx gas limit is derived from the fork so the +divisibility residue holds on every fork. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + StateTestFiller, + Transaction, +) +from execution_testing.vm import Op + +REFERENCE_SPEC_GIT_PATH = "N/A" +REFERENCE_SPEC_VERSION = "N/A" + +GAS_SLOT = 0x1 +# Far larger than any gas the frame can hold: the clamp always applies. +OVERSIZED_GAS_ASK = 2**61 + + +@pytest.mark.ported_from( + [ + "state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json", + "state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json", + "state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json", + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "residue", + [ + pytest.param(0, id="d64e0"), + pytest.param(-1, id="d64m1"), + pytest.param(1, id="d64p1"), + ], +) +def test_transaction64_rule( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + residue: int, +) -> None: + """A subcall asking above the tx budget receives `base - base // 64`.""" + # Callee returns the gas it observed on entry back to the caller. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), + ) + + call_code = Op.CALL( + gas=OVERSIZED_GAS_ASK, + address=gas_return_contract, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=0x20, + ) + # The observed-gas store is the only op after the call; the callee's + # returned surplus always covers it. + store_code = Op.SSTORE( + key=GAS_SLOT, + value=Op.MLOAD(0), + key_warm=False, + original_value=0, + new_value=1, + ) + caller = pre.deploy_contract(code=call_code + store_code + Op.STOP) + + # Choose the 63/64 rounding base: large enough that the frame can + # afford the trailing store from what the callee hands back, shaped to + # the parametrized residue mod 64. The +1024 margin absorbs the ops + # around the store. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + min_base = store_code.gas_cost(fork) + 1024 + base = -(-min_base // 64) * 64 + residue + assert base < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + gas_limit = intrinsic + call_code.gas_cost(fork) + base + + tx = Transaction( + sender=pre.fund_eoa(), + to=caller, + gas_limit=gas_limit, + ) + + # The EVM floors the forwarded gas as `base - base // 64`; the callee + # observes it minus its own GAS opcode. An implementation using + # `base * 63 // 64` is exactly one gas short on the m1/p1 residues. + forwarded = base - base // 64 + expected_gas = forwarded - Op.GAS.gas_cost(fork) + + post = {caller: Account(storage={GAS_SLOT: expected_gas})} + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py deleted file mode 100644 index 256cf7ea0bb..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64e0. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64e0( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64e0.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160062, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py deleted file mode 100644 index dd89bd167ec..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64m1. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64m1( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64m1.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160061, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py deleted file mode 100644 index 2dead5d9e1a..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64p1. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64p1( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64p1.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160063, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py index e181c5c7f03..f080fc4a5db 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py @@ -1,17 +1,24 @@ """ -Test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding... +Verify the EIP-150 63/64 clamp at call depth 2 when the calls also expand +memory: a first-level call receives its exact (affordable) ask, and its own +oversized ask is clamped to 63/64 of what remains after the memory +expansion. Ported from: state_tests/stMemExpandingEIP150Calls/CallAskMoreGasOnDepth2ThenTransactionHasWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The lower frames return their +observed GAS up the stack instead of SSTORE-ing it (the ported lower-frame +gas snapshots are EIP-8037 state-gas traps); every expectation is derived +from the fork, including the top frame's entry snapshot, which pins the +transaction intrinsic cost. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,86 +27,111 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x0 +DEPTH2_GAS_SLOT = 0x1 +DEPTH1_GAS_SLOT = 0x2 +ENTRY_GAS_SLOT = 0x3 + +# The ported depth-1 budget: affordable, so it is forwarded exactly. +CALLER_GAS = 0x30D40 +# The ported depth-2 ask: above anything the depth-1 frame can hold, so +# the 63/64 clamp decides what the depth-2 frame receives. +ASK_GAS = 0x927C0 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CallAskMoreGasOnDepth2ThenTransactionHasWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls( # noqa: E501 state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expa...""" # noqa: E501 - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A depth-2 memory-expanding call is clamped to 63/64 of its frame.""" + # Depth 2: returns the gas it observed on entry. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), ) - # Source: hex - # 0x5a600855 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS), - nonce=0, + # Depth 1: records its own entry gas, then asks depth 2 for more gas + # than this frame holds, expanding memory through the args window; + # both observations return to the top frame. + entry_snapshot = Op.MSTORE(0x20, Op.GAS, new_memory_size=0x40) + depth2_call = Op.CALL( + gas=ASK_GAS, + address=gas_return_contract, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, + old_memory_size=0x40, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, - ), - ), - nonce=0, + caller = pre.deploy_contract( + code=entry_snapshot + depth2_call + Op.RETURN(0, 0x40), ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff60007362030d40f1600955 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + + # Top frame: snapshots its entry gas (pinning the tx intrinsic), then + # forwards the exact depth-1 budget and stores the success flag plus + # both returned observations. + entry_code = ( + Op.SSTORE(key=ENTRY_GAS_SLOT, value=Op.GAS) + Op.SSTORE( - key=0x9, + key=FLAG_SLOT, value=Op.CALL( - gas=0x30D40, - address=addr_2, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=CALLER_GAS, + address=caller, + ret_size=0x40, + address_warm=False, + account_new=False, + new_memory_size=0x40, ), - ), - nonce=0, + ) + + Op.SSTORE(key=DEPTH2_GAS_SLOT, value=Op.MLOAD(0)) + + Op.SSTORE(key=DEPTH1_GAS_SLOT, value=Op.MLOAD(0x20)) ) + entry = pre.deploy_contract(code=entry_code + Op.STOP) + + # Conservative fork-derived budget: the entry's own costs (incl. the + # trailing state-priced stores) plus the full depth-1 grant. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = intrinsic + entry_code.gas_cost(fork) + CALLER_GAS tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + gas_limit=gas_limit, ) + # The entry snapshot observes everything after the intrinsic; depth 1 + # received exactly CALLER_GAS; the depth-2 base is what remains after + # the snapshot and the call's own costs (incl. memory expansion), + # clamped by EIP-150. + entry_observed = gas_limit - intrinsic - Op.GAS.gas_cost(fork) + depth1_observed = CALLER_GAS - Op.GAS.gas_cost(fork) + base = ( + CALLER_GAS - entry_snapshot.gas_cost(fork) - depth2_call.gas_cost(fork) + ) + assert 0 < base < ASK_GAS, "the 63/64 clamp must apply at depth 2" + forwarded = base - base // 64 + depth2_observed = forwarded - Op.GAS.gas_cost(fork) + post = { - sender: Account(nonce=1), - target: Account(storage={8: 0x8D5B6, 9: 1}), - addr: Account(storage={8: 0x2A1C7}), - addr_2: Account(storage={8: 0x30D3E, 9: 1}), + entry: Account( + storage={ + ENTRY_GAS_SLOT: entry_observed, + FLAG_SLOT: 1, + DEPTH2_GAS_SLOT: depth2_observed, + DEPTH1_GAS_SLOT: depth1_observed, + }, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From da5b867b0435d712704b5a103851f1936e3ce659 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:35:21 +0200 Subject: [PATCH 07/25] fix(tests): un-skip Amsterdam stInitCodeTest OOG creation tests via derived budgets --- .claude/commands/enhance-ported-test.md | 14 +- tests/ported_static/amsterdam_skip_list.txt | 11 +- .../test_out_of_gas_contract_creation.py | 202 ++++++++-------- ..._out_of_gas_prefunded_contract_creation.py | 215 ++++++++++-------- 4 files changed, 227 insertions(+), 215 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 37b4980601b..46041c1da2a 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -426,11 +426,19 @@ separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. **A creation transaction's top frame pays new-account state gas -(EIP-8037).** When deriving a create-tx budget, the intrinsic calculator -does not include the created account's state gas — add +(EIP-8037) — but only for a fresh target.** When deriving a create-tx +budget, the intrinsic calculator does not include the created account's +state gas — add `fork.transaction_top_frame_state_gas(contract_creation=True)` (183,600 on Amsterdam, 0 before) or the whole creation silently OOGs only on the -future fork. +future fork. Exception: `prepare_dispatch` charges it only when the +target's *pre-state* account is `EMPTY_ACCOUNT` — a prefunded create +address pays nothing (validated on +`test_out_of_gas_prefunded_contract_creation`, whose budgets omit the +term). A nested CREATE's new-account state is charged to the parent +before the 63/64 withhold and refunded if the child fails, so a derived +budget must cover its *peak* (use the composite `gas_cost(fork)`), even +on paths where the net is zero. **Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** Ported fillers probe "how much gas does a subcall receive when it asks for more diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index a3fe6dc3556..d172f98ee4c 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 130 +# Total entries: 123 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -117,15 +117,6 @@ stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] -# stInitCodeTest (7) -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g1] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g0] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g2] - # stMemExpandingEIP150Calls (3) stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py index f346844b949..a35d2a4453a 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py @@ -1,150 +1,132 @@ """ -Test_out_of_gas_contract_creation. +Verify a contract-creation transaction whose init code runs out of gas (or +halts on invalid code) leaves no account behind, while a sufficient budget +creates it. Ported from: state_tests/stInitCodeTest/OutOfGasContractCreationFiller.json + +@manually-enhanced: Do not overwrite. Both transaction budgets are derived +from the fork (intrinsic + the created account's top-frame state gas + the +init code's metadata-priced cost), so the insufficient arm keeps running +out mid-init-code and the sufficient arm keeps succeeding on every fork; +the success post pins the final storage value, not just the nonce. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +def storage_writes_initcode() -> Bytecode: + """Six stores to one slot: one cold set, then five dirty warm writes.""" + code = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + for value in range(2, 7): + code += Op.SSTORE( + key=0x1, + value=value, + key_warm=True, + original_value=0, + current_value=value - 1, + new_value=value, + ) + return code + + +def stack_underflow_initcode() -> Bytecode: + """The ported junk init code: CALLCODE underflows the stack.""" + return ( + Op.PUSH1[0xA] + + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) + + Op.PUSH1[0x0] + + Op.CALLCODE + + Op.STOP + + Op.PUSH1[0x1] + + Op.PUSH1[0x0] + + Op.BYTE(Op.DUP2, Op.CALLDATALOAD(offset=Op.DUP1)) + + Op.DUP2 + + Op.STOP + ) + + @pytest.mark.ported_from( ["state_tests/stInitCodeTest/OutOfGasContractCreationFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "invalid_initcode", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1", - ), + pytest.param(True, id="d0"), + pytest.param(False, id="d1"), + ], +) +@pytest.mark.parametrize( + "enough_gas", + [ + pytest.param(False, id="g0"), + pytest.param(True, id="g1"), ], ) def test_out_of_gas_contract_creation( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + invalid_initcode: bool, + enough_gas: bool, ) -> None: - """Test_out_of_gas_contract_creation.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + """An under-budgeted or invalid init code creates no account.""" + if invalid_initcode: + initcode = stack_underflow_initcode() + else: + initcode = storage_writes_initcode() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000000000, + # The insufficient budget runs out midway through the init code; the + # sufficient one covers it with margin. EIP-8037 charges the created + # account's state gas to the creation transaction's top frame. + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + # The sufficient margin must exceed the EIP-2200 stipend (2300), or + # the final SSTOREs of the init code fail their minimum-gas check. + initcode_cost = storage_writes_initcode().gas_cost(fork) + gas_limit = overhead + ( + initcode_cost + 5_000 if enough_gas else initcode_cost // 2 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address(address=sender, nonce=0): Account( - nonce=1 - ), - }, - }, - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.PUSH1[0xA] - + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.CALLCODE - + Op.STOP - + Op.PUSH1[0x1] - + Op.PUSH1[0x0] - + Op.BYTE(Op.DUP2, Op.CALLDATALOAD(offset=Op.DUP1)) - + Op.DUP2 - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.SSTORE(key=0x1, value=0x2) - + Op.SSTORE(key=0x1, value=0x3) - + Op.SSTORE(key=0x1, value=0x4) - + Op.SSTORE(key=0x1, value=0x5) - + Op.SSTORE(key=0x1, value=0x6), - ] - tx_gas = [56000, 150000] - tx_value = [1] - + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=1, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + if enough_gas and not invalid_initcode: + created_account: Account | type = Account( + nonce=1, code=b"", storage={1: 6}, balance=1 + ) + else: + # OOG / invalid init code: the creation is rolled back entirely. + created_account = Account.NONEXISTENT + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py index 93f7f151c9e..d3cc55522fa 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py @@ -1,137 +1,168 @@ """ -Test_out_of_gas_prefunded_contract_creation. +Verify a contract-creation transaction targeting a prefunded address, whose +init code CREATEs a value-bearing child: the budget decides whether the +outer creation fails (prefund untouched), the child fails (value stays), +or the child succeeds (one wei moves into it). Ported from: state_tests/stInitCodeTest/OutOfGasPrefundedContractCreationFiller.json + +@manually-enhanced: Do not overwrite. All three budgets are derived from +the fork (intrinsic + top-frame state gas + the composed init/child code +costs), and the child account is asserted, disambiguating the ported +"balance 1" outcomes (outer-failure vs child-success) that were previously +indistinguishable. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PREFUND = 1 +TX_VALUE = 1 +CHILD_VALUE = 1 +CHILD_STORED = 0x112233 + @pytest.mark.ported_from( [ "state_tests/stInitCodeTest/OutOfGasPrefundedContractCreationFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "outcome", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - pytest.param( - 0, - 2, - 0, - id="-g2", - ), + pytest.param("child_succeeds", id="g0"), + pytest.param("outer_oog", id="g1"), + pytest.param("child_oog", id="g2"), ], ) -@pytest.mark.pre_alloc_mutable def test_out_of_gas_prefunded_contract_creation( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + outcome: str, ) -> None: - """Test_out_of_gas_prefunded_contract_creation.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Budget decides how deep a prefunded creation's child CREATE gets.""" + # Child init code: one cold store, deposits nothing. + child_code = ( + Op.SSTORE( + key=0x0, + value=CHILD_STORED, + key_warm=False, + original_value=0, + new_value=CHILD_STORED, + ) + + Op.STOP * 2 ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000000, + # Outer init code: copy the child init code from its own tail, then + # CREATE a value-bearing child from it; deposits nothing. The copy + # window and memory usage stay within one word. + inner_create = Op.CREATE( + value=CHILD_VALUE, + offset=0x0, + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), ) - - pre[sender] = Account(balance=0xF424000) - # Source: hex - # 0x - contract_0 = pre.deploy_contract( # noqa: F841 - code="", - balance=1, - nonce=0, - address=Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F), # noqa: E501 + prefix = Op.CODECOPY( + dest_offset=0x0, + offset=0x1A, # placeholder; recomputed below + size=len(child_bytes), + data_size=len(child_bytes), + new_memory_size=0x20, ) + body = prefix + Op.POP(inner_create) + Op.STOP + # The child code sits immediately after the executable body. + initcode_prefix_len = len(bytes(body)) + prefix = Op.CODECOPY( + dest_offset=0x0, + offset=initcode_prefix_len, + size=len(child_bytes), + data_size=len(child_bytes), + new_memory_size=0x20, + ) + body = prefix + Op.POP(inner_create) + Op.STOP + assert len(bytes(body)) == initcode_prefix_len, "stable code layout" + initcode = body + child_code - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": [0, 1], "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(balance=1), - }, - }, - { - "indexes": {"data": -1, "gas": [2], "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(balance=2), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Fork-derived budgets. The prefunded target is not EMPTY_ACCOUNT in + # the pre-state, so EIP-8037 charges no top-frame new-account state + # gas for this creation — an Amsterdam behavior this test pins. The + # inner CREATE's composite cost covers its peak charge (its + # new-account state gas is refunded if the child fails, but must be + # affordable when charged). + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + + prefix.gas_cost(fork) + + inner_create.gas_cost(fork) + ) + child_needed = child_code.gas_cost(fork) + if outcome == "outer_oog": + # Dies charging the inner CREATE. + gas_limit = overhead - inner_create.gas_cost(fork) // 2 + elif outcome == "child_oog": + # Outer completes; the child's 63/64 grant undercuts its cost. + gas_limit = overhead + child_needed // 2 + else: + # Child completes too and keeps the transferred wei. + gas_limit = overhead + -(-child_needed * 64 // 63) + 2_000 - tx_data = [ - Op.PUSH1[0x9] - + Op.CODECOPY(dest_offset=0x0, offset=0x11, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.POP(Op.CREATE) - + Op.STOP * 2 - + Op.INVALID - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP * 2, - ] - tx_gas = [154000, 65000, 95000] - tx_value = [1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre.fund_address(created, PREFUND) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + child = compute_create_address(address=created, nonce=1) + if outcome == "outer_oog": + # Creation rolled back: only the prefund remains, nonce untouched. + created_account = Account(nonce=0, balance=PREFUND) + child_account: Account | type = Account.NONEXISTENT + elif outcome == "child_oog": + # The inner CREATE increments the creator's nonce even when the + # child fails. + created_account = Account( + nonce=2, code=b"", balance=PREFUND + TX_VALUE + ) + child_account = Account.NONEXISTENT + else: + created_account = Account( + nonce=2, code=b"", balance=PREFUND + TX_VALUE - CHILD_VALUE + ) + child_account = Account( + nonce=1, + balance=CHILD_VALUE, + storage={0: CHILD_STORED}, + ) + + post = { + sender: Account(nonce=1), + created: created_account, + child: child_account, + } + + state_test(pre=pre, post=post, tx=tx) From d434aacb68a758b63b6bb51e71bf576a7f0a9fad Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:54:03 +0200 Subject: [PATCH 08/25] fix(tests): un-skip Amsterdam EIP-150 create/delegate/oog-cascade ported tests --- tests/ported_static/amsterdam_skip_list.txt | 7 +- .../test_create_and_gas_inside_create.py | 123 ++++++++---- .../test_delegate_call_on_eip.py | 113 ++++++----- ...n_second_level_with_mem_expanding_calls.py | 180 ++++++++++++------ ..._inside_create_with_mem_expanding_calls.py | 150 +++++++++++---- 5 files changed, 390 insertions(+), 183 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index d172f98ee4c..6cc835d20c9 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 123 +# Total entries: 119 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -106,7 +106,7 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP150Specific (3) +# stEIP150Specific (1) stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] @@ -117,8 +117,7 @@ stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] -# stMemExpandingEIP150Calls (3) -stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] +# stMemExpandingEIP150Calls (1) stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] diff --git a/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py b/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py index 0bcf52c7d85..47a20e3a725 100644 --- a/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py +++ b/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py @@ -1,17 +1,23 @@ """ -Test_create_and_gas_inside_create. +Verify the gas a CREATE's init code observes: the child receives all but +one 64th of what remains in the creating frame, and the parent's CREATE +cost is measured alongside it. Ported from: state_tests/stEIP150Specific/CreateAndGasInsideCreateFiller.json + +@manually-enhanced: Do not overwrite. An outer call pins the creating +frame's budget so the child's stored GAS observation is fork-derived +(`63/64` of the derived base); the parent measures the CREATE with +CodeGasMeasure instead of raw snapshots. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,58 +27,105 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0xB +GAS_SLOT = 0x9 +CHILD_GAS_SLOT = 0xFD + +# The creating frame's pinned budget (the ported transaction's). +CALLER_GAS = 600_000 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/CreateAndGasInsideCreateFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_and_gas_inside_create( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_and_gas_inside_create.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes into its own storage + # and deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The child bytes sit right-aligned in the first memory word. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, + ) + creator = pre.deploy_contract( + code=setup + + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), ) - # Source: lll - # { [100] (GAS) (MSTORE 0 0x5a60fd55) (SSTORE 11 (CREATE 0 28 4)) (SSTORE 9 (SUB @100 (GAS))) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x64, value=Op.GAS) - + Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.SSTORE(key=0xB, value=Op.CREATE(value=0x0, offset=0x1C, size=0x4)) - + Op.SSTORE(key=0x9, value=Op.SUB(Op.MLOAD(offset=0x64), Op.GAS)) + # The outer call pins the creating frame's budget so the child's + # observation does not depend on the tx gas limit. + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=creator)) + Op.STOP, - nonce=0, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, + ) + + # The child receives all but one 64th of what remains after the + # setup, the measuring GAS read, and the CREATE's own charges (its + # new-account state gas is taken before the withhold). + base = ( + CALLER_GAS + - setup.gas_cost(fork) + - Op.GAS.gas_cost(fork) + - create_code.gas_cost(fork) ) + assert base > 0, "CALLER_GAS must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) + measured_create = create_store.gas_cost(fork) + child_code.gas_cost(fork) + created = compute_create_address(address=creator, nonce=1) post = { - contract_0: Account( + entry: Account(storage={0: 1}), + creator: Account( storage={ - 9: 0x129DB, - 11: compute_create_address(address=contract_0, nonce=0), + ADDRESS_SLOT: created, + GAS_SLOT: measured_create, }, ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={253: 0x83729} + created: Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py b/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py index 5156516411b..89d33ae0248 100644 --- a/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py +++ b/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py @@ -1,17 +1,23 @@ """ -Test_delegate_call_on_eip. +Measure a DELEGATECALL that asks for more gas than its frame holds: the +EIP-150 clamp decides the grant, the delegate writes into the caller's +storage, and the measured cost is the call plus the delegate's work. Ported from: state_tests/stEIP150Specific/DelegateCallOnEIPFiller.json + +@manually-enhanced: Do not overwrite. An outer call pins the frame budget +so the oversized ask always clamps; the DELEGATECALL is measured with +CodeGasMeasure (success flag inside the window) and the expectation is the +composite plus the delegate's fork-priced store. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, ) @@ -20,62 +26,79 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +DELEGATE_VALUE = 0x12 +FLAG_SLOT = 0x9 +GAS_SLOT = 0x8 + +# The ported ask (600000): above the pinned frame budget, so the EIP-150 +# clamp decides the grant on every fork. +ASK_GAS = 0x927C0 +CALLER_GAS = 400_000 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/DelegateCallOnEIPFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_delegate_call_on_eip( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_delegate_call_on_eip.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Measure a clamped DELEGATECALL running a store in the caller.""" + # Runs in the caller's storage context: one cold fresh store. + delegate_store = Op.SSTORE( + key=0x0, + value=DELEGATE_VALUE, + key_warm=False, + original_value=0, + new_value=DELEGATE_VALUE, ) + delegate = pre.deploy_contract(code=delegate_store + Op.STOP) - # Source: lll - # { (SSTORE 0 0x12) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x12) + Op.STOP, - nonce=0, + delegatecall_code = Op.DELEGATECALL( + gas=ASK_GAS, + address=delegate, + address_warm=False, ) - # Source: lll - # { [8] (GAS) (SSTORE 9 (DELEGATECALL 600000 0 0 0 0)) [[8]] (SUB @8 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.DELEGATECALL( - gas=0x927C0, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x8, value=Op.SUB(Op.MLOAD(offset=0x8), Op.GAS)) + flag_store = Op.SSTORE( + key=FLAG_SLOT, + value=delegatecall_code, + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=CodeGasMeasure( + code=flag_store, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), + ) + + assert CALLER_GAS < ASK_GAS, "the 63/64 clamp must apply" + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=target)) + Op.STOP, - nonce=0, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, ) - post = {target: Account(storage={0: 18, 8: 46841, 9: 1})} + measured = flag_store.gas_cost(fork) + delegate_store.gas_cost(fork) + + post = { + entry: Account(storage={0: 1}), + target: Account( + storage={ + 0: DELEGATE_VALUE, + GAS_SLOT: measured, + FLAG_SLOT: 1, + }, + ), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py index 5f31ce42a8a..5163dea2f5e 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py @@ -1,17 +1,23 @@ """ -Test_call_goes_oog_on_second_level_with_mem_expanding_calls. +Verify a two-level call chain (with memory-expanding call windows) where +the second-level frame runs out of gas: its own frame and everything below +it revert, while the top frame survives and records the failure. Ported from: state_tests/stMemExpandingEIP150Calls/CallGoesOOGOnSecondLevelWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The first-level budget is pinned and +derived from the fork so the second level keeps starving on every fork +(its 1/64 retention cannot afford the post-call store); the second-level +ask stays oversized; the top frame's entry snapshot is derived and pins +the transaction intrinsic. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,88 +26,142 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +SNAPSHOT_SLOT = 0x8 +FLAG_SLOT = 0x9 +# The ported second-level ask: far above the pinned budget. +ASK_GAS = 0x927C0 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CallGoesOOGOnSecondLevelWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_goes_oog_on_second_level_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_goes_oog_on_second_level_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + """A starved second-level frame reverts itself and everything below.""" + # Deepest contract: snapshots and creates twice; its cost anchors the + # starvation budget. + deep_snapshot = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, ) - - # Source: hex - # 0x5a600855600060006000f050600060006000f0505a6009555a600a55 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + deep = pre.deploy_contract( + code=deep_snapshot + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) * 2 - + Op.SSTORE(key=0x9, value=Op.GAS) + + Op.SSTORE(key=FLAG_SLOT, value=Op.GAS) + Op.SSTORE(key=0xA, value=Op.GAS), - nonce=0, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + + # Second level: snapshots, then asks far more than it holds; the ported + # memory-expanding argument window is kept. + mid = pre.deploy_contract( + code=Op.SSTORE(key=SNAPSHOT_SLOT, value=Op.GAS) + Op.SSTORE( - key=0x9, + key=FLAG_SLOT, value=Op.CALL( - gas=0x927C0, - address=addr, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=ASK_GAS, + address=deep, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, ), ), - nonce=0, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr_2, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, - ), + + # Pin the second level's budget so it starves on every fork: enough + # to pay its own snapshot and call, but its grant to the deep frame + # undercuts the deep frame's first store, and its 1/64 retention + # cannot afford its own post-call flag store. + mid_snapshot_cost = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + mid_call_cost = Op.CALL( + gas=ASK_GAS, + address=deep, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, + ).gas_cost(fork) + deep_needed = deep_snapshot.gas_cost(fork) + caller_gas = mid_snapshot_cost + mid_call_cost + deep_needed // 2 + assert caller_gas < ASK_GAS, "the second-level ask must exceed its frame" + + # Top frame: derived entry snapshot (pins the intrinsic), the pinned + # call, and the failure flag. + entry_snapshot = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + flag_store = Op.SSTORE( + key=FLAG_SLOT, + value=Op.CALL( + gas=caller_gas, + address=mid, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, ), - nonce=0, + key_warm=False, + original_value=0, + new_value=0, + ) + target = pre.deploy_contract( + code=entry_snapshot + flag_store + Op.STOP, + ) + + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + entry_snapshot.gas_cost(fork) + + flag_store.gas_cost(fork) + + caller_gas + + 5_000 ) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=220000, + gas_limit=gas_limit, ) post = { - sender: Account(nonce=1), - target: Account(storage={8: 0x30956}), - addr_2: Account(storage={}), - addr: Account(storage={}), + # The failed call's flag slot stays zero; the entry snapshot pins + # the intrinsic. + target: Account( + storage={ + SNAPSHOT_SLOT: gas_limit - intrinsic - Op.GAS.gas_cost(fork), + }, + ), + # Both lower frames reverted entirely. + mid: Account(storage={}), + deep: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py index 705e9e760f9..74f0be4227b 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py @@ -1,17 +1,22 @@ """ -Test_create_and_gas_inside_create_with_mem_expanding_calls. +Verify the gas a CREATE's init code observes when the creating frame also +expands memory: the child receives all but one 64th of what remains, and +the creating frame's entry and post-CREATE gas readings are asserted. Ported from: state_tests/stMemExpandingEIP150Calls/CreateAndGasInsideCreateWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode is kept, but the +transaction budget and every stored gas reading (entry snapshot, child +observation, post-CREATE reading) are derived from the fork instead of +pinned — the entry snapshot doubles as a transaction-intrinsic pin. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,62 +26,129 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ENTRY_GAS_SLOT = 0xA +ADDRESS_SLOT = 0xB +AFTER_GAS_SLOT = 0x9 +CHILD_GAS_SLOT = 0xFD + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CreateAndGasInsideCreateWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_and_gas_inside_create_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_and_gas_inside_create_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes, deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, ) + child_bytes = bytes(child_code) - # Source: hex - # 0x5a600a55635a60fd556000526004601c6000f0600b555a600955 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0xA, value=Op.GAS) - + Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.SSTORE(key=0xB, value=Op.CREATE(value=0x0, offset=0x1C, size=0x4)) - + Op.SSTORE(key=0x9, value=Op.GAS), - nonce=0, + entry_snapshot = Op.SSTORE( + key=ENTRY_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, + ) + after_snapshot = Op.SSTORE( + key=AFTER_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + creator = pre.deploy_contract( + code=entry_snapshot + setup + create_store + after_snapshot + Op.STOP, ) + # Fork-derived budget: the ported 600000 no longer covers the three + # state-priced stores plus the CREATE under EIP-8037. The margin + # keeps the final store above the EIP-2200 stipend. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + tx_gas = ( + intrinsic + + entry_snapshot.gas_cost(fork) + + setup.gas_cost(fork) + + create_store.gas_cost(fork) + + child_code.gas_cost(fork) + + after_snapshot.gas_cost(fork) + + 5_000 + ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=creator, + gas_limit=tx_gas, ) + # Entry reading: everything after the intrinsic, minus the GAS opcode + # itself (it executes first in the store's operand order). + entry_observed = tx_gas - intrinsic - Op.GAS.gas_cost(fork) + # The child receives all but one 64th of what remains after the entry + # store, the setup, and the CREATE's own charges. + base = ( + tx_gas + - intrinsic + - entry_snapshot.gas_cost(fork) + - setup.gas_cost(fork) + - create_code.gas_cost(fork) + ) + assert base > 0, "the budget must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) + # After the CREATE: the child's consumption and the address store are + # gone; the address store's own cost is the composite minus the + # CREATE it wraps. + after_observed = ( + base + - child_code.gas_cost(fork) + - (create_store.gas_cost(fork) - create_code.gas_cost(fork)) + - Op.GAS.gas_cost(fork) + ) + + created = compute_create_address(address=creator, nonce=1) post = { - sender: Account(nonce=1), - contract_0: Account( + creator: Account( storage={ - 9: 0x75596, - 10: 0x8D5B6, - 11: compute_create_address(address=contract_0, nonce=0), + ENTRY_GAS_SLOT: entry_observed, + ADDRESS_SLOT: created, + AFTER_GAS_SLOT: after_observed, }, - nonce=1, ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={253: 0x7E23D} + created: Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 481ec5469b47e41026e583e0403e8532a0827d8a Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 15:59:33 +0200 Subject: [PATCH 09/25] fix(tests): un-skip Amsterdam name-registrator creation ported tests --- tests/ported_static/amsterdam_skip_list.txt | 7 +- ...name_registrator_per_txs_not_enough_gas.py | 150 +++++++++--------- ...e_registrator_pre_store1_not_enough_gas.py | 134 +++++++++++----- 3 files changed, 171 insertions(+), 120 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 6cc835d20c9..f2043f1dfbf 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 119 +# Total entries: 116 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -18,16 +18,13 @@ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] -# stCallCreateCallCodeTest (9) +# stCallCreateCallCodeTest (6) stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g3] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py::test_create_name_registrator_pre_store1_not_enough_gas[fork_Amsterdam] # stCallDelegateCodesCallCodeHomestead (1) stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py::test_callcallcallcode_001_suicide_end[fork_Amsterdam] diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py index 87fb08afa78..7e3bd9b3473 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py @@ -1,104 +1,71 @@ """ -Legacy Test from Christoph. J. +Verify a name-registrator contract creation succeeds or fails with the +transaction budget: the init code writes a storage slot and deposits the +registrar's runtime code. Ported from: state_tests/stCallCreateCallCodeTest/createNameRegistratorPerTxsNotEnoughGasFiller.json + +@manually-enhanced: Do not overwrite. Both budgets are derived from the +fork (intrinsic + top-frame state gas + init code execution + code deposit +regular and state costs); the success arm also pins the deposited code and +transferred balance, which the ported post never checked. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 100_000 +COPY_OFFSET = 0xC +DEPOSITED_SIZE = 0x10 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/createNameRegistratorPerTxsNotEnoughGasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "enough_gas", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(False, id="g0"), + pytest.param(True, id="g1"), ], ) def test_create_name_registrator_per_txs_not_enough_gas( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + enough_gas: bool, ) -> None: - """Legacy Test from Christoph.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000000, + """An under-budgeted registrar creation leaves no account behind.""" + # The ported init code: write slot 1, then deposit 16 bytes of + # registrar runtime copied from the init code's own bytes. + store = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address(address=sender, nonce=0): Account( - storage={1: 1} - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1) - + Op.PUSH1[0x10] - + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) + initcode = ( + store + + Op.PUSH1[DEPOSITED_SIZE] + + Op.CODECOPY( + dest_offset=0x0, + offset=COPY_OFFSET, + size=Op.DUP1, + data_size=DEPOSITED_SIZE, + new_memory_size=0x20, + ) + Op.PUSH1[0x0] + Op.RETURN + Op.STOP @@ -109,19 +76,50 @@ def test_create_name_registrator_per_txs_not_enough_gas( + Op.STOP + Op.JUMPDEST + Op.SSTORE( - key=Op.CALLDATALOAD(offset=0x0), value=Op.CALLDATALOAD(offset=0x20) - ), - ] - tx_gas = [56157, 86157] - tx_value = [100000] + key=Op.CALLDATALOAD(offset=0x0), + value=Op.CALLDATALOAD(offset=0x20), + ) + ) + deposited = bytes(initcode)[COPY_OFFSET : COPY_OFFSET + DEPOSITED_SIZE] + + # Fork-derived budgets: the sufficient one covers the init code, the + # code deposit (regular and EIP-8037 state), and the created account's + # top-frame state gas; the insufficient one dies mid-init-code. + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + execution_cost = ( + initcode.gas_cost(fork) + + DEPOSITED_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=DEPOSITED_SIZE) + ) + gas_limit = overhead + ( + execution_cost + 5_000 if enough_gas else execution_cost // 2 + ) + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + if enough_gas: + created_account: Account | type = Account( + nonce=1, + code=deposited, + balance=TX_VALUE, + storage={1: 1}, + ) + else: + created_account = Account.NONEXISTENT + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py index cc76b3587f5..212b244d922 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py @@ -1,17 +1,22 @@ """ -Legacy Test from Christoph. J. +Verify a nested CREATE of the name registrar whose child grant cannot cover +the init code: the child account never materializes, while the creating +frame completes (its nonce still advances). Ported from: state_tests/stCallCreateCallCodeTest/createNameRegistratorPreStore1NotEnoughGasFiller.json + +@manually-enhanced: Do not overwrite. The registrar init code is composed +(not a hex blob) and the transaction budget is derived from the fork so +the child's 63/64 grant undercuts its cost on every fork; the creator's +balance is asserted (the endowment returns on failure). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,60 +26,111 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 0x186A0 +CREATE_VALUE = 0x17 +INITIAL_BALANCE = 10**15 +COPY_OFFSET = 0xC +DEPOSITED_SIZE = 0x10 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/createNameRegistratorPreStore1NotEnoughGasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_name_registrator_pre_store1_not_enough_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Legacy Test from Christoph.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, + """A starved nested registrar creation leaves no account behind.""" + # The registrar init code (same as the per-txs sibling): write slot 1, + # deposit 16 bytes of runtime copied from its own bytes. + initcode = ( + Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.PUSH1[DEPOSITED_SIZE] + + Op.CODECOPY( + dest_offset=0x0, + offset=COPY_OFFSET, + size=Op.DUP1, + data_size=DEPOSITED_SIZE, + new_memory_size=0x20, + ) + + Op.PUSH1[0x0] + + Op.RETURN + + Op.STOP + + Op.JUMPI( + pc=0x9, + condition=Op.ISZERO(Op.SLOAD(key=Op.CALLDATALOAD(offset=0x0))), + ) + + Op.STOP + + Op.JUMPDEST + + Op.SSTORE( + key=Op.CALLDATALOAD(offset=0x0), + value=Op.CALLDATALOAD(offset=0x20), + ) ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) == 0x22, "ported init code is 34 bytes" - # Source: lll - # {(MSTORE 0 0x6001600155601080600c6000396000f3006000355415600957005b6020356000 ) (MSTORE8 32 0x35) (MSTORE8 33 0x55) (CREATE 23 0 34) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( + # Memory setup derived from the composed bytes (one word plus two + # trailing byte stores, as in the ported filler). + setup = ( + Op.MSTORE( offset=0x0, - value=0x6001600155601080600C6000396000F3006000355415600957005B6020356000, # noqa: E501 + value=int.from_bytes(initcode_bytes[:0x20], "big"), + new_memory_size=0x20, ) - + Op.MSTORE8(offset=0x20, value=0x35) - + Op.MSTORE8(offset=0x21, value=0x55) - + Op.CREATE(value=0x17, offset=0x0, size=0x22) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, + + Op.MSTORE8( + offset=0x20, value=initcode_bytes[0x20], new_memory_size=0x40 + ) + + Op.MSTORE8( + offset=0x21, value=initcode_bytes[0x21], new_memory_size=0x40 + ) + ) + create_code = Op.CREATE( + value=CREATE_VALUE, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), + ) + creator = pre.deploy_contract( + code=setup + Op.POP(create_code) + Op.STOP, + balance=INITIAL_BALANCE, + ) + + # Budget: covers the frame's own work and the CREATE's peak charge, + # but the child's 63/64 grant undercuts the init code plus deposit. + child_needed = ( + initcode.gas_cost(fork) + + DEPOSITED_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=DEPOSITED_SIZE) + ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + setup.gas_cost(fork) + + create_code.gas_cost(fork) + + child_needed // 2 ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=73071, - value=0x186A0, + sender=pre.fund_eoa(), + to=creator, + gas_limit=gas_limit, + value=TX_VALUE, ) post = { - contract_0: Account(nonce=1), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, + # The CREATE advanced the nonce even though its child failed, and + # the endowment returned. + creator: Account(nonce=2, balance=INITIAL_BALANCE + TX_VALUE), + compute_create_address(address=creator, nonce=1): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 012f6067981e490a9ad68cea364eee0e59c27a83 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 16:09:00 +0200 Subject: [PATCH 10/25] fix(tests): un-skip Amsterdam gas-price-for-codes and memory OOG ported tests --- .claude/commands/enhance-ported-test.md | 14 + tests/ported_static/amsterdam_skip_list.txt | 15 +- .../test_new_gas_price_for_codes.py | 242 ++++++++++------ ...rice_for_codes_with_mem_expanding_calls.py | 261 ++++++++++++------ tests/ported_static/stMemoryTest/test_oog.py | 27 +- 5 files changed, 371 insertions(+), 188 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 46041c1da2a..5aafcc152b5 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -386,6 +386,20 @@ previous_bytes=)`; EIP-3860 init-code words → `fork.gas_costs().CODE_INIT_PER_ * ceil(size/32)`. You can also call `.gas_cost` / `.regular_cost` / `.state_cost` on exactly the measured bytecode. +**Reservoir-less sub-calls pay state gas from their regular grant.** With +the tx reservoir at 0, a sub-frame's state charges spill from its own +`gas_left` — a delegate that does one first-set SSTORE needs its *whole* +~111k inside the forwarded grant on Amsterdam, not just the ~13k regular +part. Size derived sub-call budgets from the callee composite's full +`gas_cost(fork)`. Corollaries: (a) a *failed* sub-frame contributes its +entire forfeited grant to the parent's measured window, not its "cost"; +(b) `SSTORE(flag, )` silently degrades to a ~3k no-op store when +the call fails — the flag reads 0 and no state gas is charged, which can +mask a broken callee behind a plausible-looking measurement. Validated on +`test_new_gas_price_for_codes` (delegate budget derived; failed value +calls return their stipends: subtract one `CALL_STIPEND` per failed +value-bearing call from window measurements). + **Nested / callee-side measurements.** When the measured op is a `CALL` whose callee does real work, the measured cost = `call_code.gas_cost(fork) + callee_code.gas_cost(fork)` (the CALL's own cost plus what the callee consumed). diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index f2043f1dfbf..2ff669c7937 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 116 +# Total entries: 112 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -103,25 +103,12 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP150Specific (1) -stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] -stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] -stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] - # stEIP158Specific (1) stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] -# stMemExpandingEIP150Calls (1) -stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] -stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] - -# stMemoryTest (2) -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success14] -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success15] - # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] diff --git a/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py b/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py index 0be45107816..6ed52393b2d 100644 --- a/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py +++ b/tests/ported_static/stEIP150Specific/test_new_gas_price_for_codes.py @@ -1,18 +1,25 @@ """ -Test_new_gas_price_for_codes. +Verify the EIP-150 repriced code/account operations in one frame: +EXTCODESIZE, EXTCODECOPY, SLOAD, failing value CALL/CALLCODE (insufficient +balance), DELEGATECALL that writes the caller's storage, a call to a +nonexistent account, BALANCE, and the whole window's measured gas. Ported from: state_tests/stEIP150Specific/NewGasPriceForCodesFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode shape is kept, +but the window delta, the mid-execution sender balance, and the copied +code word are derived (opcode metadata, fee formula, the deployed bytes); +the delegate's budget is derived so its store — state-priced under +EIP-8037 — fits inside the grant (a reservoir-less sub-call pays state +gas from its regular grant); each failed value call returns its stipend. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,133 +28,196 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +EXTCODE_BYTES = bytes.fromhex( + "1122334455667788991011121314151617181920212223242526272829303132" +) +COPY_SIZE = 0x14 +DELEGATE_VALUE = 0x11 +# Budget for the calls whose outcome does not depend on it (the value +# calls fail on insufficient balance; the absent target runs nothing). +FORWARDED_GAS = 0x7530 +GAS_PRICE = 10 +INITIAL_BALANCE = 10**15 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/NewGasPriceForCodesFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_new_gas_price_for_codes( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_new_gas_price_for_codes.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 + """Measure a frame exercising every repriced code/account operation.""" + sender = pre.fund_eoa(amount=INITIAL_BALANCE) + code_target = pre.deploy_contract(code=EXTCODE_BYTES, balance=111) + delegate_store = Op.SSTORE( + key=0x64, + value=DELEGATE_VALUE, + key_warm=False, + original_value=0, + new_value=DELEGATE_VALUE, ) + storage_writer = pre.deploy_contract(code=delegate_store + Op.STOP) + absent = pre.nonexistent_account() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + # The delegate must succeed: with a zero reservoir its state-priced + # store is paid from the regular grant, so the budget is derived. + delegate_budget = delegate_store.gas_cost(fork) + 2_000 - pre[sender] = Account(balance=0xE8D4A51000) - # Source: raw - # 0x1122334455667788991011121314151617181920212223242526272829303132 - addr = pre.deploy_contract( # noqa: F841 - code=bytes.fromhex( - "1122334455667788991011121314151617181920212223242526272829303132" - ), - balance=111, - nonce=0, - address=Address(0xC572A70AFAAB9D01D0A2AFB855BFBAFB47C8211B), # noqa: E501 - ) - # Source: lll - # { (SSTORE 100 0x11) } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x64, value=0x11) + Op.STOP, - nonce=0, - address=Address(0xAD9D325B811CB0701839C07C6F139F3799476798), # noqa: E501 - ) - # Source: lll - # { [999] (GAS) (SSTORE 1 (EXTCODESIZE )) (EXTCODECOPY 0 0 20) (SSTORE 2 (MLOAD 0)) (SSTORE 4 (SLOAD 0)) (SSTORE 5 (CALL 30000 1 0 0 0 0)) (SSTORE 6 (CALLCODE 30000 1 0 0 0 0)) (SSTORE 7 (DELEGATECALL 30000 0 0 0 0)) (SSTORE 8 (CALL 30000 0x1000000000000000000000000000000000000013 0 0 0 0 0)) (SSTORE 3 (BALANCE )) (SSTORE 10 (SUB (MLOAD 999) (GAS))) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x3E7, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.EXTCODESIZE(address=addr)) - + Op.EXTCODECOPY(address=addr, dest_offset=0x0, offset=0x0, size=0x14) - + Op.SSTORE(key=0x2, value=Op.MLOAD(offset=0x0)) - + Op.SSTORE(key=0x4, value=Op.SLOAD(key=0x0)) + # The measured window: entry GAS snapshot through the closing GAS. + # The value-bearing CALL and CALLCODE fail on insufficient balance + # (this contract holds nothing), costing their access and transfer + # charges minus the returned stipend; the DELEGATECALL runs the + # writer against this contract's storage. + window = ( + Op.MSTORE(offset=0x3E7, value=Op.GAS, new_memory_size=0x407) + + Op.SSTORE( + key=0x1, + value=Op.EXTCODESIZE(address=code_target, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.EXTCODECOPY( + address=code_target, + dest_offset=0x0, + offset=0x0, + size=COPY_SIZE, + address_warm=True, + data_size=COPY_SIZE, + new_memory_size=0x407, + old_memory_size=0x407, + ) + + Op.SSTORE( + key=0x2, + value=Op.MLOAD(offset=0x0), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x4, + value=Op.SLOAD(key=0x0, key_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + Op.SSTORE( key=0x5, value=Op.CALL( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + address_warm=False, + value_transfer=True, + account_new=False, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x6, value=Op.CALLCODE( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + address_warm=True, + value_transfer=True, + account_new=False, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x7, value=Op.DELEGATECALL( - gas=0x7530, - address=addr_2, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + gas=delegate_budget, + address=storage_writer, + address_warm=True, ), + key_warm=False, + original_value=0, + new_value=1, ) + Op.SSTORE( key=0x8, value=Op.CALL( - gas=0x7530, - address=0x1000000000000000000000000000000000000013, + gas=FORWARDED_GAS, + address=absent, value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + address_warm=False, + value_transfer=False, + account_new=False, ), + key_warm=False, + original_value=0, + new_value=1, ) - + Op.SSTORE(key=0x3, value=Op.BALANCE(address=sender)) - + Op.SSTORE(key=0xA, value=Op.SUB(Op.MLOAD(offset=0x3E7), Op.GAS)) - + Op.STOP, + + Op.SSTORE( + key=0x3, + value=Op.BALANCE(address=sender, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + ) + delta_store = Op.SSTORE( + key=0xA, + value=Op.SUB(Op.MLOAD(offset=0x3E7), Op.GAS), + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=window + delta_store + Op.STOP, storage={0: 18}, - nonce=0, - address=Address(0xFD9AFC8315A88141164E2A753157EA3E0F72C707), # noqa: E501 ) + # Window delta: everything from the entry GAS read to the closing + # one; the lead GAS and the closing GAS cancel out of the composite, + # the delegate's work is added on top, and each failed value call + # hands back its stipend along with the unused grant. + measured = ( + window.gas_cost(fork) + + delegate_store.gas_cost(fork) + - 2 * fork.gas_costs().CALL_STIPEND + ) + + # Fork-derived budget with an EIP-2200 stipend margin for the + # trailing delta store. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = intrinsic + measured + delta_store.gas_cost(fork) + 5_000 + tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + copied_word = int.from_bytes( + EXTCODE_BYTES[:COPY_SIZE].ljust(0x20, b"\x00"), "big" + ) post = { target: Account( storage={ - 0: 18, - 1: 32, - 2: 0x1122334455667788991011121314151617181920000000000000000000000000, # noqa: E501 - 3: 0xE8D4498280, - 4: 18, - 7: 1, - 8: 1, - 10: 0x2CB0A, - 100: 17, + 0x0: 18, + 0x1: len(EXTCODE_BYTES), + 0x2: copied_word, + # Mid-execution balance: the full fee is charged upfront. + 0x3: INITIAL_BALANCE - gas_limit * GAS_PRICE, + 0x4: 18, + # Slots 5 and 6 stay zero: the value calls failed. + 0x7: 1, + 0x8: 1, + 0xA: measured, + 0x64: DELEGATE_VALUE, }, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py index 47aed815167..3a6aabd3bc2 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py @@ -1,18 +1,26 @@ """ -Test_new_gas_price_for_codes_with_mem_expanding_calls. +Verify the EIP-150 repriced code/account operations in one frame whose +calls also expand memory: EXTCODESIZE, EXTCODECOPY, failing value +CALL/CALLCODE (insufficient balance), DELEGATECALL that writes the +caller's storage, a call to a nonexistent account, BALANCE, and a final +raw gas reading that pins the whole execution. Ported from: state_tests/stMemExpandingEIP150Calls/NewGasPriceForCodesWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode shape is kept, +but the final gas reading, the mid-execution sender balance, and the +copied code word are derived (opcode metadata, fee formula, the deployed +bytes); the delegate's budget is derived so its store — state-priced +under EIP-8037 — fits inside the grant; each failed value call returns +its stipend. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,135 +29,216 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +EXTCODE_BYTES = bytes.fromhex( + "1122334455667788991011121314151617181920212223242526272829303132" +) +COPY_SIZE = 0x14 +DELEGATE_VALUE = 0x11 +# Budget for the calls whose outcome does not depend on it. +FORWARDED_GAS = 0x7530 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF +GAS_PRICE = 10 +INITIAL_BALANCE = 10**15 + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/NewGasPriceForCodesWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_new_gas_price_for_codes_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_new_gas_price_for_codes_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x3956FC06BD55836ACDB92DA0E38A15F2E568C088022CF2278180477F3F7702A + """Measure repriced operations with memory-expanding call windows.""" + sender = pre.fund_eoa(amount=INITIAL_BALANCE) + code_target = pre.deploy_contract(code=EXTCODE_BYTES, balance=111) + delegate_store = Op.SSTORE( + key=0x64, + value=DELEGATE_VALUE, + key_warm=False, + original_value=0, + new_value=DELEGATE_VALUE, ) + storage_writer = pre.deploy_contract(code=delegate_store) + absent = pre.nonexistent_account() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + # The delegate must succeed: with a zero reservoir its state-priced + # store is paid from the regular grant, so the budget is derived. + delegate_budget = delegate_store.gas_cost(fork) + 2_000 - pre[sender] = Account(balance=0xE8D4A5100000) - # Source: hex - # 0x1122334455667788991011121314151617181920212223242526272829303132 - addr = pre.deploy_contract( # noqa: F841 - code=bytes.fromhex( - "1122334455667788991011121314151617181920212223242526272829303132" - ), - balance=111, - nonce=0, - address=Address(0x6B6AF3C6E1714081C8C3085ACBAC8C2B21FADF0B), # noqa: E501 - ) - # Source: hex - # 0x6011606455 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x64, value=0x11), - nonce=0, - address=Address(0x7B8C83E74CC8DFADB03138C2743C70588ACE4222), # noqa: E501 - ) - # Source: hex - # 0x733b600155601460006000733c60005160025560005460045560ff60ff60ff60ff600173617530f160055560ff60ff60ff60ff600173617530f260065560ff60ff60ff60ff73617530f460075560ff60ff60ff60ff6000731000000000000000000000000000000000000013617530f160085573316003555a600a55 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=Op.EXTCODESIZE(address=addr)) - + Op.EXTCODECOPY(address=addr, dest_offset=0x0, offset=0x0, size=0x14) - + Op.SSTORE(key=0x2, value=Op.MLOAD(offset=0x0)) - + Op.SSTORE(key=0x4, value=Op.SLOAD(key=0x0)) + call_window = MEM_OFFSET + MEM_SIZE + body = ( + Op.SSTORE( + key=0x1, + value=Op.EXTCODESIZE(address=code_target, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.EXTCODECOPY( + address=code_target, + dest_offset=0x0, + offset=0x0, + size=COPY_SIZE, + address_warm=True, + data_size=COPY_SIZE, + new_memory_size=0x20, + ) + + Op.SSTORE( + key=0x2, + value=Op.MLOAD(offset=0x0), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x4, + value=Op.SLOAD(key=0x0, key_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + Op.SSTORE( key=0x5, value=Op.CALL( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + value_transfer=True, + account_new=False, + new_memory_size=call_window, + old_memory_size=0x20, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x6, value=Op.CALLCODE( - gas=0x7530, - address=addr_2, + gas=FORWARDED_GAS, + address=storage_writer, value=0x1, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=True, + value_transfer=True, + account_new=False, + new_memory_size=call_window, + old_memory_size=call_window, ), + key_warm=False, + original_value=0, + new_value=0, ) + Op.SSTORE( key=0x7, value=Op.DELEGATECALL( - gas=0x7530, - address=addr_2, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=delegate_budget, + address=storage_writer, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=True, + new_memory_size=call_window, + old_memory_size=call_window, ), + key_warm=False, + original_value=0, + new_value=1, ) + Op.SSTORE( key=0x8, value=Op.CALL( - gas=0x7530, - address=0x1000000000000000000000000000000000000013, + gas=FORWARDED_GAS, + address=absent, value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + value_transfer=False, + account_new=False, + new_memory_size=call_window, + old_memory_size=call_window, ), + key_warm=False, + original_value=0, + new_value=1, ) - + Op.SSTORE(key=0x3, value=Op.BALANCE(address=sender)) - + Op.SSTORE(key=0xA, value=Op.GAS), + + Op.SSTORE( + key=0x3, + value=Op.BALANCE(address=sender, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + ) + final_store = Op.SSTORE( + key=0xA, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=body + final_store + Op.STOP, storage={0: 18}, - nonce=0, - address=Address(0x23A2EC54F5F8589778DA7C2199CAF3B179A24CB9), # noqa: E501 ) + # Consumption before the final GAS read: the body's composite plus + # the delegate's work, minus the two returned stipends. + consumed = ( + body.gas_cost(fork) + + delegate_store.gas_cost(fork) + - 2 * fork.gas_costs().CALL_STIPEND + ) + + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = intrinsic + consumed + final_store.gas_cost(fork) + 5_000 + tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + copied_word = int.from_bytes( + EXTCODE_BYTES[:COPY_SIZE].ljust(0x20, b"\x00"), "big" + ) post = { - addr: Account(balance=111), target: Account( storage={ - 0: 18, - 1: 32, - 2: 0x1122334455667788991011121314151617181920000000000000000000000000, # noqa: E501 - 3: 0xE8D4A4B47280, - 4: 18, - 7: 1, - 8: 1, - 10: 0x60AE9, - 100: 17, + 0x0: 18, + 0x1: len(EXTCODE_BYTES), + 0x2: copied_word, + # Mid-execution balance: the full fee is charged upfront. + 0x3: INITIAL_BALANCE - gas_limit * GAS_PRICE, + 0x4: 18, + # Slots 5 and 6 stay zero: the value calls failed. + 0x7: 1, + 0x8: 1, + # Raw reading: everything left after the body's work and + # the GAS opcode itself. + 0xA: gas_limit - intrinsic - consumed - Op.GAS.gas_cost(fork), + 0x64: DELEGATE_VALUE, }, ), - sender: Account(nonce=1), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemoryTest/test_oog.py b/tests/ported_static/stMemoryTest/test_oog.py index 5efece6b89d..2a94bc58e56 100644 --- a/tests/ported_static/stMemoryTest/test_oog.py +++ b/tests/ported_static/stMemoryTest/test_oog.py @@ -312,6 +312,29 @@ def test_oog( # nested CALL to a cold contract plus the copy; the reprice eats the # slack, so add it back to that one budget. cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 + # The CREATE/CREATE2 success budgets are derived: EIP-8037 adds the + # new-account state gas (~183k), far past the ported 0xFFFF budget. + create_budget = ( + Op.CREATE( + value=0x0, + offset=0x10000, + size=0x20, + new_memory_size=0x10020, + init_code_size=0x20, + ).gas_cost(fork) + + 1_000 + ) + create2_budget = ( + Op.CREATE2( + value=0x0, + offset=0x10000, + size=0x20, + salt=0x5A17, + new_memory_size=0x10020, + init_code_size=0x20, + ).gas_cost(fork) + + 1_000 + ) coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x0000000000000000000000000000000000010020) contract_1 = Address(0x0000000000000000000000000000000000010037) @@ -787,9 +810,9 @@ def test_oog( Bytes("1a8451e6") + Hash(0xA3) + Hash(0x39D0), Bytes("1a8451e6") + Hash(0xA4) + Hash(0xFFFF), Bytes("1a8451e6") + Hash(0xA4) + Hash(0x39D0), - Bytes("1a8451e6") + Hash(0xF0) + Hash(0xFFFF), + Bytes("1a8451e6") + Hash(0xF0) + Hash(create_budget), Bytes("1a8451e6") + Hash(0xF0) + Hash(0x7D00), - Bytes("1a8451e6") + Hash(0xF5) + Hash(0xFFFF), + Bytes("1a8451e6") + Hash(0xF5) + Hash(create2_budget), Bytes("1a8451e6") + Hash(0xF5) + Hash(0x7D00), Bytes("1a8451e6") + Hash(0xF3) + Hash(0xFFFF), Bytes("1a8451e6") + Hash(0xF3) + Hash(0x36B0), From d2fbb934385e0195c0f8c12186ef928ae9036f86 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 18:56:36 +0200 Subject: [PATCH 11/25] fix(tests): un-skip Amsterdam exp-empty, homestead-oog, and create-warmth ported tests --- tests/ported_static/amsterdam_skip_list.txt | 35 +-- .../test_create_address_warm_after_fail.py | 221 +++++++++++------- .../stEIP158Specific/test_exp_empty.py | 150 ++++++------ ...nt_leave_empty_contract_via_transaction.py | 118 +++++----- 4 files changed, 271 insertions(+), 253 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 2ff669c7937..7353a6bc6aa 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 112 +# Total entries: 97 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -62,20 +62,13 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] -# stCreateTest (33) -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-0xef-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-contructor-revert-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-high-nonce-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-invalid-opcode-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-ok-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-oog-constructor-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-oog-post-constr-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-0xef-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-contructor-revert-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-invalid-opcode-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-ok-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-oog-constructor-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-oog-post-constr-v1] +# stCreateTest (20) +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d0] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d1] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d2] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d4] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d5] +stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d6] stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] @@ -84,12 +77,6 @@ stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_ref stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG2] stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG3] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d0] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d1] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d2] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d4] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d5] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d6] stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v0] stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v1] stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v0] @@ -103,11 +90,9 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP158Specific (1) -stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] +# stEIP158Specific (0) -# stHomesteadSpecific (1) -stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] +# stHomesteadSpecific (0) # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] diff --git a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py index dd8381ad14c..87f2013a574 100644 --- a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py +++ b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py @@ -10,13 +10,13 @@ Ported from: state_tests/stCreateTest/CreateAddressWarmAfterFailFiller.yml -@manually-enhanced: Do not overwrite. The post-state records the -measured cost of accessing the create address after a failed CREATE, -which is a cold account access. EIP-8038 reprices a cold account -access from 2 600 to 3 000, so each such measurement gains 400 at -Amsterdam. Derive that delta from the fork's gas model so it is -exactly 0 pre-EIP-8037 and tracks parameter changes; do not hardcode -the Amsterdam value. +@manually-enhanced: Do not overwrite. The post-state records measured +probe-CALL costs; derive them from the fork's gas model (CALL regular +cost with warm/cold, value-transfer, and new-account metadata, minus +the returned stipend) plus the dispatcher's fixed framing gas, so +EIP-2929/8037/8038 repricings track automatically. The transaction +gas limit carries reservoir headroom for the state gas the dispatcher +incurs on EIP-8037 forks, keeping state gas out of the measurements. """ import pytest @@ -390,10 +390,43 @@ def test_create_address_warm_after_fail( address=Address(0x00000000000000000000000000000000000C0DEC), # noqa: E501 ) - # The create address access after a failed CREATE is cold here; - # EIP-8038 reprices a cold account access from 2 600 to 3 000. - # Derive the delta from the fork so it is 0 pre-EIP-8037. - cold_account_delta = fork.gas_costs().COLD_ACCOUNT_ACCESS - 2600 + # The dispatcher measures each probe CALL with a GAS-delta window. + # The window's framing (stack shuffling, the two dirty-warm SSTOREs + # bracketing the call, and the closing GAS read) is baked into the + # ported bytecode blob and fork-stable; the CALL's own cost is + # derived from the fork so warm/cold, value-transfer, and + # new-account repricings (EIP-2929, EIP-8037, EIP-8038) track + # automatically. On EIP-8037 forks the state-gas component is paid + # from the transaction's reservoir (see the gas limit below), so + # the windows observe only the regular cost. + first_call_frame = 228 + repeat_call_frame = 216 + + def measured_call(frame: int, *, warm: bool, new: bool) -> int: + """Compute the gas one dispatcher probe-CALL window measures.""" + call = Op.CALL( + address_warm=warm, + value_transfer=bool(v), + account_new=new and bool(v), + ) + measured = frame + call.regular_cost(fork) + if v: + # The callee is empty (or STOP-only), so the stipend + # forwarded with the value returns unused. + measured -= fork.gas_costs().CALL_STIPEND + return measured + + # Slot 12: first call to the CREATE target. Warm if a failed CREATE + # accessed it (the subject, EIP-2929), cold if the creating frame + # itself failed (or CREATE aborted on a nonce overflow), and an + # existing account when the CREATE succeeded. + warm_new_call = measured_call(first_call_frame, warm=True, new=True) + cold_new_call = measured_call(first_call_frame, warm=False, new=True) + warm_existing_call = measured_call(first_call_frame, warm=True, new=False) + # Slots 13/15: repeated calls, always warm to an existing account. + repeat_call = measured_call(repeat_call_frame, warm=True, new=False) + # Slot 14: first call to the never-created empty address. + empty_call = cold_new_call expect_entries_: list[dict] = [ { @@ -408,10 +441,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -432,10 +465,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -459,10 +492,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -483,10 +516,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -510,10 +543,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -534,10 +567,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -561,10 +594,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -585,10 +618,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -612,10 +645,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -636,10 +669,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 32028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -663,10 +696,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 2828 + cold_account_delta, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -690,10 +723,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 34528, - 13: 7016, - 14: 34528, - 15: 7016, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -717,10 +750,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 2828 + cold_account_delta, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -741,10 +774,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 34528, - 13: 7016, - 14: 34528, - 15: 7016, + 12: cold_new_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=0, ), @@ -765,10 +798,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -789,10 +822,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 7028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -816,10 +849,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 328, - 13: 316, - 14: 2828 + cold_account_delta, - 15: 316, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -840,10 +873,10 @@ def test_create_address_warm_after_fail( 3: 1, 4: 1, 5: 1, - 12: 7028, - 13: 7016, - 14: 34528, - 15: 7016, + 12: warm_existing_call, + 13: repeat_call, + 14: empty_call, + 15: repeat_call, }, nonce=1, ), @@ -876,12 +909,22 @@ def test_create_address_warm_after_fail( Bytes("52c3fd24") + Hash(0x7), Bytes("52c3fd24") + Hash(0x11), ] - # The dispatcher writes to ~14 fresh storage slots; under EIP-8037 - # each slot's 32-byte cost is settled at frame end out of the - # reservoir/`gas_left` (~37_500 gas/slot on Amsterdam). Add that - # headroom — `sstore_state_gas` is 0 pre-EIP-8037, so the budget - # is unchanged on older forks. - tx_gas = [16777216 + 14 * Op.SSTORE(new_value=1).state_cost(fork)] + # Under EIP-8037 the gas above the execution cap becomes the + # state-gas reservoir. Size it to cover every state charge the + # dispatcher can incur — nine 0→non-zero SSTOREs, the CREATE's + # peak new-account charge plus up to two accounts created by the + # value-bearing probe calls (one spare), and the code deposit — + # so no state gas spills into the measured windows. The headroom + # is 0 pre-EIP-8037, leaving the budget unchanged on older forks. + state_gas_headroom = ( + 9 * Op.SSTORE(new_value=1).state_cost(fork) + + 4 + * Op.CALL( + address_warm=True, value_transfer=True, account_new=True + ).state_cost(fork) + + fork.code_deposit_state_gas(code_size=1) + ) + tx_gas = [16777216 + state_gas_headroom] tx_value = [0, 1] tx = Transaction( diff --git a/tests/ported_static/stEIP158Specific/test_exp_empty.py b/tests/ported_static/stEIP158Specific/test_exp_empty.py index 9b7b323744c..558f1b6d071 100644 --- a/tests/ported_static/stEIP158Specific/test_exp_empty.py +++ b/tests/ported_static/stEIP158Specific/test_exp_empty.py @@ -1,17 +1,23 @@ """ -Test_exp_empty. +Measure the gas cost of EXP with a zero base or a zero exponent across +exponent widths (the per-byte exponent charge applies only to the +exponent operand). Ported from: state_tests/stEIP158Specific/EXP_EmptyFiller.json + +@manually-enhanced: Do not overwrite. The eight measurement windows are +generated from one case list and every stored delta is derived from +opcode metadata (`exponent=` drives the per-byte charge); the transaction +budget is fork-derived. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, ) @@ -20,100 +26,78 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# (base, exponent) pairs: a zero on either side, exponent widths 1-32. +EXP_CASES = [ + (0x0, 0xC), + (0xC, 0x0), + (0x0, 2**64 - 1), + (0x0, 2**128 - 1), + (0x0, 2**256 - 1), + (2**64 - 1, 0x0), + (2**128 - 1, 0x0), + (2**256 - 1, 0x0), +] + @pytest.mark.ported_from( ["state_tests/stEIP158Specific/EXP_EmptyFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_exp_empty( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_exp_empty.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """Measure EXP's cost for zero-base and zero-exponent operands.""" + code = Bytecode() + storage: dict = {} + budget = 0 + for i, (base, exponent) in enumerate(EXP_CASES): + result = 1 if exponent == 0 else 0 + result_slot = 1 + 2 * i + # The last window stores its delta at slot 100, as ported. + delta_slot = 0x64 if i == len(EXP_CASES) - 1 else result_slot + 1 - # Source: lll - # { [0](GAS) [[1]](EXP 0 12) [[2]](SUB @0 (GAS)) [0](GAS) [[3]](EXP 12 0) [[4]](SUB @0 (GAS)) [0](GAS) [[5]](EXP 0 0xffffffffffffffff) [[6]](SUB @0 (GAS)) [0](GAS) [[7]](EXP 0 0xffffffffffffffffffffffffffffffff) [[8]](SUB @0 (GAS)) [0](GAS) [[9]](EXP 0 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) [[10]](SUB @0 (GAS)) [0](GAS) [[11]](EXP 0xffffffffffffffff 0) [[12]](SUB @0 (GAS)) [0](GAS) [[13]](EXP 0xffffffffffffffffffffffffffffffff 0) [[14]](SUB @0 (GAS)) [0] (GAS) [[15]](EXP 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 0) [[100]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.EXP(0x0, 0xC)) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0x3, value=Op.EXP(0xC, 0x0)) - + Op.SSTORE(key=0x4, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0x5, value=Op.EXP(0x0, 0xFFFFFFFFFFFFFFFF)) - + Op.SSTORE(key=0x6, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x7, value=Op.EXP(0x0, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + lead = Op.MSTORE( + offset=0x0, + value=Op.GAS, + new_memory_size=0x20, + old_memory_size=0x0 if i == 0 else 0x20, ) - + Op.SSTORE(key=0x8, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.EXP( - 0x0, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, # noqa: E501 - ), + exp_store = Op.SSTORE( + key=result_slot, + value=Op.EXP(base, exponent, exponent=exponent), + key_warm=False, + original_value=0, + new_value=result, ) - + Op.SSTORE(key=0xA, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE(key=0xB, value=Op.EXP(0xFFFFFFFFFFFFFFFF, 0x0)) - + Op.SSTORE(key=0xC, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0xD, value=Op.EXP(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 0x0) + # The window's measured delta: both GAS reads cancel out of the + # two composites' sum. + measured = lead.gas_cost(fork) + exp_store.gas_cost(fork) + delta_store = Op.SSTORE( + key=delta_slot, + value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS), + key_warm=False, + original_value=0, + new_value=measured, ) - + Op.SSTORE(key=0xE, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.SSTORE( - key=0xF, - value=Op.EXP( - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, # noqa: E501 - 0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) + code += lead + exp_store + delta_store + storage[result_slot] = result + storage[delta_slot] = measured + budget += measured + delta_store.gas_cost(fork) + 9 + + target = pre.deploy_contract(code=code + Op.STOP) + + # Fork-derived budget with an EIP-2200 stipend margin for the final + # store. + gas_limit = fork.transaction_intrinsic_cost_calculator()() + budget + 5_000 tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=600000, + gas_limit=gas_limit, ) - post = { - target: Account( - storage={ - 2: 2280, - 3: 1, - 4: 22127, - 6: 2627, - 8: 3027, - 10: 3827, - 11: 1, - 12: 22127, - 13: 1, - 14: 22127, - 15: 1, - 100: 22127, - }, - ), - } + post = {target: Account(storage=storage)} - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py index 82d92551fc3..57bec829c78 100644 --- a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py +++ b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py @@ -1,17 +1,23 @@ """ -Test_contract_creation_oo_gdont_leave_empty_contract_via_transaction. +Verify an out-of-gas contract creation leaves no account behind (the +Homestead-era bug left empty shells), while a sufficient budget creates a +codeless account whose init code called out to a storage writer. Ported from: state_tests/stHomesteadSpecific/contractCreationOOGdontLeaveEmptyContractViaTransactionFiller.json + +@manually-enhanced: Do not overwrite. The ported single case had silently +become success-only (its OOG arm was gone); both arms are restored with +fork-derived budgets, the init code's call budget is derived (the writer's +store is state-priced under EIP-8037), and the writer's slot plus the +created account's fields are asserted. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -27,72 +33,72 @@ "state_tests/stHomesteadSpecific/contractCreationOOGdontLeaveEmptyContractViaTransactionFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "enough_gas", + [ + pytest.param(True, id="created"), + pytest.param(False, id="oog_no_account"), + ], +) def test_contract_creation_oo_gdont_leave_empty_contract_via_transaction( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + enough_gas: bool, ) -> None: - """Test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """An OOG creation must not leave an account behind.""" + writer_store = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The init code calls the writer and deposits nothing. The forwarded + # budget is derived: with a zero reservoir the writer's state-priced + # store must fit inside its grant. + writer_needed = writer_store.gas_cost(fork) + call_code = Op.CALL( + gas=writer_needed + 1_000, + address=writer, + args_size=0x40, + ret_size=0x40, + address_warm=False, + value_transfer=False, + account_new=False, + new_memory_size=0x40, ) + initcode = call_code + Op.STOP - pre[sender] = Account(balance=0x10C8E0) - # Source: lll - # {(SSTORE 1 1)} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # {(CALL 50000 0x1000000000000000000000000000000000000001 0 0 64 0 64)} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, - ) - + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + execution = call_code.gas_cost(fork) + writer_needed + # The OOG arm dies charging the init code's own CALL (a failed inner + # call alone would not fail the creation): a bare 100-gas allowance + # over the fixed charges cannot cover the call's access cost even + # with the intrinsic estimate's slack. + gas_limit = overhead + (execution + 2_000 if enough_gas else 100) + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, - ), - gas_limit=96000, + data=initcode, + gas_limit=gas_limit, ) + created = compute_create_address(address=sender, nonce=0) + if enough_gas: + created_account: Account | type = Account(nonce=1, code=b"", balance=0) + writer_storage = {1: 1} + else: + created_account = Account.NONEXISTENT + writer_storage = {1: 0} post = { - compute_create_address(address=sender, nonce=0): Account(balance=0) + sender: Account(nonce=1), + created: created_account, + writer: Account(storage=writer_storage), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 10fe50c929e74bd53a310f475309985d802e029d Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:02:01 +0200 Subject: [PATCH 12/25] fix(tests): un-skip Amsterdam attack-replay and callcode-suicide ported tests --- tests/ported_static/amsterdam_skip_list.txt | 12 +---- .../stAttackTest/test_crashing_transaction.py | 31 +++++++++--- .../test_callcallcallcode_001_suicide_end.py | 48 +++++++++++-------- 3 files changed, 53 insertions(+), 38 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 7353a6bc6aa..fc45312a611 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,10 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 97 - -# stAttackTest (1) -stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] +# Total entries: 95 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -26,9 +23,6 @@ stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam- stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] -# stCallDelegateCodesCallCodeHomestead (1) -stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py::test_callcallcallcode_001_suicide_end[fork_Amsterdam] - # stCreate2 (31) stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_CallCode_Refund_NoOoG] @@ -90,10 +84,6 @@ stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterd stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] -# stEIP158Specific (0) - -# stHomesteadSpecific (0) - # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] diff --git a/tests/ported_static/stAttackTest/test_crashing_transaction.py b/tests/ported_static/stAttackTest/test_crashing_transaction.py index 8f8320dd1b8..6e62bb785ee 100644 --- a/tests/ported_static/stAttackTest/test_crashing_transaction.py +++ b/tests/ported_static/stAttackTest/test_crashing_transaction.py @@ -1,8 +1,17 @@ """ -Https://ropsten.etherscan.io/tx/0x8ec445380649f6c75a042a438ea9256c2fab2a... +Verify the Ropsten "crashing transaction" attack replay: a creation +transaction whose init code loops CREATEing children while more than +50000 gas remains, then deposits its runtime code. Ported from: state_tests/stAttackTest/CrashingTransactionFiller.json + +@manually-enhanced: Do not overwrite. On pre-EIP-8037 forks the loop +drains to the ported child count (created nonce 124); under EIP-8037 an +iteration costs more than the loop's 50000-gas guard (new-account plus +code-deposit state gas spill from the frame), so the loop enters an +iteration it cannot afford and the whole creation deterministically +reverts — the split post pins both behaviors. """ import pytest @@ -11,6 +20,7 @@ Address, Alloc, Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -25,12 +35,14 @@ ["state_tests/stAttackTest/CrashingTransactionFiller.json"], ) @pytest.mark.valid_from("Cancun") +# Required: the sender is funded at the attack's historical nonce 3270. @pytest.mark.pre_alloc_mutable def test_crashing_transaction( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Https://ropsten.""" + """Replay the attack loop; EIP-8037 makes the creation revert.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xDE0B6B3A7640000, nonce=3270) @@ -95,13 +107,20 @@ def test_crashing_transaction( gas_price=11, ) - post = { - sender: Account(nonce=3271), - compute_create_address(address=sender, nonce=3270): Account( + created = compute_create_address(address=sender, nonce=3270) + if fork.is_eip_enabled(8037): + # An iteration's state gas exceeds the loop's 50000-gas guard, + # so the init frame dies mid-CREATE and no account survives. + created_account: Account | type = Account.NONEXISTENT + else: + created_account = Account( code=bytes.fromhex("60606040526008565b00"), balance=1, nonce=124, - ), + ) + post = { + sender: Account(nonce=3271), + created: created_account, } state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py b/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py index 9d540998144..ae588b130d8 100644 --- a/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py +++ b/tests/ported_static/stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py @@ -1,12 +1,15 @@ """ -Test_callcallcallcode_001_suicide_end. +Verify a CALLCODE -> CALLCODE -> (DELEGATECALL + SELFDESTRUCT) chain: +every store lands in the outermost target's storage and the SELFDESTRUCT +(running in the target's context) destroys the target. Ported from: state_tests/stCallDelegateCodesCallCodeHomestead/callcallcallcode_001_SuicideEndFiller.json -@manually-enhanced: Do not overwrite. The hardcoded inner-CALL gas -values (50k / 100k / 150k) were tuned to the pre-EIP-8037 gas budget. - +@manually-enhanced: Do not overwrite. The three call budgets are derived +bottom-up from the fork (each frame pays its stores' state gas from its +own grant under EIP-8037), and the target's post code is coupled to the +composed bytecode (the gas operand varies by fork). """ import pytest @@ -38,17 +41,16 @@ def test_callcallcallcode_001_suicide_end( pre: Alloc, fork: Fork, ) -> None: - """Test_callcallcallcode_001_suicide_end.""" - # EIP-8037 inner-CALL gas bumps: original values restored for - # pre-EIP-8037 forks; bumped values cover the per-storage state- - # gas spill into regular gas on Amsterdam. - outer_call_gas = 150000 - middle_call_gas = 100000 - inner_call_gas = 50000 - if fork.is_eip_enabled(8037): - outer_call_gas = 1000000 - middle_call_gas = 800000 - inner_call_gas = 100000 + """Chained callcode stores land in the target; it self-destructs.""" + # Derived bottom-up call budgets: with a zero state-gas reservoir + # each frame pays its stores' state gas from its own grant, so every + # level's budget covers its callee plus its own work with margin. + store_cost = Op.SSTORE( + key=0x3, value=0x1, key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) + inner_call_gas = store_cost + 5_000 + middle_call_gas = inner_call_gas + 2 * store_cost + 30_000 + outer_call_gas = middle_call_gas + store_cost + 30_000 coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) @@ -72,8 +74,8 @@ def test_callcallcallcode_001_suicide_end( ) # Source: lll # { [[ 0 ]] (CALLCODE 150000 0 0 64 0 64 ) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( + target_code = ( + Op.SSTORE( key=0x0, value=Op.CALLCODE( gas=outer_call_gas, @@ -85,7 +87,10 @@ def test_callcallcallcode_001_suicide_end( ret_size=0x40, ), ) - + Op.STOP, + + Op.STOP + ) + target = pre.deploy_contract( # noqa: F841 + code=target_code, balance=0xDE0B6B3A7640000, nonce=0, address=Address(0xA74CA10B765DCDA3B60687F73F2881E2A56EDA64), # noqa: E501 @@ -141,9 +146,10 @@ def test_callcallcallcode_001_suicide_end( post = { target: Account( storage={0: 1, 1: 1, 2: 1, 3: 1}, - code=bytes.fromhex( - "6040600060406000600073eaf8c2ae0d01a880cea4e1aa88def5edd153d57b620249f0f260005500" # noqa: E501 - ), + # Coupled to the deployed bytecode (the gas operand varies + # by fork), proving SELFDESTRUCT in a CALLCODE context kills + # nothing here. + code=target_code, balance=0, nonce=0, ), From b9723f05d63b17f94f19a384705d2e67a5eee9a8 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:05:40 +0200 Subject: [PATCH 13/25] fix(tests): un-skip Amsterdam stSolidityTest via fork-derived state-gas surcharges --- tests/ported_static/amsterdam_skip_list.txt | 7 +------ .../test_recursive_create_contracts.py | 19 ++++++++++++++++--- .../test_test_contract_interaction.py | 18 +++++++++++++++--- .../test_test_contract_suicide.py | 18 +++++++++++++++--- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index fc45312a611..014211929e7 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 95 +# Total entries: 92 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -107,11 +107,6 @@ stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_rever stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] -# stSolidityTest (3) -stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contracts[fork_Amsterdam] -stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] -stSolidityTest/test_test_contract_suicide.py::test_test_contract_suicide[fork_Amsterdam] - # stSystemOperationsTest (5) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] diff --git a/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py b/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py index 836f16024d1..8f23e6ec41e 100644 --- a/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py +++ b/tests/ported_static/stSolidityTest/test_recursive_create_contracts.py @@ -1,8 +1,14 @@ """ -Test_recursive_create_contracts. +Verify recursively self-creating Solidity contracts stop when the +transaction budget runs dry, leaving exactly one child. Ported from: state_tests/stSolidityTest/RecursiveCreateContractsFiller.json + +@manually-enhanced: Do not overwrite. The EIP-8037 state gas of the +in-test creations is added to the ported budget as a fork-derived +surcharge (exactly 0 before EIP-8037), preserving the ported +behavior on every fork. """ import pytest @@ -17,6 +23,7 @@ Transaction, compute_create_address, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -31,8 +38,9 @@ def test_recursive_create_contracts( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_recursive_create_contracts.""" + """Recursive contract creation runs dry at the expected depth.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) sender = pre.fund_eoa(amount=0x1DCD6500) @@ -250,7 +258,12 @@ def test_recursive_create_contracts( sender=sender, to=contract_0, data=Bytes("a444f5e9") + Hash(0x304), - gas_limit=300000, + # EIP-8037 surcharge (0 before): the first child creation's + # new-account and code-deposit state gas spill into this + # budget; the recursion still runs dry at the ported depth. + gas_limit=300000 + + fork.create_state_gas() + + fork.code_deposit_state_gas(code_size=0xC8), value=1, ) diff --git a/tests/ported_static/stSolidityTest/test_test_contract_interaction.py b/tests/ported_static/stSolidityTest/test_test_contract_interaction.py index 0bcc63132a1..594a260899a 100644 --- a/tests/ported_static/stSolidityTest/test_test_contract_interaction.py +++ b/tests/ported_static/stSolidityTest/test_test_contract_interaction.py @@ -1,8 +1,14 @@ """ -Test_test_contract_interaction. +Verify a Solidity contract creating a child and interacting with it +through its dispatcher within the same transaction. Ported from: state_tests/stSolidityTest/TestContractInteractionFiller.json + +@manually-enhanced: Do not overwrite. The EIP-8037 state gas of the +in-test creations is added to the ported budget as a fork-derived +surcharge (exactly 0 before EIP-8037), preserving the ported +behavior on every fork. """ import pytest @@ -15,6 +21,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +36,9 @@ def test_test_contract_interaction( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_test_contract_interaction.""" + """Create a child contract and interact with it in one transaction.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0x5F5E100) @@ -165,7 +173,11 @@ def test_test_contract_interaction( sender=sender, to=target, data=Bytes("c0406226"), - gas_limit=350000, + # EIP-8037 surcharge (0 before): the created child's new-account + # and code-deposit state gas spill into this budget. + gas_limit=350000 + + fork.create_state_gas() + + fork.code_deposit_state_gas(code_size=0x81), value=1, ) diff --git a/tests/ported_static/stSolidityTest/test_test_contract_suicide.py b/tests/ported_static/stSolidityTest/test_test_contract_suicide.py index 1dd26ffea76..fc12dd3e319 100644 --- a/tests/ported_static/stSolidityTest/test_test_contract_suicide.py +++ b/tests/ported_static/stSolidityTest/test_test_contract_suicide.py @@ -1,8 +1,14 @@ """ -Test_test_contract_suicide. +Verify a Solidity contract that creates a child, tells it to +self-destruct, and re-calls it within the same transaction. Ported from: state_tests/stSolidityTest/TestContractSuicideFiller.json + +@manually-enhanced: Do not overwrite. The EIP-8037 state gas of the +in-test creations is added to the ported budget as a fork-derived +surcharge (exactly 0 before EIP-8037), preserving the ported +behavior on every fork. """ import pytest @@ -15,6 +21,7 @@ StateTestFiller, Transaction, ) +from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -29,8 +36,9 @@ def test_test_contract_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_test_contract_suicide.""" + """Create a child, destroy it, and call it again in one transaction.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0x5F5E100) @@ -187,7 +195,11 @@ def test_test_contract_suicide( sender=sender, to=target, data=Bytes("c0406226"), - gas_limit=350000, + # EIP-8037 surcharge (0 before): the created child's new-account + # and code-deposit state gas spill into this budget. + gas_limit=350000 + + fork.create_state_gas() + + fork.code_deposit_state_gas(code_size=0x81), value=1, ) From e13f2256ba282bc15bdbf4f32caf2e3d34f81ebc Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:10:00 +0200 Subject: [PATCH 14/25] fix(tests): un-skip Amsterdam double-selfdestruct-touch ported test --- tests/ported_static/amsterdam_skip_list.txt | 28 ++----------------- .../test_double_selfdestruct_touch_paris.py | 24 ++++++++++++---- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 014211929e7..f7e9c2ad716 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 92 +# Total entries: 70 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -56,28 +56,6 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] -# stCreateTest (20) -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d0] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d1] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d2] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d4] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d5] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d6] -stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] -stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] -stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] -stCreateTest/test_create_oo_gafter_init_code_returndata_size.py::test_create_oo_gafter_init_code_returndata_size[fork_Amsterdam] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG2] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG3] -stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v1] -stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v1] -stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v1] - # stDelegatecallTestHomestead (4) stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] @@ -107,12 +85,10 @@ stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_rever stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] -# stSystemOperationsTest (5) +# stSystemOperationsTest (3) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] stSystemOperationsTest/test_call_recursive_bomb3.py::test_call_recursive_bomb3[fork_Amsterdam] -stSystemOperationsTest/test_double_selfdestruct_touch_paris.py::test_double_selfdestruct_touch_paris[fork_Amsterdam--v1] -stSystemOperationsTest/test_double_selfdestruct_touch_paris.py::test_double_selfdestruct_touch_paris[fork_Amsterdam--v2] # stTransactionTest (4) stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-d120] diff --git a/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py b/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py index b20492dea03..fcb7f0e4d7a 100644 --- a/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py +++ b/tests/ported_static/stSystemOperationsTest/test_double_selfdestruct_touch_paris.py @@ -1,11 +1,13 @@ """ -A single contract can execute SELFDESTRUCT multiple times using by... - -multiple times. The second and later SELFDESTRUCTs have little effect but can -touch some new beneficiary addresses. +Verify a contract executing SELFDESTRUCT twice in one transaction: the +second has little effect but touches a new beneficiary address. Ported from: state_tests/stSystemOperationsTest/doubleSelfdestructTouch_ParisFiller.yml + +@manually-enhanced: Do not overwrite. Both forwarded call budgets derive +from the fork (the callee's cold first-set store is state-priced under +EIP-8037 and must fit the grant). """ import pytest @@ -85,6 +87,16 @@ def test_double_selfdestruct_touch_paris( gas_limit=30000000, ) + # Derived budget for each selfdestruct call: the callee's cold + # first-set store is state-priced under EIP-8037 and must fit the + # grant; the margin covers its SLOAD, SELFDESTRUCT, and accesses. + sd_call_gas = ( + Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ).gas_cost(fork) + + 20_000 + ) + pre[sender] = Account(balance=0x5F5E102) pre[empty_account_1] = Account(balance=10) pre[empty_account_2] = Account(balance=10) @@ -120,7 +132,7 @@ def test_double_selfdestruct_touch_paris( + Op.SWAP1 + Op.POP( Op.CALL( - gas=0x11170, + gas=sd_call_gas, address=0x29E4504A3D2A0E0AE0EBBBEFEDD4570639B3EBEE, value=Op.DUP6, args_offset=Op.DUP1, @@ -131,7 +143,7 @@ def test_double_selfdestruct_touch_paris( ) + Op.SUB + Op.PUSH20[0x29E4504A3D2A0E0AE0EBBBEFEDD4570639B3EBEE] - + Op.PUSH3[0x11170] + + Op.PUSH3[sd_call_gas] + Op.CALL + Op.STOP, nonce=0, From 60f48bebe4cc8ad8d25ce74934dc27b3088bcd54 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:17:12 +0200 Subject: [PATCH 15/25] fix(tests): un-skip Amsterdam stTransactionTest ported tests via derived budgets --- .claude/commands/enhance-ported-test.md | 9 ++ tests/ported_static/amsterdam_skip_list.txt | 7 +- .../test_opcodes_transaction_init.py | 20 +++- .../test_store_gas_on_create.py | 96 ++++++++++++------- ...ides_and_internal_call_suicides_success.py | 30 +++++- 5 files changed, 115 insertions(+), 47 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index 5aafcc152b5..ce96fcea0d4 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -439,6 +439,15 @@ persists with the sentinel) plus the callee-side observable already separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. +**Match the intrinsic calculator's kwargs to the transaction's shape.** +`fork.transaction_intrinsic_cost_calculator()()` defaults to +`sends_value=False`; under EIP-2780 a value-bearing transaction's intrinsic +includes the folded value-transfer cost (~5.9k), so a derived budget or +GAS-observation formula silently skews by that amount on Amsterdam only. +Pass `sends_value=True` when the tx carries value — or drop an incidental +tx `value` entirely (step 5) so the default holds. Validated on +`test_store_gas_on_create`. + **A creation transaction's top frame pays new-account state gas (EIP-8037) — but only for a fresh target.** When deriving a create-tx budget, the intrinsic calculator does not include the created account's diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index f7e9c2ad716..411ecaea29f 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 70 +# Total entries: 66 # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] @@ -90,8 +90,3 @@ stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] stSystemOperationsTest/test_call_recursive_bomb3.py::test_call_recursive_bomb3[fork_Amsterdam] -# stTransactionTest (4) -stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-d120] -stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-side_effects] -stTransactionTest/test_store_gas_on_create.py::test_store_gas_on_create[fork_Amsterdam] -stTransactionTest/test_suicides_and_internal_call_suicides_success.py::test_suicides_and_internal_call_suicides_success[fork_Amsterdam-d1] diff --git a/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py b/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py index e14591f4675..44ea5d78185 100644 --- a/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py +++ b/tests/ported_static/stTransactionTest/test_opcodes_transaction_init.py @@ -1,5 +1,6 @@ """ -Test_opcodes_transaction_init. +Verify each opcode family executes inside a creation transaction's init +code, including invalid-code and side-effect cases. Ported from: state_tests/stTransactionTest/Opcodes_TransactionInitFiller.json @@ -838,7 +839,7 @@ def test_opcodes_transaction_init( g: int, v: int, ) -> None: - """Test_opcodes_transaction_init.""" + """Run each opcode inside a creation transaction's init code.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) contract_1 = Address(0x0F572E5295C57F15886F9B263E2F6D2D6C7B5EC6) @@ -1468,7 +1469,16 @@ def test_opcodes_transaction_init( Op.SELFDESTRUCT(address=Op.ORIGIN), Bytes("ef"), Op.CALL( - gas=0xC350, + # Derived: the callee's cold first-set store is state-priced + # under EIP-8037 and must fit the grant. + gas=Op.SSTORE( + key=0x0, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + + 5_000, address=contract_0, value=Op.DUP1, args_offset=Op.DUP1, @@ -1503,7 +1513,9 @@ def test_opcodes_transaction_init( + Op.MSTORE8(offset=0x0, value=0xEF) + Op.RETURN(offset=0x0, size=0x1), ] - tx_gas = [400000] + # The d120 arm's nested CREATE adds a new-account state charge under + # EIP-8037 (0 before); every other arm keeps the ported budget. + tx_gas = [400000 + (fork.create_state_gas() if d == 120 else 0)] tx_value = [100000] tx = Transaction( diff --git a/tests/ported_static/stTransactionTest/test_store_gas_on_create.py b/tests/ported_static/stTransactionTest/test_store_gas_on_create.py index 2d97f61d885..0cad55501a9 100644 --- a/tests/ported_static/stTransactionTest/test_store_gas_on_create.py +++ b/tests/ported_static/stTransactionTest/test_store_gas_on_create.py @@ -1,17 +1,21 @@ """ -Test_store_gas_on_create. +Verify the gas a CREATE's init code observes when the creating contract is +entered directly by the transaction: the child receives all but one 64th +of what remains in the creating frame. Ported from: state_tests/stTransactionTest/StoreGasOnCreateFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode is kept, but the +transaction budget and the child's stored GAS observation are derived from +the fork (the ported absolute pin moved with every schedule change). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,51 +25,79 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CHILD_GAS_SLOT = 0xFD + @pytest.mark.ported_from( ["state_tests/stTransactionTest/StoreGasOnCreateFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_store_gas_on_create( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_store_gas_on_create.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0x17D78400) + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes, deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + creator = pre.deploy_contract( + code=setup + Op.POP(create_code) + Op.STOP, ) - # Source: lll - # { (MSTORE 0 0x5a60fd55) (CREATE 0 28 4)} - coinbase = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.CREATE(value=0x0, offset=0x1C, size=0x4) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Fork-derived budget with margin left after the child's work. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + setup.gas_cost(fork) + + create_code.gas_cost(fork) + + child_code.gas_cost(fork) + + 15_000 ) tx = Transaction( - sender=sender, - to=coinbase, - data=Bytes(""), - gas_limit=131882, - value=100, + sender=pre.fund_eoa(), + to=creator, + gas_limit=gas_limit, + ) + + # The child receives all but one 64th of what remains after the + # setup and the CREATE's own charges; its GAS read costs 2. + base = ( + gas_limit + - intrinsic + - setup.gas_cost(fork) + - create_code.gas_cost(fork) ) + assert base > 0, "the budget must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) post = { - compute_create_address(address=coinbase, nonce=0): Account( - storage={253: 0x12F39} + compute_create_address(address=creator, nonce=1): Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py b/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py index 03537745a43..91b32041a05 100644 --- a/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py +++ b/tests/ported_static/stTransactionTest/test_suicides_and_internal_call_suicides_success.py @@ -1,8 +1,14 @@ """ -Test_suicides_and_internal_call_suicides_success. +Verify SELFDESTRUCT inside an internal call: the callee self-destructs to +a previously nonexistent beneficiary, which materializes only when the +forwarded gas covers the new-account charge. Ported from: state_tests/stTransactionTest/SuicidesAndInternalCallSuicidesSuccessFiller.json + +@manually-enhanced: Do not overwrite. The two forwarded-gas calldata words +derive from the fork's SELFDESTRUCT new-account cost (state-priced under +EIP-8037), keeping one arm starved and one funded on every fork. """ import pytest @@ -59,7 +65,7 @@ def test_suicides_and_internal_call_suicides_success( g: int, v: int, ) -> None: - """Test_suicides_and_internal_call_suicides_success.""" + """A funded SELFDESTRUCT materializes its beneficiary.""" coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) contract_0 = Address(0x0000000000000000000000000000000000000000) contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) @@ -127,11 +133,25 @@ def test_suicides_and_internal_call_suicides_success( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # The calldata word is the gas forwarded to the self-destructing + # callee. Its SELFDESTRUCT pays a new-account charge for the funded + # beneficiary (state-priced under EIP-8037, spilling from the + # callee's grant), so both budgets derive from that cost: one starves + # it, one funds it with margin. + sd_cost = Op.SELFDESTRUCT.with_metadata( + address_warm=True, account_new=True + ).gas_cost(fork) tx_data = [ - Hash(0x55F0), - Hash(0xAAF0), + Hash(sd_cost // 2), + Hash(sd_cost + 5_000), + ] + tx_gas = [ + fork.transaction_intrinsic_cost_calculator()( + calldata=Hash(0), sends_value=True + ) + + sd_cost + + 40_000 ] - tx_gas = [150000] tx_value = [10] tx = Transaction( From f722c80185e2b022f2b7a203801925971205a9e6 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:21:20 +0200 Subject: [PATCH 16/25] fix(tests): un-skip Amsterdam refund600 via derived refund-cap formula --- tests/ported_static/amsterdam_skip_list.txt | 5 - .../stRefundTest/test_refund600.py | 116 +++++++++++------- 2 files changed, 70 insertions(+), 51 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 411ecaea29f..30412768fab 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -16,10 +16,6 @@ stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_ stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] # stCallCreateCallCodeTest (6) -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g3] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] @@ -65,7 +61,6 @@ stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.p # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] -stRefundTest/test_refund600.py::test_refund600[fork_Amsterdam] stRefundTest/test_refund_call_a.py::test_refund_call_a[fork_Amsterdam] stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d0] stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d1] diff --git a/tests/ported_static/stRefundTest/test_refund600.py b/tests/ported_static/stRefundTest/test_refund600.py index cce448a08c4..3f5def95619 100644 --- a/tests/ported_static/stRefundTest/test_refund600.py +++ b/tests/ported_static/stRefundTest/test_refund600.py @@ -1,18 +1,21 @@ """ -Test_refund600. +Verify the EIP-3529 refund cap over six storage clears: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund600Filler.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,64 +24,85 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund600Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund600( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund600.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xDC4EFA209AECDD4C2D5201A419EA27506151B4EC687F14A613229E310932491B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Six storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.POP(Op.SLOAD(key=0x1, key_warm=False)) + + Op.POP(Op.SLOAD(key=0x2, key_warm=False)) + # EXP(2, 0xFFFF) wraps to 0 mod 2^256, so this store is a no-op. + + Op.SSTORE( + key=0xA, + value=Op.EXP(0x2, 0xFFFF, exponent=0xFFFF), + key_warm=False, + original_value=0, + new_value=0, + ) + + Op.SSTORE( + key=0xB, + value=Op.BALANCE(address=Op.ADDRESS, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x6, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x989680) - # Source: lll - # { @@1 @@2 [[ 10 ]] (EXP 2 0xffff) [[ 11 ]] (BALANCE (ADDRESS)) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP(Op.SLOAD(key=0x1)) - + Op.POP(Op.SLOAD(key=0x2)) - + Op.SSTORE(key=0xA, value=Op.EXP(0x2, 0xFFFF)) - + Op.SSTORE(key=0xB, value=Op.BALANCE(address=Op.ADDRESS)) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xC09923E2275E4EE7822A1FEB5EEE1C18143575C7), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - target: Account(storage={11: 0xDE0B6B3A7640000}), - coinbase: Account(balance=0), - sender: Account(balance=0x8F5CF0), + target: Account( + storage={0xA: 0, 0xB: CONTRACT_BALANCE}, + ), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From f2a203fad9ece1850274fe7e21ad940a47aaae14 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:30:40 +0200 Subject: [PATCH 17/25] fix(tests): un-skip Amsterdam 1024-depth recursion and initcode-call ported tests --- tests/ported_static/amsterdam_skip_list.txt | 9 - ...llcode_in_initcode_to_existing_contract.py | 251 +++++-------- ...o_existing_contract_with_value_transfer.py | 120 ++++--- .../test_call1024_oog.py | 334 +++++++++++------- .../test_callcode1024_oog.py | 313 +++++++++++----- .../test_call1024_oog.py | 313 +++++++++++----- .../test_delegatecall1024_oog.py | 271 +++++++++++--- ...tecall_in_initcode_to_existing_contract.py | 143 ++++---- 8 files changed, 1108 insertions(+), 646 deletions(-) diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 30412768fab..79e930c6a20 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -11,13 +11,8 @@ # Total entries: 66 # stCallCodes (3) -stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] -stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] -stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] # stCallCreateCallCodeTest (6) -stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] # stCreate2 (31) stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] @@ -53,10 +48,6 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] # stDelegatecallTestHomestead (4) -stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] -stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] -stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] -stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] # stRefundTest (7) stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] diff --git a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py index 900c2e78bda..5e2547c7a1f 100644 --- a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py +++ b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py @@ -1,199 +1,132 @@ """ -Callcode inside create/create2 contract init to existing contract. +Verify a CALLCODE made from inside init code to an existing contract. + +The created account's init code CALLCODEs an already-deployed contract, +so that contract's code runs in the freshly created account's context: +its storage write lands in the created account (never in the existing +contract), and the transferred value stays with the created account. Ported from: state_tests/stCallCodes/callcodeInInitcodeToExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The calldata-dispatch entry +contract is collapsed into a direct transaction to the create-runner, +the init code is composed and shared with the CREATE2 address +computation, sub-calls forward all gas (EIP-8037-proof), and the post +also pins the created account's code/nonce/balance and that the +existing contract's own storage stays untouched. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, - Hash, + Bytecode, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) +from execution_testing.vm import Op, Opcode REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Endowment the runner sends into the creation; the init code's CALLCODE +# then names the same amount, a self-to-self transfer the created +# account's balance must cover and keep. +CREATE_ENDOWMENT = 1 +CALLCODE_VALUE = CREATE_ENDOWMENT +RUNNER_BALANCE = 10_000 +CREATE2_SALT = 0 + +# Written by the init code with the CALLCODE's success flag. +SUCCESS_FLAG_SLOT = 1 +# Written by the existing contract's code, in the caller's context. +DELEGATE_SLOT = 2 + + +def memory_stores(data: bytes) -> Bytecode: + """Write the given bytes to memory starting at offset zero.""" + code = Bytecode() + for offset in range(0, len(data), 32): + chunk = data[offset : offset + 32].ljust(32, b"\x00") + code += Op.MSTORE(offset, int.from_bytes(chunk, "big")) + return code + @pytest.mark.ported_from( [ "state_tests/stCallCodes/callcodeInInitcodeToExistingContractFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Constantinople") +@pytest.mark.parametrize("opcode", [Op.CREATE, Op.CREATE2]) def test_callcode_in_initcode_to_existing_contract( state_test: StateTestFiller, pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, + opcode: Opcode, ) -> None: - """Callcode inside create/create2 contract init to existing contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1100000000000000000000000000000000000000) - contract_1 = Address(0x1000000000000000000000000000000000000000) - contract_2 = Address(0x2000000000000000000000000000000000000000) - contract_3 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CALLCODE in init code runs in the created account's context.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_SLOT, value=1) + Op.STOP, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (CALL 300000 (CALLDATALOAD 0) 0 0 0 0 0) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0x493E0, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + initcode = ( + Op.SSTORE( + key=SUCCESS_FLAG_SLOT, + value=Op.CALLCODE(address=existing, value=CALLCODE_VALUE), ) - + Op.STOP, - nonce=0, - address=Address(0x1100000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (SSTORE 2 1) } - contract_3 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # {(seq (CREATE2 1 0 (lll (seq [[1]] (CALLCODE 50000 0x1000000000000000000000000000000000000001 1 0 0 0 0)) 0) 0) )} # noqa: E501 - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH1[0x0] - + Op.PUSH1[0x27] - + Op.CODECOPY(dest_offset=0x0, offset=0x11, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.CREATE2 + Op.STOP - + Op.INVALID - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x2000000000000000000000000000000000000000), # noqa: E501 ) - # Source: lll - # {(seq (CREATE 1 0 (lll (seq [[1]] (CALLCODE 50000 0x1000000000000000000000000000000000000001 1 0 0 0 0)) 0) ) )} # noqa: E501 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH1[0x27] - + Op.CODECOPY(dest_offset=0x0, offset=0xF, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.CREATE - + Op.STOP - + Op.INVALID - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + initcode_bytes = bytes(initcode) + + if opcode == Op.CREATE2: + create_call = Op.CREATE2( + value=CREATE_ENDOWMENT, + offset=0, + size=len(initcode_bytes), + salt=CREATE2_SALT, + ) + else: + create_call = Op.CREATE( + value=CREATE_ENDOWMENT, offset=0, size=len(initcode_bytes) ) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 + runner = pre.deploy_contract( + code=memory_stores(initcode_bytes) + create_call + Op.STOP, + balance=RUNNER_BALANCE, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=contract_1, nonce=0): Account( - storage={1: 1, 2: 1}, balance=1 - ), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x11B62573BE8F72B4085BAFE5B675B3E7F08ED522): Account( - storage={1: 1, 2: 1}, balance=1 - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(contract_1, left_padding=True), - Hash(contract_2, left_padding=True), - ] - tx_gas = [1000000] + if opcode == Op.CREATE2: + created = compute_create_address( + address=runner, + salt=CREATE2_SALT, + initcode=initcode, + opcode=Op.CREATE2, + ) + else: + # Deployed contracts start at nonce 1. + created = compute_create_address(address=runner, nonce=1) tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + sender=pre.fund_eoa(), + to=runner, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + created: Account( + # The init code deploys no code but writes its own storage. + code=b"", + nonce=1, + balance=CREATE_ENDOWMENT, + storage={SUCCESS_FLAG_SLOT: 1, DELEGATE_SLOT: 1}, + ), + runner: Account( + nonce=2, + balance=RUNNER_BALANCE - CREATE_ENDOWMENT, + storage={}, + ), + # The existing contract's own storage must stay untouched. + existing: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py index 4822486a1ea..f58442f3cf0 100644 --- a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py +++ b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py @@ -1,18 +1,28 @@ """ -Callcode inside create/create2 contract init to existing contract. +Verify a value-bearing CALLCODE made from inside init code to an +existing contract. + +The runner endows the creation with value; the init code CALLCODEs an +already-deployed contract naming that same value, so the existing +contract's code runs in the created account's context: its storage +write lands in the created account, and the value transfer is +self-to-self, leaving the endowment with the created account. Ported from: state_tests/stCallCodes/callcodeInInitcodeToExistingContractWithValueTransferFiller.json + +@manually-enhanced: Do not overwrite. The raw-word init code is +composed, sub-calls forward all gas (EIP-8037-proof), the transaction +budget is maxed, and the post also pins the created account's +code/nonce/balance and that the existing contract's own storage stays +untouched. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, StateTestFiller, Transaction, compute_create_address, @@ -22,72 +32,82 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Endowment the runner sends into the creation; the init code's CALLCODE +# then names the same amount, a self-to-self transfer the created +# account's balance must cover and keep. +CREATE_ENDOWMENT = 5 +CALLCODE_VALUE = CREATE_ENDOWMENT +RUNNER_BALANCE = 10_000 + +# Written by the init code with the CALLCODE's success flag. +SUCCESS_FLAG_SLOT = 0 +# Written by the existing contract's code, in the caller's context. +DELEGATE_SLOT = 2 + + +def memory_stores(data: bytes) -> Bytecode: + """Write the given bytes to memory starting at offset zero.""" + code = Bytecode() + for offset in range(0, len(data), 32): + chunk = data[offset : offset + 32].ljust(32, b"\x00") + code += Op.MSTORE(offset, int.from_bytes(chunk, "big")) + return code + @pytest.mark.ported_from( [ "state_tests/stCallCodes/callcodeInInitcodeToExistingContractWithValueTransferFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("SpuriousDragon") def test_callcode_in_initcode_to_existing_contract_with_value_transfer( state_test: StateTestFiller, pre: Alloc, ) -> None: - """Callcode inside create/create2 contract init to existing contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1000000000000000000000000000000000000000) - contract_1 = Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A value-bearing CALLCODE in init code keeps the endowment.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_SLOT, value=1) + Op.STOP, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + initcode = ( + Op.SSTORE( + key=SUCCESS_FLAG_SLOT, + value=Op.CALLCODE(address=existing, value=CALLCODE_VALUE), + ) + + Op.STOP ) + initcode_bytes = bytes(initcode) - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (MSTORE 0 0x6040600060406000600573945304eb96065b2a98b57a48a06ae28d285a71b562) (MSTORE 32 0x0186a0f260005500000000000000000000000000000000000000000000000000) (CREATE 5 0 64) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0x6040600060406000600573945304EB96065B2A98B57A48A06AE28D285A71B562, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x186A0F260005500000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.CREATE(value=0x5, offset=0x0, size=0x40) + runner = pre.deploy_contract( + code=memory_stores(initcode_bytes) + + Op.CREATE(value=CREATE_ENDOWMENT, offset=0, size=len(initcode_bytes)) + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (SSTORE 2 1) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5), # noqa: E501 + balance=RUNNER_BALANCE, ) + # Deployed contracts start at nonce 1. + created = compute_create_address(address=runner, nonce=1) + tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=453081, + sender=pre.fund_eoa(), + to=runner, ) post = { - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 1, 2: 1}, balance=5 + created: Account( + # The init code deploys no code but writes its own storage. + code=b"", + nonce=1, + balance=CREATE_ENDOWMENT, + storage={SUCCESS_FLAG_SLOT: 1, DELEGATE_SLOT: 1}, + ), + runner: Account( + nonce=2, + balance=RUNNER_BALANCE - CREATE_ENDOWMENT, + storage={}, ), + # The existing contract's own storage must stay untouched. + existing: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py b/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py index 16729e411a8..c73e13d0235 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py @@ -1,153 +1,249 @@ """ -Calldepth with oog. +Verify a self-recursive CALL chain that terminates by out-of-gas. + +Each level bumps a shared depth counter, forwards almost all its gas to +a call to itself (keeping a 10,000 reserve for its post-call stores), +then records the call's success flag and a depth marker. Levels too deep +to afford their stores halt and roll back, so the surviving storage pins +the exact depth the budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stCallCreateCallCodeTest/Call1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.CALL + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stCallCreateCallCodeTest/Call1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - pytest.param( - 0, - 2, - 0, - id="-g2", - ), - pytest.param( - 0, - 3, - 0, - id="-g3", - ), - ], + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [13_120_826, 9_320_826, 15_720_826, 11_220_826], ) -@pytest.mark.pre_alloc_mutable def test_call1024_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + tx_gas_limit: int, ) -> None: - """Calldepth with oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (CALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x878BC1C3D660907B056E31C854A309F7EF1B4C4, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x0878BC1C3D660907B056E31C854A309F7EF1B4C4), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 113, 1: 1, 2: 0x1B969})}, - }, - { - "indexes": {"data": -1, "gas": 2, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - { - "indexes": {"data": -1, "gas": 3, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 124, 1: 1, 2: 0x1E461})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [13120826, 9320826, 15720826, 11220826] - tx_value = [10] + """Pin the depth an OOG-terminated CALL self-recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + gas_limit=tx_gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py b/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py index c67dfb77762..c475d6e6a48 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py @@ -1,131 +1,250 @@ """ -Calldepth and oog. +Verify a self-recursive CALLCODE chain that terminates by out-of-gas. + +Each level bumps a shared depth counter, forwards almost all its gas to +a CALLCODE to its own address (same code, same storage context, keeping +a 10,000 reserve for its post-call stores), then records the call's +success flag and a depth marker. Levels too deep to afford their stores +halt and roll back, so the surviving storage pins the exact depth the +budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stCallCreateCallCodeTest/Callcode1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.CALLCODE + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stCallCreateCallCodeTest/Callcode1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [15_720_826, 13_120_826], ) -@pytest.mark.pre_alloc_mutable def test_callcode1024_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + tx_gas_limit: int, ) -> None: - """Calldepth and oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (CALLCODE (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x1B803058288DC00000F98311B059597434253374, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x1B803058288DC00000F98311B059597434253374), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [15720826, 13120826] - tx_value = [10] + """Pin the depth an OOG-terminated CALLCODE self-recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + gas_limit=tx_gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py b/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py index 0f6b6855f24..96a5cf26fab 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py @@ -1,130 +1,251 @@ """ -Test_call1024_oog. +Verify a self-recursive DELEGATECALL chain that terminates by +out-of-gas (the Homestead delegatecall suite's variant of Call1024OOG). + +Each level bumps a shared depth counter, forwards almost all its gas to +a DELEGATECALL to its own address (same code, same storage context, +keeping a 10,000 reserve for its post-call stores), then records the +call's success flag and a depth marker. Levels too deep to afford their +stores halt and roll back, so the surviving storage pins the exact +depth the budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stDelegatecallTestHomestead/Call1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.DELEGATECALL + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stDelegatecallTestHomestead/Call1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [13_120_826, 15_720_826], ) -@pytest.mark.pre_alloc_mutable def test_call1024_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + tx_gas_limit: int, ) -> None: - """Test_call1024_oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (DELEGATECALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [13120826, 15720826] - tx_value = [10] + """Pin the depth an OOG-terminated DELEGATECALL recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + gas_limit=tx_gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py index 42959c58111..2bc71a23c44 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py @@ -1,17 +1,29 @@ """ -Test_delegatecall1024_oog. +Verify a self-recursive DELEGATECALL chain that terminates by +out-of-gas. + +Each level bumps a shared depth counter, forwards almost all its gas to +a DELEGATECALL to its own address (same code, same storage context, +keeping a 10,000 reserve for its post-call stores), then records the +call's success flag and a depth marker. Levels too deep to afford their +stores halt and roll back, so the surviving storage pins the exact +depth the budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,65 +32,220 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + +RECURSIVE_CALL_OP = Op.DELEGATECALL + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=RECURSIVE_CALL_OP( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB(1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF)), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = RECURSIVE_CALL_OP(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + RECURSIVE_CALL_OP( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( ["state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [15_720_826], +) def test_delegatecall1024_oog( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + tx_gas_limit: int, ) -> None: - """Test_delegatecall1024_oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (DELEGATECALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B), # noqa: E501 - ) + """Pin the depth an OOG-terminated DELEGATECALL recursion reaches.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=15720826, - value=10, + gas_limit=tx_gas_limit, ) - post = {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})} + post = { + target: Account(storage=predict_recursion_storage(fork, tx_gas_limit)), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py index e20c162ae59..2ceeefd5137 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py @@ -1,18 +1,28 @@ """ -Test_delegatecall_in_initcode_to_existing_contract. +Verify a DELEGATECALL made from inside init code to an existing +contract. + +The created account's init code DELEGATECALLs an already-deployed +contract, so that contract's code runs in the freshly created account's +context with the init frame's caller preserved: both the delegate and +the init code itself observe the creating contract as CALLER, and every +storage write lands in the created account, never in the delegate. Ported from: state_tests/stDelegatecallTestHomestead/delegatecallInInitcodeToExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The port's unused second creator +contract is deleted, the raw-word init code is composed, the delegate +call forwards all gas (EIP-8037-proof), the transaction budget is +maxed, and the post also pins the created account's code/nonce/balance +and that the delegate's own storage stays untouched. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, StateTestFiller, Transaction, compute_create_address, @@ -22,86 +32,91 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CREATE_ENDOWMENT = 1 +RUNNER_BALANCE = 10_000 + +# Written by the init code with the DELEGATECALL's success flag. +DELEGATE_RESULT_SLOT = 0 +# Written by the init code with the CALLER it observes (the runner). +INITCODE_CALLER_SLOT = 1 +# Written by the delegate's code, in the created account's context. +DELEGATE_WRITE_SLOT = 2 +# Written by the delegate with the CALLER it observes (still the +# runner: DELEGATECALL preserves the init frame's caller). +DELEGATE_CALLER_SLOT = 0xB + + +def memory_stores(data: bytes) -> Bytecode: + """Write the given bytes to memory starting at offset zero.""" + code = Bytecode() + for offset in range(0, len(data), 32): + chunk = data[offset : offset + 32].ljust(32, b"\x00") + code += Op.MSTORE(offset, int.from_bytes(chunk, "big")) + return code + @pytest.mark.ported_from( [ "state_tests/stDelegatecallTestHomestead/delegatecallInInitcodeToExistingContractFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("SpuriousDragon") def test_delegatecall_in_initcode_to_existing_contract( state_test: StateTestFiller, pre: Alloc, ) -> None: - """Test_delegatecall_in_initcode_to_existing_contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1000000000000000000000000000000000000000) - contract_1 = Address(0x1000000000000000000000000000000000000001) - contract_2 = Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """A DELEGATECALL in init code runs in the created account.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_WRITE_SLOT, value=1) + + Op.SSTORE(key=DELEGATE_CALLER_SLOT, value=Op.CALLER) + + Op.STOP, ) - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (MSTORE 0 0x604060006040600073945304eb96065b2a98b57a48a06ae28d285a71b5620186) (MSTORE 32 0xa0f4600055336001550000000000000000000000000000000000000000000000) (CREATE 1 0 64) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0x604060006040600073945304EB96065B2A98B57A48A06AE28D285A71B5620186, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0xA0F4600055336001550000000000000000000000000000000000000000000000, # noqa: E501 + initcode = ( + Op.SSTORE( + key=DELEGATE_RESULT_SLOT, + value=Op.DELEGATECALL(address=existing), ) - + Op.CREATE(value=0x1, offset=0x0, size=0x40) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (MSTORE 0 0x6001600055) (CREATE 1 27 5) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x6001600055) - + Op.CREATE(value=0x1, offset=0x1B, size=0x5) - + Op.STOP, - balance=1000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 + + Op.SSTORE(key=INITCODE_CALLER_SLOT, value=Op.CALLER) + + Op.STOP ) - # Source: lll - # { (SSTORE 2 1) [[ 11 ]] (CALLER) } - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) - + Op.SSTORE(key=0xB, value=Op.CALLER) + initcode_bytes = bytes(initcode) + + runner = pre.deploy_contract( + code=memory_stores(initcode_bytes) + + Op.CREATE(value=CREATE_ENDOWMENT, offset=0, size=len(initcode_bytes)) + Op.STOP, - nonce=0, - address=Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5), # noqa: E501 + balance=RUNNER_BALANCE, ) + # Deployed contracts start at nonce 1. + created = compute_create_address(address=runner, nonce=1) + tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=453081, + sender=pre.fund_eoa(), + to=runner, ) post = { - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 1, 1: contract_0, 2: 1, 11: contract_0}, - balance=1, + created: Account( + # The init code deploys no code but writes its own storage. + code=b"", + nonce=1, + balance=CREATE_ENDOWMENT, + storage={ + DELEGATE_RESULT_SLOT: 1, + INITCODE_CALLER_SLOT: runner, + DELEGATE_WRITE_SLOT: 1, + DELEGATE_CALLER_SLOT: runner, + }, + ), + runner: Account( + nonce=2, + balance=RUNNER_BALANCE - CREATE_ENDOWMENT, + storage={}, ), + # The delegate's own storage must stay untouched. + existing: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 0e9cc030ce967a53343c08783317e9fb6145f00e Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:43:31 +0200 Subject: [PATCH 18/25] fix(tests): un-skip Amsterdam stCreateTest remainder via derived budgets and boundaries --- .claude/commands/enhance-ported-test.md | 10 + tests/ported_static/amsterdam_skip_list.txt | 12 - ...tract_create_ne_contract_in_init_oog_tr.py | 209 ++++--- ..._contract_then_call_to_non_existent_acc.py | 133 ++-- ...ate_oo_gafter_init_code_returndata_size.py | 120 ++-- .../test_create_oog_from_call_refunds.py | 80 ++- .../stCreateTest/test_create_results.py | 590 +++++++----------- .../test_transaction_collision_to_empty2.py | 158 ++--- ...transaction_collision_to_empty_but_code.py | 156 ++--- ...ransaction_collision_to_empty_but_nonce.py | 110 ++-- 10 files changed, 710 insertions(+), 868 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index ce96fcea0d4..b15231cb32b 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -439,6 +439,16 @@ persists with the sentinel) plus the callee-side observable already separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. +**EIP-8037 repriced the code deposit's regular part — boundaries beware.** +On 8037 forks the deposit charges only the keccak word cost +(`OPCODE_KECCAK256_PER_WORD * ceil32(len)/32`, ~6 gas) as regular gas plus +`len * 1530` state; `fork.gas_costs().CODE_DEPOSIT_PER_BYTE` (200) is the +*pre-8037* constant. Using 200/byte in a *sufficiency* budget merely +overshoots (safe); using it in a one-gas-short *boundary* silently funds +the deposit on Amsterdam. Branch on `fork.is_eip_enabled(8037)` for exact +deposit boundaries. Validated on +`test_create_oo_gafter_init_code_returndata_size`. + **Match the intrinsic calculator's kwargs to the transaction's shape.** `fork.transaction_intrinsic_cost_calculator()()` defaults to `sends_value=False`; under EIP-2780 a value-bearing transaction's intrinsic diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 79e930c6a20..cb02f797b6b 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -15,7 +15,6 @@ # stCallCreateCallCodeTest (6) # stCreate2 (31) -stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_CallCode_Refund_NoOoG] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] @@ -23,7 +22,6 @@ stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refu stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d0] stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d1] stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d2] -stCreate2/test_create2no_cash.py::test_create2no_cash[fork_Amsterdam-d1] stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g0] stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g1] stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v0] @@ -50,12 +48,6 @@ stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_dept # stDelegatecallTestHomestead (4) # stRefundTest (7) -stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] -stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] -stRefundTest/test_refund_call_a.py::test_refund_call_a[fork_Amsterdam] -stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d0] -stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d1] -stRefundTest/test_refund_tx_to_suicide.py::test_refund_tx_to_suicide[fork_Amsterdam] # stRevertTest (12) stRevertTest/test_loop_calls_depth_then_revert.py::test_loop_calls_depth_then_revert[fork_Amsterdam] @@ -66,10 +58,6 @@ stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_ stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v1] stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v0] stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v1] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d0-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d1-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] # stSystemOperationsTest (3) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] diff --git a/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py b/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py index 943824d3990..bac280a1984 100644 --- a/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py +++ b/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py @@ -1,141 +1,154 @@ """ -Test_create_e_contract_create_ne_contract_in_init_oog_tr. +Verify a contract-creation transaction whose init code first calls an +existing contract and then CREATEs a child: with a full budget both the +call and the nested creation land (the child at the creator's nonce-1 +address, not nonce 0); with a starved budget the callee and the whole +creation fail together. Ported from: state_tests/stCreateTest/CREATE_EContractCreateNEContractInInitOOG_TrFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(intrinsic + EIP-8037 top-frame and nested-create state gas + composed +code costs), the callee call forwards all gas instead of a ported fixed +budget, and the nested child is now asserted at its real nonce-1 address +(the port only checked the vacuous nonce-0 address). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALLEE_STORED = 0xC + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CREATE_EContractCreateNEContractInInitOOG_TrFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize("oog", [False, True], ids=["enough-gas", "oog"]) def test_create_e_contract_create_ne_contract_in_init_oog_tr( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oog: bool, ) -> None: - """Test_create_e_contract_create_ne_contract_in_init_oog_tr.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """Budget decides how far a creation's call-then-CREATE init gets.""" + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # Callee: one cold zero->non-zero store observed in the post. + callee_code = ( + Op.SSTORE( + key=0x1, + value=CALLEE_STORED, + key_warm=False, + original_value=0, + new_value=CALLEE_STORED, + ) + + Op.STOP ) + callee = pre.deploy_contract(code=callee_code) - # Source: lll - # {[[1]]12} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, + # Child init code: return a small runtime code from memory. + child_runtime = Op.SSTORE(key=0x0, value=CALLEE_STORED) + child_initcode = Op.MSTORE( + offset=0x0, + value=int.from_bytes(bytes(child_runtime), "big"), + new_memory_size=0x20, + ) + Op.RETURN( + offset=32 - len(bytes(child_runtime)), + size=len(bytes(child_runtime)), ) + child_initcode_bytes = bytes(child_initcode) - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={1: 12}), - compute_create_address(address=sender, nonce=0): Account( - nonce=2 - ), - compute_create_address( - address=compute_create_address(address=sender, nonce=0), - nonce=0, - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={1: 0}), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - compute_create_address( - address=compute_create_address(address=sender, nonce=0), - nonce=0, - ): Account.NONEXISTENT, - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Transaction init code: call the callee (forwarding all gas), then + # CREATE the child from memory; deploys nothing itself. + call_code = Op.POP(Op.CALL(address=callee)) + stage_code = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_initcode_bytes, "big"), + new_memory_size=0x20, + old_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=32 - len(child_initcode_bytes), + size=len(child_initcode_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_initcode_bytes), + ) + initcode = call_code + stage_code + create_code - tx_data = [ - Op.POP( - Op.CALL( - gas=0xEA60, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + if oog: + # Enough to start executing, but the forwarded 63/64 undercuts + # the callee's store and the CREATE is unaffordable after it. + gas_limit = intrinsic + callee_code.gas_cost(fork) // 2 + else: + # Everything must land: the fresh create target's top-frame + # state gas (EIP-8037), the callee, and the nested creation's + # peak charge plus the child's execution and code deposit. + runtime_size = len(bytes(child_runtime)) + child_total = ( + child_initcode.gas_cost(fork) + + runtime_size * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=runtime_size) ) - + Op.MSTORE(offset=0x0, value=0x64600C6000556000526005601BF3) - + Op.CREATE(value=0x0, offset=0x12, size=0xE), - ] - tx_gas = [160000, 60000] + needed = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + + callee_code.gas_cost(fork) + + child_total + ) + # Headroom for the 63/64 withhold at the call and the CREATE. + gas_limit = needed + needed // 63 tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=initcode, + gas_limit=gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + # The nested CREATE runs while the creator's nonce is 1 (EIP-161), + # so the child lands at the nonce-1 address and the nonce-0 address + # must stay empty. + child = compute_create_address(address=created, nonce=1) + child_at_nonce0 = compute_create_address(address=created, nonce=0) + + if oog: + post = { + sender: Account(nonce=1), + callee: Account(storage={1: 0}), + created: Account.NONEXISTENT, + child: Account.NONEXISTENT, + child_at_nonce0: Account.NONEXISTENT, + } + else: + post = { + sender: Account(nonce=1), + callee: Account(storage={1: CALLEE_STORED}), + created: Account(nonce=2, code=b""), + child: Account(nonce=1, code=bytes(child_runtime), storage={}), + child_at_nonce0: Account.NONEXISTENT, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py b/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py index f752c8b1f6b..b85370ef6d2 100644 --- a/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py +++ b/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py @@ -1,18 +1,24 @@ """ -Test_create_e_contract_then_call_to_non_existent_acc. +Verify a CREATE of an empty contract followed by a CALL to a non-existent +account: both operations are gas-measured, the created address and the +call's success flag are stored, and the absent callee stays non-existent. Ported from: state_tests/stCreateTest/CREATE_EContract_ThenCALLToNonExistentAccFiller.json + +@manually-enhanced: Do not overwrite. The ported absolute GAS snapshots +(slots 0/2/100) are re-expressed as two CodeGasMeasure windows asserted +via the fork's gas model, the created address and call flag stay in the +measured windows' SSTOREs, and the callee is a dynamic non-existent +account called with all gas forwarded. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -22,80 +28,95 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CREATE_GAS_SLOT = 0x0 +ADDRESS_SLOT = 0x1 +CALL_GAS_SLOT = 0x2 +FLAG_SLOT = 0x3 + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CREATE_EContract_ThenCALLToNonExistentAccFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_e_contract_then_call_to_non_existent_acc( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_e_contract_then_call_to_non_existent_acc.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Measure a CREATE of an empty contract and a call to no account.""" + absent = pre.nonexistent_account() + + # CREATE over never-written memory: the all-STOP init code deposits + # nothing, leaving an empty account with nonce 1. + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, + ) + # Storing the created address keeps it observable and folds the + # store into the measured window (the address is non-zero, so the + # placeholder new_value only sizes the zero->non-zero transition). + store_create = Op.SSTORE( + ADDRESS_SLOT, + create_code, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # A value-less call to an absent account creates nothing on any + # fork; the callee consumes no gas, so the window measures only the + # cold CALL itself. Storing the success flag keeps it observable. + call_code = Op.CALL( + address=absent, + address_warm=False, + value_transfer=False, + account_new=False, + ) + store_flag = Op.SSTORE( + FLAG_SLOT, + call_code, + key_warm=False, + original_value=0, + new_value=1, ) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]] (CALL 60000 0xe1ecf98489fa9ed60a664fc4998db699cfa39d40 0 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=0xE1ECF98489FA9ED60A664FC4998DB699CFA39D40, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + contract = pre.deploy_contract( + code=CodeGasMeasure( + code=store_create, + sstore_key=CREATE_GAS_SLOT, + ) + + CodeGasMeasure( + code=store_flag, + sstore_key=CALL_GAS_SLOT, ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, ) post = { - contract_0: Account( + contract: Account( storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6F50B, + CREATE_GAS_SLOT: store_create.gas_cost(fork), + ADDRESS_SLOT: compute_create_address( + address=contract, nonce=1 + ), + CALL_GAS_SLOT: store_flag.gas_cost(fork), + FLAG_SLOT: 1, }, ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - Address( - 0xE1ECF98489FA9ED60A664FC4998DB699CFA39D40 - ): Account.NONEXISTENT, + compute_create_address(address=contract, nonce=1): Account( + nonce=1, code=b"", balance=0 + ), + absent: Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py index 6f458740e9d..aaa2962d0ec 100644 --- a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py +++ b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py @@ -1,17 +1,23 @@ """ -Calls a contract that runs CREATE which deploy a code. then OOG happens... +Verify a CREATE whose child completes its init code but cannot afford the +code deposit: the creation fails (no account is deployed), yet the parent +frame survives on its 63/64 retention and the transaction succeeds. Ported from: state_tests/stCreateTest/CreateOOGafterInitCodeReturndataSizeFiller.json + +@manually-enhanced: Do not overwrite. The gas limit is derived from the +fork so the child's 63/64 grant covers init execution but not the code +deposit (including EIP-8037 deposit state gas), and the budget carries +the CREATE's peak new-account state charge, refunded when the child +fails. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,57 +27,99 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 1 + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CreateOOGafterInitCodeReturndataSizeFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_oo_gafter_init_code_returndata_size( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Calls a contract that runs CREATE which deploy a code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """CREATE fails at the code deposit; the parent frame completes.""" + # The child would deploy two stores; it never runs, only its deposit + # price matters. + child_runtime = Op.SSTORE(key=0x1, value=0x1) + Op.SSTORE( + key=0x2, value=0x1 + ) + runtime_size = len(bytes(child_runtime)) + + # Child init code: return the runtime code from memory. + child_initcode = Op.MSTORE( + offset=0x0, + value=int.from_bytes(bytes(child_runtime), "big"), + new_memory_size=0x20, + ) + Op.RETURN( + offset=32 - runtime_size, + size=runtime_size, + ) + initcode_bytes = bytes(child_initcode) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # Parent: stage the init code in memory, CREATE from it, then read + # RETURNDATASIZE (zero after the deposit failure) before stopping. + stage_code = Op.MSTORE( + offset=0x0, + value=int.from_bytes(initcode_bytes, "big"), + new_memory_size=0x20, ) + create_code = Op.CREATE( + value=0x0, + offset=32 - len(initcode_bytes), + size=len(initcode_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(initcode_bytes), + ) + tail_code = Op.POP + Op.EXP(0x2, Op.RETURNDATASIZE) + Op.STOP + contract = pre.deploy_contract(code=stage_code + create_code + tail_code) - # Source: lll - # { (MSTORE 0 0x6960016001556001600255600052600a6016f3) (CREATE 0 13 19) (EXP 2 (RETURNDATASIZE)) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, value=0x6960016001556001600255600052600A6016F3 + # Grant the child enough for its init execution but one gas short of + # the code deposit, so the deposit is what fails. Under EIP-8037 the + # regular deposit cost is only the keccak word cost (the per-byte + # price moved into deposit state gas); before it, 200 per byte. + child_exec = child_initcode.gas_cost(fork) + if fork.is_eip_enabled(8037): + deposit_regular = fork.gas_costs().OPCODE_KECCAK256_PER_WORD * ( + (runtime_size + 31) // 32 ) - + Op.POP(Op.CREATE(value=0x0, offset=0xD, size=0x13)) - + Op.EXP(0x2, Op.RETURNDATASIZE) - + Op.STOP, - nonce=0, + else: + deposit_regular = runtime_size * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + deposit = deposit_regular + fork.code_deposit_state_gas( + code_size=runtime_size + ) + available = (child_exec + deposit - 1) * 64 // 63 + forwarded = available - available // 64 + assert child_exec <= forwarded < child_exec + deposit, ( + "63/64 grant must cover init execution but not the deposit" + ) + # The parent's 1/64 retention must still afford the tail. + assert available // 64 > tail_code.gas_cost(fork), ( + "retention must cover the post-CREATE tail" + ) + + gas_limit = ( + fork.transaction_intrinsic_cost_calculator()(sends_value=True) + + stage_code.gas_cost(fork) + + create_code.gas_cost(fork) + + available ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=55054, - value=1, + sender=pre.fund_eoa(), + to=contract, + gas_limit=gas_limit, + value=TX_VALUE, ) post = { - contract_0: Account(balance=1), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, + # The transferred value proves the parent frame completed. + contract: Account(balance=TX_VALUE, storage={}), + compute_create_address(address=contract, nonce=1): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py index 2f7088ae13d..8e7d0d5e68f 100644 --- a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py +++ b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py @@ -1,8 +1,17 @@ """ -Test_create_oog_from_call_refunds. +Verify that gas refunds earned during (or via calls made from) a CREATE's +init code cannot rescue the creation from an out-of-gas failure: each OoG +variant burns the whole budget through the dispatcher's INVALID, while +the NoOoG variants deploy and keep their refunds. Ported from: state_tests/stCreateTest/CreateOOGFromCallRefundsFiller.yml + +@manually-enhanced: Do not overwrite. The gas limit and the sender's +exact prefund are derived from the fork so the child's 63/64 grant +covers the deepest nested-create chain (EIP-8037 state gas included) +yet stays below the 5000-byte code-deposit price that drives the OoG +arms. """ import pytest @@ -12,7 +21,6 @@ Address, Alloc, Bytes, - Environment, Hash, StateTestFiller, Transaction, @@ -28,6 +36,11 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Returning this much code makes the deposit unaffordable in the OoG +# variants; the gas limit below is derived against it. +OOG_DEPOSIT_SIZE = 0x1388 +TX_GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stCreateTest/CreateOOGFromCallRefundsFiller.yml"], @@ -191,8 +204,7 @@ def test_create_oog_from_call_refunds( g: int, v: int, ) -> None: - """Test_create_oog_from_call_refunds.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) + """Refunds earned inside a creation cannot avert its OOG.""" contract_0 = Address(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) contract_1 = Address(0x000000000000000000000000000000000000001A) contract_2 = Address(0x000000000000000000000000000000000000001B) @@ -226,15 +238,38 @@ def test_create_oog_from_call_refunds( key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # Budget: covers the deepest NoOoG chain (a nested CREATE with + # EIP-8037 peak state gas at each level) while any init frame's + # 63/64 grant stays below the OoG arms' code-deposit price. The + # intrinsic bound uses all-non-zero calldata (selector + address). + gas_costs = fork.gas_costs() + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"\xff" * 36 + ) + create_op = Op.CREATE( + value=0x0, + offset=0x0, + size=0x40, + new_memory_size=0x40, + init_code_size=0x40, + ) + gas_limit = ( + intrinsic + + create_op.gas_cost(fork) + + OOG_DEPOSIT_SIZE * gas_costs.CODE_DEPOSIT_PER_BYTE + ) + deposit_price = ( + OOG_DEPOSIT_SIZE * gas_costs.CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) + ) + # No init frame can receive enough to pay the OoG arms' deposit. + grant_bound = gas_limit - intrinsic - create_op.regular_cost(fork) + assert grant_bound * 63 // 64 < deposit_price, ( + "63/64 grant must stay below the OoG deposit price" ) - pre[sender] = Account(balance=0x3D0900, nonce=1) + # The exact prefund makes "all gas burned" observable as balance 0. + pre[sender] = Account(balance=gas_limit * TX_GAS_PRICE, nonce=1) # Source: yul # berlin # { @@ -294,7 +329,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000001B), # noqa: E501 ) @@ -464,7 +499,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000002B), # noqa: E501 ) @@ -560,7 +595,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000004B), # noqa: E501 ) @@ -583,7 +618,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000003B), # noqa: E501 ) @@ -655,7 +690,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000005B), # noqa: E501 ) @@ -745,7 +780,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000006B), # noqa: E501 ) @@ -789,7 +824,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.PUSH2[0x1388] + + Op.PUSH2[OOG_DEPOSIT_SIZE] + Op.PUSH1[0x1] + Op.PUSH1[0x0] + Op.PUSH3[0xC0DE1] @@ -855,7 +890,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.PUSH2[0x1388] + + Op.PUSH2[OOG_DEPOSIT_SIZE] + Op.PUSH1[0x1] + Op.PUSH1[0x0] + Op.PUSH3[0xC0DE1] @@ -1123,15 +1158,14 @@ def test_create_oog_from_call_refunds( Bytes("693c6139") + Hash(contract_23, left_padding=True), Bytes("693c6139") + Hash(contract_24, left_padding=True), ] - tx_gas = [400000] - tx = Transaction( sender=sender, to=contract_0, data=tx_data[d], - gas_limit=tx_gas[g], + gas_limit=gas_limit, + gas_price=TX_GAS_PRICE, nonce=1, error=_exc, ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_results.py b/tests/ported_static/stCreateTest/test_create_results.py index a18d11e9cc6..1a92cd79ad2 100644 --- a/tests/ported_static/stCreateTest/test_create_results.py +++ b/tests/ported_static/stCreateTest/test_create_results.py @@ -1,224 +1,139 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Verify the value CREATE/CREATE2 leaves on the stack, the returndata, and +the deployed code for each constructor outcome — success, OOG, empty +revert, revert with data, empty deploy, and in-init SELFDESTRUCT — plus +each CALL-kind's result when calling the successfully created contract, +and the frame-aborting RETURNDATACOPY past an empty return buffer. + +Written by Ori Pomerantz (qbzzt1@gmail.com). Ported from: state_tests/stCreateTest/CreateResultsFiller.yml + +@manually-enhanced: Do not overwrite. The ported PUSH2 0xFFFF sub-call +budgets are replaced by length-preserving forward-all-gas sequences (the +fixed budget starves EIP-8037 state gas), the created accounts are now +asserted per case (code, nonce, or non-existence), and the per-case +posts are an explicit switch over the decoded calldata triple. """ import pytest from execution_testing import ( - EOA, Account, Address, Alloc, Bytes, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create2_address, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The dispatcher's LLL-derived bytecode hardcodes every jump target and +# code-copy offset, so all edits below preserve instruction lengths. +CONTRACT_1_ADDRESS = 0x60A7 +CREATE2_SALT = 0x5A17 +# PC values the dispatcher snapshots right after the create (slot 0x20) +# and after the call section (slot 0x21); fixed by the code layout. +CREATE_PC = 295 +CALL_PC = 551 +# Below the SHA3-OOG constructor's memory-expansion cost (which must +# fail) and above Amsterdam's state-gas needs (which must not). +TX_GAS = 9_437_184 + +# Calldata triples (creation kind, call kind, constructor kind) in the +# ported data order. creation: 1=CREATE, 2=CREATE2. call: 0=none, +# 1=CALL, 2=CALLCODE, 3=DELEGATECALL, 4=STATICCALL. constructor: +# 0/4=success, 1=OOG, 2=revert, 3=revert-with-data, 5=empty deploy, +# 6=SELFDESTRUCT in init (4 also RETURNDATACOPYs past the empty +# return buffer, aborting the whole dispatcher frame). +CASES: list[tuple[int, int, int]] = [ + (1, 1, 0), + (1, 2, 0), + (1, 3, 0), + (1, 4, 0), + (2, 1, 0), + (2, 2, 0), + (2, 3, 0), + (2, 4, 0), + (1, 0, 1), + (2, 0, 1), + (1, 0, 2), + (2, 0, 2), + (1, 0, 5), + (2, 0, 5), + (1, 0, 6), + (2, 0, 6), + (1, 0, 3), + (2, 0, 3), + (1, 1, 4), + (1, 2, 4), + (1, 3, 4), + (1, 4, 4), + (2, 1, 4), + (2, 2, 4), + (2, 3, 4), + (2, 4, 4), +] + +# Constructor fragment (offset, size) within the dispatcher's code: +# the dispatcher CODECOPYs these windows as the init code it creates +# from, keyed by the constructor kind. +FRAGMENTS: dict[int, tuple[int, int]] = { + 0: (0x250, 0x21), + 1: (0x271, 0x29), + 2: (0x29A, 0x26), + 3: (0x2C0, 0x2C), + 4: (0x250, 0x21), + 5: (0x2EC, 0x28), + 6: (0x314, 0x2A), +} + @pytest.mark.ported_from( ["state_tests/stCreateTest/CreateResultsFiller.yml"], ) @pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), - pytest.param( - 3, - 0, - 0, - id="d3", - ), - pytest.param( - 4, - 0, - 0, - id="d4", - ), - pytest.param( - 5, - 0, - 0, - id="d5", - ), - pytest.param( - 6, - 0, - 0, - id="d6", - ), - pytest.param( - 7, - 0, - 0, - id="d7", - ), - pytest.param( - 8, - 0, - 0, - id="d8", - ), - pytest.param( - 9, - 0, - 0, - id="d9", - ), - pytest.param( - 10, - 0, - 0, - id="d10", - ), - pytest.param( - 11, - 0, - 0, - id="d11", - ), - pytest.param( - 12, - 0, - 0, - id="d12", - ), - pytest.param( - 13, - 0, - 0, - id="d13", - ), - pytest.param( - 14, - 0, - 0, - id="d14", - ), - pytest.param( - 15, - 0, - 0, - id="d15", - ), - pytest.param( - 16, - 0, - 0, - id="d16", - ), - pytest.param( - 17, - 0, - 0, - id="d17", - ), - pytest.param( - 18, - 0, - 0, - id="d18", - ), - pytest.param( - 19, - 0, - 0, - id="d19", - ), - pytest.param( - 20, - 0, - 0, - id="d20", - ), - pytest.param( - 21, - 0, - 0, - id="d21", - ), - pytest.param( - 22, - 0, - 0, - id="d22", - ), - pytest.param( - 23, - 0, - 0, - id="d23", - ), - pytest.param( - 24, - 0, - 0, - id="d24", - ), - pytest.param( - 25, - 0, - 0, - id="d25", - ), - ], -) +@pytest.mark.parametrize("d", range(len(CASES)), ids=lambda d: f"d{d}") @pytest.mark.pre_alloc_mutable def test_create_results( state_test: StateTestFiller, pre: Alloc, fork: Fork, d: int, - g: int, - v: int, ) -> None: - """Ori Pomerantz qbzzt1@gmail.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC) - contract_1 = Address(0x00000000000000000000000000000000000060A7) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) + """Verify create results and follow-up calls per constructor kind.""" + creation, call_kind, constructor = CASES[d] + contract_1 = Address(CONTRACT_1_ADDRESS) + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # Length-preserving stand-in for the ported PUSH2 0xFFFF gas + # operand: two JUMPDESTs pad the 3-byte slot so every hardcoded + # jump target and code-copy offset stays valid, while GAS forwards + # everything (a fixed budget starves EIP-8037 state gas). + forward_all_gas = Op.JUMPDEST + Op.JUMPDEST + Op.GAS + + # The 18-byte contract each successful constructor deploys: call + # contract_1 (as a 2-byte push, part of the fixed layout) and stop. + contract_code = ( + Op.CALL( + gas=forward_all_gas, + address=CONTRACT_1_ADDRESS, + value=0x0, + args_offset=0x0, + args_size=0x0, + ret_offset=0x0, + ret_size=0x0, + ) + + Op.STOP ) - pre[sender] = Account(balance=0xBA1A9CE0BA1A9CE) # Source: lll # { # ; Variables are 0x20 bytes (= 256 bits) apart, except for @@ -251,8 +166,8 @@ def test_create_results( # ) # ; I did not want to rely on knowing the address at which the contract # ... (138 more lines) - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x100, value=Op.CALLDATALOAD(offset=0x4)) + dispatcher_code = ( + Op.MSTORE(offset=0x100, value=Op.CALLDATALOAD(offset=0x4)) + Op.MSTORE(offset=0x120, value=Op.CALLDATALOAD(offset=0x24)) + Op.MSTORE(offset=0x140, value=Op.CALLDATALOAD(offset=0x44)) + Op.JUMPI( @@ -386,7 +301,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.CALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), value=0x0, args_offset=0x0, @@ -403,7 +318,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.CALLCODE( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), value=0x0, args_offset=0x0, @@ -420,7 +335,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.DELEGATECALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), args_offset=0x0, args_size=0x0, @@ -436,7 +351,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.STATICCALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), args_offset=0x0, args_size=0x0, @@ -463,16 +378,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.POP(Op.SHA3(offset=0x0, size=0x2FFFFF)) + Op.PUSH1[0x12] + Op.CODECOPY(dest_offset=0x200, offset=0x17, size=Op.DUP1) @@ -480,16 +386,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.REVERT(offset=0x0, size=0x0) + Op.PUSH1[0x12] + Op.CODECOPY(dest_offset=0x200, offset=0x14, size=Op.DUP1) @@ -497,16 +394,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.REVERT(offset=0x0, size=0x20) + Op.PUSH1[0x12] @@ -515,16 +403,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.STOP + Op.PUSH1[0x12] @@ -533,16 +412,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.SELFDESTRUCT(address=0x0) + Op.PUSH1[0x12] @@ -551,26 +421,26 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, + + contract_code + + contract_code + ) + + # Guard the hardcoded layout the bytecode's jump targets and the + # FRAGMENTS table rely on. + dispatcher_bytes = bytes(dispatcher_code) + assert len(dispatcher_bytes) == 0x350, "dispatcher layout drifted" + assert dispatcher_bytes[0x33E:0x350] == bytes(contract_code), ( + "reference contract code slot drifted" + ) + # The SHA3-OOG constructor must stay unaffordable. + assert TX_GAS < fork.memory_expansion_gas_calculator()( + new_bytes=0x2FFFFF + ), "budget must not afford the SHA3-OOG constructor" + + # Slots 16-33 hold non-zero sentinels so every overwrite (even with + # zero) is observable. + contract_0 = pre.deploy_contract( + code=dispatcher_code, storage={ 16: contract_1, 18: contract_1, @@ -580,135 +450,99 @@ def test_create_results( 32: contract_1, 33: contract_1, }, - balance=0xBA1A9CE0BA1A9CE, - nonce=0, - address=Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC), # noqa: E501 ) # Source: lll # { # [[0]] 0x60A7 # } ; end of LLL code - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x60A7) + Op.STOP, - balance=0xBA1A9CE0BA1A9CE, - nonce=0, - address=Address(0x00000000000000000000000000000000000060A7), # noqa: E501 + pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=CONTRACT_1_ADDRESS) + Op.STOP, + address=contract_1, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 1, 2, 4, 5, 6], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={32: 295, 33: 551}), - contract_1: Account(storage={0: contract_1}), - }, - }, - { - "indexes": {"data": [3, 7], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": {contract_0: Account(storage={32: 295, 33: 551})}, - }, - { - "indexes": { - "data": [8, 9, 10, 11, 12, 13, 14, 15], - "gas": 0, - "value": 0, - }, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 18: 18, - 19: 0x600060006000600060006160A761FFFFF1000000000000000000000000000000, # noqa: E501 - 20: contract_1, - 21: contract_1, - 32: 295, - 33: 551, - }, - ), - }, - }, - { - "indexes": {"data": [16, 17], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 16: 32, - 17: contract_1, - 18: 18, - 19: 0x600060006000600060006160A761FFFFF1000000000000000000000000000000, # noqa: E501 - 20: contract_1, - 21: contract_1, - 32: 295, - 33: 551, - }, - ), - }, - }, - { - "indexes": { - "data": [18, 19, 20, 21, 22, 23, 24, 25], - "gas": 0, - "value": 0, - }, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 16: contract_1, - 17: 0, - 18: contract_1, - 19: contract_1, - 20: contract_1, - 21: contract_1, - 32: contract_1, - 33: contract_1, - }, - ), - }, - }, - ] + # Decode the case into the created account's address and the + # expected post-state. + if creation == 1: + created = compute_create_address(address=contract_0, nonce=1) + else: + frag_offset, frag_size = FRAGMENTS[constructor] + created = compute_create2_address( + contract_0, + CREATE2_SALT, + dispatcher_bytes[frag_offset : frag_offset + frag_size], + ) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # The word the dispatcher stores when comparing its reference copy + # of the contract code against a non-existent account's ext code. + contract_code_word = int.from_bytes( + bytes(contract_code).ljust(32, b"\x00"), "big" + ) - tx_data = [ - Bytes("048071d3") + Hash(0x1) + Hash(0x1) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x2) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x3) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x4) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x1) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x2) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x3) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x4) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x1), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x1), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x2), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x2), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x5), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x5), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x6), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x6), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x3), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x3), - Bytes("048071d3") + Hash(0x1) + Hash(0x1) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x2) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x3) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x4) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x1) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x2) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x3) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x4) + Hash(0x4), - ] - tx_gas = [9437184] + post: dict = {} + if constructor == 4: + # The create succeeds with an empty return buffer, so the + # forced RETURNDATACOPY of 32 bytes aborts the whole dispatcher + # frame: every sentinel survives and nothing was created. + post[contract_0] = Account( + storage={ + 16: contract_1, + 18: contract_1, + 19: contract_1, + 20: contract_1, + 21: contract_1, + 32: contract_1, + 33: contract_1, + }, + ) + post[contract_1] = Account(storage={}) + post[created] = Account.NONEXISTENT + elif constructor == 0: + # Successful creation and a follow-up call to the new contract, + # which calls contract_1. Every sentinel is overwritten (the + # zero results are observable), and only the non-static call + # kinds let contract_1 store its own address. + post[contract_0] = Account( + storage={32: CREATE_PC, 33: CALL_PC}, + ) + post[contract_1] = Account( + storage={} if call_kind == 4 else {0: contract_1}, + ) + post[created] = Account(code=bytes(contract_code), nonce=1, storage={}) + else: + # No follow-up call: slots 20/21 keep their sentinels, and the + # dispatcher records the code/length differences against the + # created (or never-created) account's empty ext code. + storage = { + 18: len(bytes(contract_code)), + 19: contract_code_word, + 20: contract_1, + 21: contract_1, + 32: CREATE_PC, + 33: CALL_PC, + } + if constructor == 3: + # The constructor reverted 32 bytes holding contract_1's + # address; the dispatcher copied them out. + storage[16] = 32 + storage[17] = contract_1 + post[contract_0] = Account(storage=storage) + post[contract_1] = Account(storage={}) + if constructor == 5: + # Empty deploy: the account exists with no code. + post[created] = Account(code=b"", nonce=1, storage={}) + else: + # OOG (1), reverts (2, 3), and an in-init SELFDESTRUCT (6, + # destroyed in its creation transaction per EIP-6780). + post[created] = Account.NONEXISTENT tx = Transaction( sender=sender, to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=Bytes("048071d3") + + Hash(creation) + + Hash(call_kind) + + Hash(constructor), + gas_limit=TX_GAS, ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py index e49cd593e8f..c697e7352be 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py @@ -1,134 +1,96 @@ """ -Test_transaction_collision_to_empty2. +Verify a contract-creation transaction targeting an address that holds +only a balance: the prefund is not a collision, so creation proceeds and +the budget alone decides whether the init code completes. Ported from: state_tests/stCreateTest/TransactionCollisionToEmpty2Filler.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(intrinsic + init code cost, success arm exact), pinning that a prefunded +create target incurs no EIP-8037 top-frame new-account state gas. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PREFUND = 10 + @pytest.mark.ported_from( ["state_tests/stCreateTest/TransactionCollisionToEmpty2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize("oog", [False, True], ids=["enough-gas", "oog"]) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) def test_transaction_collision_to_empty2( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oog: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Prefunded create target is no collision; budget decides the rest.""" + # Init code: one cold zero->non-zero store, deploys nothing. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + # The prefunded target is not EMPTY_ACCOUNT in the pre-state, so + # EIP-8037 charges no top-frame new-account state gas: the exact + # success budget below would OOG if it were charged. + success_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, + ) + initcode.gas_cost(fork) + # The OOG arm misses half the store's cost rather than one gas: the + # intrinsic calculator over-estimates by the initcode word cost on + # pre-Shanghai forks, so a one-gas boundary is not portable. + gas_limit = success_gas + if oog: + gas_limit -= initcode.gas_cost(fork) // 2 - pre[sender] = Account(balance=0xE8D4A51000) - pre[contract_0] = Account(balance=10) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={1: 1}, balance=10, nonce=1), - }, - }, - { - "indexes": {"data": -1, "gas": 0, "value": 1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={1: 1}, balance=11, nonce=1), - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={}, balance=10, nonce=0), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre.fund_address(created, PREFUND) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + if oog: + # Creation rolled back: prefund kept, no value, nonce untouched. + created_account = Account( + storage={}, code=b"", nonce=0, balance=PREFUND + ) + else: + created_account = Account( + storage={1: 1}, code=b"", nonce=1, balance=PREFUND + tx_value + ) + + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py index 468a026fc98..ff51bba2eee 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py @@ -1,149 +1,99 @@ """ -Test_transaction_collision_to_empty_but_code. +Verify a contract-creation transaction whose target address already holds +code: the collision aborts the creation, consumes the whole gas limit, +transfers no value, and leaves the existing account untouched. Ported from: state_tests/stCreateTest/TransactionCollisionToEmptyButCodeFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(bare intrinsic and a fully-funded creation); the post asserts the +colliding account's code, nonce, and unchanged zero balance. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Header, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Any non-empty code at the target address triggers the collision. +COLLIDING_CODE = bytes.fromhex("1122334455") + @pytest.mark.ported_from( ["state_tests/stCreateTest/TransactionCollisionToEmptyButCodeFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], + "full_budget", [True, False], ids=["full-budget", "intrinsic-only"] ) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) @pytest.mark.pre_alloc_mutable def test_transaction_collision_to_empty_but_code( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_budget: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty_but_code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Creation collision with code burns the whole gas limit.""" + # Init code that would store a flag if it ever ran. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, ) + if full_budget: + # Enough to fund the whole creation (even at the fresh-target + # EIP-8037 price) — the collision must still consume all of it. + gas_limit = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + ) + else: + gas_limit = intrinsic - pre[sender] = Account(balance=0xE8D4A51000) - # Source: raw - # 0x1122334455 - contract_0 = pre.deploy_contract( # noqa: F841 - code=bytes.fromhex("1122334455"), - nonce=0, - address=Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account( - storage={1: 0}, - code=bytes.fromhex("1122334455"), - nonce=0, - ), - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account( - storage={}, - code=bytes.fromhex("1122334455"), - nonce=0, - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre[created] = Account(code=COLLIDING_CODE) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) + post = { + sender: Account(nonce=1), + # The colliding account is untouched: the init code never ran and + # the transferred value never arrived. + created: Account(storage={}, code=COLLIDING_CODE, nonce=0, balance=0), + } + state_test( - env=env, pre=pre, post=post, tx=tx, - blockchain_test_header_verify=Header( - gas_used=tx_gas[g], - ), + blockchain_test_header_verify=Header(gas_used=gas_limit), ) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py index 14a3c066470..a4b4c40e06a 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py @@ -1,22 +1,26 @@ """ -Test_transaction_collision_to_empty_but_nonce. +Verify a contract-creation transaction whose target address already has a +non-zero nonce: the collision aborts the creation, consumes the whole gas +limit, transfers no value, and leaves the existing account untouched. Ported from: state_tests/stCreateTest/TransactionCollisionToEmptyButNonceFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(bare intrinsic and a fully-funded creation); the post asserts the +colliding account's empty code, nonce, and unchanged zero balance. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Header, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -28,89 +32,67 @@ "state_tests/stCreateTest/TransactionCollisionToEmptyButNonceFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], + "full_budget", [True, False], ids=["full-budget", "intrinsic-only"] ) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) @pytest.mark.pre_alloc_mutable def test_transaction_collision_to_empty_but_nonce( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_budget: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty_but_nonce.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Creation collision with a nonce burns the whole gas limit.""" + # Init code that would store a flag if it ever ran. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, ) + if full_budget: + # Enough to fund the whole creation (even at the fresh-target + # EIP-8037 price) — the collision must still consume all of it. + gas_limit = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + ) + else: + gas_limit = intrinsic - pre[sender] = Account(balance=0xE8D4A51000) - pre[contract_0] = Account(balance=0, nonce=1) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre[created] = Account(nonce=1) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) post = { sender: Account(nonce=1), - contract_0: Account(storage={1: 0}, nonce=1), + # The colliding account is untouched: the init code never ran and + # the transferred value never arrived. + created: Account(storage={}, code=b"", nonce=1, balance=0), } state_test( - env=env, pre=pre, post=post, tx=tx, - blockchain_test_header_verify=Header( - gas_used=tx_gas[g], - ), + blockchain_test_header_verify=Header(gas_used=gas_limit), ) From f50af14915226e1f78d28df374767372db382f2f Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 19:50:48 +0200 Subject: [PATCH 19/25] fix(tests): un-skip Amsterdam mutual-recursion and call-bomb ported tests --- .../stSystemOperationsTest/test_ab_acalls0.py | 259 +++++++++++++----- .../stSystemOperationsTest/test_ab_acalls3.py | 234 ++++++++++++---- .../test_call_recursive_bomb3.py | 223 ++++++++++++--- 3 files changed, 549 insertions(+), 167 deletions(-) diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py index 863e937e7bb..c87ef000bf1 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py @@ -1,8 +1,24 @@ """ -Test_ab_acalls0. +Verify mutual A<->B recursion with value transfers and fixed gas asks. + +Contract A calls B forwarding a fixed 100,000-gas ask with 24 wei; B +calls its caller back with a 50,000 ask and 23 wei, storing one plus +the result. Both store into a PC-derived slot only after their call +returns, so every level's store competes with what the descent left +behind: levels too deep to afford it halt and forfeit, rolling back +their stores and transfers, and the surviving storage and balances pin +exactly how far the budget reaches. Ported from: state_tests/stSystemOperationsTest/ABAcalls0Filler.json + +@manually-enhanced: Do not overwrite. The post state (stores and +balances) is predicted by an exact fork-derived replay of the gas flow +(EIP-150 grants, stipend gifting and return, warm/cold and SSTORE +pricing via opcode metadata, EIP-8037 state-gas spill), validated +against the ported Cancun stores. B reaches A as its CALLER instead of +a hardcoded address, which shifts B's PC-derived slot; both slots are +computed from the assembled code. """ import pytest @@ -10,8 +26,7 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,84 +35,198 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +A_CALL_GAS = 100_000 +A_CALL_VALUE = 0x18 +B_CALL_GAS = 50_000 +B_CALL_VALUE = 0x17 +# One transfer per level up to the call-depth limit can never run dry. +A_INITIAL_BALANCE = A_CALL_VALUE * 1024 +# Exactly one return payment before any income (the ported balance). +B_INITIAL_BALANCE = B_CALL_VALUE +# Ported budget; pins how deep the mutual recursion reaches. +TX_GAS_LIMIT = 1_000_000 + + +def predict_final_state( + fork: Fork, tx_gas_limit: int, b_address: Address +) -> tuple[int, int, int, int]: + """ + Replay the mutual recursion's gas flow. + + Return A's stored value, B's stored value, and the committed + balance deltas of A and B. Descend the alternating call chain + computing each level's EIP-150 grant (both asks are pushed + constants; a value-bearing call gifts the callee the stipend and + gets any unused part back), then unwind: a level that cannot afford + its post-call store (EIP-2200's stipend rule included) halts and + forfeits its grant, reverting its own store and the transfer that + funded it. Every cost is derived from the fork via opcode metadata, + including EIP-8037 state gas: with a sub-cap gas limit the state + reservoir is zero, so state charges spill from the charging frame's + own gas. + """ + stipend = fork.gas_costs().CALL_STIPEND + pc_cost = Op.PC.gas_cost(fork) + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + # A's charges before forwarding: argument pushes plus the call's + # upfront costs (B is cold only in the top level). The ask is a + # pushed constant, so the whole call expression charges up front. + def a_charges(b_warm: bool) -> int: + return Op.CALL( + gas=A_CALL_GAS, + address=b_address, + value=A_CALL_VALUE, + address_warm=b_warm, + value_transfer=True, + ).gas_cost(fork) + + b_value_expr = Op.ADD( + 1, + Op.CALL( + gas=B_CALL_GAS, + address=Op.CALLER, + value=B_CALL_VALUE, + # A is the transaction target: always warm. + address_warm=True, + value_transfer=True, + ), + ) + # B's ADD and its constant push run only after the call returns. + b_post_call = Op.PUSH1[0].gas_cost(fork) + Op.ADD.gas_cost(fork) + b_charges = b_value_expr.gas_cost(fork) - b_post_call + + # Descend: alternate A and B levels until one dies mid-charges. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + balance = {"A": A_INITIAL_BALANCE, "B": B_INITIAL_BALANCE} + while True: + level += 1 + is_a = level % 2 == 1 + if is_a: + gas -= a_charges(b_warm=level > 1) + ask, value = A_CALL_GAS, A_CALL_VALUE + else: + gas -= b_charges + ask, value = B_CALL_GAS, B_CALL_VALUE + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + payer = "A" if is_a else "B" + assert balance[payer] >= value, "value transfer must be funded" + balance[payer] -= value + balance["B" if is_a else "A"] += value + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + stipend + + # Unwind: a failed level forfeits its grant and reverts the whole + # committed state below it (stores, warmth, and transfers). + child_ok = False + leftover = 0 + a_val, a_warm, b_val, b_warm = 0, False, 0, False + a_delta, b_delta = 0, 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + is_a = lvl % 2 == 1 + gas = available - forwarded + (leftover if child_ok else 0) + result = 1 if child_ok else 0 + if is_a: + gas -= pc_cost + store_value, current, warm = result, a_val, a_warm + else: + gas -= b_post_call + pc_cost + store_value, current, warm = 1 + result, b_val, b_warm + ok = gas >= 0 and gas > stipend + if ok: + gas -= raw_store_cost(warm, current, store_value) + ok = gas >= 0 + if ok: + # Commit this level: its store and the transfer into it. + if is_a: + a_val, a_warm = store_value, True + if lvl > 1: + a_delta += B_CALL_VALUE + b_delta -= B_CALL_VALUE + else: + b_val, b_warm = store_value, True + a_delta -= A_CALL_VALUE + b_delta += A_CALL_VALUE + leftover = gas + child_ok = True + else: + child_ok = False + leftover = 0 + a_val, a_warm, b_val, b_warm = 0, False, 0, False + a_delta, b_delta = 0, 0 + assert child_ok, "the top level must complete" + return a_val, b_val, a_delta, b_delta + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/ABAcalls0Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_ab_acalls0( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_ab_acalls0.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Pin how deep a value-bearing A<->B recursion reaches.""" + # B calls whoever called it, so it needs no embedded address. + b_value_expr = Op.ADD( + 1, + Op.CALL(gas=B_CALL_GAS, address=Op.CALLER, value=B_CALL_VALUE), + ) + contract_b = pre.deploy_contract( + code=Op.SSTORE(key=Op.PC, value=b_value_expr) + Op.STOP, + balance=B_INITIAL_BALANCE, ) - # Source: lll - # { [[ (PC) ]] (CALL 100000 24 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=Op.PC, - value=Op.CALL( - gas=0x186A0, - address=0x44EB1162303B6A60F2F8882D43D661787B3011E6, - value=0x18, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xD6CD6EC9ADCA299F2BBFD754FF8BCF6A4B9AAE40), # noqa: E501 + a_value_expr = Op.CALL( + gas=A_CALL_GAS, address=contract_b, value=A_CALL_VALUE ) - # Source: lll - # { [[ (PC) ]] (ADD 1 (CALL 50000 23 0 0 0 0)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=Op.PC, - value=Op.ADD( - 0x1, - Op.CALL( - gas=0xC350, - address=0xD6CD6EC9ADCA299F2BBFD754FF8BCF6A4B9AAE40, - value=0x17, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ), - ) - + Op.STOP, - balance=23, - nonce=0, - address=Address(0x44EB1162303B6A60F2F8882D43D661787B3011E6), # noqa: E501 + contract_a = pre.deploy_contract( + code=Op.SSTORE(key=Op.PC, value=a_value_expr) + Op.STOP, + balance=A_INITIAL_BALANCE, ) + # PC keys: each store's key is the code offset of its PC opcode, + # which sits right after the assembled value expression. + a_key = len(bytes(a_value_expr)) + b_key = len(bytes(b_value_expr)) + tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=1000000, - value=0x186A0, + sender=pre.fund_eoa(), + to=contract_a, + gas_limit=TX_GAS_LIMIT, ) + a_val, b_val, a_delta, b_delta = predict_final_state( + fork, TX_GAS_LIMIT, contract_b + ) post = { - target: Account(storage={36: 1}), - addr: Account(storage={38: 1}), + contract_a: Account( + storage={a_key: a_val}, + balance=A_INITIAL_BALANCE + a_delta, + ), + contract_b: Account( + storage={b_key: b_val}, + balance=B_INITIAL_BALANCE + b_delta, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py index 8f2b77d0966..4ad46a6f155 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py @@ -1,8 +1,22 @@ """ -Test_ab_acalls3. +Verify mutual A<->B recursion where each side reserves 100,000 gas. + +Both contracts bump their own depth counter during descent, then call +the other side forwarding everything but a 100,000-gas reserve (A sends +one wei each level; B sends nothing back). Nothing runs after the call, +so only the single deepest level dies of gas and every completed +level's counter bump and transfer persist: the counters and balances +pin exactly how many rounds the budget sustains. Ported from: state_tests/stSystemOperationsTest/ABAcalls3Filler.json + +@manually-enhanced: Do not overwrite. The post state (counters and +balances) is predicted by an exact fork-derived replay of the gas flow +(EIP-150 grants, stipend gifting, warm/cold and SSTORE pricing via +opcode metadata, EIP-8037 state-gas spill), validated against the +ported Cancun counters. B reaches A as its CALLER instead of a +hardcoded address. """ import pytest @@ -10,8 +24,8 @@ Account, Address, Alloc, - Bytes, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, ) @@ -20,76 +34,182 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +# Gas each level keeps back for itself before forwarding the rest. +GAS_RESERVE = 100_000 +A_CALL_VALUE = 1 +# One transfer per level up to the call-depth limit can never run dry. +A_INITIAL_BALANCE = A_CALL_VALUE * 1024 +# Ported budget; pins how many rounds the recursion sustains. +TX_GAS_LIMIT = 10_000_000 + + +def predict_depths( + fork: Fork, tx_gas_limit: int, b_address: Address +) -> tuple[int, int]: + """ + Replay the mutual recursion's gas flow. + + Return how many A and B levels complete. Descend the alternating + call chain: each level bumps its own counter (one cold set per + contract, then dirty rewrites), pays its call charges, and forwards + everything but the reserve under the EIP-150 63/64 rule; once the + reserve underflows, the wrapped ask forwards the 63/64 maximum. + Nothing runs after a call, so only the single deepest level dies + and its bump and incoming transfer revert. Every cost is derived + from the fork via opcode metadata, including EIP-8037 state gas: + with a sub-cap gas limit the state reservoir is zero, so state + charges spill from the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The ask expression's SUB runs after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + def call_split(address: object, warm: bool, value: int) -> tuple[int, int]: + """Pre-GAS-read and upfront charges of one side's call.""" + upfront = Op.CALL( + address_warm=warm, value_transfer=value > 0 + ).gas_cost(fork) + composite = Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=address, + value=value, + address_warm=warm, + value_transfer=value > 0, + ).gas_cost(fork) + return composite - upfront - post_gas_read, upfront + + a_pre, a_upfront_cold = call_split(b_address, False, A_CALL_VALUE) + _, a_upfront_warm = call_split(b_address, True, A_CALL_VALUE) + # A is the transaction target: always warm for B's call back. + b_pre, b_upfront = call_split(Op.CALLER, True, 0) + + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + level = 0 + a_balance = A_INITIAL_BALANCE + while True: + level += 1 + is_a = level % 2 == 1 + # Each contract's first level pays the cold counter set. + first = level <= 2 + gas -= bump_statics(key_warm=not first) + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= a_pre if is_a else b_pre + if gas < 0: + break + gas_read = gas + if is_a: + gas -= post_gas_read + ( + a_upfront_cold if level == 1 else a_upfront_warm + ) + else: + gas -= post_gas_read + b_upfront + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + if is_a: + assert a_balance >= A_CALL_VALUE, "transfer must be funded" + a_balance -= A_CALL_VALUE + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + gas = forwarded + (stipend if is_a else 0) + + completed = level - 1 + assert completed >= 2, "both sides must run at least once" + a_count = (completed + 1) // 2 + b_count = completed // 2 + return a_count, b_count + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/ABAcalls3Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_ab_acalls3( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_ab_acalls3.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) + """Pin how many rounds a reserve-throttled A<->B recursion runs.""" - # Source: lll - # { [[ 0 ]] (ADD (SLOAD 0) 1) (CALL (- (GAS) 100000) 1 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.SUB(Op.GAS, 0x186A0), - address=0xA890CEB693666313E0A5A1BE4F59F06C1E33F5C9, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + def bounce_code(call: Bytecode) -> Bytecode: + """Bump the own-depth counter, then call the other side.""" + return ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + call + + Op.STOP ) - + Op.STOP, - balance=0xFA3E8, - nonce=0, - address=Address(0x4776B53DEB22F16581088F679DBA75E205B65D34), # noqa: E501 + + # B calls whoever called it, so it needs no embedded address. + contract_b = pre.deploy_contract( + code=bounce_code( + Op.CALL(gas=Op.SUB(Op.GAS, GAS_RESERVE), address=Op.CALLER) + ), ) - # Source: lll - # { [[ 0 ]] (ADD (SLOAD 0) 1) (CALL (- (GAS) 100000) 0 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.SUB(Op.GAS, 0x186A0), - address=0x4776B53DEB22F16581088F679DBA75E205B65D34, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xA890CEB693666313E0A5A1BE4F59F06C1E33F5C9), # noqa: E501 + contract_a = pre.deploy_contract( + code=bounce_code( + Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=contract_b, + value=A_CALL_VALUE, + ) + ), + balance=A_INITIAL_BALANCE, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, - value=0x186A0, + sender=pre.fund_eoa(), + to=contract_a, + gas_limit=TX_GAS_LIMIT, ) + a_count, b_count = predict_depths(fork, TX_GAS_LIMIT, contract_b) + # Each completed B level keeps the wei its calling A level sent. post = { - target: Account(storage={0: 52}), - addr: Account(storage={0: 52}), + contract_a: Account( + storage={COUNTER_SLOT: a_count}, + balance=A_INITIAL_BALANCE - b_count * A_CALL_VALUE, + ), + contract_b: Account( + storage={COUNTER_SLOT: b_count}, + balance=b_count * A_CALL_VALUE, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py b/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py index ee666d8bf61..eaac392a28b 100644 --- a/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py +++ b/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py @@ -1,17 +1,29 @@ """ -Test_call_recursive_bomb3. +Verify a self-recursive CALL bomb that keeps only a 224-gas reserve. + +Each level bumps a shared depth counter and forwards everything but a +tiny reserve to a call to itself, so descent is throttled only by the +EIP-150 63/64 withhold. On the way back up a level must afford its +success-flag store from its 1/64 retention plus whatever its child +returned; levels that cannot (EIP-2200's stipend rule included) halt +and forfeit, so the surviving storage pins the exact depth the budget +sustains. Ported from: state_tests/stSystemOperationsTest/CallRecursiveBomb3Filler.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +returned-leftover propagation, warm/cold and SSTORE pricing via opcode +metadata, EIP-8037 state-gas spill), validated against the ported +Cancun depth. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,58 +32,179 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +# Gas each level keeps back; far below a cold store, so completing the +# post-call flag store depends on the 1/64 retention and the child's +# returned leftover. +GAS_RESERVE = 224 +# Ported budget; pins the OOG-terminated depth. +TX_GAS_LIMIT = 1_000_000 + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=Op.ADDRESS, + ), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its flag store halts and + forfeits its entire grant to its parent, so the deepest level that + completes fixes the surviving depth counter (deeper levels' writes + and warmth all revert). The level above the deepest survivor funds + its more expensive zero-to-one flag set partly from the survivor's + returned leftover. Every cost is derived from the fork via opcode + metadata, including EIP-8037 state gas: with a sub-cap gas limit + the state reservoir is zero, so state charges spill from the + charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The ask expression's SUB runs after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.SUB(Op.GAS, GAS_RESERVE) + call_upfront = Op.CALL(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the reserve push plus the GAS opcode itself. + pre_gas_read = ( + Op.CALL(gas=ask_expr, address=Op.ADDRESS, address_warm=True).gas_cost( + fork + ) + - call_upfront + - post_gas_read + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Flag store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own store finds a cold slot and a zero current value. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return {COUNTER_SLOT: survivor, RESULT_SLOT: result_below} + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/CallRecursiveBomb3Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_recursive_bomb3( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_recursive_bomb3.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[ 0 ]] (+ (SLOAD 0) 1) [[ 1 ]] (CALL (- (GAS) 224) (ADDRESS) 0 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.SUB(Op.GAS, 0xE0), - address=Op.ADDRESS, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0x1312D00, - nonce=0, - ) + """Pin the depth a thin-reserve CALL self-recursion sustains.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=1000000, - value=0x186A0, + gas_limit=TX_GAS_LIMIT, ) - post = {target: Account(storage={0: 18, 1: 1})} + post = { + target: Account(storage=predict_recursion_storage(fork, TX_GAS_LIMIT)), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From 004230418534fae5f63c50085ee0c096b66354b0 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:01:45 +0200 Subject: [PATCH 20/25] fix(tests): un-skip remaining Amsterdam ported static tests, emptying the skip list --- tests/ported_static/amsterdam_skip_list.txt | 57 +- ...est_create2_oo_gafter_init_code_revert2.py | 174 ++++-- .../test_create2_oog_from_call_refunds.py | 50 +- ...est_create2collision_selfdestructed_oog.py | 245 ++++----- .../stCreate2/test_create2no_cash.py | 188 +++---- ...st_create_message_reverted_oog_in_init2.py | 188 ++++--- .../test_revert_depth_create2_oog.py | 347 ++++++------ .../test_revert_depth_create2_oog_berlin.py | 200 ------- ...t_revert_depth_create_address_collision.py | 394 ++++++++------ ...t_depth_create_address_collision_berlin.py | 224 -------- .../stRefundTest/test_refund50_2.py | 90 +-- .../stRefundTest/test_refund50percent_cap.py | 116 ++-- .../stRefundTest/test_refund_call_a.py | 101 ++-- .../test_refund_suicide50procent_cap.py | 253 +++++---- .../stRefundTest/test_refund_tx_to_suicide.py | 85 +-- .../test_loop_calls_depth_then_revert.py | 102 ++-- ...t_loop_delegate_calls_depth_then_revert.py | 100 ++-- ...t_revert_depth_create_address_collision.py | 319 +++++------ .../test_revert_depth_create_oog.py | 289 +++++----- ...pcode_in_calls_on_non_empty_return_data.py | 513 +++++------------- 20 files changed, 1722 insertions(+), 2313 deletions(-) delete mode 100644 tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py delete mode 100644 tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index cb02f797b6b..1baa46c3978 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,59 +8,4 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 66 - -# stCallCodes (3) - -# stCallCreateCallCodeTest (6) - -# stCreate2 (31) -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_CallCode_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_DelegateCall_Refund_NoOoG] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d0] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d1] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d2] -stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g0] -stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g1] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v0] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v1] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d0-g1-v0] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d0-g1-v1] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g0-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g0-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g0-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g0-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d0-g0-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d0-g0-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g0-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g0-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] - -# stDelegatecallTestHomestead (4) - -# stRefundTest (7) - -# stRevertTest (12) -stRevertTest/test_loop_calls_depth_then_revert.py::test_loop_calls_depth_then_revert[fork_Amsterdam] -stRevertTest/test_loop_delegate_calls_depth_then_revert.py::test_loop_delegate_calls_depth_then_revert[fork_Amsterdam] -stRevertTest/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g1-v0] -stRevertTest/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g1-v1] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v0] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v1] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v0] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v1] - -# stSystemOperationsTest (3) -stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] -stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] -stSystemOperationsTest/test_call_recursive_bomb3.py::test_call_recursive_bomb3[fork_Amsterdam] - +# Total entries: 0 diff --git a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py index 2b48b4cb0bd..8ca16e80b5b 100644 --- a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py +++ b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py @@ -1,98 +1,158 @@ """ -Calls a contract that runs CREATE2 which deploy a code. then after... +Verify a CREATE2 whose child completes its init code but runs out of gas +at the code-deposit charge, inside a frame that then REVERTs: the revert +payload carries the CREATE2 result (zero) back to the caller and every +side effect of the creating frame is rolled back. Ported from: state_tests/stCreate2/Create2OOGafterInitCodeRevert2Filler.json + +@manually-enhanced: Do not overwrite. The forwarded grant is derived from +fork composites so the child fails exactly at the deposit charge on every +fork; the revert payload now also carries the CREATE2 result, and the +caller stores the call result plus both payload words. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, - compute_create_address, + compute_create2_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PAYLOAD_SLOT = 0x1 +CREATE2_RESULT_SLOT = 0x2 +CALL_RESULT_SLOT = 0x3 + +# The init code returns this many memory bytes as the code to deposit; +# the grant is sized so this charge is exactly what the child cannot pay. +DEPOSIT_SIZE = 0x40 + @pytest.mark.ported_from( ["state_tests/stCreate2/Create2OOGafterInitCodeRevert2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Constantinople") def test_create2_oo_gafter_init_code_revert2( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Calls a contract that runs CREATE2 which deploy a code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 child dies at the deposit charge; its creator reverts.""" + # The child's init code: one word of scratch data, then a deposit + # request the sized grant cannot cover. The would-be deposited code + # (an SSTORE) never runs. + child_mstore = Op.MSTORE( + offset=0x0, + value=0x6001600155, + new_memory_size=0x20, ) + child_return = Op.RETURN( + offset=0x0, + size=DEPOSIT_SIZE, + new_memory_size=DEPOSIT_SIZE, + old_memory_size=0x20, + ) + initcode = child_mstore + child_return + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x20, "init code must fit one word" - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The creator writes the init code into memory (right-aligned in the + # first word), runs CREATE2, appends the CREATE2 result to memory and + # reverts both words back to the caller. + creator_setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(initcode_bytes, "big"), + new_memory_size=0x20, + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(initcode_bytes), + size=len(initcode_bytes), + salt=0x0, + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(initcode_bytes), + ) + creator = pre.deploy_contract( + code=creator_setup + + Op.MSTORE( + offset=0x20, + value=create2_code, + new_memory_size=0x40, + old_memory_size=0x20, + ) + + Op.REVERT(offset=0x0, size=0x40) ) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { (MSTORE 0 0x6460016001556000526005601bf3) (CREATE2 0 18 14 0) (REVERT 0 32) } # noqa: E501 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x6460016001556000526005601BF3) - + Op.POP(Op.CREATE2(value=0x0, offset=0x12, size=0xE, salt=0x0)) - + Op.REVERT(offset=0x0, size=0x20) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Size the grant so the child's grant covers its init-code execution + # but not the deposit charge, and the creator's 1/64 retention still + # covers its tail (the result MSTORE and the REVERT). + child_exec = child_mstore.gas_cost(fork) + child_return.gas_cost(fork) + deposit_cost = DEPOSIT_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + deposit_cost += fork.code_deposit_state_gas(code_size=DEPOSIT_SIZE) + child_grant = child_exec + deposit_cost // 2 + rem_after_create = -(-child_grant * 64 // 63) + forwarded = ( + creator_setup.gas_cost(fork) + + create2_code.gas_cost(fork) + + rem_after_create + ) + granted = rem_after_create - rem_after_create // 64 + assert child_exec + 10 <= granted <= child_exec + deposit_cost - 10, ( + "the child grant must die exactly at the deposit charge" ) - # Source: lll - # { (CALL 33000 0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b 0 0 0 0 32) [[ 1 ]] (MLOAD 0) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x80E8, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x20, - ) + creator_tail = Op.MSTORE( + offset=0x20, + value=0x0, + new_memory_size=0x40, + old_memory_size=0x20, + ).gas_cost(fork) + Op.REVERT(offset=0x0, size=0x40).gas_cost(fork) + assert rem_after_create // 64 >= creator_tail + 5, ( + "the creator's retention must cover its tail" + ) + + # The caller forwards the sized grant, then stores the call result + # and both words of the revert payload. + call_code = Op.CALL(gas=forwarded, address=creator, ret_size=0x40) + caller = pre.deploy_contract( + code=Op.SSTORE(key=CALL_RESULT_SLOT, value=Op.ADD(0x1, call_code)) + + Op.SSTORE(key=PAYLOAD_SLOT, value=Op.MLOAD(offset=0x0)) + + Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD(0x1, Op.MLOAD(offset=0x20)), ) - + Op.SSTORE(key=0x1, value=Op.MLOAD(offset=0x0)) + Op.STOP, - storage={1: 1}, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + storage={PAYLOAD_SLOT: 0x1}, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=75000, + sender=pre.fund_eoa(), + to=caller, + state_gas_reservoir=0, ) post = { - contract_0: Account(storage={1: 0x6460016001556000526005601BF3}), - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, + caller: Account( + storage={ + # The creator reverted: the call result is 0. + CALL_RESULT_SLOT: 0x1, + # First payload word: the init code the creator staged. + PAYLOAD_SLOT: int.from_bytes(initcode_bytes, "big"), + # Second payload word: the failed CREATE2 returned 0. + CREATE2_RESULT_SLOT: 0x1, + } + ), + # Everything inside the creator was rolled back. + creator: Account(nonce=1, storage={}), + compute_create2_address(creator, 0, initcode): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py b/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py index 9efd489bbb9..9aef43337a4 100644 --- a/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py +++ b/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py @@ -1,8 +1,17 @@ """ -Test_create2_oog_from_call_refunds. +Verify gas refunds earned inside a CREATE2's init code (storage clears, +via direct stores and CALL/CALLCODE/DELEGATECALL helpers, selfdestructs, +and nested creations) against out-of-gas boundaries: each scenario runs +once completing normally and twice dying — on an oversized code deposit +and on an INVALID that pins the refund bookkeeping. Ported from: state_tests/stCreate2/Create2OOGFromCallRefundsFiller.yml + +@manually-enhanced: Do not overwrite. The transaction budget and the +sender's funding derive from the fork: the ported 400k regular budget +plus the deepest arm's peak outstanding EIP-8037 state gas, guarded to +stay below the 5000-byte deposit charge that starves the OoG arms. """ import pytest @@ -27,6 +36,13 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +GAS_PRICE = 10 +# The ported budget, proven to cover every NoOoG arm's regular gas. +PORTED_GAS_LIMIT = 400_000 +# The OoG arms return this much memory as code; the deposit charge is +# what must exceed the transaction budget so they starve. +OOG_DEPOSIT_SIZE = 0x1388 + @pytest.mark.ported_from( ["state_tests/stCreate2/Create2OOGFromCallRefundsFiller.yml"], @@ -233,7 +249,34 @@ def test_create2_oog_from_call_refunds( base_fee_per_gas=10, ) - pre[sender] = Account(balance=0x3D0900, nonce=1) + # EIP-8037 charges state gas on top of the ported regular budget. + # The headroom is the deepest arm's (create-inside-create2) peak + # outstanding state gas: a NEW_ACCOUNT charge and one live fresh + # slot set at each creation depth, plus the one-byte code deposits; + # all terms are zero before Amsterdam. + fresh_set_state = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ).state_cost(fork) + new_account_state = Op.CREATE2( + value=0x0, offset=0x0, size=0x0, salt=0x0 + ).state_cost(fork) + tx_gas_limit = ( + PORTED_GAS_LIMIT + + 2 * new_account_state + + 2 * fresh_set_state + + 3 * fork.code_deposit_state_gas(code_size=1) + + 20_000 + ) + # The budget must stay below the oversized deposit charge so the + # OoG arms keep starving on it on every fork. + oog_deposit = ( + OOG_DEPOSIT_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) + ) + assert tx_gas_limit < oog_deposit, "the OoG arms must stay starved" + + # The exact funding makes the OoG arms' post-state balance zero. + pre[sender] = Account(balance=tx_gas_limit * GAS_PRICE, nonce=1) # Source: yul # berlin # { @@ -1189,13 +1232,14 @@ def test_create2_oog_from_call_refunds( Bytes("693c6139") + Hash(contract_23, left_padding=True), Bytes("693c6139") + Hash(contract_24, left_padding=True), ] - tx_gas = [400000] + tx_gas = [tx_gas_limit] tx = Transaction( sender=sender, to=contract_0, data=tx_data[d], gas_limit=tx_gas[g], + gas_price=GAS_PRICE, nonce=1, error=_exc, ) diff --git a/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py b/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py index 4def59bad89..70ae08055a7 100644 --- a/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py +++ b/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py @@ -1,52 +1,57 @@ """ -Collision with address that has been selfdestructed in the same... +Verify a CREATE2 whose target address holds a pre-existing account that +SELFDESTRUCTed earlier in the same transaction: the collision stands +(the account is only emptied, not freed), consuming the child's grant, +and the sized budget then runs the creating init code out of gas so the +whole creation transaction rolls back — including the selfdestruct's +balance transfer. Ported from: state_tests/stCreate2/create2collisionSelfdestructedOOGFiller.json + +@manually-enhanced: Do not overwrite. Collider and beneficiary addresses +are computed instead of hardcoded, the budget is derived from fork +composites, and the post-collision work is sized above the collision's +1/64 retention on every fork (the alive collider means the CREATE2 +charges — and refunds — no new-account state gas). """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, + compute_create2_address, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COLLIDER_BALANCE = 1 +# Gas left for the collision to consume: the CREATE2's child grant. Its +# 1/64 retention (plus any state refund) must stay below the two-store +# victim cost, which the guard below asserts. +CHILD_GRANT_SLACK = 30_000 + @pytest.mark.ported_from( ["state_tests/stCreate2/create2collisionSelfdestructedOOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "inner_initcode", [ + pytest.param(Bytecode(), id="empty_initcode"), + pytest.param(Op.SSTORE(key=0x1, value=0x1), id="storing_initcode"), pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", + Op.MSTORE(offset=0x0, value=0x6001600155) + + Op.RETURN(offset=0x1B, size=0x5), + id="depositing_initcode", ), ], ) @@ -55,120 +60,118 @@ def test_create2collision_selfdestructed_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + inner_initcode: Bytecode, ) -> None: - """Collision with address that has been selfdestructed in the same...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xE2B35478FDD26477CC576DD906E6277761246A3C) - contract_1 = Address(0xAF3ECBA2FE09A4F6C19F16A9D119E44E08C2DA01) - contract_2 = Address(0xEC2C6832D00680ECE8FF9254F81FDAB0A5A2AC50) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) + """A selfdestructed account still collides, and its grant is lost.""" + sender = pre.fund_eoa() + outer_created = compute_create_address(address=sender, nonce=0) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The collider occupies the CREATE2 target (which depends on the + # init code below through its hash) and selfdestructs when called; + # its address is derived from the pre-funded sender, so the pre + # allocation must stay mutable. + beneficiary = pre.nonexistent_account() + collider_work = Op.SELFDESTRUCT( + address=beneficiary, + address_warm=False, + account_new=True, + ) + collider = compute_create2_address(outer_created, 0, inner_initcode) + pre.deploy_contract( + code=collider_work, + balance=COLLIDER_BALANCE, + address=collider, ) - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xE2B35478FDD26477CC576DD906E6277761246A3C), # noqa: E501 + # The outer init code selfdestructs the collider, stages the child's + # init code (never executed: the collision aborts before dispatch) + # and runs the CREATE2 into the collision; the two stores after it + # are the victims the burned grant leaves unaffordable. + inner_bytes = bytes(inner_initcode) + assert len(inner_bytes) <= 0x20, "inner init code must fit one word" + call_code = Op.CALL( + address=collider, + address_warm=False, + value_transfer=False, + account_new=False, ) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xAF3ECBA2FE09A4F6C19F16A9D119E44E08C2DA01), # noqa: E501 + setup = ( + Op.MSTORE( + offset=0x0, + value=int.from_bytes(inner_bytes, "big"), + new_memory_size=0x20, + ) + if inner_bytes + else Bytecode() + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(inner_bytes) if inner_bytes else 0x0, + size=len(inner_bytes), + salt=0x0, + new_memory_size=0x20 if inner_bytes else 0x0, + old_memory_size=0x20 if inner_bytes else 0x0, + init_code_size=len(inner_bytes), + account_new=False, ) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xEC2C6832D00680ECE8FF9254F81FDAB0A5A2AC50), # noqa: E501 + victim_stores = Op.SSTORE( + key=0x0, + value=0x112233, + key_warm=False, + original_value=0, + new_value=0x112233, + ) + Op.SSTORE( + key=0x1, + value=0x112233, + key_warm=False, + original_value=0, + new_value=0x112233, + ) + outer_initcode = ( + Op.POP(call_code) + setup + Op.POP(create2_code) + victim_stores ) - tx_data = [ - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + # The budget covers everything up to and including the CREATE2's own + # charges plus the slack the collision consumes; the guard proves + # the victims exceed what the collision leaves behind, so the outer + # frame must die and the whole creation rolls back. The collider is + # alive at the CREATE2 (only emptied by its selfdestruct), so no + # new-account state gas is charged — or refunded — there. + gas_limit = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=outer_initcode, + contract_creation=True, ) - + Op.MSTORE(offset=0x0, value=0x6001600155) - + Op.POP(Op.CREATE2(value=0x0, offset=0x1B, size=0x5, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.MSTORE(offset=0x0, value=0x6460016001556000526005601BF3) - + Op.POP(Op.CREATE2(value=0x0, offset=0x12, size=0xE, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - ] - tx_gas = [200000] - tx_value = [1] + + fork.transaction_top_frame_state_gas(contract_creation=True) + + call_code.gas_cost(fork) + + collider_work.gas_cost(fork) + + setup.gas_cost(fork) + + create2_code.gas_cost(fork) + + CHILD_GRANT_SLACK + ) + leftover = CHILD_GRANT_SLACK // 64 + assert leftover + 2_500 < victim_stores.gas_cost(fork), ( + "the collision's leavings must not afford the victim stores" + ) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], + data=outer_initcode, + gas_limit=gas_limit, ) post = { - contract_0: Account(code=bytes.fromhex("6010ff00"), balance=1), - contract_1: Account(code=bytes.fromhex("6010ff00"), balance=1), - contract_2: Account(code=bytes.fromhex("6010ff00"), balance=1), - Address( - 0x0000000000000000000000000000000000000010 - ): Account.NONEXISTENT, - compute_create_address(address=sender, nonce=0): Account.NONEXISTENT, sender: Account(nonce=1), + # Rolled back wholesale: the collider keeps its code and its + # balance, the beneficiary was never credited, nothing created. + collider: Account( + code=collider_work, + balance=COLLIDER_BALANCE, + nonce=1, + ), + beneficiary: Account.NONEXISTENT, + outer_created: Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create2no_cash.py b/tests/ported_static/stCreate2/test_create2no_cash.py index 587c64889c1..5dc49d75d32 100644 --- a/tests/ported_static/stCreate2/test_create2no_cash.py +++ b/tests/ported_static/stCreate2/test_create2no_cash.py @@ -1,161 +1,105 @@ """ -Create2 fails with not enough cash (endowment of a new account) +... +Verify CREATE2's endowment balance preflight: a creator one wei short of +the endowment fails without creating (and without a nonce bump), a +one-wei top-up sent with the call makes the same CREATE2 succeed, and in +a static context the CREATE2 faults the whole frame instead. Ported from: state_tests/stCreate2/create2noCashFiller.json + +@manually-enhanced: Do not overwrite. The creation-transaction wrapper +and tuned gas budgets are replaced by a deployed entry contract that +records the call result; the created account and the creator's nonce +(no bump on the balance preflight) are asserted explicitly. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, StateTestFiller, Transaction, + compute_create2_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALL_RESULT_SLOT = 0x0 +CREATE2_ENDOWMENT = 0x65 + @pytest.mark.ported_from( ["state_tests/stCreate2/create2noCashFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "opcode, top_up", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), + pytest.param(Op.CALL, 0, id="call_insufficient_balance"), + pytest.param(Op.CALL, 1, id="call_topped_up_balance"), + pytest.param(Op.STATICCALL, 0, id="staticcall_write_protection"), ], ) -@pytest.mark.pre_alloc_mutable def test_create2no_cash( state_test: StateTestFiller, pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, + opcode: Op, + top_up: int, ) -> None: - """Create2 fails with not enough cash (endowment of a new account) +...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xE2B35478FDD26477CC576DD906E6277761246A3C) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 endowment beyond the creator's balance cannot create.""" + # The creator holds one wei less than the endowment it attempts to + # transfer; only the topped-up arm can afford it. + creator = pre.deploy_contract( + code=Op.POP( + Op.CREATE2(value=CREATE2_ENDOWMENT, offset=0x0, size=0x0, salt=0x0) + ) + + Op.STOP, + balance=CREATE2_ENDOWMENT - 1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The entry contract forwards the transaction's value (the optional + # top-up) and records the call result, shifted so that a failed call + # (1), a successful call (2) and no call at all (0) all differ. + if opcode == Op.CALL: + call_code = Op.CALL(address=creator, value=top_up) + else: + call_code = Op.STATICCALL(address=creator) + entry = pre.deploy_contract( + code=Op.SSTORE(key=CALL_RESULT_SLOT, value=Op.ADD(0x1, call_code)) + + Op.STOP, ) - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (CREATE2 101 0 0 0) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CREATE2(value=0x65, offset=0x0, size=0x0, salt=0x0) + Op.STOP, - balance=100, - nonce=0, - address=Address(0xE2B35478FDD26477CC576DD906E6277761246A3C), # noqa: E501 + tx = Transaction( + sender=pre.fund_eoa(), + to=entry, + value=top_up, + state_gas_reservoir=0, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(balance=100), - Address( - 0x12AAEFBC0350A026228076E5369E6CE148CE67BE - ): Account.NONEXISTENT, - sender: Account(nonce=1), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(balance=0), - Address(0x12AAEFBC0350A026228076E5369E6CE148CE67BE): Account( - balance=101 - ), - sender: Account(nonce=1), - }, - }, - ] + created = compute_create2_address(creator, 0, b"") + create_succeeds = opcode == Op.CALL and top_up > 0 + if create_succeeds: + # The whole (topped-up) balance moved into the created account, + # and the creator's nonce was consumed by the creation. + creator_account = Account(nonce=2, balance=0) + created_account = Account(nonce=1, code=b"", balance=CREATE2_ENDOWMENT) + else: + # The balance preflight (or the static fault) aborts before any + # account is touched: no creation and no nonce bump. + creator_account = Account(nonce=1, balance=CREATE2_ENDOWMENT - 1) + created_account = Account.NONEXISTENT - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # A static frame faults on CREATE2, so only that arm's call fails. + call_result = 0 if opcode == Op.STATICCALL else 1 - tx_data = [ - Op.CALL( - gas=0x249F0, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - Op.CALL( - gas=0x249F0, - address=contract_0, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - Op.STATICCALL( - gas=0x249F0, - address=contract_0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - ] - tx_gas = [400000] - tx_value = [1] - - tx = Transaction( - sender=sender, - to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + entry: Account( + storage={CALL_RESULT_SLOT: 0x1 + call_result}, balance=0 + ), + creator: creator_account, + created: created_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py b/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py index 4e4ef237ca4..e75adcb8de8 100644 --- a/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py +++ b/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py @@ -1,127 +1,145 @@ """ -Create2 oog during the init code, + when create2 is from transaction... +Verify a CREATE2 issued from a contract-creation transaction's init +code: the transaction budget decides whether the CREATE2's child +completes its storage-writing init code or dies out of gas, while the +outer creation completes either way. Ported from: state_tests/stCreate2/CreateMessageRevertedOOGInInit2Filler.json + +@manually-enhanced: Do not overwrite. Both budgets are derived from fork +composites (intrinsic + top-frame state gas + the composed init code); +the outer created account is asserted on both arms with a pre-CREATE2 +canary, and the child account's storage on the success arm. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, + compute_create2_address, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CANARY_SLOT = 0x2 +CANARY = 0xFF +TX_VALUE = 100 +CHILD_STORED = {0x0: 0xC, 0x1: 0xD} + @pytest.mark.ported_from( ["state_tests/stCreate2/CreateMessageRevertedOOGInInit2Filler.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "child_covered", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(False, id="child_oog"), + pytest.param(True, id="child_succeeds"), ], ) -@pytest.mark.pre_alloc_mutable def test_create_message_reverted_oog_in_init2( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + child_covered: bool, ) -> None: - """Create2 oog during the init code, + when create2 is from...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """The budget decides how far an init-code CREATE2's child gets.""" + # The child's init code writes two fresh slots and deposits nothing. + inner_initcode = Op.SSTORE( + key=0x0, + value=CHILD_STORED[0x0], + key_warm=False, + original_value=0, + new_value=CHILD_STORED[0x0], + ) + Op.SSTORE( + key=0x1, + value=CHILD_STORED[0x1], + key_warm=False, + original_value=0, + new_value=CHILD_STORED[0x1], ) + inner_bytes = bytes(inner_initcode) + assert len(inner_bytes) <= 0x20, "inner init code must fit one word" - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000000000, + # The outer init code writes a completion canary before the CREATE2 + # (only a POP runs after it: on the starved arm the 1/64 retention + # cannot afford an SSTORE), stages the child's init code in memory + # and runs the CREATE2; it deposits no code. + canary_store = Op.SSTORE( + key=CANARY_SLOT, + value=CANARY, + key_warm=False, + original_value=0, + new_value=CANARY, ) - - pre[sender] = Account(balance=0x2DC6C0) - # Source: hex - # 0x - contract_0 = pre.deploy_contract( # noqa: F841 - code="", - balance=10, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(inner_bytes, "big"), + new_memory_size=0x20, ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(inner_bytes), + size=len(inner_bytes), + salt=0x0, + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(inner_bytes), + ) + outer_initcode = canary_store + setup + Op.POP(create2_code) + Op.STOP - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - Address( - 0xF3059E18A327C662766F6BA11808C400635847EF - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - Address(0xF3059E18A327C662766F6BA11808C400635847EF): Account( - storage={0: 12, 1: 13}, balance=0, nonce=1 - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.MSTORE(offset=0x0, value=0x600C600055600D600155) - + Op.CREATE2(value=0x0, offset=0x16, size=0xA, salt=0x0) - + Op.STOP, - ] - tx_gas = [110000, 150000] - tx_value = [100] + # Both budgets derive from the same overhead: everything the outer + # frame pays before (and including) the CREATE2's own charges. The + # child's grant is what remains after the 1/64 withhold. + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=outer_initcode, + contract_creation=True, + sends_value=True, + ) + + fork.transaction_top_frame_state_gas(contract_creation=True) + + canary_store.gas_cost(fork) + + setup.gas_cost(fork) + + create2_code.gas_cost(fork) + ) + child_needed = inner_initcode.gas_cost(fork) + if child_covered: + gas_limit = overhead + -(-child_needed * 64 // 63) + 3_000 + else: + gas_limit = overhead + child_needed // 2 + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=outer_initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + outer_created = compute_create_address(address=sender, nonce=0) + child = compute_create2_address(outer_created, 0, inner_initcode) + post = { + sender: Account(nonce=1), + # The outer creation completes on both arms: the CREATE2 always + # bumps its nonce, and a failed child costs it only the grant. + outer_created: Account( + nonce=2, + code=b"", + balance=TX_VALUE, + storage={CANARY_SLOT: CANARY}, + ), + child: Account(nonce=1, code=b"", balance=0, storage=CHILD_STORED) + if child_covered + else Account.NONEXISTENT, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py index 00ddb3595a4..a5dd5bc6260 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py @@ -1,200 +1,235 @@ """ -Test_revert_depth_create2_oog. +Verify a CREATE2 two frames deep under out-of-gas pressure: the calldata +sets the grant a caller forwards to a creating contract, and the two +budgets decide whether the creation completes, the creator dies mid-way, +or the whole outer frame runs dry — each with a distinct post-state. Ported from: state_tests/stCreate2/RevertDepthCreate2OOGFiller.json +state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json + +@manually-enhanced: Do not overwrite. The byte-identical Berlin twin is +folded in; every budget derives from fork composites; the creator now +stores the CREATE2 result so a wrongly failed (or wrongly succeeding) +creation is visible beyond the created account itself. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create2_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Caller slots (as in the ported filler). +CALLER_START_SLOT = 0x0 +CALL_RESULT_SLOT = 0x1 +CALLER_DONE_SLOT = 0x4 +# Creator slots: 0x2/0x3 as ported, plus the CREATE2 result. +CREATOR_START_SLOT = 0x2 +CREATOR_DONE_SLOT = 0x3 +CREATE2_RESULT_SLOT = 0x5 + +# Gas available in the caller frame at the CALL on the starved-outer +# arms: far below the creator's needs, and retaining under 1/64th of the +# EIP-2200 stipend so no post-call store can run — the caller must die. +STARVED_AVAILABLE = 20_000 + @pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreate2OOGFiller.json"], + [ + "state_tests/stCreate2/RevertDepthCreate2OOGFiller.json", + "state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json", + ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "creator_covered", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), + pytest.param(False, id="creator_oog"), + pytest.param(True, id="creator_ok"), ], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize( + "outer_covered", + [ + pytest.param(False, id="outer_oog"), + pytest.param(True, id="outer_ok"), + ], +) +@pytest.mark.parametrize("tx_value", [1, 0]) def test_revert_depth_create2_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + creator_covered: bool, + outer_covered: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create2_oog.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Two stacked budgets decide how deep a nested CREATE2 gets.""" + # The creator: entry marker, an empty-init-code CREATE2 whose result + # is stored (success leaves the created address plus one, a failure + # leaves exactly one), and a completion marker. + sstore_2 = Op.SSTORE( + key=CREATOR_START_SLOT, + value=0x8, + key_warm=False, + original_value=0, + new_value=0x8, + ) + result_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD( + 0x1, Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0) + ), + key_warm=False, + original_value=0, + new_value=0x1, + ) + sstore_3 = Op.SSTORE( + key=CREATOR_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, + ) + creator = pre.deploy_contract( + code=sstore_2 + result_store + sstore_3 + Op.STOP ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # The empty-init-code child consumes nothing and returns its whole + # grant, so the creator's needs are just its composite costs. + creator_needed = ( + sstore_2.gas_cost(fork) + + result_store.gas_cost(fork) + + sstore_3.gas_cost(fork) ) + if creator_covered: + forwarded = creator_needed + 1_000 + else: + forwarded = creator_needed // 2 - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 + # The caller: entry marker, the CALL with its grant taken from + # calldata (as in the ported filler), result store, completion + # marker. + sstore_0 = Op.SSTORE( + key=CALLER_START_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=0x1, + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=creator, + address_warm=False, + value_transfer=False, + account_new=False, + ) + sstore_1 = Op.SSTORE( + key=CALL_RESULT_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=0x1, ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + sstore_4 = Op.SSTORE( + key=CALLER_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, + ) + caller_code = sstore_0 + sstore_1 + sstore_4 + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + tx_data = Hash(forwarded) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=tx_data, + sends_value=tx_value > 0, + ) + sstore_0.gas_cost(fork) + if outer_covered: + # Enough at the CALL that the EIP-150 clamp still grants the + # full ask, plus the caller's post-call stores. + available = -(-forwarded * 64 // 63) + 64 + assert available - available // 64 >= forwarded, ( + "the full ask must be granted" + ) + gas_limit = ( + overhead + + sstore_1.gas_cost(fork) + + sstore_4.gas_cost(fork) + + available ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0xA000000000000000000000000000000000000000), # noqa: E501 + else: + # The clamped grant starves the creator, and the 1/64 retention + # cannot run any store afterwards: the caller must die too. + granted = STARVED_AVAILABLE - STARVED_AVAILABLE // 64 + assert granted < creator_needed, "the creator must be starved" + assert STARVED_AVAILABLE // 64 <= fork.gas_costs().CALL_STIPEND, ( + "the retention must not afford the post-call store" + ) + gas_limit = overhead + call_code.gas_cost(fork) + STARVED_AVAILABLE + + sender = pre.fund_eoa() + tx = Transaction( + sender=sender, + to=caller, + data=tx_data, + gas_limit=gas_limit, + value=tx_value, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x05A28FC366483258507BCF739658573CB47E4FAD): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), + created = compute_create2_address(creator, 0, b"") + if not outer_covered: + # The whole transaction ran dry: only the code survives. + caller_account = Account(storage={}, code=caller_code, balance=0) + creator_account = Account(storage={}, nonce=1) + created_account: Account | type = Account.NONEXISTENT + elif not creator_covered: + # The creator died mid-creation and was rolled back. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x0, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), + balance=tx_value, + ) + creator_account = Account(storage={}, nonce=1) + created_account = Account.NONEXISTENT + else: + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x1, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), + balance=tx_value, + ) + creator_account = Account( + storage={ + CREATOR_START_SLOT: 0x8, + CREATE2_RESULT_SLOT: int.from_bytes(bytes(created), "big") + 1, + CREATOR_DONE_SLOT: 0xC, }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 170000] - tx_value = [1, 0] + nonce=2, + ) + created_account = Account(nonce=1, code=b"", balance=0) - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + sender: Account(nonce=1), + caller: caller_account, + creator: creator_account, + created: created_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py deleted file mode 100644 index c15b8760d49..00000000000 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -Test_revert_depth_create2_oog_berlin. - -Ported from: -state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Environment, - Hash, - StateTestFiller, - Transaction, -) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_revert_depth_create2_oog_berlin( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Test_revert_depth_create2_oog_berlin.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0xA000000000000000000000000000000000000000), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x05A28FC366483258507BCF739658573CB47E4FAD): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 170000] - tx_value = [1, 0] - - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py index 38873c202b1..ac9cbaf99d6 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py @@ -1,222 +1,264 @@ """ -Copy of this test for CREATE2. +Verify a CREATE2 that collides with a live account — the very caller +that funded the attempt: the collision burns the child's grant and bumps +the creator's nonce without creating anything, and the two stacked +budgets decide whether the creator survives its aftermath, dies on it, +or the whole outer frame runs dry. Ported from: state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json +state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json -@manually-enhanced: Do not overwrite. `tx_gas` raised on Amsterdam to -cover EIP-8037 NEW_ACCOUNT state-gas spill on the CREATE2-via-revert -path. Pre-EIP-8037 keeps the original [110_000, 170_000] tuned budgets; -post-state expectations unchanged on all forks. - +@manually-enhanced: Do not overwrite. The byte-identical Berlin twin is +folded in, and the legacy fillers' vacancy is repaired: they kept the +collider at contract_1's CREATE address while the code runs CREATE2, so +nothing ever collided — the caller now occupies the CREATE2 target. The +creator pre-writes its result slot so the post-collision store is a +dirty-warm write its 1/64 retention can afford, and all budgets derive +from fork composites. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create2_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Caller slots (as in the ported filler). +CALLER_START_SLOT = 0x0 +CALL_RESULT_SLOT = 0x1 +CALLER_DONE_SLOT = 0x4 +# Creator slots: 0x2 as ported, plus the pre-written CREATE2 result. +CREATOR_START_SLOT = 0x2 +CREATE2_RESULT_SLOT = 0x5 +# Pre-written sentinel, overwritten by the CREATE2 result plus one: a +# surviving creator must show 1 (collision), never 0xFF or an address. +RESULT_PREWRITE = 0xFF + +# Post-collision slack for the starved-creator arm: retains under 1/64th +# of the EIP-2200 stipend, so the result store cannot run. +STARVED_SLACK = 10_000 +# The SSTORE composite prices a dirty re-store at the Berlin-era 100 on +# every fork, but the un-metered pre-Berlin schedule charges up to 5000 +# (EIP-1283 was reverted in ConstantinopleFix); the covered arm's +# retention carries this headroom so those forks stay covered too. +DIRTY_STORE_HEADROOM = 5_000 +# Gas available in the caller frame at the CALL on the starved-outer +# arms: too little for the creator, and retaining too little for any +# post-call store — the caller must die. +STARVED_AVAILABLE = 20_000 + @pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json"], + [ + "state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json", + "state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json", # noqa: E501 + ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "creator_covered", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), + pytest.param(False, id="creator_oog"), + pytest.param(True, id="creator_ok"), ], ) +@pytest.mark.parametrize( + "outer_covered", + [ + pytest.param(False, id="outer_oog"), + pytest.param(True, id="outer_ok"), + ], +) +@pytest.mark.parametrize("tx_value", [1, 0]) @pytest.mark.pre_alloc_mutable def test_revert_depth_create_address_collision( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + creator_covered: bool, + outer_covered: bool, + tx_value: int, ) -> None: - """Copy of this test for CREATE2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 collision burns the grant; budgets decide what's left.""" + # The creator: entry marker, result-slot pre-write, then the CREATE2 + # aimed at the caller's (occupied) address, whose result overwrites + # the sentinel as a dirty-warm store the 1/64 retention can pay. + sstore_2 = Op.SSTORE( + key=CREATOR_START_SLOT, + value=0x8, + key_warm=False, + original_value=0, + new_value=0x8, + ) + sentinel_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=RESULT_PREWRITE, + key_warm=False, + original_value=0, + new_value=RESULT_PREWRITE, + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x0, + size=0x0, + salt=0x0, + account_new=False, + ) + result_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD(0x1, create2_code), + key_warm=True, + original_value=0, + current_value=RESULT_PREWRITE, + new_value=0x1, + ) + creator = pre.deploy_contract( + code=sstore_2 + sentinel_store + result_store + Op.STOP ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # The caller occupies the creator's CREATE2 target (this is the + # repaired collision, and why the pre allocation must stay mutable). + sstore_0 = Op.SSTORE( + key=CALLER_START_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=0x1, + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=creator, + address_warm=False, + value_transfer=False, + account_new=False, + ) + sstore_1 = Op.SSTORE( + key=CALL_RESULT_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=0x1, ) + sstore_4 = Op.SSTORE( + key=CALLER_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, + ) + caller_code = sstore_0 + sstore_1 + sstore_4 + Op.STOP + caller = compute_create2_address(creator, 0, b"") + pre.deploy_contract(code=caller_code, address=caller) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 + # The collision consumes everything left after the CREATE2's charges + # but one 64th (the live target means no new-account state gas moves + # in either direction), so the creator's fate is set by the slack + # riding on top of its pre-collision costs. + create2_charge = create2_code.gas_cost(fork) + result_tail = result_store.gas_cost(fork) - create2_charge + stipend = fork.gas_costs().CALL_STIPEND + if creator_covered: + slack = 64 * (stipend + result_tail + DIRTY_STORE_HEADROOM + 100) + else: + slack = STARVED_SLACK + assert slack // 64 <= stipend, ( + "the retention must not afford the result store" + ) + forwarded = ( + sstore_2.gas_cost(fork) + + sentinel_store.gas_cost(fork) + + create2_charge + + slack ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB000000000000000000000000000000000000000, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + tx_data = Hash(forwarded) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=tx_data, + sends_value=tx_value > 0, + ) + sstore_0.gas_cost(fork) + if outer_covered: + # Enough at the CALL that the EIP-150 clamp still grants the + # full ask, plus the caller's post-call stores. + available = -(-forwarded * 64 // 63) + 64 + assert available - available // 64 >= forwarded, ( + "the full ask must be granted" + ) + gas_limit = ( + overhead + + sstore_1.gas_cost(fork) + + sstore_4.gas_cost(fork) + + available + ) + else: + granted = STARVED_AVAILABLE - STARVED_AVAILABLE // 64 + assert granted < sstore_2.gas_cost(fork) + sentinel_store.gas_cost( + fork + ), "the creator must die before its CREATE2" + assert STARVED_AVAILABLE // 64 <= stipend, ( + "the retention must not afford the post-call store" ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682), # noqa: E501 + gas_limit = overhead + call_code.gas_cost(fork) + STARVED_AVAILABLE + + sender = pre.fund_eoa() + tx = Transaction( + sender=sender, + to=caller, + data=tx_data, + gas_limit=gas_limit, + value=tx_value, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 1: 1, 4: 12}, nonce=54), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 4: 12}, nonce=54), - contract_1: Account(storage={}), + if not outer_covered: + # The whole transaction ran dry: only the code survives. + caller_account = Account(storage={}, code=caller_code, balance=0) + creator_account = Account(storage={}, nonce=1) + elif not creator_covered: + # The creator reached the collision but died on its aftermath + # and was rolled back — including the collision's nonce bump. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x0, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - contract_1: Account(storage={}), + code=caller_code, + balance=tx_value, + ) + creator_account = Account(storage={}, nonce=1) + else: + # The collision's signature: a nonce bump with nothing created, + # a zero CREATE2 result, and the caller's account untouched. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x1, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - contract_1: Account(storage={}), + code=caller_code, + balance=tx_value, + ) + creator_account = Account( + storage={ + CREATOR_START_SLOT: 0x8, + CREATE2_RESULT_SLOT: 0x1, }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - # EIP-8037 NEW_ACCOUNT state-gas spill on Amsterdam exceeds the - # original tuned tx_gas budgets; pre-EIP-8037 keeps the originals. - tx_gas = [110000, 170000] - if fork.is_eip_enabled(8037): - tx_gas = [500_000, 700_000] - tx_value = [1, 0] + nonce=2, + ) - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + sender: Account(nonce=1), + caller: caller_account, + creator: creator_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py deleted file mode 100644 index 907358ab624..00000000000 --- a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -Copy of this test for CREATE2. - -Ported from: -state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json - -@manually-enhanced: Do not overwrite. `tx_gas` raised on Amsterdam to -cover EIP-8037 NEW_ACCOUNT state-gas spill on the CREATE2-via-revert -path. Pre-EIP-8037 keeps the original [110_000, 170_000] tuned budgets; -post-state expectations unchanged on all forks. - -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Environment, - Hash, - StateTestFiller, - Transaction, -) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_revert_depth_create_address_collision_berlin( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Copy of this test for CREATE2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB000000000000000000000000000000000000000, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 1: 1, 4: 12}, nonce=54), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 4: 12}, nonce=54), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - contract_1: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - # EIP-8037 NEW_ACCOUNT state-gas spill on Amsterdam exceeds the - # original tuned tx_gas budgets; pre-EIP-8037 keeps the originals. - tx_gas = [110000, 170000] - if fork.is_eip_enabled(8037): - tx_gas = [500_000, 700_000] - tx_value = [1, 0] - - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund50_2.py b/tests/ported_static/stRefundTest/test_refund50_2.py index 4d471f24074..94ed5341671 100644 --- a/tests/ported_static/stRefundTest/test_refund50_2.py +++ b/tests/ported_static/stRefundTest/test_refund50_2.py @@ -1,17 +1,21 @@ """ -Test_refund50_2. +Verify the EIP-3529 refund cap over five storage clears: the sender's +final balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund50_2Filler.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,57 +24,67 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund50_2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund50_2( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund50_2.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x989680) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Five storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.SSTORE( + key=0xA, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.SSTORE( + key=0xB, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [[ 10 ]] 1 [[ 11 ]] 1 [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0xA, value=0x1) - + Op.SSTORE(key=0xB, value=0x1) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - target: Account(storage={10: 1, 11: 1}), - coinbase: Account(balance=0), - sender: Account(balance=0x8D926C, nonce=1), + target: Account(storage={0xA: 1, 0xB: 1}), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund50percent_cap.py b/tests/ported_static/stRefundTest/test_refund50percent_cap.py index d92a991123c..ece04d36914 100644 --- a/tests/ported_static/stRefundTest/test_refund50percent_cap.py +++ b/tests/ported_static/stRefundTest/test_refund50percent_cap.py @@ -1,18 +1,21 @@ """ -Test_refund50percent_cap. +Verify the EIP-3529 refund cap over six storage clears: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund50percentCapFiller.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,69 +24,84 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund50percentCapFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund50percent_cap( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund50percent_cap.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xDC4EFA209AECDD4C2D5201A419EA27506151B4EC687F14A613229E310932491B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Six storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.POP(Op.SLOAD(key=0x1, key_warm=False)) + + Op.POP(Op.SLOAD(key=0x2, key_warm=False)) + + Op.SSTORE( + key=0xA, + value=Op.EXP(0x2, 0xFF, exponent=0xFF), + key_warm=False, + original_value=0, + new_value=2**255, + ) + + Op.SSTORE( + key=0xB, + value=Op.BALANCE(address=Op.ADDRESS, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x6, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x989680) - # Source: lll - # { @@1 @@2 [[ 10 ]] (EXP 2 0xff) [[ 11 ]] (BALANCE (ADDRESS)) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP(Op.SLOAD(key=0x1)) - + Op.POP(Op.SLOAD(key=0x2)) - + Op.SSTORE(key=0xA, value=Op.EXP(0x2, 0xFF)) - + Op.SSTORE(key=0xB, value=Op.BALANCE(address=Op.ADDRESS)) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xEF67F354C8505E1056889970C3D9B5E0FE65D1E2), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { target: Account( - storage={ - 10: 0x8000000000000000000000000000000000000000000000000000000000000000, # noqa: E501 - 11: 0xDE0B6B3A7640000, - }, + storage={0xA: 2**255, 0xB: CONTRACT_BALANCE}, ), - coinbase: Account(balance=0), - sender: Account(balance=0x8CF0A0), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_call_a.py b/tests/ported_static/stRefundTest/test_refund_call_a.py index bc745f449be..c42de668a97 100644 --- a/tests/ported_static/stRefundTest/test_refund_call_a.py +++ b/tests/ported_static/stRefundTest/test_refund_call_a.py @@ -1,17 +1,21 @@ """ -Test_refund_call_a. +Verify a storage-clear refund earned inside a sub-call: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund_CallAFiller.json + +@manually-enhanced: Do not overwrite. The sub-call forwards all gas +instead of a schedule-sized constant, and the sender's balance, refund cap +and budget derive from the fork (`code.gas_cost` / `code.refund` +composites), so EIP-8037's repriced stores are tracked instead of pinned. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,72 +24,65 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund_CallAFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund_call_a( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund_call_a.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x1312D00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """A callee's storage clear refunds gas up to the EIP-3529 cap.""" + callee_code = Op.SSTORE( + key=0x1, value=0x0, key_warm=False, original_value=1, new_value=0 ) - - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [[ 1 ]] 0 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x0) + Op.STOP, + callee = pre.deploy_contract( + code=callee_code + Op.STOP, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) - # Source: lll - # { [[ 0 ]] (CALL 5500 0 0 0 0 0 )} # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=0x157C, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, + + # The caller's own slot 1 stays set: the callee clears its own storage. + caller_code = Op.SSTORE( + key=0x0, + value=Op.CALL(address=callee, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + caller = pre.deploy_contract( + code=caller_code + Op.STOP, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = ( + intrinsic + caller_code.gas_cost(fork) + callee_code.gas_cost(fork) + ) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=200000, - value=10, + to=caller, + gas_limit=gas_limit, + gas_price=GAS_PRICE, + ) + + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min( + caller_code.refund(fork) + callee_code.refund(fork), executed // 5 ) + gas_used = executed - refund post = { - target: Account(storage={0: 1, 1: 1}, balance=0xDE0B6B3A764000A), - coinbase: Account(balance=0), - sender: Account(balance=0x12A2AD2, nonce=1), - addr: Account(storage={}), + caller: Account(storage={0: 1, 1: 1}), + callee: Account(storage={}), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py b/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py index 4083b826958..812a0cb1ed4 100644 --- a/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py +++ b/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py @@ -1,158 +1,181 @@ """ -Test_refund_suicide50procent_cap. +Verify the EIP-3529 refund cap when eight storage clears surround a +gas-limited call to a self-destructing contract: the stored gas delta and +the sender's final balance track the executed gas minus the capped refund, +for both a starved and a fully funded sub-call. Ported from: state_tests/stRefundTest/refundSuicide50procentCapFiller.json + +@manually-enhanced: Do not overwrite. The sub-call grant, the stored gas +delta, the refund cap and the budget all derive from fork composites; the +destructor self-destructs to CALLER so every address is dynamic; the post +branches on EIP-6780 for the destructor's survival. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TARGET_BALANCE = 0xDE0B6B3A7640000 +DESTRUCTOR_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 +FLAG_SLOT = 0xA +RESULT_SLOT = 0xB +GAS_SLOT = 0x17 +SNAPSHOT_OFFSET = 0x16 +MEMORY_SIZE = SNAPSHOT_OFFSET + 32 +GRANT_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refundSuicide50procentCapFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("London") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - ], + "call_succeeds", + [False, True], + ids=["starved_grant", "full_grant"], ) -@pytest.mark.pre_alloc_mutable def test_refund_suicide50procent_cap( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_succeeds: bool, ) -> None: - """Test_refund_suicide50procent_cap.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x3B9ACA00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, + """Storage clears around a self-destruct call refund up to the cap.""" + destructor_code = Op.SELFDESTRUCT( + address=Op.CALLER, address_warm=True, account_new=False + ) + destructor = pre.deploy_contract( + code=destructor_code, + balance=DESTRUCTOR_BALANCE, ) - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [22] (GAS) [[ 10 ]] 1 [[ 11 ]] (CALL (CALLDATALOAD 0) 0 0 0 0 0 ) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 [[ 7 ]] 0 [[ 8 ]] 0 [[ 23 ]] (SUB @22 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x16, value=Op.GAS) - + Op.SSTORE(key=0xA, value=0x1) - + Op.SSTORE( - key=0xB, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0x4FF65047CE9C85F968689E4369C10003026A41A9, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.SSTORE(key=0x7, value=0x0) - + Op.SSTORE(key=0x8, value=0x0) - + Op.SSTORE(key=0x17, value=Op.SUB(Op.MLOAD(offset=0x16), Op.GAS)) - + Op.STOP, - storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xA6CC2CA5611255D50118601AA8ECE6F124FC4C45), # noqa: E501 + # The grant either covers the destructor completely or falls one gas + # short, so the sub-call forfeits its whole grant. + destructor_cost = destructor_code.gas_cost(fork) + if call_succeeds: + grant = destructor_cost + GRANT_MARGIN + inner_consumed = destructor_cost + else: + grant = destructor_cost - 1 + inner_consumed = grant + call_result = 1 if call_succeeds else 0 + + # First GAS read: the delta window opens after the GAS opcode itself. + head = Op.MSTORE( + offset=SNAPSHOT_OFFSET, value=Op.GAS, new_memory_size=MEMORY_SIZE + ) + body = Op.SSTORE( + key=FLAG_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, + ) + Op.SSTORE( + key=RESULT_SLOT, + value=Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=destructor, + address_warm=False, + ), + key_warm=False, + original_value=0, + new_value=call_result, ) - # Source: lll - # { (SELFDESTRUCT ) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT( - address=0xA6CC2CA5611255D50118601AA8ECE6F124FC4C45 + for slot in range(1, 9): + body += Op.SSTORE( + key=slot, + value=0x0, + key_warm=False, + original_value=1, + new_value=0, ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x4FF65047CE9C85F968689E4369C10003026A41A9), # noqa: E501 + # Second GAS read closes the window; the head's own GAS cost stands in + # for it in the derived delta (both GAS reads cost the same). + # new_value is a placeholder: an SSTORE's cost depends only on the + # zero/non-zero transition, not the stored magnitude. + tail = Op.SSTORE( + key=GAS_SLOT, + value=Op.SUB( + Op.MLOAD( + offset=SNAPSHOT_OFFSET, + new_memory_size=MEMORY_SIZE, + old_memory_size=MEMORY_SIZE, + ), + Op.GAS, + ), + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=head + body + tail + Op.STOP, + storage=dict.fromkeys(range(1, 9), 1), + balance=TARGET_BALANCE, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={10: 1, 11: 0, 23: 0x107A7}, - balance=0xDE0B6B3A7640000, - ), - sender: Account(nonce=1), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={10: 1, 11: 1, 23: 0x166FA}, - balance=0x1BC16D674EC80000, - ), - sender: Account(nonce=1), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + gas_delta = head.gas_cost(fork) + body.gas_cost(fork) + inner_consumed - tx_data = [ - Hash(0x1F4), - Hash(0x10000), - ] - tx_gas = [10000000] + data = Hash(grant) + # The refund cap is a fifth of the gas actually deducted before + # execution, which excludes the EIP-7623 calldata floor. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, return_cost_deducted_prior_execution=True + ) + executed = intrinsic + gas_delta + tail.gas_cost(fork) + gas_limit = executed + 5_000 + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=data, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + # EIP-3529 caps the refund at a fifth of the executed gas. + total_refund = body.refund(fork) + ( + destructor_code.refund(fork) if call_succeeds else 0 + ) + refund = min(total_refund, executed // 5) + gas_used = executed - refund + + post = { + target: Account( + storage={ + FLAG_SLOT: 1, + RESULT_SLOT: call_result, + GAS_SLOT: gas_delta, + }, + balance=TARGET_BALANCE + + (DESTRUCTOR_BALANCE if call_succeeds else 0), + ), + # EIP-6780: a pre-existing contract is no longer deleted, only + # its balance is transferred. + destructor: ( + ( + Account(balance=0) + if fork.is_eip_enabled(6780) + else Account.NONEXISTENT + ) + if call_succeeds + else Account(balance=DESTRUCTOR_BALANCE, storage={}) + ), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py b/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py index ab754657c75..0d9d5be2f6b 100644 --- a/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py +++ b/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py @@ -1,18 +1,21 @@ """ -Test_refund_tx_to_suicide. +Verify a transaction into a self-destructing contract: the balance +(including the transaction value) moves to the beneficiary and, post +EIP-3529, no self-destruct refund is granted. Ported from: state_tests/stRefundTest/refund_TxToSuicideFiller.json + +@manually-enhanced: Do not overwrite. Beneficiary and budget are derived +(nonexistent account, `code.gas_cost` composite) and the post branches on +EIP-6780 (pre-Cancun the contract is deleted, after it persists). """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,59 +24,61 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 +TX_VALUE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund_TxToSuicideFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund_tx_to_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund_tx_to_suicide.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xA2333EEF5630066B928DEA5FD85A239F511B5B067D1441EE7AC290D0122B917B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Self-destruct moves the balance and grants no refund.""" + beneficiary = pre.nonexistent_account() + code = Op.SELFDESTRUCT( + address=beneficiary, address_warm=False, account_new=True ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x5F5E100) - # Source: lll - # { (SELFDESTRUCT 0x095e7baea6a6c7c4c2dfeb977efac326af552d87) } - target = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x95E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) - + Op.STOP, + target = pre.deploy_contract( + code=code, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x2BC33A472F0FBA1E30BF2317D07910367908C7F6), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()(sends_value=True) + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=61003, - value=10, + gas_limit=gas_limit, + gas_price=GAS_PRICE, + value=TX_VALUE, ) + # EIP-3529 removed the self-destruct refund entirely. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87): Account( - storage={}, balance=0xDE0B6B3A764000A + beneficiary: Account(balance=CONTRACT_BALANCE + TX_VALUE), + # EIP-6780: a pre-existing contract is no longer deleted, only + # its balance is transferred. + target: ( + Account(storage={1: 1}, balance=0) + if fork.is_eip_enabled(6780) + else Account.NONEXISTENT + ), + sender: Account( + balance=INITIAL_BALANCE - TX_VALUE - gas_used * GAS_PRICE ), - coinbase: Account(balance=0), - sender: Account(balance=0x5EDB318, nonce=1), - target: Account(storage={1: 1}, balance=0, nonce=0), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py b/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py index fc4b0089a75..f94a727fd09 100644 --- a/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py +++ b/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py @@ -1,8 +1,16 @@ """ -Test_loop_calls_depth_then_revert. +Verify a mutual CALL recursion that terminates by gas exhaustion: two +contracts increment their own counters and call each other until the +EIP-150 63/64 attenuation starves the deepest frame, whose failed store +reverts alone while every ancestor's increment persists. Ported from: state_tests/stRevertTest/LoopCallsDepthThenRevertFiller.json + +@manually-enhanced: Do not overwrite. The reached depth is bounded by the +fixed gas budget (not the 1024 depth limit), so the frame counts are +pinned per gas-schedule era: EIP-8037/EIP-2780 shift the attenuation on +Amsterdam. One address literal remains to break the reference cycle. """ import pytest @@ -10,16 +18,31 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The recursion depth is a function of this budget via EIP-150's 63/64 +# forwarding rule; changing it changes the pinned frame counts. +GAS_BUDGET = 10_000_000 +# Fixed address for the second contract: it must be known before the +# first contract's code (which calls it) can be built. +PONG_ADDRESS = Address(0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2) + + +def loop_code(partner: Address) -> Bytecode: + """Increment the own counter, then recurse into the partner.""" + return ( + Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) + + Op.CALL(address=partner) + + Op.STOP + ) + @pytest.mark.ported_from( ["state_tests/stRevertTest/LoopCallsDepthThenRevertFiller.json"], @@ -29,65 +52,30 @@ def test_loop_calls_depth_then_revert( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_loop_calls_depth_then_revert.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) - - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (CALL (GAS) 0 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.GAS, - address=0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xF59FD1C021541704A4A52C067454304566717666), # noqa: E501 - ) - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (CALL (GAS) 0 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.GAS, - address=0xF59FD1C021541704A4A52C067454304566717666, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2), # noqa: E501 - ) + """Only the gas-starved deepest frame of a call loop reverts.""" + ping = pre.deploy_contract(code=loop_code(PONG_ADDRESS)) + pong = pre.deploy_contract(code=loop_code(ping), address=PONG_ADDRESS) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, + to=ping, + gas_limit=GAS_BUDGET, ) + # Completed frames under GAS_BUDGET, pinned per gas-schedule era: + # EIP-8037's state gas for the two first stores trims one frame off + # the depth the 63/64 attenuation allows. + if fork.is_eip_enabled(8037): + ping_frames, pong_frames = 192, 192 + else: + ping_frames, pong_frames = 193, 192 + post = { - target: Account(storage={0: 193}), - addr: Account(storage={0: 192}), + ping: Account(storage={0: ping_frames}), + pong: Account(storage={0: pong_frames}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py b/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py index 1be3fb70d90..9267e1e2355 100644 --- a/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py +++ b/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py @@ -1,8 +1,17 @@ """ -Test_loop_delegate_calls_depth_then_revert. +Verify a mutual DELEGATECALL recursion that terminates by gas +exhaustion: both contracts' code increments the entry contract's counter +(the storage context never changes) until the EIP-150 63/64 attenuation +starves the deepest frame, whose failed store reverts alone while every +ancestor's increment persists. Ported from: state_tests/stRevertTest/LoopDelegateCallsDepthThenRevertFiller.json + +@manually-enhanced: Do not overwrite. The reached depth is bounded by the +fixed gas budget (not the 1024 depth limit), so the frame count is +pinned per gas-schedule era: EIP-8037/EIP-2780 shift the attenuation on +Amsterdam. One address literal remains to break the reference cycle. """ import pytest @@ -10,16 +19,31 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The recursion depth is a function of this budget via EIP-150's 63/64 +# forwarding rule; changing it changes the pinned frame count. +GAS_BUDGET = 10_000_000 +# Fixed address for the second contract: it must be known before the +# first contract's code (which delegate-calls it) can be built. +PONG_ADDRESS = Address(0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228) + + +def loop_code(partner: Address) -> Bytecode: + """Increment the context counter, then recurse into the partner.""" + return ( + Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) + + Op.DELEGATECALL(address=partner) + + Op.STOP + ) + @pytest.mark.ported_from( ["state_tests/stRevertTest/LoopDelegateCallsDepthThenRevertFiller.json"], @@ -29,63 +53,29 @@ def test_loop_delegate_calls_depth_then_revert( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_loop_delegate_calls_depth_then_revert.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) - - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (DELEGATECALL (GAS) 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.DELEGATECALL( - gas=Op.GAS, - address=0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xB0923C4A632DE291FCDAC653E6C6CC2B4E4CDFA8), # noqa: E501 - ) - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (DELEGATECALL (GAS) 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.DELEGATECALL( - gas=Op.GAS, - address=0xB0923C4A632DE291FCDAC653E6C6CC2B4E4CDFA8, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228), # noqa: E501 - ) + """Only the gas-starved deepest frame of a delegate loop reverts.""" + ping = pre.deploy_contract(code=loop_code(PONG_ADDRESS)) + pong = pre.deploy_contract(code=loop_code(ping), address=PONG_ADDRESS) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, + to=ping, + gas_limit=GAS_BUDGET, ) + # Completed frames under GAS_BUDGET, pinned per gas-schedule era: + # every frame increments the entry contract's counter because + # DELEGATECALL keeps the storage context; the partner's own storage + # is never touched. EIP-8037's state gas for the first store shifts + # the depth the 63/64 attenuation allows. + frames = 385 if fork.is_eip_enabled(8037) else 386 + post = { - target: Account(storage={0: 386}), - addr: Account(storage={}), + ping: Account(storage={0: frames}), + pong: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py b/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py index 11c3aaa568c..b123500878c 100644 --- a/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py +++ b/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py @@ -1,222 +1,183 @@ """ -Test_revert_depth_create_address_collision. +Verify revert propagation around a nested CREATE whose target address +collides with an existing contract - its own caller: the creating frame +always fails and the collided account survives untouched, while the +caller completes only when its budget covers the forfeited grant. Ported from: state_tests/stRevertTest/RevertDepthCreateAddressCollisionFiller.json + +@manually-enhanced: Do not overwrite. Restores the collision the machine +port lost: the caller is deployed at the creator's CREATE address (as in +the original filler). Grants and budgets derive from fork composites and +the collided account's code, nonce and storage are pinned in every arm. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas left in the creator frame when its CREATE executes: the collision +# consumes it, so the following store can never be paid. +BURN_MARGIN = 5_000 +# Head room on top of a derived budget. +BUDGET_MARGIN = 5_000 +# Gas left at the caller's call site in the starved arm: too little for +# any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRevertTest/RevertDepthCreateAddressCollisionFiller.json"], ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], + "oversized_ask", + [False, True], + ids=["modest_ask", "oversized_ask"], ) +@pytest.mark.parametrize( + "ample_budget", + [False, True], + ids=["starved", "ample"], +) +@pytest.mark.parametrize("tx_value", [1, 0], ids=["v1", "v0"]) @pytest.mark.pre_alloc_mutable def test_revert_depth_create_address_collision( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oversized_ask: bool, + ample_budget: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create_address_collision.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 + """A CREATE address collision leaves the collided account intact.""" + creator_store = Op.SSTORE( + key=0x2, value=0x8, key_warm=False, original_value=0, new_value=8 ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + create_code = Op.POP( + Op.CREATE( + value=0x0, + offset=0x0, + size=0x0, + init_code_size=0, + new_memory_size=0, + ) ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE 0 0 0) [[3]] 12} - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB1B49241A4ECF7860872E686090781C906B1B437), # noqa: E501 + creator_tail = Op.SSTORE( + key=0x3, value=0xC, key_warm=False, original_value=0, new_value=0xC ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0 0 0 0 0) [[4]] 12 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB1B49241A4ECF7860872E686090781C906B1B437, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x97E33A176B7C8D61B356D1C170AC2119D28867DF), # noqa: E501 + creator = pre.deploy_contract( + code=creator_store + create_code + creator_tail + Op.STOP ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={0: 1, 4: 12}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + head_store = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), address=creator, address_warm=False + ) + # The creator's frame always fails, so the call result is always 0. + call_store = Op.SSTORE( + key=0x1, + value=call_code, + key_warm=False, + original_value=0, + new_value=0, + ) + tail_store = Op.SSTORE( + key=0x4, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + caller_code = head_store + call_store + tail_store + Op.STOP + # The caller sits exactly at the creator's CREATE address: the + # nested creation collides with the very contract that called it. + caller = pre.deploy_contract( + code=caller_code, + address=compute_create_address(address=creator, nonce=1), + ) - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 160000] - tx_value = [1, 0] + intrinsic_calculator = fork.transaction_intrinsic_cost_calculator() + modest_grant = (creator_store + create_code).gas_cost(fork) + BURN_MARGIN + gas_limit = ( + intrinsic_calculator( + calldata=Hash(modest_grant), + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + + head_store.gas_cost(fork) + + call_store.gas_cost(fork) + + modest_grant + + tail_store.gas_cost(fork) + + BUDGET_MARGIN + ) + # An oversized ask is clamped to the EIP-150 63/64 cap, leaving the + # caller only 1/64 of its remaining gas: it can never complete. + grant = gas_limit if oversized_ask else modest_grant + data = Hash(grant) + intrinsic = intrinsic_calculator( + calldata=data, + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + if ample_budget: + available = ( + gas_limit + - intrinsic + - head_store.gas_cost(fork) + - call_code.gas_cost(fork) + ) + if oversized_ask: + store_costs = ( + call_store.gas_cost(fork) + - call_code.gas_cost(fork) + + tail_store.gas_cost(fork) + ) + assert available // 64 < store_costs, "caller must fail" + else: + assert grant <= available - available // 64, "grant is granted" + else: + # The caller reaches its call with only STARVE_MARGIN left: the + # creator halts at its first store and the retained 1/64 cannot + # pass the EIP-2200 stipend check, so everything reverts. + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_code.gas_cost(fork) + + STARVE_MARGIN + ) + assert STARVE_MARGIN - STARVE_MARGIN // 64 <= 2300, "creator halts" + assert STARVE_MARGIN // 64 <= 2300, "caller store must halt" + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + to=caller, + data=data, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + caller_completes = ample_budget and not oversized_ask + post = { + # The collided account survives with its code and nonce intact. + caller: Account( + code=caller_code, + nonce=1, + storage={0: 1, 4: 0xC} if caller_completes else {}, + balance=tx_value if caller_completes else 0, + ), + creator: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py b/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py index 3cd7ee838c3..9eca41eee77 100644 --- a/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py +++ b/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py @@ -1,195 +1,180 @@ """ -Test_revert_depth_create_oog. +Verify revert propagation around a nested CREATE that runs out of gas: +a sub-call either funds its CREATE-then-store sequence completely, or +runs out of gas after the CREATE, reverting the created account but not +the caller; a starved outer budget reverts everything. Ported from: state_tests/stRevertTest/RevertDepthCreateOOGFiller.json + +@manually-enhanced: Do not overwrite. The sub-call grants and both +transaction budgets derive from fork composites (EIP-8037 state gas is +tracked instead of pinned), all addresses are dynamic, and every account +including the created one is pinned in each arm. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas left in the creator frame after its CREATE: enough to reach the +# following store, never enough to pay for it. +PARTIAL_MARGIN = 5_000 +# Head room on top of a derived budget. +BUDGET_MARGIN = 5_000 +# Gas left at the caller's call site in the starved arm: too little for +# any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRevertTest/RevertDepthCreateOOGFiller.json"], ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], + "full_grant", + [False, True], + ids=["partial_grant", "full_grant"], +) +@pytest.mark.parametrize( + "ample_budget", + [False, True], + ids=["starved", "ample"], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize("tx_value", [1, 0], ids=["v1", "v0"]) def test_revert_depth_create_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_grant: bool, + ample_budget: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create_oog.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + """An out-of-gas CREATE frame reverts alone; a starved caller fully.""" + creator_store = Op.SSTORE( + key=0x2, value=0x8, key_warm=False, original_value=0, new_value=8 ) - - # Source: lll - # { [[2]] 8 (CREATE 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + create_code = Op.POP( + Op.CREATE( + value=0x0, + offset=0x0, + size=0x0, + init_code_size=0, + new_memory_size=0, ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, ) + creator_tail = Op.SSTORE( + key=0x3, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + creator_code = creator_store + create_code + creator_tail + Op.STOP + creator = pre.deploy_contract(code=creator_code) + created = compute_create_address(address=creator, nonce=1) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=contract_1, nonce=0): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), - }, - }, - ] + # The grant covers the creator completely, or only up to and + # including its CREATE, leaving too little for the following store. + if full_grant: + grant = creator_code.gas_cost(fork) + BUDGET_MARGIN + inner_consumed = creator_code.gas_cost(fork) + else: + grant = (creator_store + create_code).gas_cost(fork) + PARTIAL_MARGIN + inner_consumed = grant + inner_succeeds = ample_budget and full_grant + inner_fails_reaching_create = ample_budget and not full_grant - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + head_store = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), address=creator, address_warm=False + ) + call_store = Op.SSTORE( + key=0x1, + value=call_code, + key_warm=False, + original_value=0, + new_value=1 if inner_succeeds else 0, + ) + tail_store = Op.SSTORE( + key=0x4, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + caller = pre.deploy_contract( + code=head_store + call_store + tail_store + Op.STOP + ) - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 180000] - tx_value = [1, 0] + data = Hash(grant) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + if ample_budget: + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_store.gas_cost(fork) + + inner_consumed + + tail_store.gas_cost(fork) + + BUDGET_MARGIN + ) + # The requested grant must fit under the EIP-150 63/64 cap. + available = ( + gas_limit + - intrinsic + - head_store.gas_cost(fork) + - call_code.gas_cost(fork) + ) + assert grant <= available - available // 64, "grant must be granted" + else: + # The caller reaches its call with only STARVE_MARGIN left: the + # creator halts at its first store and the retained 1/64 cannot + # pass the EIP-2200 stipend check, so everything reverts. + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_code.gas_cost(fork) + + STARVE_MARGIN + ) + assert STARVE_MARGIN - STARVE_MARGIN // 64 <= 2300, "creator halts" + assert STARVE_MARGIN // 64 <= 2300, "caller store must halt" + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + to=caller, + data=data, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post: dict + if inner_succeeds: + post = { + created: Account(nonce=1), + caller: Account(storage={0: 1, 1: 1, 4: 0xC}, balance=tx_value), + creator: Account(storage={2: 8, 3: 0xC}), + } + elif inner_fails_reaching_create: + post = { + created: Account.NONEXISTENT, + caller: Account(storage={0: 1, 4: 0xC}, balance=tx_value), + creator: Account(storage={}), + } + else: + post = { + created: Account.NONEXISTENT, + caller: Account(storage={}, balance=0), + creator: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py b/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py index a49cc7b38bc..2f6ab5355af 100644 --- a/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py +++ b/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py @@ -1,35 +1,46 @@ """ -Test: this test checks that the returndata buffer is changed when a... +Verify that a reverting callee's return data replaces the empty return +data left by a previously failed call: each prober records the failed +call's result and RETURNDATASIZE, for CALL, CALLCODE, DELEGATECALL and a +nested CALL chain, with an ample and a starved transaction budget. Ported from: state_tests/stRevertTest/RevertOpcodeInCallsOnNonEmptyReturnDataFiller.json -@manually-enhanced: Do not overwrite. Inner-CALL/DELEGATECALL gas -bumped on Amsterdam to cover EIP-8037 state-gas spill into regular gas; -pre-EIP-8037 unchanged. +@manually-enhanced: Do not overwrite. Sub-calls forward all gas and the +ample arm omits the gas limit (maxing the EIP-8037 reservoir), replacing +per-fork gas bumps; the starved budget derives from fork composites; all +addresses are dynamic and every contract is pinned in the post. """ import pytest from execution_testing import ( - EOA, Account, Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +RESULT_SLOT = 0x0 +RETURN_DATA_SIZE_SLOT = 0x2 +NESTED_RESULT_SLOT = 0x4 +NESTED_RETURN_DATA_SIZE_SLOT = 0x5 +ENTRY_SLOT = 0xA +ENTRY_SLOT_INITIAL = 255 +# A zero-gas grant: the prelude call must fail without touching the +# return data buffer. +FAILING_CALL_GAS = 0 +# Gas left at the entry frame's call site in the starved arm: enough to +# start the call, too little for any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( [ @@ -38,396 +49,146 @@ ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1", - ), - pytest.param( - 2, - 0, - 0, - id="d2-g0", - ), - pytest.param( - 2, - 1, - 0, - id="d2-g1", - ), - pytest.param( - 3, - 0, - 0, - id="d3-g0", - ), - pytest.param( - 3, - 1, - 0, - id="d3-g1", - ), - ], + "call_op", + [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, None], + ids=["call", "callcode", "delegatecall", "nested_call"], +) +@pytest.mark.parametrize( + "ample_gas", + [True, False], + ids=["ample", "starved"], ) -@pytest.mark.pre_alloc_mutable def test_revert_opcode_in_calls_on_non_empty_return_data( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_op: Op | None, + ample_gas: bool, ) -> None: - """Test: tis test checks that the returndata buffer is changed when a...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # EIP-8037 inner-CALL/DELEGATECALL gas bumps: original values - # restored for pre-EIP-8037 forks; bumped for state-gas spill on - # Amsterdam. - inner_call_gas = 50000 - deeper_call_gas = 100000 - deepest_call_gas = 260000 - if fork.is_eip_enabled(8037): - inner_call_gas = 100000 - deeper_call_gas = 1000000 - deepest_call_gas = 1000000 - # Source: lll - # { [[1]] 12 (REVERT 0 1) [[3]] 13 } - addr_6 = pre.deploy_contract( # noqa: F841 + """A revert's return data is observed after a failed call.""" + # Reverts one byte of return data; the store before the REVERT is + # undone and the code after it must never run. + reverter = pre.deploy_contract( code=Op.SSTORE(key=0x1, value=0xC) + Op.REVERT(offset=0x0, size=0x1) + Op.SSTORE(key=0x3, value=0xD) + Op.STOP, - balance=1, - nonce=0, - address=Address(0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B), # noqa: E501 ) - # Source: lll - # { [1] 12 (RETURN 0 64) } - addr_7 = pre.deploy_contract( # noqa: F841 + # Would return 64 bytes, but is only ever called with zero gas. + returner = pre.deploy_contract( code=Op.MSTORE(offset=0x1, value=0xC) - + Op.RETURN(offset=0x0, size=0x40) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0x127EAF7E31D691A8393B7A2F84A6E94372190C01), # noqa: E501 + + Op.RETURN(offset=0x0, size=0x40), ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (DELEGATECALL 50000 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_3 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.DELEGATECALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xF20CCAF271BEAA36E7CF4C9CED2867FAC9558F14), # noqa: E501 + + prelude = Op.POP( + Op.CALL(gas=FAILING_CALL_GAS, address=returner, address_warm=False) ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALLCODE 50000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.CALLCODE( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + def prober_code( + op: Op, callee: Address, result_slot: int, rds_slot: int + ) -> Bytecode: + """Call the callee and record its result and RETURNDATASIZE.""" + return ( + prelude + + Op.SSTORE(key=result_slot, value=op(address=callee)) + + Op.SSTORE(key=rds_slot, value=Op.RETURNDATASIZE) + + Op.STOP ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xC9DA6CD8413F64323F12CD44C99671F280F15E1C), # noqa: E501 + + prober_call = pre.deploy_contract( + code=prober_code(Op.CALL, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT) ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[4]] (CALL 50000 0 0 0 0 0) [[5]] (RETURNDATASIZE) } # noqa: E501 - addr_5 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + prober_callcode = pre.deploy_contract( + code=prober_code( + Op.CALLCODE, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE( - key=0x4, - value=Op.CALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x5, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xEA519C47889074E6378B0D83747F2C3EA0B9CBC9), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALL 50000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + prober_delegatecall = pre.deploy_contract( + code=prober_code( + Op.DELEGATECALL, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xE73611B5B479B30C93AC377AEB3BFB199764F3C3), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[10]] (CALL 260000 (CALLDATALOAD 0) 0 0 0 0 0)} # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + inner_prober = pre.deploy_contract( + code=prober_code( + Op.CALL, + reverter, + NESTED_RESULT_SLOT, + NESTED_RETURN_DATA_SIZE_SLOT, ) - + Op.SSTORE( - key=0xA, - value=Op.CALL( - gas=deepest_call_gas, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - storage={10: 255}, - balance=1, - nonce=0, - address=Address(0x172A8F572404293AA810685DFDC6F740C300CC4B), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALL 100000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_4 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + prober_nested = pre.deploy_contract( + code=prober_code( + Op.CALL, inner_prober, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=deeper_call_gas, - address=0xEA519C47889074E6378B0D83747F2C3EA0B9CBC9, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0x6BACDFA8216DBB2A09819F8739E57AE3574C9FFF), # noqa: E501 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_2: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr_2: Account(storage={}), - }, - }, - { - "indexes": {"data": 2, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_3: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 2, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr_3: Account(storage={}), - }, - }, - { - "indexes": {"data": 3, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_4: Account(storage={0: 1}, nonce=0), - addr_5: Account(storage={5: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 3, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 255}), - addr_4: Account(storage={0: 0}, nonce=0), - addr_5: Account(storage={5: 0}, nonce=0), - }, - }, - ] + big_call = Op.CALL(address=Op.CALLDATALOAD(offset=0x0), address_warm=False) + entry = pre.deploy_contract( + code=prelude + Op.SSTORE(key=ENTRY_SLOT, value=big_call) + Op.STOP, + storage={ENTRY_SLOT: ENTRY_SLOT_INITIAL}, + ) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + probers = { + Op.CALL: prober_call, + Op.CALLCODE: prober_callcode, + Op.DELEGATECALL: prober_delegatecall, + None: prober_nested, + } + prober = probers[call_op] + data = Hash(prober, left_padding=True) - tx_data = [ - Hash(addr, left_padding=True), - Hash(addr_2, left_padding=True), - Hash(addr_3, left_padding=True), - Hash(addr_4, left_padding=True), - ] - tx_gas = [860000, 28000] + sender = pre.fund_eoa() + if ample_gas: + tx = Transaction(sender=sender, to=entry, data=data) + else: + # The entry frame reaches its call with only STARVE_MARGIN left: + # the prober cannot even pay its prelude, and the retained 1/64 + # cannot pass the EIP-2200 stipend check, so everything reverts. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, return_cost_deducted_prior_execution=True + ) + starved = ( + intrinsic + + prelude.gas_cost(fork) + + big_call.gas_cost(fork) + + STARVE_MARGIN + ) + forwarded = STARVE_MARGIN - STARVE_MARGIN // 64 + assert forwarded < prelude.gas_cost(fork), "prober must starve" + assert STARVE_MARGIN // 64 <= 2300, "entry store must halt" + tx = Transaction(sender=sender, to=entry, data=data, gas_limit=starved) - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, - ) + untouched = { + reverter: Account(storage={}), + prober_call: Account(storage={}), + prober_callcode: Account(storage={}), + prober_delegatecall: Account(storage={}), + inner_prober: Account(storage={}), + prober_nested: Account(storage={}), + } + if not ample_gas: + # The transaction runs out of gas at the entry frame: everything + # reverts. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: ENTRY_SLOT_INITIAL}), + } + elif call_op is None: + # The nested prober's callee completes (its own probe fails), so + # the outer call succeeds and returns no data. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: 1}), + prober_nested: Account(storage={RESULT_SLOT: 1}), + inner_prober: Account(storage={NESTED_RETURN_DATA_SIZE_SLOT: 1}), + } + else: + # The probed call reverts with one byte of return data: result 0, + # RETURNDATASIZE 1. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: 1}), + prober: Account(storage={RETURN_DATA_SIZE_SLOT: 1}), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) From e406aefcd847cf1135306b7d3690cfbd562257f3 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:04:04 +0200 Subject: [PATCH 21/25] fix(tests): correct Account.NONEXISTENT annotations for mypy --- tests/ported_static/stAttackTest/test_crashing_transaction.py | 2 +- .../test_create_name_registrator_per_txs_not_enough_gas.py | 2 +- tests/ported_static/stCreate2/test_create2no_cash.py | 4 +++- .../ported_static/stCreate2/test_revert_depth_create2_oog.py | 2 +- ..._creation_oo_gdont_leave_empty_contract_via_transaction.py | 2 +- .../stInitCodeTest/test_out_of_gas_contract_creation.py | 2 +- .../test_out_of_gas_prefunded_contract_creation.py | 2 +- tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py | 4 +++- 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/ported_static/stAttackTest/test_crashing_transaction.py b/tests/ported_static/stAttackTest/test_crashing_transaction.py index 6e62bb785ee..d45d2a1b9fe 100644 --- a/tests/ported_static/stAttackTest/test_crashing_transaction.py +++ b/tests/ported_static/stAttackTest/test_crashing_transaction.py @@ -111,7 +111,7 @@ def test_crashing_transaction( if fork.is_eip_enabled(8037): # An iteration's state gas exceeds the loop's 50000-gas guard, # so the init frame dies mid-CREATE and no account survives. - created_account: Account | type = Account.NONEXISTENT + created_account: Account | None = Account.NONEXISTENT else: created_account = Account( code=bytes.fromhex("60606040526008565b00"), diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py index 7e3bd9b3473..48a1e1efced 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py @@ -109,7 +109,7 @@ def test_create_name_registrator_per_txs_not_enough_gas( created = compute_create_address(address=sender, nonce=0) if enough_gas: - created_account: Account | type = Account( + created_account: Account | None = Account( nonce=1, code=deposited, balance=TX_VALUE, diff --git a/tests/ported_static/stCreate2/test_create2no_cash.py b/tests/ported_static/stCreate2/test_create2no_cash.py index 5dc49d75d32..c61a5f6fed3 100644 --- a/tests/ported_static/stCreate2/test_create2no_cash.py +++ b/tests/ported_static/stCreate2/test_create2no_cash.py @@ -84,7 +84,9 @@ def test_create2no_cash( # The whole (topped-up) balance moved into the created account, # and the creator's nonce was consumed by the creation. creator_account = Account(nonce=2, balance=0) - created_account = Account(nonce=1, code=b"", balance=CREATE2_ENDOWMENT) + created_account: Account | None = Account( + nonce=1, code=b"", balance=CREATE2_ENDOWMENT + ) else: # The balance preflight (or the static fault) aborts before any # account is touched: no creation and no nonce bump. diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py index a5dd5bc6260..17782417272 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py @@ -193,7 +193,7 @@ def test_revert_depth_create2_oog( # The whole transaction ran dry: only the code survives. caller_account = Account(storage={}, code=caller_code, balance=0) creator_account = Account(storage={}, nonce=1) - created_account: Account | type = Account.NONEXISTENT + created_account: Account | None = Account.NONEXISTENT elif not creator_covered: # The creator died mid-creation and was rolled back. caller_account = Account( diff --git a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py index 57bec829c78..b59d7c67858 100644 --- a/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py +++ b/tests/ported_static/stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py @@ -90,7 +90,7 @@ def test_contract_creation_oo_gdont_leave_empty_contract_via_transaction( created = compute_create_address(address=sender, nonce=0) if enough_gas: - created_account: Account | type = Account(nonce=1, code=b"", balance=0) + created_account: Account | None = Account(nonce=1, code=b"", balance=0) writer_storage = {1: 1} else: created_account = Account.NONEXISTENT diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py index a35d2a4453a..078fb6506fe 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py @@ -118,7 +118,7 @@ def test_out_of_gas_contract_creation( created = compute_create_address(address=sender, nonce=0) if enough_gas and not invalid_initcode: - created_account: Account | type = Account( + created_account: Account | None = Account( nonce=1, code=b"", storage={1: 6}, balance=1 ) else: diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py index d3cc55522fa..c72984e1633 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py @@ -141,7 +141,7 @@ def test_out_of_gas_prefunded_contract_creation( if outcome == "outer_oog": # Creation rolled back: only the prefund remains, nonce untouched. created_account = Account(nonce=0, balance=PREFUND) - child_account: Account | type = Account.NONEXISTENT + child_account: Account | None = Account.NONEXISTENT elif outcome == "child_oog": # The inner CREATE increments the creator's nonce even when the # child fails. diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py index 4ad46a6f155..5647ebd2c95 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py @@ -88,7 +88,9 @@ def bump_statics(key_warm: bool) -> int: + push_cost ) - def call_split(address: object, warm: bool, value: int) -> tuple[int, int]: + def call_split( + address: Address | Op, warm: bool, value: int + ) -> tuple[int, int]: """Pre-GAS-read and upfront charges of one side's call.""" upfront = Op.CALL( address_warm=warm, value_transfer=value > 0 From bc96a4ae69942362c1078cf442fc2cb4c0b311c9 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:04:27 +0200 Subject: [PATCH 22/25] docs(claude): add refund-cap, collision, and depth-loop lessons to enhance-ported-test --- .claude/commands/enhance-ported-test.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index b15231cb32b..a518ed9151d 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -439,6 +439,31 @@ persists with the sentinel) plus the callee-side observable already separate the outcomes. Validated on `test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. +**Refund-cap derivations need the EIP-7623 kwarg.** The EIP-3529 cap's +base is the gas deducted before execution, which excludes the calldata +floor: pass `return_cost_deducted_prior_execution=True` to the intrinsic +calculator whenever the tx has calldata, or the derived `executed` (and +the cap) overstate. Validated on `test_refund_suicide50procent_cap`. + +**A CREATE address collision burns the child's gas allowance** (the +EIP-684 path): the withheld child grant is consumed, nothing is created, +and under EIP-8037 the new-account state charge is refunded. Useful to +build always-failing creator frames with predictable consumption. +Validated on `test_revert_depth_create_address_collision`. + +**Loop-to-depth-1024 cannot replace loop-to-OOG.** With 63/64 +attenuation, reaching depth 1024 needs ~e^16 × the terminal gas — no +legal budget gets there. For call-loop depth tests the honest shape is a +fixed named budget with per-gas-schedule-era pinned depth counts, each +shift explained (±1 frame ≈ 64·ln(cost ratio)). Validated on +`test_loop_calls_depth_then_revert`. + +**Framework wart: the SSTORE dirty-rewrite composite prices 100 on every +fork**, but Constantinople/Petersburg charge 5,000 for a dirty re-store — +a derived budget that must survive pre-Istanbul forks needs an explicit +headroom constant for it (named, commented). Observed on +`test_revert_depth_create_address_collision`'s ConstantinopleFix sweep. + **EIP-8037 repriced the code deposit's regular part — boundaries beware.** On 8037 forks the deposit charges only the keccak word cost (`OPCODE_KECCAK256_PER_WORD * ceil32(len)/32`, ~6 gas) as regular gas plus From 90a6986357bf71f4fadc4a77995fdded221d6db2 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 29 Jul 2026 20:23:34 +0200 Subject: [PATCH 23/25] fix(tests): reword docstring to satisfy codespell --- tests/ported_static/stAttackTest/test_crashing_transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ported_static/stAttackTest/test_crashing_transaction.py b/tests/ported_static/stAttackTest/test_crashing_transaction.py index d45d2a1b9fe..a7874ced2d1 100644 --- a/tests/ported_static/stAttackTest/test_crashing_transaction.py +++ b/tests/ported_static/stAttackTest/test_crashing_transaction.py @@ -1,6 +1,6 @@ """ Verify the Ropsten "crashing transaction" attack replay: a creation -transaction whose init code loops CREATEing children while more than +transaction whose init code CREATEs children in a loop while more than 50000 gas remains, then deposits its runtime code. Ported from: From c8fff527dda138edc394bffae3fe33ba29c64f1e Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 30 Jul 2026 09:47:57 +0200 Subject: [PATCH 24/25] fix(tests): apply execution-gas rename in ported create tests --- .../stCreateTest/test_create_address_warm_after_fail.py | 2 +- .../stCreateTest/test_create_oog_from_call_refunds.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py index 87f2013a574..a6f301f95bf 100644 --- a/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py +++ b/tests/ported_static/stCreateTest/test_create_address_warm_after_fail.py @@ -409,7 +409,7 @@ def measured_call(frame: int, *, warm: bool, new: bool) -> int: value_transfer=bool(v), account_new=new and bool(v), ) - measured = frame + call.regular_cost(fork) + measured = frame + call.execution_cost(fork) if v: # The callee is empty (or STOP-only), so the stipend # forwarded with the value returns unused. diff --git a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py index 8e7d0d5e68f..307bc3d7c06 100644 --- a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py +++ b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py @@ -263,7 +263,7 @@ def test_create_oog_from_call_refunds( + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) ) # No init frame can receive enough to pay the OoG arms' deposit. - grant_bound = gas_limit - intrinsic - create_op.regular_cost(fork) + grant_bound = gas_limit - intrinsic - create_op.execution_cost(fork) assert grant_bound * 63 // 64 < deposit_price, ( "63/64 grant must stay below the OoG deposit price" ) From 23ab88b8f4158d31ac8354df66beac44521ebf19 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Thu, 30 Jul 2026 10:04:24 +0200 Subject: [PATCH 25/25] chore(tests): remove the emptied Amsterdam ported static skip list and its conftest --- .claude/commands/enhance-ported-test.md | 28 ++++----- tests/ported_static/amsterdam_skip_list.txt | 11 ---- tests/ported_static/conftest.py | 68 --------------------- 3 files changed, 14 insertions(+), 93 deletions(-) delete mode 100644 tests/ported_static/amsterdam_skip_list.txt delete mode 100644 tests/ported_static/conftest.py diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index a518ed9151d..0d0404b2a04 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -42,11 +42,12 @@ so a failure is attributable. - **Checkpoint / done:** fill the whole `valid_from` range (omit `--fork`) so all deployed forks are exercised. - **Probe the future fork:** explicitly `--fork Amsterdam` (or the latest fork - that enables new EIPs). A ported test listed in `amsterdam_skip_list.txt` will - always show `sss` there — to see its *real* behavior, temporarily remove its - entry from that file, fill, then restore (or, once fixed, remove it for good — - see Finishing). A gas/state-cost change there is the most likely future - breakage. + that enables new EIPs). A gas/state-cost change there is the most likely + future breakage. (Historical note: broken tests used to be parked in a + `tests/ported_static/amsterdam_skip_list.txt` consumed by a local conftest; + the list was emptied and both were removed. If a future fork's repricing + breaks tests en masse, the same parking pattern — a substring-matched skip + list plus a `pytest_collection_modifyitems` hook — is in git history.) - **`fill` output:** writes to `./fixtures` (`--clean` resets it), or pass `--output ` for a scratch location. Do **not** use `-o` — that is pytest's `--override-ini`, not the output dir. @@ -77,8 +78,8 @@ gas the tx receives, so the body executes fully. See `write-test.md` "Transactio - **Gas-snapshot tests are gas-sensitive.** If the post asserts a stored `GAS` reading or a `SUB(@gas_before, GAS)` delta (legacy slots `0` / `0x64`), the test *measures gas* — handle it under step 10 (preserve via `CodeGasMeasure`), - do not just strip `gas_limit`. This is the dominant `amsterdam_skip_list.txt` - shape: the stored gas value is exactly what EIP-8037 re-prices and breaks. + do not just strip `gas_limit`. This was the dominant skip-list shape: + the stored gas value is exactly what EIP-8037 re-prices and breaks. - **EIP-8037 caveat:** when you omit `gas_limit` on a test that *measures* an operation incurring **state gas** (account creation, storage writes), add `state_gas_reservoir=0` to the tx, or that state gas is silently dropped from @@ -294,8 +295,8 @@ ties a `CREATE`'s `size` operand to the memory/gas math that depends on it. on `test_make_money`. ### 10. (Gas-subject / gas-snapshot tests) Replace hardcoded gas with dynamic calculation -Covers both tests that *assert* a gas amount and the dominant -`amsterdam_skip_list.txt` shape: a legacy `GAS` snapshot / `SUB(@gas_before, +Covers both tests that *assert* a gas amount and the dominant broken-port +shape: a legacy `GAS` snapshot / `SUB(@gas_before, GAS)` delta stored to slot `0`/`0x64`. That stored value is *why* EIP-8037 breaks the test, but it is real coverage — **preserve and fork-robustify it, do not drop it.** @@ -595,11 +596,10 @@ Not yet covered by a validated walkthrough; figure out and append when hit: ## Finishing -**Remove the skip-list entry.** Once the test passes on the future fork, delete -its line from `tests/ported_static/amsterdam_skip_list.txt` and decrement both -its per-directory count header (`# stXxx (N)`) and the `# Total entries:` count. -Confirm with a full-range fill (`--fork` omitted) with the entry gone — that is -the definition of done. +**Confirm with a full-range fill** (`--fork` omitted) — every deployed fork +green is the definition of done. (If a skip list is ever reintroduced for a +future fork, also delete the test's entry and keep its count headers +accurate.) **Final sweep checklist** — each of these has been missed in practice; check them one by one before calling the test done: diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt deleted file mode 100644 index 1baa46c3978..00000000000 --- a/tests/ported_static/amsterdam_skip_list.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Amsterdam ported static skip list. -# -# Test cases in this list are temporarily skipped for the Amsterdam -# fork due to EIP-8037's two-dimensional gas model. Gas limits in the -# underlying ported static tests have not yet been updated to account -# for state gas. -# -# Entries are substring-matched against each pytest nodeid (after -# stripping the fixture-format suffix in conftest.py). -# -# Total entries: 0 diff --git a/tests/ported_static/conftest.py b/tests/ported_static/conftest.py deleted file mode 100644 index c5d9aff2a9d..00000000000 --- a/tests/ported_static/conftest.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Conftest for ported static tests. - -Temporarily skip ported static tests that fail on Amsterdam and its -descendant forks due to EIP-8037's two-dimensional gas model. The gas -limits in these ported static test cases have not yet been updated to -account for state gas. - -TODO: Update gas limits in the 3452 failing ported static test cases and -remove this skip list. -""" - -from pathlib import Path - -import pytest -from execution_testing.forks import Amsterdam - -_SKIP_LIST_PATH = Path(__file__).parent / "amsterdam_skip_list.txt" -_AMSTERDAM_SKIP_CASES: frozenset[str] = frozenset( - line.strip() - for line in _SKIP_LIST_PATH.read_text().splitlines() - if line.strip() and not line.lstrip().startswith("#") -) - -# Fixture format suffixes pytest appends inside the parametrize id. These -# must be stripped from the nodeid before substring-matching against the -# skip list, because the skip list predates these suffixes. -_FIXTURE_FORMAT_TOKENS: tuple[str, ...] = ( - "-blockchain_test_engine_from_state_test", - "-blockchain_test_from_state_test", - "-blockchain_test_engine", - "-blockchain_test", - "-state_test", -) - - -def _normalize_nodeid(nodeid: str) -> str: - """Strip pytest fixture-format suffixes to match the skip list format.""" - for token in _FIXTURE_FORMAT_TOKENS: - nodeid = nodeid.replace(token, "") - return nodeid - - -def pytest_collection_modifyitems( - config: pytest.Config, items: list[pytest.Item] -) -> None: - """Skip ported static test cases listed in amsterdam_skip_list.txt.""" - skip_marker = pytest.mark.skip( - reason="Ported static test gas limits not yet updated for EIP-8037" - ) - for item in items: - if "ported_static" not in item.nodeid: - continue - callspec = getattr(item, "callspec", None) - fork = callspec.params.get("fork") if callspec else None - if fork is None or not fork >= Amsterdam: - continue - # The skip list is written against fork_Amsterdam, but the - # EIP-8037 breakage applies equally to its descendant forks. - # Rewriting the item's fork token to Amsterdam's lets one list - # cover them all. - normalized = _normalize_nodeid(item.nodeid).replace( - f"fork_{fork.name()}", "fork_Amsterdam" - ) - for skip_case in _AMSTERDAM_SKIP_CASES: - if skip_case in normalized: - item.add_marker(skip_marker) - break