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..2943eddc540 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,23 +8,19 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 153 +# Total entries: 109 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] -# stBadOpcode (4) -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE] +# stBadOpcode (0) # stCallCodes (3) stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py::test_callcode_in_initcode_to_existing_contract_with_value_transfer[fork_Amsterdam] -# stCallCreateCallCodeTest (11) +# stCallCreateCallCodeTest (8) stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] @@ -33,9 +29,6 @@ stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Am stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g0] stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py::test_create_name_registrator_pre_store1_not_enough_gas[fork_Amsterdam] # stCallDelegateCodesCallCodeHomestead (1) stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py::test_callcallcallcode_001_suicide_end[fork_Amsterdam] @@ -73,7 +66,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 (13) 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] @@ -87,29 +80,6 @@ stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_af 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/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g0] -stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py::test_create_e_contract_create_ne_contract_in_init_oog_tr[fork_Amsterdam--g1] -stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py::test_create_e_contract_then_call_to_non_existent_acc[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage.py::test_create_empty_contract_with_storage[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py::test_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] -stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py::test_create_empty_contract_with_storage_and_call_it_1wei[fork_Amsterdam] -stCreateTest/test_create_oo_gafter_init_code_returndata_size.py::test_create_oo_gafter_init_code_returndata_size[fork_Amsterdam] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG2] -stCreateTest/test_create_oog_from_call_refunds.py::test_create_oog_from_call_refunds[fork_Amsterdam-SStore_Refund_NoOoG3] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d0] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d1] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d2] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d4] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d5] -stCreateTest/test_create_results.py::test_create_results[fork_Amsterdam-d6] -stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty2.py::test_transaction_collision_to_empty2[fork_Amsterdam--g1-v1] -stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_collision_to_empty_but_code[fork_Amsterdam--g1-v1] -stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v0] -stCreateTest/test_transaction_collision_to_empty_but_nonce.py::test_transaction_collision_to_empty_but_nonce[fork_Amsterdam--g1-v1] # stDelegatecallTestHomestead (4) stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] @@ -117,10 +87,8 @@ 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 (5) 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] @@ -136,19 +104,10 @@ stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] # stHomesteadSpecific (1) stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] -# stInitCodeTest (7) -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g1] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g0] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g2] +# stInitCodeTest (0) -# stMemExpandingEIP150Calls (4) +# stMemExpandingEIP150Calls (2) 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) @@ -189,10 +148,7 @@ stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contrac stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] stSolidityTest/test_test_contract_suicide.py::test_test_contract_suicide[fork_Amsterdam] -# stStaticCall (3) -stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py::test_static_create_empty_contract_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py::test_static_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py::test_static_execute_call_that_ask_fore_gas_then_trabsaction_has[fork_Amsterdam-d0] +# stStaticCall (0) # stSystemOperationsTest (5) stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] diff --git a/tests/ported_static/stBadOpcode/test_measure_gas.py b/tests/ported_static/stBadOpcode/test_measure_gas.py index 5f9d16570a6..59832975c4a 100644 --- a/tests/ported_static/stBadOpcode/test_measure_gas.py +++ b/tests/ported_static/stBadOpcode/test_measure_gas.py @@ -1,17 +1,21 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the minimum gas each opcode needs to succeed via a binary +search (by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stBadOpcode/measureGasFiller.yml @manually-enhanced: Do not overwrite. A binary search measures the gas -an opcode needs to succeed. Only the EXTCODE case shifts: it runs a -warm `EXTCODESIZE` plus a warm `EXTCODECOPY` (the target is warmed by -earlier search iterations), and EIP-8038 adds a flat +100 to each warm -extcode access. The stored threshold therefore grows by the sum of the -two opcodes' warm `(Amsterdam - Cancun)` cost deltas, derived from the -fork's own gas model so it is exactly 0 before EIP-8038; do not -hardcode the Amsterdam number. +an opcode needs to succeed. The EXTCODE case runs a warm `EXTCODESIZE` +plus a warm `EXTCODECOPY` (the target is warmed by earlier search +iterations), and EIP-8038 adds a flat +100 to each warm extcode +access; its threshold grows by the two opcodes' warm cost deltas. The +CREATE/CREATE2 thresholds equal the probe bytecode's own +`gas_cost(fork)` (EIP-8037 adds new-account state gas), and the search +bound is supplied via calldata (same 3-byte width as the ported PUSH2 +60000, keeping JUMP targets and the CODESIZE trick intact) so those +cases cannot saturate. All values derive from the fork's own gas +model; do not hardcode them. """ import pytest @@ -240,7 +244,12 @@ def test_measure_gas( # sstore(0, max) # } contract_12 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH2[0xEA60] + # The search's upper bound comes from calldata (word at 0x24): + # EIP-8037's state gas pushes the CREATE/CREATE2 thresholds past + # the ported PUSH2 60000 bound, and CALLDATALOAD keeps the same + # 3-byte width so the hand-coded JUMP targets and the CODESIZE + # constant trick below are unaffected. + code=Op.CALLDATALOAD(offset=0x24) + Op.ADD(Op.CALLDATALOAD(offset=0x4), 0xC0DE00) + Op.PUSH1[0x0] + Op.JUMPDEST @@ -393,16 +402,36 @@ def test_measure_gas( - 103 ) + # The measured threshold for the CREATE/CREATE2 probes is exactly the + # probe bytecode's own cost (operand pushes + opcode); mirroring the + # deployed code in the metadata keeps the expectation fork-derived — + # EIP-8037 adds the new-account state gas and reprices the base. + create_probe_cost = Op.CREATE( + value=Op.DUP1, + offset=0x0, + size=0x200, + new_memory_size=0x200, + init_code_size=0x200, + ).gas_cost(fork) + create2_probe_cost = Op.CREATE2( + value=Op.DUP1, + offset=0x0, + size=0x200, + salt=Op.ADD(0x5A17, Op.GAS), + new_memory_size=0x200, + init_code_size=0x200, + ).gas_cost(fork) + expect_entries_: list[dict] = [ { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 32089})}, + "result": {contract_12: Account(storage={0: create_probe_cost})}, }, { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 32193})}, + "result": {contract_12: Account(storage={0: create2_probe_cost})}, }, { "indexes": {"data": [2, 3], "gas": -1, "value": -1}, @@ -440,18 +469,22 @@ def test_measure_gas( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Second calldata word: the binary search's upper bound. The bisection + # boundary is independent of the starting bound, so one generous value + # (covering EIP-8037's ~216k CREATE threshold) works on every fork. + search_max = Hash(0x100000) tx_data = [ - Bytes("693c6139") + Hash(0xF0), - Bytes("693c6139") + Hash(0xF5), - Bytes("693c6139") + Hash(0xF1), - Bytes("693c6139") + Hash(0xF2), - Bytes("693c6139") + Hash(0xF4), - Bytes("693c6139") + Hash(0xFA), - Bytes("693c6139") + Hash(0x51), - Bytes("693c6139") + Hash(0x52), - Bytes("693c6139") + Hash(0x53), - Bytes("693c6139") + Hash(0x20), - Bytes("693c6139") + Hash(0x3B), + Bytes("693c6139") + Hash(0xF0) + search_max, + Bytes("693c6139") + Hash(0xF5) + search_max, + Bytes("693c6139") + Hash(0xF1) + search_max, + Bytes("693c6139") + Hash(0xF2) + search_max, + Bytes("693c6139") + Hash(0xF4) + search_max, + Bytes("693c6139") + Hash(0xFA) + search_max, + Bytes("693c6139") + Hash(0x51) + search_max, + Bytes("693c6139") + Hash(0x52) + search_max, + Bytes("693c6139") + Hash(0x53) + search_max, + Bytes("693c6139") + Hash(0x20) + search_max, + Bytes("693c6139") + Hash(0x3B) + search_max, ] tx_gas = [16777216] diff --git a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py index bb80de6c8d9..241f8d4a4a5 100644 --- a/tests/ported_static/stBadOpcode/test_operation_diff_gas.py +++ b/tests/ported_static/stBadOpcode/test_operation_diff_gas.py @@ -1,11 +1,17 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Measure the minimum gas each opcode needs to succeed via a linear +search in 100-gas steps (by Ori Pomerantz qbzzt1@gmail.com). Ported from: state_tests/stBadOpcode/operationDiffGasFiller.yml @manually-enhanced: Do not overwrite. A search measures the gas an -opcode needs to succeed. Two access classes shift under EIP-8038: the +opcode needs to succeed. The CREATE/CREATE2 thresholds equal the probe +bytecode's own `gas_cost(fork)` rounded up to the search step — +EIP-8037 adds new-account and storage-set state gas — and their search +start is supplied via calldata a few steps below the threshold so the +linear probe loop cannot exhaust the transaction's gas on Amsterdam. +Two access classes also shift under EIP-8038: the CALL-family probes (`CALL`/`CALLCODE`/`DELEGATECALL`/`STATICCALL`) make one cold account access to the callee, repricing by `COLD_ACCOUNT_ACCESS - 2600`; the EXTCODE probe runs a cold @@ -378,6 +384,44 @@ def test_operation_diff_gas( # memory) so only the account-access component varies across forks. gas_costs = fork.gas_costs() cold_account_delta = gas_costs.COLD_ACCOUNT_ACCESS - 2600 + # The CREATE/CREATE2 probes wrap the create in a cold zero->nonzero + # SSTORE of the returned address (cost depends only on the + # transition, so new_value=1 stands in for the address). The stored + # threshold is the first search step (multiples of GAS_DIFF) at or + # above the probe bytecode's own fork-derived cost; EIP-8037 adds + # the new-account and storage-set state gas. The search starts a few + # steps below the expected threshold (via calldata) so the linear + # probe loop cannot exhaust the transaction's gas on Amsterdam. + gas_diff = 0x64 + create_probe_cost = Op.SSTORE( + key=0x0, + value=Op.CREATE( + value=Op.DUP1, + offset=0x0, + size=0x200, + new_memory_size=0x200, + init_code_size=0x200, + ), + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + create2_probe_cost = Op.SSTORE( + key=0x0, + value=Op.CREATE2( + value=Op.DUP1, + offset=0x0, + size=0x200, + salt=0x5A17, + new_memory_size=0x200, + init_code_size=0x200, + ), + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + create_threshold = -(-create_probe_cost // gas_diff) * gas_diff + create2_threshold = -(-create2_probe_cost // gas_diff) * gas_diff extcode_probe_delta = ( Op.EXTCODESIZE.with_metadata(address_warm=False).gas_cost(fork) - 2600 ) + ( @@ -394,12 +438,12 @@ def test_operation_diff_gas( { "indexes": {"data": [0], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 54200})}, + "result": {contract_12: Account(storage={0: create_threshold})}, }, { "indexes": {"data": [1], "gas": -1, "value": -1}, "network": [">=Cancun"], - "result": {contract_12: Account(storage={0: 54300})}, + "result": {contract_12: Account(storage={0: create2_threshold})}, }, { "indexes": {"data": [2, 3, 4, 5], "gas": -1, "value": -1}, @@ -430,8 +474,14 @@ def test_operation_diff_gas( post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) tx_data = [ - Bytes("048071d3") + Hash(0xF0) + Hash(0x0) + Hash(0x64), - Bytes("048071d3") + Hash(0xF5) + Hash(0x0) + Hash(0x64), + Bytes("048071d3") + + Hash(0xF0) + + Hash(create_threshold - 5 * gas_diff) + + Hash(gas_diff), + Bytes("048071d3") + + Hash(0xF5) + + Hash(create2_threshold - 5 * gas_diff) + + Hash(gas_diff), Bytes("048071d3") + Hash(0xF1) + Hash(0x0) + Hash(0x64), Bytes("048071d3") + Hash(0xF2) + Hash(0x0) + Hash(0x64), Bytes("048071d3") + Hash(0xF4) + Hash(0x0) + Hash(0x64), diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py index 87fb08afa78..48a1e1efced 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py @@ -1,104 +1,71 @@ """ -Legacy Test from Christoph. J. +Verify a name-registrator contract creation succeeds or fails with the +transaction budget: the init code writes a storage slot and deposits the +registrar's runtime code. Ported from: state_tests/stCallCreateCallCodeTest/createNameRegistratorPerTxsNotEnoughGasFiller.json + +@manually-enhanced: Do not overwrite. Both budgets are derived from the +fork (intrinsic + top-frame state gas + init code execution + code deposit +regular and state costs); the success arm also pins the deposited code and +transferred balance, which the ported post never checked. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 100_000 +COPY_OFFSET = 0xC +DEPOSITED_SIZE = 0x10 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/createNameRegistratorPerTxsNotEnoughGasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "enough_gas", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(False, id="g0"), + pytest.param(True, id="g1"), ], ) def test_create_name_registrator_per_txs_not_enough_gas( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + enough_gas: bool, ) -> None: - """Legacy Test from Christoph.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000000, + """An under-budgeted registrar creation leaves no account behind.""" + # The ported init code: write slot 1, then deposit 16 bytes of + # registrar runtime copied from the init code's own bytes. + store = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address(address=sender, nonce=0): Account( - storage={1: 1} - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1) - + Op.PUSH1[0x10] - + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) + initcode = ( + store + + Op.PUSH1[DEPOSITED_SIZE] + + Op.CODECOPY( + dest_offset=0x0, + offset=COPY_OFFSET, + size=Op.DUP1, + data_size=DEPOSITED_SIZE, + new_memory_size=0x20, + ) + Op.PUSH1[0x0] + Op.RETURN + Op.STOP @@ -109,19 +76,50 @@ def test_create_name_registrator_per_txs_not_enough_gas( + Op.STOP + Op.JUMPDEST + Op.SSTORE( - key=Op.CALLDATALOAD(offset=0x0), value=Op.CALLDATALOAD(offset=0x20) - ), - ] - tx_gas = [56157, 86157] - tx_value = [100000] + key=Op.CALLDATALOAD(offset=0x0), + value=Op.CALLDATALOAD(offset=0x20), + ) + ) + deposited = bytes(initcode)[COPY_OFFSET : COPY_OFFSET + DEPOSITED_SIZE] + + # Fork-derived budgets: the sufficient one covers the init code, the + # code deposit (regular and EIP-8037 state), and the created account's + # top-frame state gas; the insufficient one dies mid-init-code. + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + execution_cost = ( + initcode.gas_cost(fork) + + DEPOSITED_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=DEPOSITED_SIZE) + ) + gas_limit = overhead + ( + execution_cost + 5_000 if enough_gas else execution_cost // 2 + ) + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + if enough_gas: + created_account: Account | None = Account( + nonce=1, + code=deposited, + balance=TX_VALUE, + storage={1: 1}, + ) + else: + created_account = Account.NONEXISTENT + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py index cc76b3587f5..212b244d922 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py @@ -1,17 +1,22 @@ """ -Legacy Test from Christoph. J. +Verify a nested CREATE of the name registrar whose child grant cannot cover +the init code: the child account never materializes, while the creating +frame completes (its nonce still advances). Ported from: state_tests/stCallCreateCallCodeTest/createNameRegistratorPreStore1NotEnoughGasFiller.json + +@manually-enhanced: Do not overwrite. The registrar init code is composed +(not a hex blob) and the transaction budget is derived from the fork so +the child's 63/64 grant undercuts its cost on every fork; the creator's +balance is asserted (the endowment returns on failure). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,60 +26,111 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 0x186A0 +CREATE_VALUE = 0x17 +INITIAL_BALANCE = 10**15 +COPY_OFFSET = 0xC +DEPOSITED_SIZE = 0x10 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/createNameRegistratorPreStore1NotEnoughGasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_name_registrator_pre_store1_not_enough_gas( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Legacy Test from Christoph.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, + """A starved nested registrar creation leaves no account behind.""" + # The registrar init code (same as the per-txs sibling): write slot 1, + # deposit 16 bytes of runtime copied from its own bytes. + initcode = ( + Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.PUSH1[DEPOSITED_SIZE] + + Op.CODECOPY( + dest_offset=0x0, + offset=COPY_OFFSET, + size=Op.DUP1, + data_size=DEPOSITED_SIZE, + new_memory_size=0x20, + ) + + Op.PUSH1[0x0] + + Op.RETURN + + Op.STOP + + Op.JUMPI( + pc=0x9, + condition=Op.ISZERO(Op.SLOAD(key=Op.CALLDATALOAD(offset=0x0))), + ) + + Op.STOP + + Op.JUMPDEST + + Op.SSTORE( + key=Op.CALLDATALOAD(offset=0x0), + value=Op.CALLDATALOAD(offset=0x20), + ) ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) == 0x22, "ported init code is 34 bytes" - # Source: lll - # {(MSTORE 0 0x6001600155601080600c6000396000f3006000355415600957005b6020356000 ) (MSTORE8 32 0x35) (MSTORE8 33 0x55) (CREATE 23 0 34) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( + # Memory setup derived from the composed bytes (one word plus two + # trailing byte stores, as in the ported filler). + setup = ( + Op.MSTORE( offset=0x0, - value=0x6001600155601080600C6000396000F3006000355415600957005B6020356000, # noqa: E501 + value=int.from_bytes(initcode_bytes[:0x20], "big"), + new_memory_size=0x20, ) - + Op.MSTORE8(offset=0x20, value=0x35) - + Op.MSTORE8(offset=0x21, value=0x55) - + Op.CREATE(value=0x17, offset=0x0, size=0x22) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, + + Op.MSTORE8( + offset=0x20, value=initcode_bytes[0x20], new_memory_size=0x40 + ) + + Op.MSTORE8( + offset=0x21, value=initcode_bytes[0x21], new_memory_size=0x40 + ) + ) + create_code = Op.CREATE( + value=CREATE_VALUE, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), + ) + creator = pre.deploy_contract( + code=setup + Op.POP(create_code) + Op.STOP, + balance=INITIAL_BALANCE, + ) + + # Budget: covers the frame's own work and the CREATE's peak charge, + # but the child's 63/64 grant undercuts the init code plus deposit. + child_needed = ( + initcode.gas_cost(fork) + + DEPOSITED_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=DEPOSITED_SIZE) + ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + setup.gas_cost(fork) + + create_code.gas_cost(fork) + + child_needed // 2 ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=73071, - value=0x186A0, + sender=pre.fund_eoa(), + to=creator, + gas_limit=gas_limit, + value=TX_VALUE, ) post = { - contract_0: Account(nonce=1), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, + # The CREATE advanced the nonce even though its child failed, and + # the endowment returned. + creator: Account(nonce=2, balance=INITIAL_BALANCE + TX_VALUE), + compute_create_address(address=creator, nonce=1): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py b/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py index 943824d3990..bac280a1984 100644 --- a/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py +++ b/tests/ported_static/stCreateTest/test_create_e_contract_create_ne_contract_in_init_oog_tr.py @@ -1,141 +1,154 @@ """ -Test_create_e_contract_create_ne_contract_in_init_oog_tr. +Verify a contract-creation transaction whose init code first calls an +existing contract and then CREATEs a child: with a full budget both the +call and the nested creation land (the child at the creator's nonce-1 +address, not nonce 0); with a starved budget the callee and the whole +creation fail together. Ported from: state_tests/stCreateTest/CREATE_EContractCreateNEContractInInitOOG_TrFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(intrinsic + EIP-8037 top-frame and nested-create state gas + composed +code costs), the callee call forwards all gas instead of a ported fixed +budget, and the nested child is now asserted at its real nonce-1 address +(the port only checked the vacuous nonce-0 address). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALLEE_STORED = 0xC + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CREATE_EContractCreateNEContractInInitOOG_TrFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize("oog", [False, True], ids=["enough-gas", "oog"]) def test_create_e_contract_create_ne_contract_in_init_oog_tr( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oog: bool, ) -> None: - """Test_create_e_contract_create_ne_contract_in_init_oog_tr.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """Budget decides how far a creation's call-then-CREATE init gets.""" + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # Callee: one cold zero->non-zero store observed in the post. + callee_code = ( + Op.SSTORE( + key=0x1, + value=CALLEE_STORED, + key_warm=False, + original_value=0, + new_value=CALLEE_STORED, + ) + + Op.STOP ) + callee = pre.deploy_contract(code=callee_code) - # Source: lll - # {[[1]]12} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, + # Child init code: return a small runtime code from memory. + child_runtime = Op.SSTORE(key=0x0, value=CALLEE_STORED) + child_initcode = Op.MSTORE( + offset=0x0, + value=int.from_bytes(bytes(child_runtime), "big"), + new_memory_size=0x20, + ) + Op.RETURN( + offset=32 - len(bytes(child_runtime)), + size=len(bytes(child_runtime)), ) + child_initcode_bytes = bytes(child_initcode) - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={1: 12}), - compute_create_address(address=sender, nonce=0): Account( - nonce=2 - ), - compute_create_address( - address=compute_create_address(address=sender, nonce=0), - nonce=0, - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={1: 0}), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - compute_create_address( - address=compute_create_address(address=sender, nonce=0), - nonce=0, - ): Account.NONEXISTENT, - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Transaction init code: call the callee (forwarding all gas), then + # CREATE the child from memory; deploys nothing itself. + call_code = Op.POP(Op.CALL(address=callee)) + stage_code = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_initcode_bytes, "big"), + new_memory_size=0x20, + old_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=32 - len(child_initcode_bytes), + size=len(child_initcode_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_initcode_bytes), + ) + initcode = call_code + stage_code + create_code - tx_data = [ - Op.POP( - Op.CALL( - gas=0xEA60, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + if oog: + # Enough to start executing, but the forwarded 63/64 undercuts + # the callee's store and the CREATE is unaffordable after it. + gas_limit = intrinsic + callee_code.gas_cost(fork) // 2 + else: + # Everything must land: the fresh create target's top-frame + # state gas (EIP-8037), the callee, and the nested creation's + # peak charge plus the child's execution and code deposit. + runtime_size = len(bytes(child_runtime)) + child_total = ( + child_initcode.gas_cost(fork) + + runtime_size * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=runtime_size) ) - + Op.MSTORE(offset=0x0, value=0x64600C6000556000526005601BF3) - + Op.CREATE(value=0x0, offset=0x12, size=0xE), - ] - tx_gas = [160000, 60000] + needed = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + + callee_code.gas_cost(fork) + + child_total + ) + # Headroom for the 63/64 withhold at the call and the CREATE. + gas_limit = needed + needed // 63 tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=initcode, + gas_limit=gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + # The nested CREATE runs while the creator's nonce is 1 (EIP-161), + # so the child lands at the nonce-1 address and the nonce-0 address + # must stay empty. + child = compute_create_address(address=created, nonce=1) + child_at_nonce0 = compute_create_address(address=created, nonce=0) + + if oog: + post = { + sender: Account(nonce=1), + callee: Account(storage={1: 0}), + created: Account.NONEXISTENT, + child: Account.NONEXISTENT, + child_at_nonce0: Account.NONEXISTENT, + } + else: + post = { + sender: Account(nonce=1), + callee: Account(storage={1: CALLEE_STORED}), + created: Account(nonce=2, code=b""), + child: Account(nonce=1, code=bytes(child_runtime), storage={}), + child_at_nonce0: Account.NONEXISTENT, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py b/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py index f752c8b1f6b..b85370ef6d2 100644 --- a/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py +++ b/tests/ported_static/stCreateTest/test_create_e_contract_then_call_to_non_existent_acc.py @@ -1,18 +1,24 @@ """ -Test_create_e_contract_then_call_to_non_existent_acc. +Verify a CREATE of an empty contract followed by a CALL to a non-existent +account: both operations are gas-measured, the created address and the +call's success flag are stored, and the absent callee stays non-existent. Ported from: state_tests/stCreateTest/CREATE_EContract_ThenCALLToNonExistentAccFiller.json + +@manually-enhanced: Do not overwrite. The ported absolute GAS snapshots +(slots 0/2/100) are re-expressed as two CodeGasMeasure windows asserted +via the fork's gas model, the created address and call flag stay in the +measured windows' SSTOREs, and the callee is a dynamic non-existent +account called with all gas forwarded. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -22,80 +28,95 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CREATE_GAS_SLOT = 0x0 +ADDRESS_SLOT = 0x1 +CALL_GAS_SLOT = 0x2 +FLAG_SLOT = 0x3 + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CREATE_EContract_ThenCALLToNonExistentAccFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_e_contract_then_call_to_non_existent_acc( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_e_contract_then_call_to_non_existent_acc.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Measure a CREATE of an empty contract and a call to no account.""" + absent = pre.nonexistent_account() + + # CREATE over never-written memory: the all-STOP init code deposits + # nothing, leaving an empty account with nonce 1. + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, + ) + # Storing the created address keeps it observable and folds the + # store into the measured window (the address is non-zero, so the + # placeholder new_value only sizes the zero->non-zero transition). + store_create = Op.SSTORE( + ADDRESS_SLOT, + create_code, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # A value-less call to an absent account creates nothing on any + # fork; the callee consumes no gas, so the window measures only the + # cold CALL itself. Storing the success flag keeps it observable. + call_code = Op.CALL( + address=absent, + address_warm=False, + value_transfer=False, + account_new=False, + ) + store_flag = Op.SSTORE( + FLAG_SLOT, + call_code, + key_warm=False, + original_value=0, + new_value=1, ) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]] (CALL 60000 0xe1ecf98489fa9ed60a664fc4998db699cfa39d40 0 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=0xE1ECF98489FA9ED60A664FC4998DB699CFA39D40, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + contract = pre.deploy_contract( + code=CodeGasMeasure( + code=store_create, + sstore_key=CREATE_GAS_SLOT, + ) + + CodeGasMeasure( + code=store_flag, + sstore_key=CALL_GAS_SLOT, ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, ) post = { - contract_0: Account( + contract: Account( storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6F50B, + CREATE_GAS_SLOT: store_create.gas_cost(fork), + ADDRESS_SLOT: compute_create_address( + address=contract, nonce=1 + ), + CALL_GAS_SLOT: store_flag.gas_cost(fork), + FLAG_SLOT: 1, }, ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - Address( - 0xE1ECF98489FA9ED60A664FC4998DB699CFA39D40 - ): Account.NONEXISTENT, + compute_create_address(address=contract, nonce=1): Account( + nonce=1, code=b"", balance=0 + ), + absent: Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py index 9ca299282ef..1f406840b63 100644 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py +++ b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage.py @@ -1,18 +1,29 @@ """ -Test_create_empty_contract_with_storage. +Measure CREATE of a codeless-but-storage-writing contract, and optionally +a following CALL to it, via CodeGasMeasure. + +The init code writes the created account's own storage and calls a +storage-writer contract, then deposits no code: the result is an "empty" +(codeless) account with storage and nonce 1. Ported from: state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json +state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json +state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json + +@manually-enhanced: Do not overwrite. Three fillers folded into one +parametrize; the init code is composed (not hex blobs) so the measured +CREATE/CALL expectations derive from the same bytecode; the init code's +inner CALL forwards all gas (the ported 0xEA60 budget OOGs under +EIP-8037); the CALL success flag stays inside the measured window. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -22,78 +33,169 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0x1 +CREATE_GAS_SLOT = 0x2 +CALL_FLAG_SLOT = 0x3 +CALL_GAS_SLOT = 0x64 +STORED_VALUE = 0xC + +FORWARDED_GAS = 0xEA60 + @pytest.mark.ported_from( - ["state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json"], + [ + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageFiller.json", + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json", # noqa: E501 + "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "call_created, call_value", + [ + pytest.param(False, 0, id="with_storage"), + pytest.param(True, 0, id="and_call_it_0wei"), + pytest.param(True, 1, id="and_call_it_1wei"), + ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable def test_create_empty_contract_with_storage( state_test: StateTestFiller, pre: Alloc, + fork: Fork, + call_created: bool, + call_value: int, ) -> None: - """Test_create_empty_contract_with_storage.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Measure CREATE (and optionally CALL) gas for a storage-only account.""" + # Called by the init code below; writes one cold fresh slot. + writer_store = Op.SSTORE( + key=0x1, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The init code writes the created account's own slot 0 and calls the + # writer, then runs off its end (STOP) so no code is deposited. The + # inner CALL forwards all remaining gas (default Op.GAS operand). + initcode = Op.SSTORE( + key=0x0, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + Op.CALL( + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x40, "init code must fit two MSTORE words" - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Memory is populated (and expanded to 0x40) before the measured + # window, so the CREATE itself expands nothing. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes( + initcode_bytes[:0x20].ljust(0x20, b"\x00"), "big" + ), + ) + Op.MSTORE( + offset=0x20, + value=int.from_bytes( + initcode_bytes[0x20:].ljust(0x20, b"\x00"), "big" + ), + ) + + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # The created address is stored inside the measured window (as in the + # ported filler) so the optional CALL can target it at runtime. + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, ) + # The created account exists (nonce 1) and is warm (CREATE accessed + # it); the CALL success flag is stored inside the measured window — a + # wrongly failed call would otherwise be unobservable for the 0wei arm. + call_code = Op.CALL( + gas=FORWARDED_GAS, + address=Op.SLOAD(key=ADDRESS_SLOT, key_warm=True), + value=call_value, + address_warm=True, + value_transfer=call_value > 0, + account_new=False, + ) + call_store = Op.SSTORE( + key=CALL_FLAG_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=1, + ) + + code = setup + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=CREATE_GAS_SLOT, + ) + if call_created: + code += CodeGasMeasure( + code=call_store, + extra_stack_items=0, + sstore_key=CALL_GAS_SLOT, + ) + contract = pre.deploy_contract(code=code, balance=call_value) + tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + # The measured CREATE includes the child's work: the init code's own + # consumption plus the writer's store it calls. + measured_create = ( + create_store.gas_cost(fork) + + initcode.gas_cost(fork) + + writer_store.gas_cost(fork) ) + # A value-bearing CALL whose codeless callee consumes nothing measures + # gas_cost minus the stipend (forwarded then returned unused). + stipend = fork.gas_costs().CALL_STIPEND if call_value else 0 + measured_call = call_store.gas_cost(fork) - stipend + + created = compute_create_address(address=contract, nonce=1) + contract_storage: dict = { + ADDRESS_SLOT: created, + CREATE_GAS_SLOT: measured_create, + } + if call_created: + contract_storage[CALL_FLAG_SLOT] = 1 + contract_storage[CALL_GAS_SLOT] = measured_call post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 100: 0x6F4F0, - }, + contract: Account(storage=contract_storage, balance=0), + # Codeless, but with storage and (for the 1wei arm) the value the + # measured CALL transferred — proving both the init code and the + # CALL executed. + created: Account( + nonce=1, + balance=call_value if call_created else 0, + storage={0: STORED_VALUE}, ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), + writer: Account(storage={1: STORED_VALUE}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py deleted file mode 100644 index d7940716427..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_0wei.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Test_create_empty_contract_with_storage_and_call_it_0wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_with_storage_and_call_it_0wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_with_storage_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (CALL 60000 (SLOAD 1) 0 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x64763, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py b/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py deleted file mode 100644 index cbd15afeba3..00000000000 --- a/tests/ported_static/stCreateTest/test_create_empty_contract_with_storage_and_call_it_1wei.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Test_create_empty_contract_with_storage_and_call_it_1wei. - -Ported from: -state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreateTest/CREATE_EmptyContractWithStorageAndCallIt_1weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_create_empty_contract_with_storage_and_call_it_1wei( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_create_empty_contract_with_storage_and_call_it_1wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (CALL 60000 (SLOAD 1) 1 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.CALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - post = { - contract_0: Account( - storage={ - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x62D37, - }, - ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 12}, balance=1 - ), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py index 6f458740e9d..aaa2962d0ec 100644 --- a/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py +++ b/tests/ported_static/stCreateTest/test_create_oo_gafter_init_code_returndata_size.py @@ -1,17 +1,23 @@ """ -Calls a contract that runs CREATE which deploy a code. then OOG happens... +Verify a CREATE whose child completes its init code but cannot afford the +code deposit: the creation fails (no account is deployed), yet the parent +frame survives on its 63/64 retention and the transaction succeeds. Ported from: state_tests/stCreateTest/CreateOOGafterInitCodeReturndataSizeFiller.json + +@manually-enhanced: Do not overwrite. The gas limit is derived from the +fork so the child's 63/64 grant covers init execution but not the code +deposit (including EIP-8037 deposit state gas), and the budget carries +the CREATE's peak new-account state charge, refunded when the child +fails. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,57 +27,99 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TX_VALUE = 1 + @pytest.mark.ported_from( [ "state_tests/stCreateTest/CreateOOGafterInitCodeReturndataSizeFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_oo_gafter_init_code_returndata_size( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Calls a contract that runs CREATE which deploy a code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """CREATE fails at the code deposit; the parent frame completes.""" + # The child would deploy two stores; it never runs, only its deposit + # price matters. + child_runtime = Op.SSTORE(key=0x1, value=0x1) + Op.SSTORE( + key=0x2, value=0x1 + ) + runtime_size = len(bytes(child_runtime)) + + # Child init code: return the runtime code from memory. + child_initcode = Op.MSTORE( + offset=0x0, + value=int.from_bytes(bytes(child_runtime), "big"), + new_memory_size=0x20, + ) + Op.RETURN( + offset=32 - runtime_size, + size=runtime_size, + ) + initcode_bytes = bytes(child_initcode) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # Parent: stage the init code in memory, CREATE from it, then read + # RETURNDATASIZE (zero after the deposit failure) before stopping. + stage_code = Op.MSTORE( + offset=0x0, + value=int.from_bytes(initcode_bytes, "big"), + new_memory_size=0x20, ) + create_code = Op.CREATE( + value=0x0, + offset=32 - len(initcode_bytes), + size=len(initcode_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(initcode_bytes), + ) + tail_code = Op.POP + Op.EXP(0x2, Op.RETURNDATASIZE) + Op.STOP + contract = pre.deploy_contract(code=stage_code + create_code + tail_code) - # Source: lll - # { (MSTORE 0 0x6960016001556001600255600052600a6016f3) (CREATE 0 13 19) (EXP 2 (RETURNDATASIZE)) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, value=0x6960016001556001600255600052600A6016F3 + # Grant the child enough for its init execution but one gas short of + # the code deposit, so the deposit is what fails. Under EIP-8037 the + # regular deposit cost is only the keccak word cost (the per-byte + # price moved into deposit state gas); before it, 200 per byte. + child_exec = child_initcode.gas_cost(fork) + if fork.is_eip_enabled(8037): + deposit_regular = fork.gas_costs().OPCODE_KECCAK256_PER_WORD * ( + (runtime_size + 31) // 32 ) - + Op.POP(Op.CREATE(value=0x0, offset=0xD, size=0x13)) - + Op.EXP(0x2, Op.RETURNDATASIZE) - + Op.STOP, - nonce=0, + else: + deposit_regular = runtime_size * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + deposit = deposit_regular + fork.code_deposit_state_gas( + code_size=runtime_size + ) + available = (child_exec + deposit - 1) * 64 // 63 + forwarded = available - available // 64 + assert child_exec <= forwarded < child_exec + deposit, ( + "63/64 grant must cover init execution but not the deposit" + ) + # The parent's 1/64 retention must still afford the tail. + assert available // 64 > tail_code.gas_cost(fork), ( + "retention must cover the post-CREATE tail" + ) + + gas_limit = ( + fork.transaction_intrinsic_cost_calculator()(sends_value=True) + + stage_code.gas_cost(fork) + + create_code.gas_cost(fork) + + available ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=55054, - value=1, + sender=pre.fund_eoa(), + to=contract, + gas_limit=gas_limit, + value=TX_VALUE, ) post = { - contract_0: Account(balance=1), - compute_create_address( - address=contract_0, nonce=0 - ): Account.NONEXISTENT, + # The transferred value proves the parent frame completed. + contract: Account(balance=TX_VALUE, storage={}), + compute_create_address(address=contract, nonce=1): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py index 2f7088ae13d..307bc3d7c06 100644 --- a/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py +++ b/tests/ported_static/stCreateTest/test_create_oog_from_call_refunds.py @@ -1,8 +1,17 @@ """ -Test_create_oog_from_call_refunds. +Verify that gas refunds earned during (or via calls made from) a CREATE's +init code cannot rescue the creation from an out-of-gas failure: each OoG +variant burns the whole budget through the dispatcher's INVALID, while +the NoOoG variants deploy and keep their refunds. Ported from: state_tests/stCreateTest/CreateOOGFromCallRefundsFiller.yml + +@manually-enhanced: Do not overwrite. The gas limit and the sender's +exact prefund are derived from the fork so the child's 63/64 grant +covers the deepest nested-create chain (EIP-8037 state gas included) +yet stays below the 5000-byte code-deposit price that drives the OoG +arms. """ import pytest @@ -12,7 +21,6 @@ Address, Alloc, Bytes, - Environment, Hash, StateTestFiller, Transaction, @@ -28,6 +36,11 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Returning this much code makes the deposit unaffordable in the OoG +# variants; the gas limit below is derived against it. +OOG_DEPOSIT_SIZE = 0x1388 +TX_GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stCreateTest/CreateOOGFromCallRefundsFiller.yml"], @@ -191,8 +204,7 @@ def test_create_oog_from_call_refunds( g: int, v: int, ) -> None: - """Test_create_oog_from_call_refunds.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) + """Refunds earned inside a creation cannot avert its OOG.""" contract_0 = Address(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) contract_1 = Address(0x000000000000000000000000000000000000001A) contract_2 = Address(0x000000000000000000000000000000000000001B) @@ -226,15 +238,38 @@ def test_create_oog_from_call_refunds( key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # Budget: covers the deepest NoOoG chain (a nested CREATE with + # EIP-8037 peak state gas at each level) while any init frame's + # 63/64 grant stays below the OoG arms' code-deposit price. The + # intrinsic bound uses all-non-zero calldata (selector + address). + gas_costs = fork.gas_costs() + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=b"\xff" * 36 + ) + create_op = Op.CREATE( + value=0x0, + offset=0x0, + size=0x40, + new_memory_size=0x40, + init_code_size=0x40, + ) + gas_limit = ( + intrinsic + + create_op.gas_cost(fork) + + OOG_DEPOSIT_SIZE * gas_costs.CODE_DEPOSIT_PER_BYTE + ) + deposit_price = ( + OOG_DEPOSIT_SIZE * gas_costs.CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) + ) + # No init frame can receive enough to pay the OoG arms' deposit. + grant_bound = gas_limit - intrinsic - create_op.execution_cost(fork) + assert grant_bound * 63 // 64 < deposit_price, ( + "63/64 grant must stay below the OoG deposit price" ) - pre[sender] = Account(balance=0x3D0900, nonce=1) + # The exact prefund makes "all gas burned" observable as balance 0. + pre[sender] = Account(balance=gas_limit * TX_GAS_PRICE, nonce=1) # Source: yul # berlin # { @@ -294,7 +329,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000001B), # noqa: E501 ) @@ -464,7 +499,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000002B), # noqa: E501 ) @@ -560,7 +595,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000004B), # noqa: E501 ) @@ -583,7 +618,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000003B), # noqa: E501 ) @@ -655,7 +690,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000005B), # noqa: E501 ) @@ -745,7 +780,7 @@ def test_create_oog_from_call_refunds( ret_offset=Op.DUP1, ret_size=0x0, ) - + Op.RETURN(offset=0x0, size=0x1388), + + Op.RETURN(offset=0x0, size=OOG_DEPOSIT_SIZE), nonce=0, address=Address(0x000000000000000000000000000000000000006B), # noqa: E501 ) @@ -789,7 +824,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.PUSH2[0x1388] + + Op.PUSH2[OOG_DEPOSIT_SIZE] + Op.PUSH1[0x1] + Op.PUSH1[0x0] + Op.PUSH3[0xC0DE1] @@ -855,7 +890,7 @@ def test_create_oog_from_call_refunds( code=Op.SSTORE(key=0x0, value=0x1) + Op.SSTORE(key=Op.DUP1, value=0x1) + Op.SSTORE(key=0x1, value=0x0) - + Op.PUSH2[0x1388] + + Op.PUSH2[OOG_DEPOSIT_SIZE] + Op.PUSH1[0x1] + Op.PUSH1[0x0] + Op.PUSH3[0xC0DE1] @@ -1123,15 +1158,14 @@ def test_create_oog_from_call_refunds( Bytes("693c6139") + Hash(contract_23, left_padding=True), Bytes("693c6139") + Hash(contract_24, left_padding=True), ] - tx_gas = [400000] - tx = Transaction( sender=sender, to=contract_0, data=tx_data[d], - gas_limit=tx_gas[g], + gas_limit=gas_limit, + gas_price=TX_GAS_PRICE, nonce=1, error=_exc, ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_create_results.py b/tests/ported_static/stCreateTest/test_create_results.py index a18d11e9cc6..1a92cd79ad2 100644 --- a/tests/ported_static/stCreateTest/test_create_results.py +++ b/tests/ported_static/stCreateTest/test_create_results.py @@ -1,224 +1,139 @@ """ -Ori Pomerantz qbzzt1@gmail.com. +Verify the value CREATE/CREATE2 leaves on the stack, the returndata, and +the deployed code for each constructor outcome — success, OOG, empty +revert, revert with data, empty deploy, and in-init SELFDESTRUCT — plus +each CALL-kind's result when calling the successfully created contract, +and the frame-aborting RETURNDATACOPY past an empty return buffer. + +Written by Ori Pomerantz (qbzzt1@gmail.com). Ported from: state_tests/stCreateTest/CreateResultsFiller.yml + +@manually-enhanced: Do not overwrite. The ported PUSH2 0xFFFF sub-call +budgets are replaced by length-preserving forward-all-gas sequences (the +fixed budget starves EIP-8037 state gas), the created accounts are now +asserted per case (code, nonce, or non-existence), and the per-case +posts are an explicit switch over the decoded calldata triple. """ import pytest from execution_testing import ( - EOA, Account, Address, Alloc, Bytes, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create2_address, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The dispatcher's LLL-derived bytecode hardcodes every jump target and +# code-copy offset, so all edits below preserve instruction lengths. +CONTRACT_1_ADDRESS = 0x60A7 +CREATE2_SALT = 0x5A17 +# PC values the dispatcher snapshots right after the create (slot 0x20) +# and after the call section (slot 0x21); fixed by the code layout. +CREATE_PC = 295 +CALL_PC = 551 +# Below the SHA3-OOG constructor's memory-expansion cost (which must +# fail) and above Amsterdam's state-gas needs (which must not). +TX_GAS = 9_437_184 + +# Calldata triples (creation kind, call kind, constructor kind) in the +# ported data order. creation: 1=CREATE, 2=CREATE2. call: 0=none, +# 1=CALL, 2=CALLCODE, 3=DELEGATECALL, 4=STATICCALL. constructor: +# 0/4=success, 1=OOG, 2=revert, 3=revert-with-data, 5=empty deploy, +# 6=SELFDESTRUCT in init (4 also RETURNDATACOPYs past the empty +# return buffer, aborting the whole dispatcher frame). +CASES: list[tuple[int, int, int]] = [ + (1, 1, 0), + (1, 2, 0), + (1, 3, 0), + (1, 4, 0), + (2, 1, 0), + (2, 2, 0), + (2, 3, 0), + (2, 4, 0), + (1, 0, 1), + (2, 0, 1), + (1, 0, 2), + (2, 0, 2), + (1, 0, 5), + (2, 0, 5), + (1, 0, 6), + (2, 0, 6), + (1, 0, 3), + (2, 0, 3), + (1, 1, 4), + (1, 2, 4), + (1, 3, 4), + (1, 4, 4), + (2, 1, 4), + (2, 2, 4), + (2, 3, 4), + (2, 4, 4), +] + +# Constructor fragment (offset, size) within the dispatcher's code: +# the dispatcher CODECOPYs these windows as the init code it creates +# from, keyed by the constructor kind. +FRAGMENTS: dict[int, tuple[int, int]] = { + 0: (0x250, 0x21), + 1: (0x271, 0x29), + 2: (0x29A, 0x26), + 3: (0x2C0, 0x2C), + 4: (0x250, 0x21), + 5: (0x2EC, 0x28), + 6: (0x314, 0x2A), +} + @pytest.mark.ported_from( ["state_tests/stCreateTest/CreateResultsFiller.yml"], ) @pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), - pytest.param( - 3, - 0, - 0, - id="d3", - ), - pytest.param( - 4, - 0, - 0, - id="d4", - ), - pytest.param( - 5, - 0, - 0, - id="d5", - ), - pytest.param( - 6, - 0, - 0, - id="d6", - ), - pytest.param( - 7, - 0, - 0, - id="d7", - ), - pytest.param( - 8, - 0, - 0, - id="d8", - ), - pytest.param( - 9, - 0, - 0, - id="d9", - ), - pytest.param( - 10, - 0, - 0, - id="d10", - ), - pytest.param( - 11, - 0, - 0, - id="d11", - ), - pytest.param( - 12, - 0, - 0, - id="d12", - ), - pytest.param( - 13, - 0, - 0, - id="d13", - ), - pytest.param( - 14, - 0, - 0, - id="d14", - ), - pytest.param( - 15, - 0, - 0, - id="d15", - ), - pytest.param( - 16, - 0, - 0, - id="d16", - ), - pytest.param( - 17, - 0, - 0, - id="d17", - ), - pytest.param( - 18, - 0, - 0, - id="d18", - ), - pytest.param( - 19, - 0, - 0, - id="d19", - ), - pytest.param( - 20, - 0, - 0, - id="d20", - ), - pytest.param( - 21, - 0, - 0, - id="d21", - ), - pytest.param( - 22, - 0, - 0, - id="d22", - ), - pytest.param( - 23, - 0, - 0, - id="d23", - ), - pytest.param( - 24, - 0, - 0, - id="d24", - ), - pytest.param( - 25, - 0, - 0, - id="d25", - ), - ], -) +@pytest.mark.parametrize("d", range(len(CASES)), ids=lambda d: f"d{d}") @pytest.mark.pre_alloc_mutable def test_create_results( state_test: StateTestFiller, pre: Alloc, fork: Fork, d: int, - g: int, - v: int, ) -> None: - """Ori Pomerantz qbzzt1@gmail.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC) - contract_1 = Address(0x00000000000000000000000000000000000060A7) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) + """Verify create results and follow-up calls per constructor kind.""" + creation, call_kind, constructor = CASES[d] + contract_1 = Address(CONTRACT_1_ADDRESS) + sender = pre.fund_eoa() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # Length-preserving stand-in for the ported PUSH2 0xFFFF gas + # operand: two JUMPDESTs pad the 3-byte slot so every hardcoded + # jump target and code-copy offset stays valid, while GAS forwards + # everything (a fixed budget starves EIP-8037 state gas). + forward_all_gas = Op.JUMPDEST + Op.JUMPDEST + Op.GAS + + # The 18-byte contract each successful constructor deploys: call + # contract_1 (as a 2-byte push, part of the fixed layout) and stop. + contract_code = ( + Op.CALL( + gas=forward_all_gas, + address=CONTRACT_1_ADDRESS, + value=0x0, + args_offset=0x0, + args_size=0x0, + ret_offset=0x0, + ret_size=0x0, + ) + + Op.STOP ) - pre[sender] = Account(balance=0xBA1A9CE0BA1A9CE) # Source: lll # { # ; Variables are 0x20 bytes (= 256 bits) apart, except for @@ -251,8 +166,8 @@ def test_create_results( # ) # ; I did not want to rely on knowing the address at which the contract # ... (138 more lines) - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x100, value=Op.CALLDATALOAD(offset=0x4)) + dispatcher_code = ( + Op.MSTORE(offset=0x100, value=Op.CALLDATALOAD(offset=0x4)) + Op.MSTORE(offset=0x120, value=Op.CALLDATALOAD(offset=0x24)) + Op.MSTORE(offset=0x140, value=Op.CALLDATALOAD(offset=0x44)) + Op.JUMPI( @@ -386,7 +301,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.CALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), value=0x0, args_offset=0x0, @@ -403,7 +318,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.CALLCODE( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), value=0x0, args_offset=0x0, @@ -420,7 +335,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.DELEGATECALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), args_offset=0x0, args_size=0x0, @@ -436,7 +351,7 @@ def test_create_results( + Op.MSTORE( offset=0x640, value=Op.STATICCALL( - gas=0xFFFF, + gas=forward_all_gas, address=Op.MLOAD(offset=0x600), args_offset=0x0, args_size=0x0, @@ -463,16 +378,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.POP(Op.SHA3(offset=0x0, size=0x2FFFFF)) + Op.PUSH1[0x12] + Op.CODECOPY(dest_offset=0x200, offset=0x17, size=Op.DUP1) @@ -480,16 +386,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.REVERT(offset=0x0, size=0x0) + Op.PUSH1[0x12] + Op.CODECOPY(dest_offset=0x200, offset=0x14, size=Op.DUP1) @@ -497,16 +394,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.REVERT(offset=0x0, size=0x20) + Op.PUSH1[0x12] @@ -515,16 +403,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.STOP + Op.PUSH1[0x12] @@ -533,16 +412,7 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP + + contract_code + Op.MSTORE(offset=0x0, value=0x60A7) + Op.SELFDESTRUCT(address=0x0) + Op.PUSH1[0x12] @@ -551,26 +421,26 @@ def test_create_results( + Op.RETURN + Op.STOP + Op.INVALID - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP - + Op.CALL( - gas=0xFFFF, - address=0x60A7, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, + + contract_code + + contract_code + ) + + # Guard the hardcoded layout the bytecode's jump targets and the + # FRAGMENTS table rely on. + dispatcher_bytes = bytes(dispatcher_code) + assert len(dispatcher_bytes) == 0x350, "dispatcher layout drifted" + assert dispatcher_bytes[0x33E:0x350] == bytes(contract_code), ( + "reference contract code slot drifted" + ) + # The SHA3-OOG constructor must stay unaffordable. + assert TX_GAS < fork.memory_expansion_gas_calculator()( + new_bytes=0x2FFFFF + ), "budget must not afford the SHA3-OOG constructor" + + # Slots 16-33 hold non-zero sentinels so every overwrite (even with + # zero) is observable. + contract_0 = pre.deploy_contract( + code=dispatcher_code, storage={ 16: contract_1, 18: contract_1, @@ -580,135 +450,99 @@ def test_create_results( 32: contract_1, 33: contract_1, }, - balance=0xBA1A9CE0BA1A9CE, - nonce=0, - address=Address(0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC), # noqa: E501 ) # Source: lll # { # [[0]] 0x60A7 # } ; end of LLL code - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x60A7) + Op.STOP, - balance=0xBA1A9CE0BA1A9CE, - nonce=0, - address=Address(0x00000000000000000000000000000000000060A7), # noqa: E501 + pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=CONTRACT_1_ADDRESS) + Op.STOP, + address=contract_1, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 1, 2, 4, 5, 6], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={32: 295, 33: 551}), - contract_1: Account(storage={0: contract_1}), - }, - }, - { - "indexes": {"data": [3, 7], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": {contract_0: Account(storage={32: 295, 33: 551})}, - }, - { - "indexes": { - "data": [8, 9, 10, 11, 12, 13, 14, 15], - "gas": 0, - "value": 0, - }, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 18: 18, - 19: 0x600060006000600060006160A761FFFFF1000000000000000000000000000000, # noqa: E501 - 20: contract_1, - 21: contract_1, - 32: 295, - 33: 551, - }, - ), - }, - }, - { - "indexes": {"data": [16, 17], "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 16: 32, - 17: contract_1, - 18: 18, - 19: 0x600060006000600060006160A761FFFFF1000000000000000000000000000000, # noqa: E501 - 20: contract_1, - 21: contract_1, - 32: 295, - 33: 551, - }, - ), - }, - }, - { - "indexes": { - "data": [18, 19, 20, 21, 22, 23, 24, 25], - "gas": 0, - "value": 0, - }, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={ - 16: contract_1, - 17: 0, - 18: contract_1, - 19: contract_1, - 20: contract_1, - 21: contract_1, - 32: contract_1, - 33: contract_1, - }, - ), - }, - }, - ] + # Decode the case into the created account's address and the + # expected post-state. + if creation == 1: + created = compute_create_address(address=contract_0, nonce=1) + else: + frag_offset, frag_size = FRAGMENTS[constructor] + created = compute_create2_address( + contract_0, + CREATE2_SALT, + dispatcher_bytes[frag_offset : frag_offset + frag_size], + ) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # The word the dispatcher stores when comparing its reference copy + # of the contract code against a non-existent account's ext code. + contract_code_word = int.from_bytes( + bytes(contract_code).ljust(32, b"\x00"), "big" + ) - tx_data = [ - Bytes("048071d3") + Hash(0x1) + Hash(0x1) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x2) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x3) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x4) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x1) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x2) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x3) + Hash(0x0), - Bytes("048071d3") + Hash(0x2) + Hash(0x4) + Hash(0x0), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x1), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x1), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x2), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x2), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x5), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x5), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x6), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x6), - Bytes("048071d3") + Hash(0x1) + Hash(0x0) + Hash(0x3), - Bytes("048071d3") + Hash(0x2) + Hash(0x0) + Hash(0x3), - Bytes("048071d3") + Hash(0x1) + Hash(0x1) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x2) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x3) + Hash(0x4), - Bytes("048071d3") + Hash(0x1) + Hash(0x4) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x1) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x2) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x3) + Hash(0x4), - Bytes("048071d3") + Hash(0x2) + Hash(0x4) + Hash(0x4), - ] - tx_gas = [9437184] + post: dict = {} + if constructor == 4: + # The create succeeds with an empty return buffer, so the + # forced RETURNDATACOPY of 32 bytes aborts the whole dispatcher + # frame: every sentinel survives and nothing was created. + post[contract_0] = Account( + storage={ + 16: contract_1, + 18: contract_1, + 19: contract_1, + 20: contract_1, + 21: contract_1, + 32: contract_1, + 33: contract_1, + }, + ) + post[contract_1] = Account(storage={}) + post[created] = Account.NONEXISTENT + elif constructor == 0: + # Successful creation and a follow-up call to the new contract, + # which calls contract_1. Every sentinel is overwritten (the + # zero results are observable), and only the non-static call + # kinds let contract_1 store its own address. + post[contract_0] = Account( + storage={32: CREATE_PC, 33: CALL_PC}, + ) + post[contract_1] = Account( + storage={} if call_kind == 4 else {0: contract_1}, + ) + post[created] = Account(code=bytes(contract_code), nonce=1, storage={}) + else: + # No follow-up call: slots 20/21 keep their sentinels, and the + # dispatcher records the code/length differences against the + # created (or never-created) account's empty ext code. + storage = { + 18: len(bytes(contract_code)), + 19: contract_code_word, + 20: contract_1, + 21: contract_1, + 32: CREATE_PC, + 33: CALL_PC, + } + if constructor == 3: + # The constructor reverted 32 bytes holding contract_1's + # address; the dispatcher copied them out. + storage[16] = 32 + storage[17] = contract_1 + post[contract_0] = Account(storage=storage) + post[contract_1] = Account(storage={}) + if constructor == 5: + # Empty deploy: the account exists with no code. + post[created] = Account(code=b"", nonce=1, storage={}) + else: + # OOG (1), reverts (2, 3), and an in-init SELFDESTRUCT (6, + # destroyed in its creation transaction per EIP-6780). + post[created] = Account.NONEXISTENT tx = Transaction( sender=sender, to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=Bytes("048071d3") + + Hash(creation) + + Hash(call_kind) + + Hash(constructor), + gas_limit=TX_GAS, ) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py index e49cd593e8f..c697e7352be 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty2.py @@ -1,134 +1,96 @@ """ -Test_transaction_collision_to_empty2. +Verify a contract-creation transaction targeting an address that holds +only a balance: the prefund is not a collision, so creation proceeds and +the budget alone decides whether the init code completes. Ported from: state_tests/stCreateTest/TransactionCollisionToEmpty2Filler.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(intrinsic + init code cost, success arm exact), pinning that a prefunded +create target incurs no EIP-8037 top-frame new-account state gas. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PREFUND = 10 + @pytest.mark.ported_from( ["state_tests/stCreateTest/TransactionCollisionToEmpty2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize("oog", [False, True], ids=["enough-gas", "oog"]) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) def test_transaction_collision_to_empty2( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oog: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Prefunded create target is no collision; budget decides the rest.""" + # Init code: one cold zero->non-zero store, deploys nothing. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + # The prefunded target is not EMPTY_ACCOUNT in the pre-state, so + # EIP-8037 charges no top-frame new-account state gas: the exact + # success budget below would OOG if it were charged. + success_gas = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, + ) + initcode.gas_cost(fork) + # The OOG arm misses half the store's cost rather than one gas: the + # intrinsic calculator over-estimates by the initcode word cost on + # pre-Shanghai forks, so a one-gas boundary is not portable. + gas_limit = success_gas + if oog: + gas_limit -= initcode.gas_cost(fork) // 2 - pre[sender] = Account(balance=0xE8D4A51000) - pre[contract_0] = Account(balance=10) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": 0}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={1: 1}, balance=10, nonce=1), - }, - }, - { - "indexes": {"data": -1, "gas": 0, "value": 1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={1: 1}, balance=11, nonce=1), - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(storage={}, balance=10, nonce=0), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre.fund_address(created, PREFUND) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + if oog: + # Creation rolled back: prefund kept, no value, nonce untouched. + created_account = Account( + storage={}, code=b"", nonce=0, balance=PREFUND + ) + else: + created_account = Account( + storage={1: 1}, code=b"", nonce=1, balance=PREFUND + tx_value + ) + + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py index 468a026fc98..ff51bba2eee 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_code.py @@ -1,149 +1,99 @@ """ -Test_transaction_collision_to_empty_but_code. +Verify a contract-creation transaction whose target address already holds +code: the collision aborts the creation, consumes the whole gas limit, +transfers no value, and leaves the existing account untouched. Ported from: state_tests/stCreateTest/TransactionCollisionToEmptyButCodeFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(bare intrinsic and a fully-funded creation); the post asserts the +colliding account's code, nonce, and unchanged zero balance. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Header, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Any non-empty code at the target address triggers the collision. +COLLIDING_CODE = bytes.fromhex("1122334455") + @pytest.mark.ported_from( ["state_tests/stCreateTest/TransactionCollisionToEmptyButCodeFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], + "full_budget", [True, False], ids=["full-budget", "intrinsic-only"] ) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) @pytest.mark.pre_alloc_mutable def test_transaction_collision_to_empty_but_code( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_budget: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty_but_code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Creation collision with code burns the whole gas limit.""" + # Init code that would store a flag if it ever ran. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, ) + if full_budget: + # Enough to fund the whole creation (even at the fresh-target + # EIP-8037 price) — the collision must still consume all of it. + gas_limit = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + ) + else: + gas_limit = intrinsic - pre[sender] = Account(balance=0xE8D4A51000) - # Source: raw - # 0x1122334455 - contract_0 = pre.deploy_contract( # noqa: F841 - code=bytes.fromhex("1122334455"), - nonce=0, - address=Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account( - storage={1: 0}, - code=bytes.fromhex("1122334455"), - nonce=0, - ), - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account( - storage={}, - code=bytes.fromhex("1122334455"), - nonce=0, - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre[created] = Account(code=COLLIDING_CODE) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) + post = { + sender: Account(nonce=1), + # The colliding account is untouched: the init code never ran and + # the transferred value never arrived. + created: Account(storage={}, code=COLLIDING_CODE, nonce=0, balance=0), + } + state_test( - env=env, pre=pre, post=post, tx=tx, - blockchain_test_header_verify=Header( - gas_used=tx_gas[g], - ), + blockchain_test_header_verify=Header(gas_used=gas_limit), ) diff --git a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py index 14a3c066470..a4b4c40e06a 100644 --- a/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py +++ b/tests/ported_static/stCreateTest/test_transaction_collision_to_empty_but_nonce.py @@ -1,22 +1,26 @@ """ -Test_transaction_collision_to_empty_but_nonce. +Verify a contract-creation transaction whose target address already has a +non-zero nonce: the collision aborts the creation, consumes the whole gas +limit, transfers no value, and leaves the existing account untouched. Ported from: state_tests/stCreateTest/TransactionCollisionToEmptyButNonceFiller.json + +@manually-enhanced: Do not overwrite. Budgets are derived from the fork +(bare intrinsic and a fully-funded creation); the post asserts the +colliding account's empty code, nonce, and unchanged zero balance. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Header, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" @@ -28,89 +32,67 @@ "state_tests/stCreateTest/TransactionCollisionToEmptyButNonceFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="-g1-v1", - ), - ], + "full_budget", [True, False], ids=["full-budget", "intrinsic-only"] ) +@pytest.mark.parametrize("tx_value", [0, 1], ids=["v0", "v1"]) @pytest.mark.pre_alloc_mutable def test_transaction_collision_to_empty_but_nonce( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_budget: bool, + tx_value: int, ) -> None: - """Test_transaction_collision_to_empty_but_nonce.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Creation collision with a nonce burns the whole gas limit.""" + # Init code that would store a flag if it ever ran. + initcode = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + sends_value=tx_value > 0, ) + if full_budget: + # Enough to fund the whole creation (even at the fresh-target + # EIP-8037 price) — the collision must still consume all of it. + gas_limit = ( + intrinsic + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) + ) + else: + gas_limit = intrinsic - pre[sender] = Account(balance=0xE8D4A51000) - pre[contract_0] = Account(balance=0, nonce=1) - - tx_data = [ - Op.SSTORE(key=0x1, value=0x1), - ] - tx_gas = [600000, 54000] - tx_value = [0, 1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre[created] = Account(nonce=1) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], + data=initcode, + gas_limit=gas_limit, + value=tx_value, ) post = { sender: Account(nonce=1), - contract_0: Account(storage={1: 0}, nonce=1), + # The colliding account is untouched: the init code never ran and + # the transferred value never arrived. + created: Account(storage={}, code=b"", nonce=1, balance=0), } state_test( - env=env, pre=pre, post=post, tx=tx, - blockchain_test_header_verify=Header( - gas_used=tx_gas[g], - ), + blockchain_test_header_verify=Header(gas_used=gas_limit), ) diff --git a/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py b/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py index 0bcf52c7d85..47a20e3a725 100644 --- a/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py +++ b/tests/ported_static/stEIP150Specific/test_create_and_gas_inside_create.py @@ -1,17 +1,23 @@ """ -Test_create_and_gas_inside_create. +Verify the gas a CREATE's init code observes: the child receives all but +one 64th of what remains in the creating frame, and the parent's CREATE +cost is measured alongside it. Ported from: state_tests/stEIP150Specific/CreateAndGasInsideCreateFiller.json + +@manually-enhanced: Do not overwrite. An outer call pins the creating +frame's budget so the child's stored GAS observation is fork-derived +(`63/64` of the derived base); the parent measures the CREATE with +CodeGasMeasure instead of raw snapshots. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,58 +27,105 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0xB +GAS_SLOT = 0x9 +CHILD_GAS_SLOT = 0xFD + +# The creating frame's pinned budget (the ported transaction's). +CALLER_GAS = 600_000 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/CreateAndGasInsideCreateFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_and_gas_inside_create( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_and_gas_inside_create.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes into its own storage + # and deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The child bytes sit right-aligned in the first memory word. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, + ) + creator = pre.deploy_contract( + code=setup + + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), ) - # Source: lll - # { [100] (GAS) (MSTORE 0 0x5a60fd55) (SSTORE 11 (CREATE 0 28 4)) (SSTORE 9 (SUB @100 (GAS))) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x64, value=Op.GAS) - + Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.SSTORE(key=0xB, value=Op.CREATE(value=0x0, offset=0x1C, size=0x4)) - + Op.SSTORE(key=0x9, value=Op.SUB(Op.MLOAD(offset=0x64), Op.GAS)) + # The outer call pins the creating frame's budget so the child's + # observation does not depend on the tx gas limit. + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=creator)) + Op.STOP, - nonce=0, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, + ) + + # The child receives all but one 64th of what remains after the + # setup, the measuring GAS read, and the CREATE's own charges (its + # new-account state gas is taken before the withhold). + base = ( + CALLER_GAS + - setup.gas_cost(fork) + - Op.GAS.gas_cost(fork) + - create_code.gas_cost(fork) ) + assert base > 0, "CALLER_GAS must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) + measured_create = create_store.gas_cost(fork) + child_code.gas_cost(fork) + created = compute_create_address(address=creator, nonce=1) post = { - contract_0: Account( + entry: Account(storage={0: 1}), + creator: Account( storage={ - 9: 0x129DB, - 11: compute_create_address(address=contract_0, nonce=0), + ADDRESS_SLOT: created, + GAS_SLOT: measured_create, }, ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={253: 0x83729} + created: Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py b/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py index 5156516411b..89d33ae0248 100644 --- a/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py +++ b/tests/ported_static/stEIP150Specific/test_delegate_call_on_eip.py @@ -1,17 +1,23 @@ """ -Test_delegate_call_on_eip. +Measure a DELEGATECALL that asks for more gas than its frame holds: the +EIP-150 clamp decides the grant, the delegate writes into the caller's +storage, and the measured cost is the call plus the delegate's work. Ported from: state_tests/stEIP150Specific/DelegateCallOnEIPFiller.json + +@manually-enhanced: Do not overwrite. An outer call pins the frame budget +so the oversized ask always clamps; the DELEGATECALL is measured with +CodeGasMeasure (success flag inside the window) and the expectation is the +composite plus the delegate's fork-priced store. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + CodeGasMeasure, + Fork, StateTestFiller, Transaction, ) @@ -20,62 +26,79 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +DELEGATE_VALUE = 0x12 +FLAG_SLOT = 0x9 +GAS_SLOT = 0x8 + +# The ported ask (600000): above the pinned frame budget, so the EIP-150 +# clamp decides the grant on every fork. +ASK_GAS = 0x927C0 +CALLER_GAS = 400_000 + @pytest.mark.ported_from( ["state_tests/stEIP150Specific/DelegateCallOnEIPFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_delegate_call_on_eip( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_delegate_call_on_eip.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Measure a clamped DELEGATECALL running a store in the caller.""" + # Runs in the caller's storage context: one cold fresh store. + delegate_store = Op.SSTORE( + key=0x0, + value=DELEGATE_VALUE, + key_warm=False, + original_value=0, + new_value=DELEGATE_VALUE, ) + delegate = pre.deploy_contract(code=delegate_store + Op.STOP) - # Source: lll - # { (SSTORE 0 0x12) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x12) + Op.STOP, - nonce=0, + delegatecall_code = Op.DELEGATECALL( + gas=ASK_GAS, + address=delegate, + address_warm=False, ) - # Source: lll - # { [8] (GAS) (SSTORE 9 (DELEGATECALL 600000 0 0 0 0)) [[8]] (SUB @8 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.DELEGATECALL( - gas=0x927C0, - address=addr, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x8, value=Op.SUB(Op.MLOAD(offset=0x8), Op.GAS)) + flag_store = Op.SSTORE( + key=FLAG_SLOT, + value=delegatecall_code, + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=CodeGasMeasure( + code=flag_store, + extra_stack_items=0, + sstore_key=GAS_SLOT, + ), + ) + + assert CALLER_GAS < ASK_GAS, "the 63/64 clamp must apply" + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=target)) + Op.STOP, - nonce=0, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, ) - post = {target: Account(storage={0: 18, 8: 46841, 9: 1})} + measured = flag_store.gas_cost(fork) + delegate_store.gas_cost(fork) + + post = { + entry: Account(storage={0: 1}), + target: Account( + storage={ + 0: DELEGATE_VALUE, + GAS_SLOT: measured, + FLAG_SLOT: 1, + }, + ), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py index f346844b949..078fb6506fe 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_contract_creation.py @@ -1,150 +1,132 @@ """ -Test_out_of_gas_contract_creation. +Verify a contract-creation transaction whose init code runs out of gas (or +halts on invalid code) leaves no account behind, while a sufficient budget +creates it. Ported from: state_tests/stInitCodeTest/OutOfGasContractCreationFiller.json + +@manually-enhanced: Do not overwrite. Both transaction budgets are derived +from the fork (intrinsic + the created account's top-frame state gas + the +init code's metadata-priced cost), so the insufficient arm keeps running +out mid-init-code and the sufficient arm keeps succeeding on every fork; +the success post pins the final storage value, not just the nonce. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +def storage_writes_initcode() -> Bytecode: + """Six stores to one slot: one cold set, then five dirty warm writes.""" + code = Op.SSTORE( + key=0x1, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + for value in range(2, 7): + code += Op.SSTORE( + key=0x1, + value=value, + key_warm=True, + original_value=0, + current_value=value - 1, + new_value=value, + ) + return code + + +def stack_underflow_initcode() -> Bytecode: + """The ported junk init code: CALLCODE underflows the stack.""" + return ( + Op.PUSH1[0xA] + + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) + + Op.PUSH1[0x0] + + Op.CALLCODE + + Op.STOP + + Op.PUSH1[0x1] + + Op.PUSH1[0x0] + + Op.BYTE(Op.DUP2, Op.CALLDATALOAD(offset=Op.DUP1)) + + Op.DUP2 + + Op.STOP + ) + + @pytest.mark.ported_from( ["state_tests/stInitCodeTest/OutOfGasContractCreationFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "invalid_initcode", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1", - ), + pytest.param(True, id="d0"), + pytest.param(False, id="d1"), + ], +) +@pytest.mark.parametrize( + "enough_gas", + [ + pytest.param(False, id="g0"), + pytest.param(True, id="g1"), ], ) def test_out_of_gas_contract_creation( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + invalid_initcode: bool, + enough_gas: bool, ) -> None: - """Test_out_of_gas_contract_creation.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa( - amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # noqa: E501 - ) + """An under-budgeted or invalid init code creates no account.""" + if invalid_initcode: + initcode = stack_underflow_initcode() + else: + initcode = storage_writes_initcode() - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000000000, + # The insufficient budget runs out midway through the init code; the + # sufficient one covers it with margin. EIP-8037 charges the created + # account's state gas to the creation transaction's top frame. + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + fork.transaction_top_frame_state_gas(contract_creation=True) + # The sufficient margin must exceed the EIP-2200 stipend (2300), or + # the final SSTOREs of the init code fail their minimum-gas check. + initcode_cost = storage_writes_initcode().gas_cost(fork) + gas_limit = overhead + ( + initcode_cost + 5_000 if enough_gas else initcode_cost // 2 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address(address=sender, nonce=0): Account( - nonce=1 - ), - }, - }, - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - compute_create_address( - address=sender, nonce=0 - ): Account.NONEXISTENT, - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.PUSH1[0xA] - + Op.CODECOPY(dest_offset=0x0, offset=0xC, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.CALLCODE - + Op.STOP - + Op.PUSH1[0x1] - + Op.PUSH1[0x0] - + Op.BYTE(Op.DUP2, Op.CALLDATALOAD(offset=Op.DUP1)) - + Op.DUP2 - + Op.STOP, - Op.SSTORE(key=0x1, value=0x1) - + Op.SSTORE(key=0x1, value=0x2) - + Op.SSTORE(key=0x1, value=0x3) - + Op.SSTORE(key=0x1, value=0x4) - + Op.SSTORE(key=0x1, value=0x5) - + Op.SSTORE(key=0x1, value=0x6), - ] - tx_gas = [56000, 150000] - tx_value = [1] - + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=1, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = compute_create_address(address=sender, nonce=0) + if enough_gas and not invalid_initcode: + created_account: Account | None = Account( + nonce=1, code=b"", storage={1: 6}, balance=1 + ) + else: + # OOG / invalid init code: the creation is rolled back entirely. + created_account = Account.NONEXISTENT + post = { + sender: Account(nonce=1), + created: created_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py index 93f7f151c9e..c72984e1633 100644 --- a/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py +++ b/tests/ported_static/stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py @@ -1,137 +1,168 @@ """ -Test_out_of_gas_prefunded_contract_creation. +Verify a contract-creation transaction targeting a prefunded address, whose +init code CREATEs a value-bearing child: the budget decides whether the +outer creation fails (prefund untouched), the child fails (value stays), +or the child succeeds (one wei moves into it). Ported from: state_tests/stInitCodeTest/OutOfGasPrefundedContractCreationFiller.json + +@manually-enhanced: Do not overwrite. All three budgets are derived from +the fork (intrinsic + top-frame state gas + the composed init/child code +costs), and the child account is asserted, disambiguating the ported +"balance 1" outcomes (outer-failure vs child-success) that were previously +indistinguishable. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PREFUND = 1 +TX_VALUE = 1 +CHILD_VALUE = 1 +CHILD_STORED = 0x112233 + @pytest.mark.ported_from( [ "state_tests/stInitCodeTest/OutOfGasPrefundedContractCreationFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "outcome", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - pytest.param( - 0, - 2, - 0, - id="-g2", - ), + pytest.param("child_succeeds", id="g0"), + pytest.param("outer_oog", id="g1"), + pytest.param("child_oog", id="g2"), ], ) -@pytest.mark.pre_alloc_mutable def test_out_of_gas_prefunded_contract_creation( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + outcome: str, ) -> None: - """Test_out_of_gas_prefunded_contract_creation.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Budget decides how deep a prefunded creation's child CREATE gets.""" + # Child init code: one cold store, deposits nothing. + child_code = ( + Op.SSTORE( + key=0x0, + value=CHILD_STORED, + key_warm=False, + original_value=0, + new_value=CHILD_STORED, + ) + + Op.STOP * 2 ) + child_bytes = bytes(child_code) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000000, + # Outer init code: copy the child init code from its own tail, then + # CREATE a value-bearing child from it; deposits nothing. The copy + # window and memory usage stay within one word. + inner_create = Op.CREATE( + value=CHILD_VALUE, + offset=0x0, + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), ) - - pre[sender] = Account(balance=0xF424000) - # Source: hex - # 0x - contract_0 = pre.deploy_contract( # noqa: F841 - code="", - balance=1, - nonce=0, - address=Address(0x6295EE1B4F6DD65047762F924ECD367C17EABF8F), # noqa: E501 + prefix = Op.CODECOPY( + dest_offset=0x0, + offset=0x1A, # placeholder; recomputed below + size=len(child_bytes), + data_size=len(child_bytes), + new_memory_size=0x20, ) + body = prefix + Op.POP(inner_create) + Op.STOP + # The child code sits immediately after the executable body. + initcode_prefix_len = len(bytes(body)) + prefix = Op.CODECOPY( + dest_offset=0x0, + offset=initcode_prefix_len, + size=len(child_bytes), + data_size=len(child_bytes), + new_memory_size=0x20, + ) + body = prefix + Op.POP(inner_create) + Op.STOP + assert len(bytes(body)) == initcode_prefix_len, "stable code layout" + initcode = body + child_code - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": [0, 1], "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(balance=1), - }, - }, - { - "indexes": {"data": -1, "gas": [2], "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - contract_0: Account(balance=2), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + # Fork-derived budgets. The prefunded target is not EMPTY_ACCOUNT in + # the pre-state, so EIP-8037 charges no top-frame new-account state + # gas for this creation — an Amsterdam behavior this test pins. The + # inner CREATE's composite cost covers its peak charge (its + # new-account state gas is refunded if the child fails, but must be + # affordable when charged). + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + ) + + prefix.gas_cost(fork) + + inner_create.gas_cost(fork) + ) + child_needed = child_code.gas_cost(fork) + if outcome == "outer_oog": + # Dies charging the inner CREATE. + gas_limit = overhead - inner_create.gas_cost(fork) // 2 + elif outcome == "child_oog": + # Outer completes; the child's 63/64 grant undercuts its cost. + gas_limit = overhead + child_needed // 2 + else: + # Child completes too and keeps the transferred wei. + gas_limit = overhead + -(-child_needed * 64 // 63) + 2_000 - tx_data = [ - Op.PUSH1[0x9] - + Op.CODECOPY(dest_offset=0x0, offset=0x11, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.POP(Op.CREATE) - + Op.STOP * 2 - + Op.INVALID - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP * 2, - ] - tx_gas = [154000, 65000, 95000] - tx_value = [1] + sender = pre.fund_eoa() + created = compute_create_address(address=sender, nonce=0) + pre.fund_address(created, PREFUND) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + child = compute_create_address(address=created, nonce=1) + if outcome == "outer_oog": + # Creation rolled back: only the prefund remains, nonce untouched. + created_account = Account(nonce=0, balance=PREFUND) + child_account: Account | None = Account.NONEXISTENT + elif outcome == "child_oog": + # The inner CREATE increments the creator's nonce even when the + # child fails. + created_account = Account( + nonce=2, code=b"", balance=PREFUND + TX_VALUE + ) + child_account = Account.NONEXISTENT + else: + created_account = Account( + nonce=2, code=b"", balance=PREFUND + TX_VALUE - CHILD_VALUE + ) + child_account = Account( + nonce=1, + balance=CHILD_VALUE, + storage={0: CHILD_STORED}, + ) + + post = { + sender: Account(nonce=1), + created: created_account, + child: child_account, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py index 5f31ce42a8a..5163dea2f5e 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py @@ -1,17 +1,23 @@ """ -Test_call_goes_oog_on_second_level_with_mem_expanding_calls. +Verify a two-level call chain (with memory-expanding call windows) where +the second-level frame runs out of gas: its own frame and everything below +it revert, while the top frame survives and records the failure. Ported from: state_tests/stMemExpandingEIP150Calls/CallGoesOOGOnSecondLevelWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The first-level budget is pinned and +derived from the fork so the second level keeps starving on every fork +(its 1/64 retention cannot afford the post-call store); the second-level +ask stays oversized; the top frame's entry snapshot is derived and pins +the transaction intrinsic. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,88 +26,142 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +SNAPSHOT_SLOT = 0x8 +FLAG_SLOT = 0x9 +# The ported second-level ask: far above the pinned budget. +ASK_GAS = 0x927C0 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CallGoesOOGOnSecondLevelWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_goes_oog_on_second_level_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_goes_oog_on_second_level_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + """A starved second-level frame reverts itself and everything below.""" + # Deepest contract: snapshots and creates twice; its cost anchors the + # starvation budget. + deep_snapshot = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, ) - - # Source: hex - # 0x5a600855600060006000f050600060006000f0505a6009555a600a55 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + deep = pre.deploy_contract( + code=deep_snapshot + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) * 2 - + Op.SSTORE(key=0x9, value=Op.GAS) + + Op.SSTORE(key=FLAG_SLOT, value=Op.GAS) + Op.SSTORE(key=0xA, value=Op.GAS), - nonce=0, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + + # Second level: snapshots, then asks far more than it holds; the ported + # memory-expanding argument window is kept. + mid = pre.deploy_contract( + code=Op.SSTORE(key=SNAPSHOT_SLOT, value=Op.GAS) + Op.SSTORE( - key=0x9, + key=FLAG_SLOT, value=Op.CALL( - gas=0x927C0, - address=addr, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=ASK_GAS, + address=deep, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, ), ), - nonce=0, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr_2, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, - ), + + # Pin the second level's budget so it starves on every fork: enough + # to pay its own snapshot and call, but its grant to the deep frame + # undercuts the deep frame's first store, and its 1/64 retention + # cannot afford its own post-call flag store. + mid_snapshot_cost = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ).gas_cost(fork) + mid_call_cost = Op.CALL( + gas=ASK_GAS, + address=deep, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, + ).gas_cost(fork) + deep_needed = deep_snapshot.gas_cost(fork) + caller_gas = mid_snapshot_cost + mid_call_cost + deep_needed // 2 + assert caller_gas < ASK_GAS, "the second-level ask must exceed its frame" + + # Top frame: derived entry snapshot (pins the intrinsic), the pinned + # call, and the failure flag. + entry_snapshot = Op.SSTORE( + key=SNAPSHOT_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + flag_store = Op.SSTORE( + key=FLAG_SLOT, + value=Op.CALL( + gas=caller_gas, + address=mid, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_offset=MEM_OFFSET, + ret_size=MEM_SIZE, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, ), - nonce=0, + key_warm=False, + original_value=0, + new_value=0, + ) + target = pre.deploy_contract( + code=entry_snapshot + flag_store + Op.STOP, + ) + + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = ( + intrinsic + + entry_snapshot.gas_cost(fork) + + flag_store.gas_cost(fork) + + caller_gas + + 5_000 ) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=220000, + gas_limit=gas_limit, ) post = { - sender: Account(nonce=1), - target: Account(storage={8: 0x30956}), - addr_2: Account(storage={}), - addr: Account(storage={}), + # The failed call's flag slot stays zero; the entry snapshot pins + # the intrinsic. + target: Account( + storage={ + SNAPSHOT_SLOT: gas_limit - intrinsic - Op.GAS.gas_cost(fork), + }, + ), + # Both lower frames reverted entirely. + mid: Account(storage={}), + deep: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py index 705e9e760f9..74f0be4227b 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py @@ -1,17 +1,22 @@ """ -Test_create_and_gas_inside_create_with_mem_expanding_calls. +Verify the gas a CREATE's init code observes when the creating frame also +expands memory: the child receives all but one 64th of what remains, and +the creating frame's entry and post-CREATE gas readings are asserted. Ported from: state_tests/stMemExpandingEIP150Calls/CreateAndGasInsideCreateWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The ported bytecode is kept, but the +transaction budget and every stored gas reading (entry snapshot, child +observation, post-CREATE reading) are derived from the fork instead of +pinned — the entry snapshot doubles as a transaction-intrinsic pin. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, compute_create_address, @@ -21,62 +26,129 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ENTRY_GAS_SLOT = 0xA +ADDRESS_SLOT = 0xB +AFTER_GAS_SLOT = 0x9 +CHILD_GAS_SLOT = 0xFD + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CreateAndGasInsideCreateWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_create_and_gas_inside_create_with_mem_expanding_calls( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_create_and_gas_inside_create_with_mem_expanding_calls.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A CREATE's init code observes 63/64 of the creating frame's gas.""" + # Child init code: stores the gas it observes, deposits no code. + child_code = Op.SSTORE( + key=CHILD_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, ) + child_bytes = bytes(child_code) - # Source: hex - # 0x5a600a55635a60fd556000526004601c6000f0600b555a600955 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0xA, value=Op.GAS) - + Op.MSTORE(offset=0x0, value=0x5A60FD55) - + Op.SSTORE(key=0xB, value=Op.CREATE(value=0x0, offset=0x1C, size=0x4)) - + Op.SSTORE(key=0x9, value=Op.GAS), - nonce=0, + entry_snapshot = Op.SSTORE( + key=ENTRY_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(child_bytes, "big"), + new_memory_size=0x20, + ) + create_code = Op.CREATE( + value=0x0, + offset=0x20 - len(child_bytes), + size=len(child_bytes), + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(child_bytes), + ) + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, + ) + after_snapshot = Op.SSTORE( + key=AFTER_GAS_SLOT, + value=Op.GAS, + key_warm=False, + original_value=0, + new_value=1, + ) + creator = pre.deploy_contract( + code=entry_snapshot + setup + create_store + after_snapshot + Op.STOP, ) + # Fork-derived budget: the ported 600000 no longer covers the three + # state-priced stores plus the CREATE under EIP-8037. The margin + # keeps the final store above the EIP-2200 stipend. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + tx_gas = ( + intrinsic + + entry_snapshot.gas_cost(fork) + + setup.gas_cost(fork) + + create_store.gas_cost(fork) + + child_code.gas_cost(fork) + + after_snapshot.gas_cost(fork) + + 5_000 + ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=creator, + gas_limit=tx_gas, ) + # Entry reading: everything after the intrinsic, minus the GAS opcode + # itself (it executes first in the store's operand order). + entry_observed = tx_gas - intrinsic - Op.GAS.gas_cost(fork) + # The child receives all but one 64th of what remains after the entry + # store, the setup, and the CREATE's own charges. + base = ( + tx_gas + - intrinsic + - entry_snapshot.gas_cost(fork) + - setup.gas_cost(fork) + - create_code.gas_cost(fork) + ) + assert base > 0, "the budget must cover the CREATE's charges" + child_observed = (base - base // 64) - Op.GAS.gas_cost(fork) + # After the CREATE: the child's consumption and the address store are + # gone; the address store's own cost is the composite minus the + # CREATE it wraps. + after_observed = ( + base + - child_code.gas_cost(fork) + - (create_store.gas_cost(fork) - create_code.gas_cost(fork)) + - Op.GAS.gas_cost(fork) + ) + + created = compute_create_address(address=creator, nonce=1) post = { - sender: Account(nonce=1), - contract_0: Account( + creator: Account( storage={ - 9: 0x75596, - 10: 0x8D5B6, - 11: compute_create_address(address=contract_0, nonce=0), + ENTRY_GAS_SLOT: entry_observed, + ADDRESS_SLOT: created, + AFTER_GAS_SLOT: after_observed, }, - nonce=1, ), - compute_create_address(address=contract_0, nonce=0): Account( - storage={253: 0x7E23D} + created: Account( + nonce=1, + code=b"", + storage={CHILD_GAS_SLOT: child_observed}, ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py b/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py index d426d271f6e..95c600b91ce 100644 --- a/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py +++ b/tests/ported_static/stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py @@ -1,106 +1,198 @@ """ -Test_static_create_empty_contract_and_call_it_0wei. +Measure CREATE of a codeless contract (optionally writing storage in its +init code) followed by a STATICCALL to it, via CodeGasMeasure. Ported from: state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json +state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json + +@manually-enhanced: Do not overwrite. Two fillers folded into one +parametrize; the storage-writing init code is composed (not hex blobs) so +the measured CREATE/STATICCALL expectations derive from the same bytecode; +the init code's inner CALL forwards all gas (the ported 0xEA60 budget OOGs +under EIP-8037); the STATICCALL success flag stays inside the measured +window. Replaces the prior EIP-8037 expect-any band-aid with fork-derived +gas assertions. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, + CodeGasMeasure, + Fork, StateTestFiller, - Storage, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +ADDRESS_SLOT = 0x1 +CREATE_GAS_SLOT = 0x2 +STATICCALL_FLAG_SLOT = 0x3 +STATICCALL_GAS_SLOT = 0x64 +STORED_VALUE = 0xC + +FORWARDED_GAS = 0xEA60 + @pytest.mark.ported_from( [ - "state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json" # noqa: E501 + "state_tests/stStaticCall/static_CREATE_EmptyContractAndCallIt_0weiFiller.json", # noqa: E501 + "state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json", # noqa: E501 + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "with_storage", + [ + pytest.param(False, id="empty_contract"), + pytest.param(True, id="with_storage"), ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable def test_static_create_empty_contract_and_call_it_0wei( state_test: StateTestFiller, pre: Alloc, fork: Fork, + with_storage: bool, ) -> None: - """Test_static_create_empty_contract_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """Measure CREATE and STATICCALL gas for a created codeless account.""" + if with_storage: + # Called by the init code below; writes one cold fresh slot. + writer_store = Op.SSTORE( + key=0x1, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) + + # The init code writes the created account's own slot 0 and calls + # the writer, then runs off its end (STOP) so no code is deposited. + # The inner CALL forwards all remaining gas (default Op.GAS). + initcode = Op.SSTORE( + key=0x0, + value=STORED_VALUE, + key_warm=False, + original_value=0, + new_value=STORED_VALUE, + ) + Op.CALL( + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, + ) + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x40, "init code must fit two words" - # Source: lll - # { [[0]](GAS) [[1]] (CREATE 0 0 32) [[2]](GAS) [[3]] (STATICCALL 60000 (SLOAD 1) 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x20)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.STATICCALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + # Memory is populated (and expanded to 0x40) before the measured + # window, so the CREATE itself expands nothing. + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes( + initcode_bytes[:0x20].ljust(0x20, b"\x00"), "big" ), + ) + Op.MSTORE( + offset=0x20, + value=int.from_bytes( + initcode_bytes[0x20:].ljust(0x20, b"\x00"), "big" + ), + ) + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=len(initcode_bytes), + new_memory_size=0x40, + old_memory_size=0x40, + init_code_size=len(initcode_bytes), ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # The measured CREATE includes the child's work: the init code's + # own consumption plus the writer's store it calls. + child_cost = initcode.gas_cost(fork) + writer_store.gas_cost(fork) + else: + # CREATE over never-written memory runs 32 zero bytes as init code + # (STOP on the first byte), depositing no code and consuming + # nothing; the memory expansion happens inside the window. + setup = Bytecode() + create_code = Op.CREATE( + value=0x0, + offset=0x0, + size=0x20, + new_memory_size=0x20, + init_code_size=0x20, + ) + child_cost = 0 + + # The created address is stored inside the measured window (as in the + # ported filler) so the STATICCALL can target it at runtime. + create_store = Op.SSTORE( + key=ADDRESS_SLOT, + value=create_code, + key_warm=False, + original_value=0, + new_value=1, ) - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, + # The created account exists (nonce 1) and is warm (CREATE accessed + # it); the success flag is stored inside the measured window — a + # wrongly failed STATICCALL would otherwise be unobservable. + staticcall_code = Op.STATICCALL( + gas=FORWARDED_GAS, + address=Op.SLOAD(key=ADDRESS_SLOT, key_warm=True), + address_warm=True, + ) + staticcall_store = Op.SSTORE( + key=STATICCALL_FLAG_SLOT, + value=staticcall_code, + key_warm=False, + original_value=0, + new_value=1, ) - if fork.is_eip_enabled(8037): - contract_0_storage = Storage.model_validate( - {1: compute_create_address(address=contract_0, nonce=0), 3: 1} - ) - contract_0_storage.set_expect_any(0) - contract_0_storage.set_expect_any(2) - contract_0_storage.set_expect_any(100) - else: - contract_0_storage = Storage.model_validate( - { - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x7ABF8, - 3: 1, - 100: 0x6FE6E, - } + contract = pre.deploy_contract( + code=setup + + CodeGasMeasure( + code=create_store, + extra_stack_items=0, + sstore_key=CREATE_GAS_SLOT, ) + + CodeGasMeasure( + code=staticcall_store, + extra_stack_items=0, + sstore_key=STATICCALL_GAS_SLOT, + ), + ) + + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + state_gas_reservoir=0, + ) + + measured_create = create_store.gas_cost(fork) + child_cost + measured_staticcall = staticcall_store.gas_cost(fork) + + created = compute_create_address(address=contract, nonce=1) post = { - contract_0: Account(storage=contract_0_storage), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), + contract: Account( + storage={ + ADDRESS_SLOT: created, + CREATE_GAS_SLOT: measured_create, + STATICCALL_FLAG_SLOT: 1, + STATICCALL_GAS_SLOT: measured_staticcall, + }, + ), + created: Account( + nonce=1, + storage={0: STORED_VALUE} if with_storage else {}, + ), } + if with_storage: + post[writer] = Account(storage={1: STORED_VALUE}) - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py b/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py deleted file mode 100644 index 91623e4efe9..00000000000 --- a/tests/ported_static/stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -Test_static_create_empty_contract_with_storage_and_call_it_0wei. - -Ported from: -state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Storage, - Transaction, - compute_create_address, -) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stStaticCall/static_CREATE_EmptyContractWithStorageAndCallIt_0weiFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow -@pytest.mark.pre_alloc_mutable -def test_static_create_empty_contract_with_storage_and_call_it_0wei( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, -) -> None: - """Test_static_create_empty_contract_with_storage_and_call_it_0wei.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[0]](GAS) (MSTORE 0 0x600c6000556000600060006000600073c94f5374fce5edbc8e2a8697c1533167) (MSTORE 32 0x7e6ebf0b61ea60f1000000000000000000000000000000000000000000000000) [[1]] (CREATE 0 0 64) [[2]] (GAS) [[3]] (STATICCALL 60000 (SLOAD 1) 0 0 0 0) [[100]] (GAS) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.GAS) - + Op.MSTORE( - offset=0x0, - value=0x600C6000556000600060006000600073C94F5374FCE5EDBC8E2A8697C1533167, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x7E6EBF0B61EA60F1000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.SSTORE(key=0x1, value=Op.CREATE(value=0x0, offset=0x0, size=0x40)) - + Op.SSTORE(key=0x2, value=Op.GAS) - + Op.SSTORE( - key=0x3, - value=Op.STATICCALL( - gas=0xEA60, - address=Op.SLOAD(key=0x1), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x64, value=Op.GAS) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - # Source: lll - # {[[1]]12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - balance=0xE8D4A51000, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=600000, - ) - - if fork.is_eip_enabled(8037): - contract_0_storage = Storage.model_validate( - {1: compute_create_address(address=contract_0, nonce=0), 3: 1} - ) - contract_0_storage.set_expect_any(0) - contract_0_storage.set_expect_any(2) - contract_0_storage.set_expect_any(100) - else: - contract_0_storage = Storage.model_validate( - { - 0: 0x8D5B6, - 1: compute_create_address(address=contract_0, nonce=0), - 2: 0x6F4F0, - 3: 1, - 100: 0x64766, - } - ) - post = { - contract_0: Account(storage=contract_0_storage), - compute_create_address(address=contract_0, nonce=0): Account(nonce=1), - contract_1: Account(storage={1: 12}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py b/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py index b5965a5da21..3ea3f5959b4 100644 --- a/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py +++ b/tests/ported_static/stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py @@ -1,162 +1,131 @@ """ -Test_static_execute_call_that_ask_fore_gas_then_trabsaction_has. +Verify a STATICCALL that asks for more gas than is available is clamped to +63/64 of the remaining gas (EIP-150), across callees that succeed, out-of-gas, +and violate the static context. Ported from: state_tests/stStaticCall/static_ExecuteCallThatAskForeGasThenTrabsactionHasFiller.json + +@manually-enhanced: Do not overwrite. An outer call caps the caller frame so +the callee budgets are fork-independent; the flag slot is pre-written so the +post-call store is a cheap dirty-warm write affordable from the 1/64 +retention even under EIP-8037; distinct flag values discriminate success, +callee failure, and caller OOG (the ported {1: 0} expectation could not). """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, - Hash, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x1 +# Pre-written sentinel: if the caller frame dies after the call, the slot +# keeps this value instead of reverting to an ambiguous zero. +FLAG_PREWRITE = 0xFF +# Stored flag = 0x10 + STATICCALL result: 0x11 success, 0x10 failure. +FLAG_BASE = 0x10 + +# Far larger than any gas the caller frame can hold, so the EIP-150 clamp +# (not the operand) decides what the callee receives. +OVERSIZED_GAS_ASK = 2**61 +# The outer call pins the caller frame's budget: large enough to cover the +# caller's own cold flag store (~111k under EIP-8037) and the successful +# callee, small enough that the looping callee (~6.5M) still runs out. +CALLER_GAS = 1_000_000 + @pytest.mark.ported_from( [ "state_tests/stStaticCall/static_ExecuteCallThatAskForeGasThenTrabsactionHasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.slow +@pytest.mark.valid_from("Byzantium") @pytest.mark.parametrize( - "d, g, v", + "callee_kind, callee_succeeds", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), + pytest.param("mstore", True, id="d0"), + pytest.param("extcodesize_loop", False, id="d1"), + pytest.param("sstore_static_violation", False, id="d2"), ], ) -@pytest.mark.pre_alloc_mutable def test_static_execute_call_that_ask_fore_gas_then_trabsaction_has( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + callee_kind: str, + callee_succeeds: bool, ) -> None: - """Test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0x989680) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) + """A STATICCALL asking for more gas than available gets 63/64 of it.""" + if callee_kind == "mstore": + # Trivial: succeeds well within the forwarded gas. + callee = pre.deploy_contract( + code=Op.MSTORE(offset=0x1, value=0x1) + Op.STOP + ) + elif callee_kind == "extcodesize_loop": + # 50000 EXTCODESIZE iterations (~6.5M gas): must exhaust the + # clamped forwarded gas, proving the callee did not receive the + # oversized ask. + callee = pre.deploy_contract( + code=Op.JUMPDEST + + Op.JUMPI( + pc=0x1C, + condition=Op.ISZERO(Op.LT(Op.MLOAD(offset=0x80), 0xC350)), + ) + + Op.POP(Op.EXTCODESIZE(address=0x1)) + + Op.MSTORE(offset=0x80, value=Op.ADD(Op.MLOAD(offset=0x80), 0x1)) + + Op.JUMP(pc=0x0) + + Op.JUMPDEST + + Op.STOP, + ) + else: + # SSTORE inside a static context: exceptional halt regardless of + # gas. + callee = pre.deploy_contract( + code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP + ) - # Source: lll - # { [[1]] (STATICCALL 600000 (CALLDATALOAD 0) 0 0 0 0) } - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x1, - value=Op.STATICCALL( - gas=0x927C0, - address=Op.CALLDATALOAD(offset=0x0), - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + # The flag slot is written twice: the pre-write pays the cold/state + # cost with the full budget, so the post-call store is a dirty-warm + # write the 1/64 retention can always afford. + caller = pre.deploy_contract( + code=Op.SSTORE(key=FLAG_SLOT, value=FLAG_PREWRITE) + + Op.SSTORE( + key=FLAG_SLOT, + value=Op.ADD( + FLAG_BASE, + Op.STATICCALL(gas=OVERSIZED_GAS_ASK, address=callee), ), ) + Op.STOP, - nonce=0, - address=Address(0xA256EBCC5536CDA56E04C39FE9584ECC7594A438), # noqa: E501 - ) - # Source: lll - # { (MSTORE 1 1) } - addr = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x3DC16A13CF554533F380CC938A2C1AB04DAC534F), # noqa: E501 ) - # Source: lll - # { (def 'i 0x80) (for {} (< @i 50000) [i](+ @i 1) (EXTCODESIZE 1)) } - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.JUMPDEST - + Op.JUMPI( - pc=0x1C, condition=Op.ISZERO(Op.LT(Op.MLOAD(offset=0x80), 0xC350)) - ) - + Op.POP(Op.EXTCODESIZE(address=0x1)) - + Op.MSTORE(offset=0x80, value=Op.ADD(Op.MLOAD(offset=0x80), 0x1)) - + Op.JUMP(pc=0x0) - + Op.JUMPDEST + + # The outer call pins the caller frame's gas so the callee budgets do + # not depend on the tx gas limit; the clamp must always bite. + assert CALLER_GAS < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + entry = pre.deploy_contract( + code=Op.SSTORE(key=0x0, value=Op.CALL(gas=CALLER_GAS, address=caller)) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x73EF1878A0F2C9629DEDC1B1E9BE8D77DCF93688), # noqa: E501 - ) - # Source: lll - # { (SSTORE 1 1) } - addr_3 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xCE4CCBFFAF450AE2126EB96DCD7C891F37764F20), # noqa: E501 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [1, 2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={1: 0})}, - }, - { - "indexes": {"data": [0], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={1: 1})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(addr, left_padding=True), - Hash(addr_2, left_padding=True), - Hash(addr_3, left_padding=True), - ] - tx_gas = [100000] - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + entry: Account(storage={0: 1}), + caller: Account( + storage={FLAG_SLOT: FLAG_BASE + (1 if callee_succeeds else 0)}, + ), + } + + state_test(pre=pre, post=post, tx=tx)