From cf739e76fda57d4c808a0ac67be6da035b334cac Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 5 Aug 2026 17:52:52 +0200 Subject: [PATCH] fix(tests): enhance & un-skip Amsterdam ported static SSTORE, refund & misc tests (Pt. 2c) --- .claude/commands/enhance-ported-test.md | 124 ++++- tests/ported_static/amsterdam_skip_list.txt | 69 +-- .../stAttackTest/test_crashing_transaction.py | 36 +- .../test_callcallcallcode_001_suicide_end.py | 48 +- .../test_create_address_warm_after_fail.py | 221 +++++---- .../test_new_gas_price_for_codes.py | 242 +++++---- .../test_gas_cost.py | 29 +- .../test_gas_cost_berlin.py | 21 +- .../stEIP158Specific/test_exp_empty.py | 150 +++--- ...nt_leave_empty_contract_via_transaction.py | 118 ++--- ...rice_for_codes_with_mem_expanding_calls.py | 261 ++++++---- tests/ported_static/stMemoryTest/test_oog.py | 27 +- .../stRefundTest/test_refund600.py | 116 +++-- .../stSStoreTest/test_sstore_gas.py | 65 ++- .../stSStoreTest/test_sstore_gas_left.py | 464 +++--------------- .../test_recursive_create_contracts.py | 19 +- .../test_test_contract_interaction.py | 18 +- .../test_test_contract_suicide.py | 18 +- .../test_double_selfdestruct_touch_paris.py | 24 +- .../test_opcodes_transaction_init.py | 20 +- .../test_store_gas_on_create.py | 96 ++-- ...ides_and_internal_call_suicides_success.py | 30 +- 22 files changed, 1208 insertions(+), 1008 deletions(-) diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index f6d3f6852e5..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.** @@ -386,6 +387,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). @@ -402,6 +417,88 @@ 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`. +**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`. + +**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 +`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 +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 +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. 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 than is available" by pinning an absolute forwarded amount — fork-fragile, @@ -499,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 index 7aba6f38bb0..eb495840c56 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,10 +8,9 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 153 +# Total entries: 116 -# stAttackTest (1) -stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] +# stAttackTest (0) # stBadOpcode (4) stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE2] @@ -37,8 +36,7 @@ stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py: 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] +# stCallDelegateCodesCallCodeHomestead (0) # stCreate2 (31) stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] @@ -73,20 +71,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/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 (23) 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] @@ -117,24 +102,19 @@ 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 (6) 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/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] -# 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] +# stEIP150singleCodeGasPrices (0) -# 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) # stInitCodeTest (7) stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g0] @@ -145,20 +125,16 @@ 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 (3) 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/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] -# stMemoryTest (2) -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success14] -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success15] +# stMemoryTest (0) -# stRefundTest (7) +# stRefundTest (6) 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] @@ -178,31 +154,18 @@ 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] +# stSStoreTest (0) -# 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] +# stSolidityTest (0) # 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 (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] -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] + +# stTransactionTest (0) diff --git a/tests/ported_static/stAttackTest/test_crashing_transaction.py b/tests/ported_static/stAttackTest/test_crashing_transaction.py index 8f8320dd1b8..d775e035754 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 CREATEs children in a loop 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 +with the revised EIP-8038 pricing an iteration is dearer (new-account +plus code-deposit state gas spill from the frame) but still fits the +loop's 50000-gas guard, so the loop drains earlier and deposits with +fewer children — the split post pins both child counts. """ 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 shrinks the child count.""" coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) sender = pre.fund_eoa(amount=0xDE0B6B3A7640000, nonce=3270) @@ -95,13 +107,25 @@ 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 spill makes each pass dearer while + # still fitting the loop's 50000-gas guard, so the loop drains + # after far fewer children than the ported count. + created_account = Account( + code=bytes.fromhex("60606040526008565b00"), + balance=1, + nonce=23, + ) + 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, ), 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..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 @@ -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.execution_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/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/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, 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..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 @@ -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 | None = 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) 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), 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) 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) 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, ) 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, 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(