diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index f6d3f6852e..0d0404b2a0 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 deleted file mode 100644 index 7aba6f38bb..0000000000 --- a/tests/ported_static/amsterdam_skip_list.txt +++ /dev/null @@ -1,208 +0,0 @@ -# Amsterdam ported static skip list. -# -# Test cases in this list are temporarily skipped for the Amsterdam -# fork due to EIP-8037's two-dimensional gas model. Gas limits in the -# underlying ported static tests have not yet been updated to account -# for state gas. -# -# Entries are substring-matched against each pytest nodeid (after -# stripping the fixture-format suffix in conftest.py). -# -# Total entries: 153 - -# stAttackTest (1) -stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] - -# stBadOpcode (4) -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_measure_gas.py::test_measure_gas[fork_Amsterdam-CREATE] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE2] -stBadOpcode/test_operation_diff_gas.py::test_operation_diff_gas[fork_Amsterdam-CREATE] - -# stCallCodes (3) -stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d0] -stCallCodes/test_callcode_in_initcode_to_existing_contract.py::test_callcode_in_initcode_to_existing_contract[fork_Amsterdam-d1] -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/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g2] -stCallCreateCallCodeTest/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g3] -stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_callcode1024_oog.py::test_callcode1024_oog[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py::test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g0] -stCallCreateCallCodeTest/test_create_name_registrator_per_txs_not_enough_gas.py::test_create_name_registrator_per_txs_not_enough_gas[fork_Amsterdam--g1] -stCallCreateCallCodeTest/test_create_name_registrator_pre_store1_not_enough_gas.py::test_create_name_registrator_pre_store1_not_enough_gas[fork_Amsterdam] - -# stCallDelegateCodesCallCodeHomestead (1) -stCallDelegateCodesCallCodeHomestead/test_callcallcallcode_001_suicide_end.py::test_callcallcallcode_001_suicide_end[fork_Amsterdam] - -# stCreate2 (31) -stCreate2/test_create2_oo_gafter_init_code_revert2.py::test_create2_oo_gafter_init_code_revert2[fork_Amsterdam] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_CallCode_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create2_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_Create_Refund_NoOoG] -stCreate2/test_create2_oog_from_call_refunds.py::test_create2_oog_from_call_refunds[fork_Amsterdam-SStore_DelegateCall_Refund_NoOoG] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d0] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d1] -stCreate2/test_create2collision_selfdestructed_oog.py::test_create2collision_selfdestructed_oog[fork_Amsterdam-d2] -stCreate2/test_create2no_cash.py::test_create2no_cash[fork_Amsterdam-d1] -stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g0] -stCreate2/test_create_message_reverted_oog_in_init2.py::test_create_message_reverted_oog_in_init2[fork_Amsterdam--g1] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v0] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d0-g1-v1] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create2_oog.py::test_revert_depth_create2_oog[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d0-g1-v0] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d0-g1-v1] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create2_oog_berlin.py::test_revert_depth_create2_oog_berlin[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g0-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g0-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g0-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g0-v1] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d1-g1-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d0-g0-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d0-g0-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g0-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g0-v1] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v0] -stCreate2/test_revert_depth_create_address_collision_berlin.py::test_revert_depth_create_address_collision_berlin[fork_Amsterdam-d1-g1-v1] - -# stCreateTest (36) -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-0xef-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-contructor-revert-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-high-nonce-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-invalid-opcode-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-ok-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-oog-constructor-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create-oog-post-constr-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-0xef-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-contructor-revert-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-invalid-opcode-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-ok-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-oog-constructor-v1] -stCreateTest/test_create_address_warm_after_fail.py::test_create_address_warm_after_fail[fork_Amsterdam-create2-oog-post-constr-v1] -stCreateTest/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] -stDelegatecallTestHomestead/test_call1024_oog.py::test_call1024_oog[fork_Amsterdam--g1] -stDelegatecallTestHomestead/test_delegatecall1024_oog.py::test_delegatecall1024_oog[fork_Amsterdam] -stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py::test_delegatecall_in_initcode_to_existing_contract[fork_Amsterdam] - -# stEIP150Specific (7) -stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py::test_call_ask_more_gas_on_depth2_then_transaction_has[fork_Amsterdam] -stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] -stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] -stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64e0.py::test_transaction64_rule_d64e0[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64m1.py::test_transaction64_rule_d64m1[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64p1.py::test_transaction64_rule_d64p1[fork_Amsterdam] - -# stEIP150singleCodeGasPrices (2) -stEIP150singleCodeGasPrices/test_gas_cost.py::test_gas_cost[fork_Amsterdam-d40] -stEIP150singleCodeGasPrices/test_gas_cost_berlin.py::test_gas_cost_berlin[fork_Amsterdam-d40] - -# stEIP158Specific (1) -stEIP158Specific/test_exp_empty.py::test_exp_empty[fork_Amsterdam] - -# stHomesteadSpecific (1) -stHomesteadSpecific/test_contract_creation_oo_gdont_leave_empty_contract_via_transaction.py::test_contract_creation_oo_gdont_leave_empty_contract_via_transaction[fork_Amsterdam] - -# stInitCodeTest (7) -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d0-g1] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g0] -stInitCodeTest/test_out_of_gas_contract_creation.py::test_out_of_gas_contract_creation[fork_Amsterdam-d1-g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g0] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g1] -stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g2] - -# stMemExpandingEIP150Calls (4) -stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py::test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls[fork_Amsterdam] -stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] -stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] -stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] - -# stMemoryTest (2) -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success14] -stMemoryTest/test_oog.py::test_oog[fork_Amsterdam-success15] - -# stRefundTest (7) -stRefundTest/test_refund50_2.py::test_refund50_2[fork_Amsterdam] -stRefundTest/test_refund50percent_cap.py::test_refund50percent_cap[fork_Amsterdam] -stRefundTest/test_refund600.py::test_refund600[fork_Amsterdam] -stRefundTest/test_refund_call_a.py::test_refund_call_a[fork_Amsterdam] -stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d0] -stRefundTest/test_refund_suicide50procent_cap.py::test_refund_suicide50procent_cap[fork_Amsterdam-d1] -stRefundTest/test_refund_tx_to_suicide.py::test_refund_tx_to_suicide[fork_Amsterdam] - -# stRevertTest (12) -stRevertTest/test_loop_calls_depth_then_revert.py::test_loop_calls_depth_then_revert[fork_Amsterdam] -stRevertTest/test_loop_delegate_calls_depth_then_revert.py::test_loop_delegate_calls_depth_then_revert[fork_Amsterdam] -stRevertTest/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g1-v0] -stRevertTest/test_revert_depth_create_address_collision.py::test_revert_depth_create_address_collision[fork_Amsterdam-d0-g1-v1] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v0] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d0-g1-v1] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v0] -stRevertTest/test_revert_depth_create_oog.py::test_revert_depth_create_oog[fork_Amsterdam-d1-g1-v1] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d0-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d1-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d2-g0] -stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py::test_revert_opcode_in_calls_on_non_empty_return_data[fork_Amsterdam-d3-g0] - -# stSStoreTest (4) -stSStoreTest/test_sstore_gas.py::test_sstore_gas[fork_Amsterdam] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d2] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d5] -stSStoreTest/test_sstore_gas_left.py::test_sstore_gas_left[fork_Amsterdam-d8] - -# stSolidityTest (3) -stSolidityTest/test_recursive_create_contracts.py::test_recursive_create_contracts[fork_Amsterdam] -stSolidityTest/test_test_contract_interaction.py::test_test_contract_interaction[fork_Amsterdam] -stSolidityTest/test_test_contract_suicide.py::test_test_contract_suicide[fork_Amsterdam] - -# stStaticCall (3) -stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py::test_static_create_empty_contract_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_create_empty_contract_with_storage_and_call_it_0wei.py::test_static_create_empty_contract_with_storage_and_call_it_0wei[fork_Amsterdam] -stStaticCall/test_static_execute_call_that_ask_fore_gas_then_trabsaction_has.py::test_static_execute_call_that_ask_fore_gas_then_trabsaction_has[fork_Amsterdam-d0] - -# stSystemOperationsTest (5) -stSystemOperationsTest/test_ab_acalls0.py::test_ab_acalls0[fork_Amsterdam] -stSystemOperationsTest/test_ab_acalls3.py::test_ab_acalls3[fork_Amsterdam] -stSystemOperationsTest/test_call_recursive_bomb3.py::test_call_recursive_bomb3[fork_Amsterdam] -stSystemOperationsTest/test_double_selfdestruct_touch_paris.py::test_double_selfdestruct_touch_paris[fork_Amsterdam--v1] -stSystemOperationsTest/test_double_selfdestruct_touch_paris.py::test_double_selfdestruct_touch_paris[fork_Amsterdam--v2] - -# stTransactionTest (4) -stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-d120] -stTransactionTest/test_opcodes_transaction_init.py::test_opcodes_transaction_init[fork_Amsterdam-side_effects] -stTransactionTest/test_store_gas_on_create.py::test_store_gas_on_create[fork_Amsterdam] -stTransactionTest/test_suicides_and_internal_call_suicides_success.py::test_suicides_and_internal_call_suicides_success[fork_Amsterdam-d1] diff --git a/tests/ported_static/conftest.py b/tests/ported_static/conftest.py deleted file mode 100644 index c5d9aff2a9..0000000000 --- a/tests/ported_static/conftest.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Conftest for ported static tests. - -Temporarily skip ported static tests that fail on Amsterdam and its -descendant forks due to EIP-8037's two-dimensional gas model. The gas -limits in these ported static test cases have not yet been updated to -account for state gas. - -TODO: Update gas limits in the 3452 failing ported static test cases and -remove this skip list. -""" - -from pathlib import Path - -import pytest -from execution_testing.forks import Amsterdam - -_SKIP_LIST_PATH = Path(__file__).parent / "amsterdam_skip_list.txt" -_AMSTERDAM_SKIP_CASES: frozenset[str] = frozenset( - line.strip() - for line in _SKIP_LIST_PATH.read_text().splitlines() - if line.strip() and not line.lstrip().startswith("#") -) - -# Fixture format suffixes pytest appends inside the parametrize id. These -# must be stripped from the nodeid before substring-matching against the -# skip list, because the skip list predates these suffixes. -_FIXTURE_FORMAT_TOKENS: tuple[str, ...] = ( - "-blockchain_test_engine_from_state_test", - "-blockchain_test_from_state_test", - "-blockchain_test_engine", - "-blockchain_test", - "-state_test", -) - - -def _normalize_nodeid(nodeid: str) -> str: - """Strip pytest fixture-format suffixes to match the skip list format.""" - for token in _FIXTURE_FORMAT_TOKENS: - nodeid = nodeid.replace(token, "") - return nodeid - - -def pytest_collection_modifyitems( - config: pytest.Config, items: list[pytest.Item] -) -> None: - """Skip ported static test cases listed in amsterdam_skip_list.txt.""" - skip_marker = pytest.mark.skip( - reason="Ported static test gas limits not yet updated for EIP-8037" - ) - for item in items: - if "ported_static" not in item.nodeid: - continue - callspec = getattr(item, "callspec", None) - fork = callspec.params.get("fork") if callspec else None - if fork is None or not fork >= Amsterdam: - continue - # The skip list is written against fork_Amsterdam, but the - # EIP-8037 breakage applies equally to its descendant forks. - # Rewriting the item's fork token to Amsterdam's lets one list - # cover them all. - normalized = _normalize_nodeid(item.nodeid).replace( - f"fork_{fork.name()}", "fork_Amsterdam" - ) - for skip_case in _AMSTERDAM_SKIP_CASES: - if skip_case in normalized: - item.add_marker(skip_marker) - break diff --git a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py index 2b48b4cb0b..8ca16e80b5 100644 --- a/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py +++ b/tests/ported_static/stCreate2/test_create2_oo_gafter_init_code_revert2.py @@ -1,98 +1,158 @@ """ -Calls a contract that runs CREATE2 which deploy a code. then after... +Verify a CREATE2 whose child completes its init code but runs out of gas +at the code-deposit charge, inside a frame that then REVERTs: the revert +payload carries the CREATE2 result (zero) back to the caller and every +side effect of the creating frame is rolled back. Ported from: state_tests/stCreate2/Create2OOGafterInitCodeRevert2Filler.json + +@manually-enhanced: Do not overwrite. The forwarded grant is derived from +fork composites so the child fails exactly at the deposit charge on every +fork; the revert payload now also carries the CREATE2 result, and the +caller stores the call result plus both payload words. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, - compute_create_address, + compute_create2_address, ) from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +PAYLOAD_SLOT = 0x1 +CREATE2_RESULT_SLOT = 0x2 +CALL_RESULT_SLOT = 0x3 + +# The init code returns this many memory bytes as the code to deposit; +# the grant is sized so this charge is exactly what the child cannot pay. +DEPOSIT_SIZE = 0x40 + @pytest.mark.ported_from( ["state_tests/stCreate2/Create2OOGafterInitCodeRevert2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Constantinople") def test_create2_oo_gafter_init_code_revert2( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Calls a contract that runs CREATE2 which deploy a code.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 child dies at the deposit charge; its creator reverts.""" + # The child's init code: one word of scratch data, then a deposit + # request the sized grant cannot cover. The would-be deposited code + # (an SSTORE) never runs. + child_mstore = Op.MSTORE( + offset=0x0, + value=0x6001600155, + new_memory_size=0x20, ) + child_return = Op.RETURN( + offset=0x0, + size=DEPOSIT_SIZE, + new_memory_size=DEPOSIT_SIZE, + old_memory_size=0x20, + ) + initcode = child_mstore + child_return + initcode_bytes = bytes(initcode) + assert len(initcode_bytes) <= 0x20, "init code must fit one word" - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The creator writes the init code into memory (right-aligned in the + # first word), runs CREATE2, appends the CREATE2 result to memory and + # reverts both words back to the caller. + creator_setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(initcode_bytes, "big"), + new_memory_size=0x20, + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(initcode_bytes), + size=len(initcode_bytes), + salt=0x0, + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(initcode_bytes), + ) + creator = pre.deploy_contract( + code=creator_setup + + Op.MSTORE( + offset=0x20, + value=create2_code, + new_memory_size=0x40, + old_memory_size=0x20, + ) + + Op.REVERT(offset=0x0, size=0x40) ) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { (MSTORE 0 0x6460016001556000526005601bf3) (CREATE2 0 18 14 0) (REVERT 0 32) } # noqa: E501 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x6460016001556000526005601BF3) - + Op.POP(Op.CREATE2(value=0x0, offset=0x12, size=0xE, salt=0x0)) - + Op.REVERT(offset=0x0, size=0x20) - + Op.STOP, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # Size the grant so the child's grant covers its init-code execution + # but not the deposit charge, and the creator's 1/64 retention still + # covers its tail (the result MSTORE and the REVERT). + child_exec = child_mstore.gas_cost(fork) + child_return.gas_cost(fork) + deposit_cost = DEPOSIT_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + deposit_cost += fork.code_deposit_state_gas(code_size=DEPOSIT_SIZE) + child_grant = child_exec + deposit_cost // 2 + rem_after_create = -(-child_grant * 64 // 63) + forwarded = ( + creator_setup.gas_cost(fork) + + create2_code.gas_cost(fork) + + rem_after_create + ) + granted = rem_after_create - rem_after_create // 64 + assert child_exec + 10 <= granted <= child_exec + deposit_cost - 10, ( + "the child grant must die exactly at the deposit charge" ) - # Source: lll - # { (CALL 33000 0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b 0 0 0 0 32) [[ 1 ]] (MLOAD 0) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x80E8, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x20, - ) + creator_tail = Op.MSTORE( + offset=0x20, + value=0x0, + new_memory_size=0x40, + old_memory_size=0x20, + ).gas_cost(fork) + Op.REVERT(offset=0x0, size=0x40).gas_cost(fork) + assert rem_after_create // 64 >= creator_tail + 5, ( + "the creator's retention must cover its tail" + ) + + # The caller forwards the sized grant, then stores the call result + # and both words of the revert payload. + call_code = Op.CALL(gas=forwarded, address=creator, ret_size=0x40) + caller = pre.deploy_contract( + code=Op.SSTORE(key=CALL_RESULT_SLOT, value=Op.ADD(0x1, call_code)) + + Op.SSTORE(key=PAYLOAD_SLOT, value=Op.MLOAD(offset=0x0)) + + Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD(0x1, Op.MLOAD(offset=0x20)), ) - + Op.SSTORE(key=0x1, value=Op.MLOAD(offset=0x0)) + Op.STOP, - storage={1: 1}, - nonce=0, - address=Address(0xC94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + storage={PAYLOAD_SLOT: 0x1}, ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=75000, + sender=pre.fund_eoa(), + to=caller, + state_gas_reservoir=0, ) post = { - contract_0: Account(storage={1: 0x6460016001556000526005601BF3}), - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, + caller: Account( + storage={ + # The creator reverted: the call result is 0. + CALL_RESULT_SLOT: 0x1, + # First payload word: the init code the creator staged. + PAYLOAD_SLOT: int.from_bytes(initcode_bytes, "big"), + # Second payload word: the failed CREATE2 returned 0. + CREATE2_RESULT_SLOT: 0x1, + } + ), + # Everything inside the creator was rolled back. + creator: Account(nonce=1, storage={}), + compute_create2_address(creator, 0, initcode): Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py b/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py index 9efd489bbb..9aef43337a 100644 --- a/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py +++ b/tests/ported_static/stCreate2/test_create2_oog_from_call_refunds.py @@ -1,8 +1,17 @@ """ -Test_create2_oog_from_call_refunds. +Verify gas refunds earned inside a CREATE2's init code (storage clears, +via direct stores and CALL/CALLCODE/DELEGATECALL helpers, selfdestructs, +and nested creations) against out-of-gas boundaries: each scenario runs +once completing normally and twice dying — on an oversized code deposit +and on an INVALID that pins the refund bookkeeping. Ported from: state_tests/stCreate2/Create2OOGFromCallRefundsFiller.yml + +@manually-enhanced: Do not overwrite. The transaction budget and the +sender's funding derive from the fork: the ported 400k regular budget +plus the deepest arm's peak outstanding EIP-8037 state gas, guarded to +stay below the 5000-byte deposit charge that starves the OoG arms. """ import pytest @@ -27,6 +36,13 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +GAS_PRICE = 10 +# The ported budget, proven to cover every NoOoG arm's regular gas. +PORTED_GAS_LIMIT = 400_000 +# The OoG arms return this much memory as code; the deposit charge is +# what must exceed the transaction budget so they starve. +OOG_DEPOSIT_SIZE = 0x1388 + @pytest.mark.ported_from( ["state_tests/stCreate2/Create2OOGFromCallRefundsFiller.yml"], @@ -233,7 +249,34 @@ def test_create2_oog_from_call_refunds( base_fee_per_gas=10, ) - pre[sender] = Account(balance=0x3D0900, nonce=1) + # EIP-8037 charges state gas on top of the ported regular budget. + # The headroom is the deepest arm's (create-inside-create2) peak + # outstanding state gas: a NEW_ACCOUNT charge and one live fresh + # slot set at each creation depth, plus the one-byte code deposits; + # all terms are zero before Amsterdam. + fresh_set_state = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ).state_cost(fork) + new_account_state = Op.CREATE2( + value=0x0, offset=0x0, size=0x0, salt=0x0 + ).state_cost(fork) + tx_gas_limit = ( + PORTED_GAS_LIMIT + + 2 * new_account_state + + 2 * fresh_set_state + + 3 * fork.code_deposit_state_gas(code_size=1) + + 20_000 + ) + # The budget must stay below the oversized deposit charge so the + # OoG arms keep starving on it on every fork. + oog_deposit = ( + OOG_DEPOSIT_SIZE * fork.gas_costs().CODE_DEPOSIT_PER_BYTE + + fork.code_deposit_state_gas(code_size=OOG_DEPOSIT_SIZE) + ) + assert tx_gas_limit < oog_deposit, "the OoG arms must stay starved" + + # The exact funding makes the OoG arms' post-state balance zero. + pre[sender] = Account(balance=tx_gas_limit * GAS_PRICE, nonce=1) # Source: yul # berlin # { @@ -1189,13 +1232,14 @@ def test_create2_oog_from_call_refunds( Bytes("693c6139") + Hash(contract_23, left_padding=True), Bytes("693c6139") + Hash(contract_24, left_padding=True), ] - tx_gas = [400000] + tx_gas = [tx_gas_limit] tx = Transaction( sender=sender, to=contract_0, data=tx_data[d], gas_limit=tx_gas[g], + gas_price=GAS_PRICE, nonce=1, error=_exc, ) diff --git a/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py b/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py index 4def59bad8..70ae08055a 100644 --- a/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py +++ b/tests/ported_static/stCreate2/test_create2collision_selfdestructed_oog.py @@ -1,52 +1,57 @@ """ -Collision with address that has been selfdestructed in the same... +Verify a CREATE2 whose target address holds a pre-existing account that +SELFDESTRUCTed earlier in the same transaction: the collision stands +(the account is only emptied, not freed), consuming the child's grant, +and the sized budget then runs the creating init code out of gas so the +whole creation transaction rolls back — including the selfdestruct's +balance transfer. Ported from: state_tests/stCreate2/create2collisionSelfdestructedOOGFiller.json + +@manually-enhanced: Do not overwrite. Collider and beneficiary addresses +are computed instead of hardcoded, the budget is derived from fork +composites, and the post-collision work is sized above the collision's +1/64 retention on every fork (the alive collider means the CREATE2 +charges — and refunds — no new-account state gas). """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, + compute_create2_address, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COLLIDER_BALANCE = 1 +# Gas left for the collision to consume: the CREATE2's child grant. Its +# 1/64 retention (plus any state refund) must stay below the two-store +# victim cost, which the guard below asserts. +CHILD_GRANT_SLACK = 30_000 + @pytest.mark.ported_from( ["state_tests/stCreate2/create2collisionSelfdestructedOOGFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "inner_initcode", [ + pytest.param(Bytecode(), id="empty_initcode"), + pytest.param(Op.SSTORE(key=0x1, value=0x1), id="storing_initcode"), pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", + Op.MSTORE(offset=0x0, value=0x6001600155) + + Op.RETURN(offset=0x1B, size=0x5), + id="depositing_initcode", ), ], ) @@ -55,120 +60,118 @@ def test_create2collision_selfdestructed_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + inner_initcode: Bytecode, ) -> None: - """Collision with address that has been selfdestructed in the same...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xE2B35478FDD26477CC576DD906E6277761246A3C) - contract_1 = Address(0xAF3ECBA2FE09A4F6C19F16A9D119E44E08C2DA01) - contract_2 = Address(0xEC2C6832D00680ECE8FF9254F81FDAB0A5A2AC50) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) + """A selfdestructed account still collides, and its grant is lost.""" + sender = pre.fund_eoa() + outer_created = compute_create_address(address=sender, nonce=0) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The collider occupies the CREATE2 target (which depends on the + # init code below through its hash) and selfdestructs when called; + # its address is derived from the pre-funded sender, so the pre + # allocation must stay mutable. + beneficiary = pre.nonexistent_account() + collider_work = Op.SELFDESTRUCT( + address=beneficiary, + address_warm=False, + account_new=True, + ) + collider = compute_create2_address(outer_created, 0, inner_initcode) + pre.deploy_contract( + code=collider_work, + balance=COLLIDER_BALANCE, + address=collider, ) - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xE2B35478FDD26477CC576DD906E6277761246A3C), # noqa: E501 + # The outer init code selfdestructs the collider, stages the child's + # init code (never executed: the collision aborts before dispatch) + # and runs the CREATE2 into the collision; the two stores after it + # are the victims the burned grant leaves unaffordable. + inner_bytes = bytes(inner_initcode) + assert len(inner_bytes) <= 0x20, "inner init code must fit one word" + call_code = Op.CALL( + address=collider, + address_warm=False, + value_transfer=False, + account_new=False, ) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xAF3ECBA2FE09A4F6C19F16A9D119E44E08C2DA01), # noqa: E501 + setup = ( + Op.MSTORE( + offset=0x0, + value=int.from_bytes(inner_bytes, "big"), + new_memory_size=0x20, + ) + if inner_bytes + else Bytecode() + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(inner_bytes) if inner_bytes else 0x0, + size=len(inner_bytes), + salt=0x0, + new_memory_size=0x20 if inner_bytes else 0x0, + old_memory_size=0x20 if inner_bytes else 0x0, + init_code_size=len(inner_bytes), + account_new=False, ) - # Source: lll - # { (SELFDESTRUCT 0x10) } - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x10) + Op.STOP, - balance=1, - nonce=0, - address=Address(0xEC2C6832D00680ECE8FF9254F81FDAB0A5A2AC50), # noqa: E501 + victim_stores = Op.SSTORE( + key=0x0, + value=0x112233, + key_warm=False, + original_value=0, + new_value=0x112233, + ) + Op.SSTORE( + key=0x1, + value=0x112233, + key_warm=False, + original_value=0, + new_value=0x112233, + ) + outer_initcode = ( + Op.POP(call_code) + setup + Op.POP(create2_code) + victim_stores ) - tx_data = [ - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + # The budget covers everything up to and including the CREATE2's own + # charges plus the slack the collision consumes; the guard proves + # the victims exceed what the collision leaves behind, so the outer + # frame must die and the whole creation rolls back. The collider is + # alive at the CREATE2 (only emptied by its selfdestruct), so no + # new-account state gas is charged — or refunded — there. + gas_limit = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=outer_initcode, + contract_creation=True, ) - + Op.MSTORE(offset=0x0, value=0x6001600155) - + Op.POP(Op.CREATE2(value=0x0, offset=0x1B, size=0x5, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - Op.POP( - Op.CALL( - gas=0xC350, - address=contract_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.MSTORE(offset=0x0, value=0x6460016001556000526005601BF3) - + Op.POP(Op.CREATE2(value=0x0, offset=0x12, size=0xE, salt=0x0)) - + Op.SSTORE(key=0x0, value=0x112233) - + Op.STOP, - ] - tx_gas = [200000] - tx_value = [1] + + fork.transaction_top_frame_state_gas(contract_creation=True) + + call_code.gas_cost(fork) + + collider_work.gas_cost(fork) + + setup.gas_cost(fork) + + create2_code.gas_cost(fork) + + CHILD_GRANT_SLACK + ) + leftover = CHILD_GRANT_SLACK // 64 + assert leftover + 2_500 < victim_stores.gas_cost(fork), ( + "the collision's leavings must not afford the victim stores" + ) tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], + data=outer_initcode, + gas_limit=gas_limit, ) post = { - contract_0: Account(code=bytes.fromhex("6010ff00"), balance=1), - contract_1: Account(code=bytes.fromhex("6010ff00"), balance=1), - contract_2: Account(code=bytes.fromhex("6010ff00"), balance=1), - Address( - 0x0000000000000000000000000000000000000010 - ): Account.NONEXISTENT, - compute_create_address(address=sender, nonce=0): Account.NONEXISTENT, sender: Account(nonce=1), + # Rolled back wholesale: the collider keeps its code and its + # balance, the beneficiary was never credited, nothing created. + collider: Account( + code=collider_work, + balance=COLLIDER_BALANCE, + nonce=1, + ), + beneficiary: Account.NONEXISTENT, + outer_created: Account.NONEXISTENT, } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create2no_cash.py b/tests/ported_static/stCreate2/test_create2no_cash.py index 587c64889c..c61a5f6fed 100644 --- a/tests/ported_static/stCreate2/test_create2no_cash.py +++ b/tests/ported_static/stCreate2/test_create2no_cash.py @@ -1,161 +1,107 @@ """ -Create2 fails with not enough cash (endowment of a new account) +... +Verify CREATE2's endowment balance preflight: a creator one wei short of +the endowment fails without creating (and without a nonce bump), a +one-wei top-up sent with the call makes the same CREATE2 succeed, and in +a static context the CREATE2 faults the whole frame instead. Ported from: state_tests/stCreate2/create2noCashFiller.json + +@manually-enhanced: Do not overwrite. The creation-transaction wrapper +and tuned gas budgets are replaced by a deployed entry contract that +records the call result; the created account and the creator's nonce +(no bump on the balance preflight) are asserted explicitly. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, StateTestFiller, Transaction, + compute_create2_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CALL_RESULT_SLOT = 0x0 +CREATE2_ENDOWMENT = 0x65 + @pytest.mark.ported_from( ["state_tests/stCreate2/create2noCashFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "opcode, top_up", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - pytest.param( - 2, - 0, - 0, - id="d2", - ), + pytest.param(Op.CALL, 0, id="call_insufficient_balance"), + pytest.param(Op.CALL, 1, id="call_topped_up_balance"), + pytest.param(Op.STATICCALL, 0, id="staticcall_write_protection"), ], ) -@pytest.mark.pre_alloc_mutable def test_create2no_cash( state_test: StateTestFiller, pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, + opcode: Op, + top_up: int, ) -> None: - """Create2 fails with not enough cash (endowment of a new account) +...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xE2B35478FDD26477CC576DD906E6277761246A3C) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 endowment beyond the creator's balance cannot create.""" + # The creator holds one wei less than the endowment it attempts to + # transfer; only the topped-up arm can afford it. + creator = pre.deploy_contract( + code=Op.POP( + Op.CREATE2(value=CREATE2_ENDOWMENT, offset=0x0, size=0x0, salt=0x0) + ) + + Op.STOP, + balance=CREATE2_ENDOWMENT - 1, ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + # The entry contract forwards the transaction's value (the optional + # top-up) and records the call result, shifted so that a failed call + # (1), a successful call (2) and no call at all (0) all differ. + if opcode == Op.CALL: + call_code = Op.CALL(address=creator, value=top_up) + else: + call_code = Op.STATICCALL(address=creator) + entry = pre.deploy_contract( + code=Op.SSTORE(key=CALL_RESULT_SLOT, value=Op.ADD(0x1, call_code)) + + Op.STOP, ) - pre[sender] = Account(balance=0xDE0B6B3A7640000) - # Source: lll - # { (CREATE2 101 0 0 0) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CREATE2(value=0x65, offset=0x0, size=0x0, salt=0x0) + Op.STOP, - balance=100, - nonce=0, - address=Address(0xE2B35478FDD26477CC576DD906E6277761246A3C), # noqa: E501 + tx = Transaction( + sender=pre.fund_eoa(), + to=entry, + value=top_up, + state_gas_reservoir=0, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": [0, 2], "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(balance=100), - Address( - 0x12AAEFBC0350A026228076E5369E6CE148CE67BE - ): Account.NONEXISTENT, - sender: Account(nonce=1), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(balance=0), - Address(0x12AAEFBC0350A026228076E5369E6CE148CE67BE): Account( - balance=101 - ), - sender: Account(nonce=1), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.CALL( - gas=0x249F0, - address=contract_0, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - Op.CALL( - gas=0x249F0, - address=contract_0, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - Op.STATICCALL( - gas=0x249F0, - address=contract_0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + created = compute_create2_address(creator, 0, b"") + create_succeeds = opcode == Op.CALL and top_up > 0 + if create_succeeds: + # The whole (topped-up) balance moved into the created account, + # and the creator's nonce was consumed by the creation. + creator_account = Account(nonce=2, balance=0) + created_account: Account | None = Account( + nonce=1, code=b"", balance=CREATE2_ENDOWMENT ) - + Op.STOP, - ] - tx_gas = [400000] - tx_value = [1] + else: + # The balance preflight (or the static fault) aborts before any + # account is touched: no creation and no nonce bump. + creator_account = Account(nonce=1, balance=CREATE2_ENDOWMENT - 1) + created_account = Account.NONEXISTENT - tx = Transaction( - sender=sender, - to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + # A static frame faults on CREATE2, so only that arm's call fails. + call_result = 0 if opcode == Op.STATICCALL else 1 + + post = { + entry: Account( + storage={CALL_RESULT_SLOT: 0x1 + call_result}, balance=0 + ), + creator: creator_account, + created: created_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py b/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py index 4e4ef237ca..e75adcb8de 100644 --- a/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py +++ b/tests/ported_static/stCreate2/test_create_message_reverted_oog_in_init2.py @@ -1,127 +1,145 @@ """ -Create2 oog during the init code, + when create2 is from transaction... +Verify a CREATE2 issued from a contract-creation transaction's init +code: the transaction budget decides whether the CREATE2's child +completes its storage-writing init code or dies out of gas, while the +outer creation completes either way. Ported from: state_tests/stCreate2/CreateMessageRevertedOOGInInit2Filler.json + +@manually-enhanced: Do not overwrite. Both budgets are derived from fork +composites (intrinsic + top-frame state gas + the composed init code); +the outer created account is asserted on both arms with a pre-CREATE2 +canary, and the child account's storage on the success arm. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, + compute_create2_address, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CANARY_SLOT = 0x2 +CANARY = 0xFF +TX_VALUE = 100 +CHILD_STORED = {0x0: 0xC, 0x1: 0xD} + @pytest.mark.ported_from( ["state_tests/stCreate2/CreateMessageRevertedOOGInInit2Filler.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "child_covered", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(False, id="child_oog"), + pytest.param(True, id="child_succeeds"), ], ) -@pytest.mark.pre_alloc_mutable def test_create_message_reverted_oog_in_init2( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + child_covered: bool, ) -> None: - """Create2 oog during the init code, + when create2 is from...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """The budget decides how far an init-code CREATE2's child gets.""" + # The child's init code writes two fresh slots and deposits nothing. + inner_initcode = Op.SSTORE( + key=0x0, + value=CHILD_STORED[0x0], + key_warm=False, + original_value=0, + new_value=CHILD_STORED[0x0], + ) + Op.SSTORE( + key=0x1, + value=CHILD_STORED[0x1], + key_warm=False, + original_value=0, + new_value=CHILD_STORED[0x1], ) + inner_bytes = bytes(inner_initcode) + assert len(inner_bytes) <= 0x20, "inner init code must fit one word" - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000000000, + # The outer init code writes a completion canary before the CREATE2 + # (only a POP runs after it: on the starved arm the 1/64 retention + # cannot afford an SSTORE), stages the child's init code in memory + # and runs the CREATE2; it deposits no code. + canary_store = Op.SSTORE( + key=CANARY_SLOT, + value=CANARY, + key_warm=False, + original_value=0, + new_value=CANARY, ) - - pre[sender] = Account(balance=0x2DC6C0) - # Source: hex - # 0x - contract_0 = pre.deploy_contract( # noqa: F841 - code="", - balance=10, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + setup = Op.MSTORE( + offset=0x0, + value=int.from_bytes(inner_bytes, "big"), + new_memory_size=0x20, ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x20 - len(inner_bytes), + size=len(inner_bytes), + salt=0x0, + new_memory_size=0x20, + old_memory_size=0x20, + init_code_size=len(inner_bytes), + ) + outer_initcode = canary_store + setup + Op.POP(create2_code) + Op.STOP - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - Address( - 0xF3059E18A327C662766F6BA11808C400635847EF - ): Account.NONEXISTENT, - }, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - sender: Account(nonce=1), - Address(0xF3059E18A327C662766F6BA11808C400635847EF): Account( - storage={0: 12, 1: 13}, balance=0, nonce=1 - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.MSTORE(offset=0x0, value=0x600C600055600D600155) - + Op.CREATE2(value=0x0, offset=0x16, size=0xA, salt=0x0) - + Op.STOP, - ] - tx_gas = [110000, 150000] - tx_value = [100] + # Both budgets derive from the same overhead: everything the outer + # frame pays before (and including) the CREATE2's own charges. The + # child's grant is what remains after the 1/64 withhold. + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=outer_initcode, + contract_creation=True, + sends_value=True, + ) + + fork.transaction_top_frame_state_gas(contract_creation=True) + + canary_store.gas_cost(fork) + + setup.gas_cost(fork) + + create2_code.gas_cost(fork) + ) + child_needed = inner_initcode.gas_cost(fork) + if child_covered: + gas_limit = overhead + -(-child_needed * 64 // 63) + 3_000 + else: + gas_limit = overhead + child_needed // 2 + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + data=outer_initcode, + gas_limit=gas_limit, + value=TX_VALUE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + outer_created = compute_create_address(address=sender, nonce=0) + child = compute_create2_address(outer_created, 0, inner_initcode) + post = { + sender: Account(nonce=1), + # The outer creation completes on both arms: the CREATE2 always + # bumps its nonce, and a failed child costs it only the grant. + outer_created: Account( + nonce=2, + code=b"", + balance=TX_VALUE, + storage={CANARY_SLOT: CANARY}, + ), + child: Account(nonce=1, code=b"", balance=0, storage=CHILD_STORED) + if child_covered + else Account.NONEXISTENT, + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py index 00ddb3595a..1778241727 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create2_oog.py @@ -1,200 +1,235 @@ """ -Test_revert_depth_create2_oog. +Verify a CREATE2 two frames deep under out-of-gas pressure: the calldata +sets the grant a caller forwards to a creating contract, and the two +budgets decide whether the creation completes, the creator dies mid-way, +or the whole outer frame runs dry — each with a distinct post-state. Ported from: state_tests/stCreate2/RevertDepthCreate2OOGFiller.json +state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json + +@manually-enhanced: Do not overwrite. The byte-identical Berlin twin is +folded in; every budget derives from fork composites; the creator now +stores the CREATE2 result so a wrongly failed (or wrongly succeeding) +creation is visible beyond the created account itself. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create2_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Caller slots (as in the ported filler). +CALLER_START_SLOT = 0x0 +CALL_RESULT_SLOT = 0x1 +CALLER_DONE_SLOT = 0x4 +# Creator slots: 0x2/0x3 as ported, plus the CREATE2 result. +CREATOR_START_SLOT = 0x2 +CREATOR_DONE_SLOT = 0x3 +CREATE2_RESULT_SLOT = 0x5 + +# Gas available in the caller frame at the CALL on the starved-outer +# arms: far below the creator's needs, and retaining under 1/64th of the +# EIP-2200 stipend so no post-call store can run — the caller must die. +STARVED_AVAILABLE = 20_000 + @pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreate2OOGFiller.json"], + [ + "state_tests/stCreate2/RevertDepthCreate2OOGFiller.json", + "state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json", + ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "creator_covered", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), + pytest.param(False, id="creator_oog"), + pytest.param(True, id="creator_ok"), ], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize( + "outer_covered", + [ + pytest.param(False, id="outer_oog"), + pytest.param(True, id="outer_ok"), + ], +) +@pytest.mark.parametrize("tx_value", [1, 0]) def test_revert_depth_create2_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + creator_covered: bool, + outer_covered: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create2_oog.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """Two stacked budgets decide how deep a nested CREATE2 gets.""" + # The creator: entry marker, an empty-init-code CREATE2 whose result + # is stored (success leaves the created address plus one, a failure + # leaves exactly one), and a completion marker. + sstore_2 = Op.SSTORE( + key=CREATOR_START_SLOT, + value=0x8, + key_warm=False, + original_value=0, + new_value=0x8, + ) + result_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD( + 0x1, Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0) + ), + key_warm=False, + original_value=0, + new_value=0x1, + ) + sstore_3 = Op.SSTORE( + key=CREATOR_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, + ) + creator = pre.deploy_contract( + code=sstore_2 + result_store + sstore_3 + Op.STOP ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # The empty-init-code child consumes nothing and returns its whole + # grant, so the creator's needs are just its composite costs. + creator_needed = ( + sstore_2.gas_cost(fork) + + result_store.gas_cost(fork) + + sstore_3.gas_cost(fork) ) + if creator_covered: + forwarded = creator_needed + 1_000 + else: + forwarded = creator_needed // 2 - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 + # The caller: entry marker, the CALL with its grant taken from + # calldata (as in the ported filler), result store, completion + # marker. + sstore_0 = Op.SSTORE( + key=CALLER_START_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=0x1, + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=creator, + address_warm=False, + value_transfer=False, + account_new=False, + ) + sstore_1 = Op.SSTORE( + key=CALL_RESULT_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=0x1, ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + sstore_4 = Op.SSTORE( + key=CALLER_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, + ) + caller_code = sstore_0 + sstore_1 + sstore_4 + Op.STOP + caller = pre.deploy_contract(code=caller_code) + + tx_data = Hash(forwarded) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=tx_data, + sends_value=tx_value > 0, + ) + sstore_0.gas_cost(fork) + if outer_covered: + # Enough at the CALL that the EIP-150 clamp still grants the + # full ask, plus the caller's post-call stores. + available = -(-forwarded * 64 // 63) + 64 + assert available - available // 64 >= forwarded, ( + "the full ask must be granted" + ) + gas_limit = ( + overhead + + sstore_1.gas_cost(fork) + + sstore_4.gas_cost(fork) + + available ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0xA000000000000000000000000000000000000000), # noqa: E501 + else: + # The clamped grant starves the creator, and the 1/64 retention + # cannot run any store afterwards: the caller must die too. + granted = STARVED_AVAILABLE - STARVED_AVAILABLE // 64 + assert granted < creator_needed, "the creator must be starved" + assert STARVED_AVAILABLE // 64 <= fork.gas_costs().CALL_STIPEND, ( + "the retention must not afford the post-call store" + ) + gas_limit = overhead + call_code.gas_cost(fork) + STARVED_AVAILABLE + + sender = pre.fund_eoa() + tx = Transaction( + sender=sender, + to=caller, + data=tx_data, + gas_limit=gas_limit, + value=tx_value, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x05A28FC366483258507BCF739658573CB47E4FAD): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), + created = compute_create2_address(creator, 0, b"") + if not outer_covered: + # The whole transaction ran dry: only the code survives. + caller_account = Account(storage={}, code=caller_code, balance=0) + creator_account = Account(storage={}, nonce=1) + created_account: Account | None = Account.NONEXISTENT + elif not creator_covered: + # The creator died mid-creation and was rolled back. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x0, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), + balance=tx_value, + ) + creator_account = Account(storage={}, nonce=1) + created_account = Account.NONEXISTENT + else: + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x1, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), + balance=tx_value, + ) + creator_account = Account( + storage={ + CREATOR_START_SLOT: 0x8, + CREATE2_RESULT_SLOT: int.from_bytes(bytes(created), "big") + 1, + CREATOR_DONE_SLOT: 0xC, }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 170000] - tx_value = [1, 0] + nonce=2, + ) + created_account = Account(nonce=1, code=b"", balance=0) - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + sender: Account(nonce=1), + caller: caller_account, + creator: creator_account, + created: created_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py b/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py deleted file mode 100644 index c15b8760d4..0000000000 --- a/tests/ported_static/stCreate2/test_revert_depth_create2_oog_berlin.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -Test_revert_depth_create2_oog_berlin. - -Ported from: -state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Environment, - Hash, - StateTestFiller, - Transaction, -) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreate2OOGBerlinFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_revert_depth_create2_oog_berlin( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Test_revert_depth_create2_oog_berlin.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0xA000000000000000000000000000000000000000), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x05A28FC366483258507BCF739658573CB47E4FAD): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - Address( - 0x05A28FC366483258507BCF739658573CB47E4FAD - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 170000] - tx_value = [1, 0] - - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py index 38873c202b..ac9cbaf99d 100644 --- a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py +++ b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision.py @@ -1,222 +1,264 @@ """ -Copy of this test for CREATE2. +Verify a CREATE2 that collides with a live account — the very caller +that funded the attempt: the collision burns the child's grant and bumps +the creator's nonce without creating anything, and the two stacked +budgets decide whether the creator survives its aftermath, dies on it, +or the whole outer frame runs dry. Ported from: state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json +state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json -@manually-enhanced: Do not overwrite. `tx_gas` raised on Amsterdam to -cover EIP-8037 NEW_ACCOUNT state-gas spill on the CREATE2-via-revert -path. Pre-EIP-8037 keeps the original [110_000, 170_000] tuned budgets; -post-state expectations unchanged on all forks. - +@manually-enhanced: Do not overwrite. The byte-identical Berlin twin is +folded in, and the legacy fillers' vacancy is repaired: they kept the +collider at contract_1's CREATE address while the code runs CREATE2, so +nothing ever collided — the caller now occupies the CREATE2 target. The +creator pre-writes its result slot so the post-collision store is a +dirty-warm write its 1/64 retention can afford, and all budgets derive +from fork composites. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create2_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Caller slots (as in the ported filler). +CALLER_START_SLOT = 0x0 +CALL_RESULT_SLOT = 0x1 +CALLER_DONE_SLOT = 0x4 +# Creator slots: 0x2 as ported, plus the pre-written CREATE2 result. +CREATOR_START_SLOT = 0x2 +CREATE2_RESULT_SLOT = 0x5 +# Pre-written sentinel, overwritten by the CREATE2 result plus one: a +# surviving creator must show 1 (collision), never 0xFF or an address. +RESULT_PREWRITE = 0xFF + +# Post-collision slack for the starved-creator arm: retains under 1/64th +# of the EIP-2200 stipend, so the result store cannot run. +STARVED_SLACK = 10_000 +# The SSTORE composite prices a dirty re-store at the Berlin-era 100 on +# every fork, but the un-metered pre-Berlin schedule charges up to 5000 +# (EIP-1283 was reverted in ConstantinopleFix); the covered arm's +# retention carries this headroom so those forks stay covered too. +DIRTY_STORE_HEADROOM = 5_000 +# Gas available in the caller frame at the CALL on the starved-outer +# arms: too little for the creator, and retaining too little for any +# post-call store — the caller must die. +STARVED_AVAILABLE = 20_000 + @pytest.mark.ported_from( - ["state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json"], + [ + "state_tests/stCreate2/RevertDepthCreateAddressCollisionFiller.json", + "state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json", # noqa: E501 + ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Constantinople") @pytest.mark.parametrize( - "d, g, v", + "creator_covered", [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), + pytest.param(False, id="creator_oog"), + pytest.param(True, id="creator_ok"), ], ) +@pytest.mark.parametrize( + "outer_covered", + [ + pytest.param(False, id="outer_oog"), + pytest.param(True, id="outer_ok"), + ], +) +@pytest.mark.parametrize("tx_value", [1, 0]) @pytest.mark.pre_alloc_mutable def test_revert_depth_create_address_collision( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + creator_covered: bool, + outer_covered: bool, + tx_value: int, ) -> None: - """Copy of this test for CREATE2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """A CREATE2 collision burns the grant; budgets decide what's left.""" + # The creator: entry marker, result-slot pre-write, then the CREATE2 + # aimed at the caller's (occupied) address, whose result overwrites + # the sentinel as a dirty-warm store the 1/64 retention can pay. + sstore_2 = Op.SSTORE( + key=CREATOR_START_SLOT, + value=0x8, + key_warm=False, + original_value=0, + new_value=0x8, + ) + sentinel_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=RESULT_PREWRITE, + key_warm=False, + original_value=0, + new_value=RESULT_PREWRITE, + ) + create2_code = Op.CREATE2( + value=0x0, + offset=0x0, + size=0x0, + salt=0x0, + account_new=False, + ) + result_store = Op.SSTORE( + key=CREATE2_RESULT_SLOT, + value=Op.ADD(0x1, create2_code), + key_warm=True, + original_value=0, + current_value=RESULT_PREWRITE, + new_value=0x1, + ) + creator = pre.deploy_contract( + code=sstore_2 + sentinel_store + result_store + Op.STOP ) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + # The caller occupies the creator's CREATE2 target (this is the + # repaired collision, and why the pre allocation must stay mutable). + sstore_0 = Op.SSTORE( + key=CALLER_START_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=0x1, + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=creator, + address_warm=False, + value_transfer=False, + account_new=False, + ) + sstore_1 = Op.SSTORE( + key=CALL_RESULT_SLOT, + value=call_code, + key_warm=False, + original_value=0, + new_value=0x1, ) + sstore_4 = Op.SSTORE( + key=CALLER_DONE_SLOT, + value=0xC, + key_warm=False, + original_value=0, + new_value=0xC, + ) + caller_code = sstore_0 + sstore_1 + sstore_4 + Op.STOP + caller = compute_create2_address(creator, 0, b"") + pre.deploy_contract(code=caller_code, address=caller) - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 + # The collision consumes everything left after the CREATE2's charges + # but one 64th (the live target means no new-account state gas moves + # in either direction), so the creator's fate is set by the slack + # riding on top of its pre-collision costs. + create2_charge = create2_code.gas_cost(fork) + result_tail = result_store.gas_cost(fork) - create2_charge + stipend = fork.gas_costs().CALL_STIPEND + if creator_covered: + slack = 64 * (stipend + result_tail + DIRTY_STORE_HEADROOM + 100) + else: + slack = STARVED_SLACK + assert slack // 64 <= stipend, ( + "the retention must not afford the result store" + ) + forwarded = ( + sstore_2.gas_cost(fork) + + sentinel_store.gas_cost(fork) + + create2_charge + + slack ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB000000000000000000000000000000000000000, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + tx_data = Hash(forwarded) + overhead = fork.transaction_intrinsic_cost_calculator()( + calldata=tx_data, + sends_value=tx_value > 0, + ) + sstore_0.gas_cost(fork) + if outer_covered: + # Enough at the CALL that the EIP-150 clamp still grants the + # full ask, plus the caller's post-call stores. + available = -(-forwarded * 64 // 63) + 64 + assert available - available // 64 >= forwarded, ( + "the full ask must be granted" + ) + gas_limit = ( + overhead + + sstore_1.gas_cost(fork) + + sstore_4.gas_cost(fork) + + available + ) + else: + granted = STARVED_AVAILABLE - STARVED_AVAILABLE // 64 + assert granted < sstore_2.gas_cost(fork) + sentinel_store.gas_cost( + fork + ), "the creator must die before its CREATE2" + assert STARVED_AVAILABLE // 64 <= stipend, ( + "the retention must not afford the post-call store" ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682), # noqa: E501 + gas_limit = overhead + call_code.gas_cost(fork) + STARVED_AVAILABLE + + sender = pre.fund_eoa() + tx = Transaction( + sender=sender, + to=caller, + data=tx_data, + gas_limit=gas_limit, + value=tx_value, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 1: 1, 4: 12}, nonce=54), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 4: 12}, nonce=54), - contract_1: Account(storage={}), + if not outer_covered: + # The whole transaction ran dry: only the code survives. + caller_account = Account(storage={}, code=caller_code, balance=0) + creator_account = Account(storage={}, nonce=1) + elif not creator_covered: + # The creator reached the collision but died on its aftermath + # and was rolled back — including the collision's nonce bump. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x0, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - contract_1: Account(storage={}), + code=caller_code, + balance=tx_value, + ) + creator_account = Account(storage={}, nonce=1) + else: + # The collision's signature: a nonce bump with nothing created, + # a zero CREATE2 result, and the caller's account untouched. + caller_account = Account( + storage={ + CALLER_START_SLOT: 0x1, + CALL_RESULT_SLOT: 0x1, + CALLER_DONE_SLOT: 0xC, }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - contract_1: Account(storage={}), + code=caller_code, + balance=tx_value, + ) + creator_account = Account( + storage={ + CREATOR_START_SLOT: 0x8, + CREATE2_RESULT_SLOT: 0x1, }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - # EIP-8037 NEW_ACCOUNT state-gas spill on Amsterdam exceeds the - # original tuned tx_gas budgets; pre-EIP-8037 keeps the originals. - tx_gas = [110000, 170000] - if fork.is_eip_enabled(8037): - tx_gas = [500_000, 700_000] - tx_value = [1, 0] + nonce=2, + ) - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) + post = { + sender: Account(nonce=1), + caller: caller_account, + creator: creator_account, + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py b/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py deleted file mode 100644 index 907358ab62..0000000000 --- a/tests/ported_static/stCreate2/test_revert_depth_create_address_collision_berlin.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -Copy of this test for CREATE2. - -Ported from: -state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json - -@manually-enhanced: Do not overwrite. `tx_gas` raised on Amsterdam to -cover EIP-8037 NEW_ACCOUNT state-gas spill on the CREATE2-via-revert -path. Pre-EIP-8037 keeps the original [110_000, 170_000] tuned budgets; -post-state expectations unchanged on all forks. - -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Environment, - Hash, - StateTestFiller, - Transaction, -) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCreate2/RevertDepthCreateAddressCollisionBerlinFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_revert_depth_create_address_collision_berlin( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Copy of this test for CREATE2.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE2 0 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE2(value=0x0, offset=0x0, size=0x0, salt=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB000000000000000000000000000000000000000, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x3E180B1862F9D158ABB5E519A6D8605540C23682), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 1: 1, 4: 12}, nonce=54), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account(storage={0: 1, 4: 12}, nonce=54), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - contract_0: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b000000000000000000000000000000000000000600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - contract_1: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - # EIP-8037 NEW_ACCOUNT state-gas spill on Amsterdam exceeds the - # original tuned tx_gas budgets; pre-EIP-8037 keeps the originals. - tx_gas = [110000, 170000] - if fork.is_eip_enabled(8037): - tx_gas = [500_000, 700_000] - tx_value = [1, 0] - - tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, - ) - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund50_2.py b/tests/ported_static/stRefundTest/test_refund50_2.py index 4d471f2407..94ed534167 100644 --- a/tests/ported_static/stRefundTest/test_refund50_2.py +++ b/tests/ported_static/stRefundTest/test_refund50_2.py @@ -1,17 +1,21 @@ """ -Test_refund50_2. +Verify the EIP-3529 refund cap over five storage clears: the sender's +final balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund50_2Filler.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,57 +24,67 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund50_2Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund50_2( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund50_2.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x989680) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Five storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.SSTORE( + key=0xA, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.SSTORE( + key=0xB, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [[ 10 ]] 1 [[ 11 ]] 1 [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0xA, value=0x1) - + Op.SSTORE(key=0xB, value=0x1) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - target: Account(storage={10: 1, 11: 1}), - coinbase: Account(balance=0), - sender: Account(balance=0x8D926C, nonce=1), + target: Account(storage={0xA: 1, 0xB: 1}), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund50percent_cap.py b/tests/ported_static/stRefundTest/test_refund50percent_cap.py index d92a991123..ece04d3691 100644 --- a/tests/ported_static/stRefundTest/test_refund50percent_cap.py +++ b/tests/ported_static/stRefundTest/test_refund50percent_cap.py @@ -1,18 +1,21 @@ """ -Test_refund50percent_cap. +Verify the EIP-3529 refund cap over six storage clears: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund50percentCapFiller.json + +@manually-enhanced: Do not overwrite. The sender's balance, the refund cap +and the transaction budget all derive from the fork (`code.gas_cost` / +`code.refund` composites), so EIP-8037's repriced stores and any future +refund change are tracked instead of pinned. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,69 +24,84 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund50percentCapFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund50percent_cap( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund50percent_cap.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xDC4EFA209AECDD4C2D5201A419EA27506151B4EC687F14A613229E310932491B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Six storage clears refund gas up to the EIP-3529 cap.""" + code = ( + Op.POP(Op.SLOAD(key=0x1, key_warm=False)) + + Op.POP(Op.SLOAD(key=0x2, key_warm=False)) + + Op.SSTORE( + key=0xA, + value=Op.EXP(0x2, 0xFF, exponent=0xFF), + key_warm=False, + original_value=0, + new_value=2**255, + ) + + Op.SSTORE( + key=0xB, + value=Op.BALANCE(address=Op.ADDRESS, address_warm=True), + key_warm=False, + original_value=0, + new_value=1, + ) + + Op.SSTORE( + key=0x1, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x2, value=0x0, key_warm=True, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x3, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x4, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x5, value=0x0, key_warm=False, original_value=1, new_value=0 + ) + + Op.SSTORE( + key=0x6, value=0x0, key_warm=False, original_value=1, new_value=0 + ) ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x989680) - # Source: lll - # { @@1 @@2 [[ 10 ]] (EXP 2 0xff) [[ 11 ]] (BALANCE (ADDRESS)) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP(Op.SLOAD(key=0x1)) - + Op.POP(Op.SLOAD(key=0x2)) - + Op.SSTORE(key=0xA, value=Op.EXP(0x2, 0xFF)) - + Op.SSTORE(key=0xB, value=Op.BALANCE(address=Op.ADDRESS)) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.STOP, + target = pre.deploy_contract( + code=code + Op.STOP, storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xEF67F354C8505E1056889970C3D9B5E0FE65D1E2), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=100000, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { target: Account( - storage={ - 10: 0x8000000000000000000000000000000000000000000000000000000000000000, # noqa: E501 - 11: 0xDE0B6B3A7640000, - }, + storage={0xA: 2**255, 0xB: CONTRACT_BALANCE}, ), - coinbase: Account(balance=0), - sender: Account(balance=0x8CF0A0), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_call_a.py b/tests/ported_static/stRefundTest/test_refund_call_a.py index bc745f449b..c42de668a9 100644 --- a/tests/ported_static/stRefundTest/test_refund_call_a.py +++ b/tests/ported_static/stRefundTest/test_refund_call_a.py @@ -1,17 +1,21 @@ """ -Test_refund_call_a. +Verify a storage-clear refund earned inside a sub-call: the sender's final +balance reflects the executed gas minus the capped refund. Ported from: state_tests/stRefundTest/refund_CallAFiller.json + +@manually-enhanced: Do not overwrite. The sub-call forwards all gas +instead of a schedule-sized constant, and the sender's balance, refund cap +and budget derive from the fork (`code.gas_cost` / `code.refund` +composites), so EIP-8037's repriced stores are tracked instead of pinned. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,72 +24,65 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund_CallAFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund_call_a( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund_call_a.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x1312D00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """A callee's storage clear refunds gas up to the EIP-3529 cap.""" + callee_code = Op.SSTORE( + key=0x1, value=0x0, key_warm=False, original_value=1, new_value=0 ) - - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [[ 1 ]] 0 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x0) + Op.STOP, + callee = pre.deploy_contract( + code=callee_code + Op.STOP, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) - # Source: lll - # { [[ 0 ]] (CALL 5500 0 0 0 0 0 )} # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=0x157C, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, + + # The caller's own slot 1 stays set: the callee clears its own storage. + caller_code = Op.SSTORE( + key=0x0, + value=Op.CALL(address=callee, address_warm=False), + key_warm=False, + original_value=0, + new_value=1, + ) + caller = pre.deploy_contract( + code=caller_code + Op.STOP, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()() + executed = ( + intrinsic + caller_code.gas_cost(fork) + callee_code.gas_cost(fork) + ) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=200000, - value=10, + to=caller, + gas_limit=gas_limit, + gas_price=GAS_PRICE, + ) + + # EIP-3529 caps the refund at a fifth of the executed gas. + refund = min( + caller_code.refund(fork) + callee_code.refund(fork), executed // 5 ) + gas_used = executed - refund post = { - target: Account(storage={0: 1, 1: 1}, balance=0xDE0B6B3A764000A), - coinbase: Account(balance=0), - sender: Account(balance=0x12A2AD2, nonce=1), - addr: Account(storage={}), + caller: Account(storage={0: 1, 1: 1}), + callee: Account(storage={}), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py b/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py index 4083b82695..812a0cb1ed 100644 --- a/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py +++ b/tests/ported_static/stRefundTest/test_refund_suicide50procent_cap.py @@ -1,158 +1,181 @@ """ -Test_refund_suicide50procent_cap. +Verify the EIP-3529 refund cap when eight storage clears surround a +gas-limited call to a self-destructing contract: the stored gas delta and +the sender's final balance track the executed gas minus the capped refund, +for both a starved and a fully funded sub-call. Ported from: state_tests/stRefundTest/refundSuicide50procentCapFiller.json + +@manually-enhanced: Do not overwrite. The sub-call grant, the stored gas +delta, the refund cap and the budget all derive from fork composites; the +destructor self-destructs to CALLER so every address is dynamic; the post +branches on EIP-6780 for the destructor's survival. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +TARGET_BALANCE = 0xDE0B6B3A7640000 +DESTRUCTOR_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 +FLAG_SLOT = 0xA +RESULT_SLOT = 0xB +GAS_SLOT = 0x17 +SNAPSHOT_OFFSET = 0x16 +MEMORY_SIZE = SNAPSHOT_OFFSET + 32 +GRANT_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refundSuicide50procentCapFiller.json"], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("London") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), - ], + "call_succeeds", + [False, True], + ids=["starved_grant", "full_grant"], ) -@pytest.mark.pre_alloc_mutable def test_refund_suicide50procent_cap( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_succeeds: bool, ) -> None: - """Test_refund_suicide50procent_cap.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = pre.fund_eoa(amount=0x3B9ACA00) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, + """Storage clears around a self-destruct call refund up to the cap.""" + destructor_code = Op.SELFDESTRUCT( + address=Op.CALLER, address_warm=True, account_new=False + ) + destructor = pre.deploy_contract( + code=destructor_code, + balance=DESTRUCTOR_BALANCE, ) - pre[coinbase] = Account(balance=0, nonce=1) - # Source: lll - # { [22] (GAS) [[ 10 ]] 1 [[ 11 ]] (CALL (CALLDATALOAD 0) 0 0 0 0 0 ) [[ 1 ]] 0 [[ 2 ]] 0 [[ 3 ]] 0 [[ 4 ]] 0 [[ 5 ]] 0 [[ 6 ]] 0 [[ 7 ]] 0 [[ 8 ]] 0 [[ 23 ]] (SUB @22 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x16, value=Op.GAS) - + Op.SSTORE(key=0xA, value=0x1) - + Op.SSTORE( - key=0xB, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0x4FF65047CE9C85F968689E4369C10003026A41A9, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x1, value=0x0) - + Op.SSTORE(key=0x2, value=0x0) - + Op.SSTORE(key=0x3, value=0x0) - + Op.SSTORE(key=0x4, value=0x0) - + Op.SSTORE(key=0x5, value=0x0) - + Op.SSTORE(key=0x6, value=0x0) - + Op.SSTORE(key=0x7, value=0x0) - + Op.SSTORE(key=0x8, value=0x0) - + Op.SSTORE(key=0x17, value=Op.SUB(Op.MLOAD(offset=0x16), Op.GAS)) - + Op.STOP, - storage={1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xA6CC2CA5611255D50118601AA8ECE6F124FC4C45), # noqa: E501 + # The grant either covers the destructor completely or falls one gas + # short, so the sub-call forfeits its whole grant. + destructor_cost = destructor_code.gas_cost(fork) + if call_succeeds: + grant = destructor_cost + GRANT_MARGIN + inner_consumed = destructor_cost + else: + grant = destructor_cost - 1 + inner_consumed = grant + call_result = 1 if call_succeeds else 0 + + # First GAS read: the delta window opens after the GAS opcode itself. + head = Op.MSTORE( + offset=SNAPSHOT_OFFSET, value=Op.GAS, new_memory_size=MEMORY_SIZE + ) + body = Op.SSTORE( + key=FLAG_SLOT, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, + ) + Op.SSTORE( + key=RESULT_SLOT, + value=Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), + address=destructor, + address_warm=False, + ), + key_warm=False, + original_value=0, + new_value=call_result, ) - # Source: lll - # { (SELFDESTRUCT ) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT( - address=0xA6CC2CA5611255D50118601AA8ECE6F124FC4C45 + for slot in range(1, 9): + body += Op.SSTORE( + key=slot, + value=0x0, + key_warm=False, + original_value=1, + new_value=0, ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x4FF65047CE9C85F968689E4369C10003026A41A9), # noqa: E501 + # Second GAS read closes the window; the head's own GAS cost stands in + # for it in the derived delta (both GAS reads cost the same). + # new_value is a placeholder: an SSTORE's cost depends only on the + # zero/non-zero transition, not the stored magnitude. + tail = Op.SSTORE( + key=GAS_SLOT, + value=Op.SUB( + Op.MLOAD( + offset=SNAPSHOT_OFFSET, + new_memory_size=MEMORY_SIZE, + old_memory_size=MEMORY_SIZE, + ), + Op.GAS, + ), + key_warm=False, + original_value=0, + new_value=1, + ) + target = pre.deploy_contract( + code=head + body + tail + Op.STOP, + storage=dict.fromkeys(range(1, 9), 1), + balance=TARGET_BALANCE, ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={10: 1, 11: 0, 23: 0x107A7}, - balance=0xDE0B6B3A7640000, - ), - sender: Account(nonce=1), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={10: 1, 11: 1, 23: 0x166FA}, - balance=0x1BC16D674EC80000, - ), - sender: Account(nonce=1), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + gas_delta = head.gas_cost(fork) + body.gas_cost(fork) + inner_consumed - tx_data = [ - Hash(0x1F4), - Hash(0x10000), - ] - tx_gas = [10000000] + data = Hash(grant) + # The refund cap is a fifth of the gas actually deducted before + # execution, which excludes the EIP-7623 calldata floor. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, return_cost_deducted_prior_execution=True + ) + executed = intrinsic + gas_delta + tail.gas_cost(fork) + gas_limit = executed + 5_000 + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=data, + gas_limit=gas_limit, + gas_price=GAS_PRICE, ) - state_test(env=env, pre=pre, post=post, tx=tx) + # EIP-3529 caps the refund at a fifth of the executed gas. + total_refund = body.refund(fork) + ( + destructor_code.refund(fork) if call_succeeds else 0 + ) + refund = min(total_refund, executed // 5) + gas_used = executed - refund + + post = { + target: Account( + storage={ + FLAG_SLOT: 1, + RESULT_SLOT: call_result, + GAS_SLOT: gas_delta, + }, + balance=TARGET_BALANCE + + (DESTRUCTOR_BALANCE if call_succeeds else 0), + ), + # EIP-6780: a pre-existing contract is no longer deleted, only + # its balance is transferred. + destructor: ( + ( + Account(balance=0) + if fork.is_eip_enabled(6780) + else Account.NONEXISTENT + ) + if call_succeeds + else Account(balance=DESTRUCTOR_BALANCE, storage={}) + ), + sender: Account(balance=INITIAL_BALANCE - gas_used * GAS_PRICE), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py b/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py index ab754657c7..0d9d5be2f6 100644 --- a/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py +++ b/tests/ported_static/stRefundTest/test_refund_tx_to_suicide.py @@ -1,18 +1,21 @@ """ -Test_refund_tx_to_suicide. +Verify a transaction into a self-destructing contract: the balance +(including the transaction value) moves to the beneficiary and, post +EIP-3529, no self-destruct refund is granted. Ported from: state_tests/stRefundTest/refund_TxToSuicideFiller.json + +@manually-enhanced: Do not overwrite. Beneficiary and budget are derived +(nonexistent account, `code.gas_cost` composite) and the post branches on +EIP-6780 (pre-Cancun the contract is deleted, after it persists). """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -21,59 +24,61 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CONTRACT_BALANCE = 0xDE0B6B3A7640000 +INITIAL_BALANCE = 10**18 +GAS_PRICE = 10 +TX_VALUE = 10 + @pytest.mark.ported_from( ["state_tests/stRefundTest/refund_TxToSuicideFiller.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("London") def test_refund_tx_to_suicide( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_refund_tx_to_suicide.""" - coinbase = Address(0xEB201D2887816E041F6E807E804F64F3A7A226FE) - sender = EOA( - key=0xA2333EEF5630066B928DEA5FD85A239F511B5B067D1441EE7AC290D0122B917B - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Self-destruct moves the balance and grants no refund.""" + beneficiary = pre.nonexistent_account() + code = Op.SELFDESTRUCT( + address=beneficiary, address_warm=False, account_new=True ) - - pre[coinbase] = Account(balance=0, nonce=1) - pre[sender] = Account(balance=0x5F5E100) - # Source: lll - # { (SELFDESTRUCT 0x095e7baea6a6c7c4c2dfeb977efac326af552d87) } - target = pre.deploy_contract( # noqa: F841 - code=Op.SELFDESTRUCT(address=0x95E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87) - + Op.STOP, + target = pre.deploy_contract( + code=code, storage={1: 1}, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0x2BC33A472F0FBA1E30BF2317D07910367908C7F6), # noqa: E501 + balance=CONTRACT_BALANCE, ) + intrinsic = fork.transaction_intrinsic_cost_calculator()(sends_value=True) + executed = intrinsic + code.gas_cost(fork) + gas_limit = executed + 5_000 + + sender = pre.fund_eoa(amount=INITIAL_BALANCE) tx = Transaction( sender=sender, to=target, - data=Bytes(""), - gas_limit=61003, - value=10, + gas_limit=gas_limit, + gas_price=GAS_PRICE, + value=TX_VALUE, ) + # EIP-3529 removed the self-destruct refund entirely. + refund = min(code.refund(fork), executed // 5) + gas_used = executed - refund + post = { - Address(0x095E7BAEA6A6C7C4C2DFEB977EFAC326AF552D87): Account( - storage={}, balance=0xDE0B6B3A764000A + beneficiary: Account(balance=CONTRACT_BALANCE + TX_VALUE), + # EIP-6780: a pre-existing contract is no longer deleted, only + # its balance is transferred. + target: ( + Account(storage={1: 1}, balance=0) + if fork.is_eip_enabled(6780) + else Account.NONEXISTENT + ), + sender: Account( + balance=INITIAL_BALANCE - TX_VALUE - gas_used * GAS_PRICE ), - coinbase: Account(balance=0), - sender: Account(balance=0x5EDB318, nonce=1), - target: Account(storage={1: 1}, balance=0, nonce=0), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py b/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py index fc4b0089a7..f94a727fd0 100644 --- a/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py +++ b/tests/ported_static/stRevertTest/test_loop_calls_depth_then_revert.py @@ -1,8 +1,16 @@ """ -Test_loop_calls_depth_then_revert. +Verify a mutual CALL recursion that terminates by gas exhaustion: two +contracts increment their own counters and call each other until the +EIP-150 63/64 attenuation starves the deepest frame, whose failed store +reverts alone while every ancestor's increment persists. Ported from: state_tests/stRevertTest/LoopCallsDepthThenRevertFiller.json + +@manually-enhanced: Do not overwrite. The reached depth is bounded by the +fixed gas budget (not the 1024 depth limit), so the frame counts are +pinned per gas-schedule era: EIP-8037/EIP-2780 shift the attenuation on +Amsterdam. One address literal remains to break the reference cycle. """ import pytest @@ -10,16 +18,31 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The recursion depth is a function of this budget via EIP-150's 63/64 +# forwarding rule; changing it changes the pinned frame counts. +GAS_BUDGET = 10_000_000 +# Fixed address for the second contract: it must be known before the +# first contract's code (which calls it) can be built. +PONG_ADDRESS = Address(0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2) + + +def loop_code(partner: Address) -> Bytecode: + """Increment the own counter, then recurse into the partner.""" + return ( + Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) + + Op.CALL(address=partner) + + Op.STOP + ) + @pytest.mark.ported_from( ["state_tests/stRevertTest/LoopCallsDepthThenRevertFiller.json"], @@ -29,65 +52,30 @@ def test_loop_calls_depth_then_revert( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_loop_calls_depth_then_revert.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) - - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (CALL (GAS) 0 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.GAS, - address=0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xF59FD1C021541704A4A52C067454304566717666), # noqa: E501 - ) - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (CALL (GAS) 0 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.GAS, - address=0xF59FD1C021541704A4A52C067454304566717666, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0x80D46FA47B41AB46A227915AE4F63559C0D4DFE2), # noqa: E501 - ) + """Only the gas-starved deepest frame of a call loop reverts.""" + ping = pre.deploy_contract(code=loop_code(PONG_ADDRESS)) + pong = pre.deploy_contract(code=loop_code(ping), address=PONG_ADDRESS) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, + to=ping, + gas_limit=GAS_BUDGET, ) + # Completed frames under GAS_BUDGET, pinned per gas-schedule era: + # EIP-8037's state gas for the two first stores trims one frame off + # the depth the 63/64 attenuation allows. + if fork.is_eip_enabled(8037): + ping_frames, pong_frames = 192, 192 + else: + ping_frames, pong_frames = 193, 192 + post = { - target: Account(storage={0: 193}), - addr: Account(storage={0: 192}), + ping: Account(storage={0: ping_frames}), + pong: Account(storage={0: pong_frames}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py b/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py index 1be3fb70d9..9267e1e235 100644 --- a/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py +++ b/tests/ported_static/stRevertTest/test_loop_delegate_calls_depth_then_revert.py @@ -1,8 +1,17 @@ """ -Test_loop_delegate_calls_depth_then_revert. +Verify a mutual DELEGATECALL recursion that terminates by gas +exhaustion: both contracts' code increments the entry contract's counter +(the storage context never changes) until the EIP-150 63/64 attenuation +starves the deepest frame, whose failed store reverts alone while every +ancestor's increment persists. Ported from: state_tests/stRevertTest/LoopDelegateCallsDepthThenRevertFiller.json + +@manually-enhanced: Do not overwrite. The reached depth is bounded by the +fixed gas budget (not the 1024 depth limit), so the frame count is +pinned per gas-schedule era: EIP-8037/EIP-2780 shift the attenuation on +Amsterdam. One address literal remains to break the reference cycle. """ import pytest @@ -10,16 +19,31 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# The recursion depth is a function of this budget via EIP-150's 63/64 +# forwarding rule; changing it changes the pinned frame count. +GAS_BUDGET = 10_000_000 +# Fixed address for the second contract: it must be known before the +# first contract's code (which delegate-calls it) can be built. +PONG_ADDRESS = Address(0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228) + + +def loop_code(partner: Address) -> Bytecode: + """Increment the context counter, then recurse into the partner.""" + return ( + Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) + + Op.DELEGATECALL(address=partner) + + Op.STOP + ) + @pytest.mark.ported_from( ["state_tests/stRevertTest/LoopDelegateCallsDepthThenRevertFiller.json"], @@ -29,63 +53,29 @@ def test_loop_delegate_calls_depth_then_revert( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_loop_delegate_calls_depth_then_revert.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) - - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (DELEGATECALL (GAS) 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.DELEGATECALL( - gas=Op.GAS, - address=0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xB0923C4A632DE291FCDAC653E6C6CC2B4E4CDFA8), # noqa: E501 - ) - # Source: lll - # { [[0]] (+ (SLOAD 0) 1) (DELEGATECALL (GAS) 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.DELEGATECALL( - gas=Op.GAS, - address=0xB0923C4A632DE291FCDAC653E6C6CC2B4E4CDFA8, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xF798CB78490DA31DFACDCD1F2B3FB1948BB2B228), # noqa: E501 - ) + """Only the gas-starved deepest frame of a delegate loop reverts.""" + ping = pre.deploy_contract(code=loop_code(PONG_ADDRESS)) + pong = pre.deploy_contract(code=loop_code(ping), address=PONG_ADDRESS) + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, + to=ping, + gas_limit=GAS_BUDGET, ) + # Completed frames under GAS_BUDGET, pinned per gas-schedule era: + # every frame increments the entry contract's counter because + # DELEGATECALL keeps the storage context; the partner's own storage + # is never touched. EIP-8037's state gas for the first store shifts + # the depth the 63/64 attenuation allows. + frames = 385 if fork.is_eip_enabled(8037) else 386 + post = { - target: Account(storage={0: 386}), - addr: Account(storage={}), + ping: Account(storage={0: frames}), + pong: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py b/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py index 11c3aaa568..b123500878 100644 --- a/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py +++ b/tests/ported_static/stRevertTest/test_revert_depth_create_address_collision.py @@ -1,222 +1,183 @@ """ -Test_revert_depth_create_address_collision. +Verify revert propagation around a nested CREATE whose target address +collides with an existing contract - its own caller: the creating frame +always fails and the collided account survives untouched, while the +caller completes only when its budget covers the forfeited grant. Ported from: state_tests/stRevertTest/RevertDepthCreateAddressCollisionFiller.json + +@manually-enhanced: Do not overwrite. Restores the collision the machine +port lost: the caller is deployed at the creator's CREATE address (as in +the original filler). Grants and budgets derive from fork composites and +the collided account's code, nonce and storage are pinned in every arm. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, + compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas left in the creator frame when its CREATE executes: the collision +# consumes it, so the following store can never be paid. +BURN_MARGIN = 5_000 +# Head room on top of a derived budget. +BUDGET_MARGIN = 5_000 +# Gas left at the caller's call site in the starved arm: too little for +# any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRevertTest/RevertDepthCreateAddressCollisionFiller.json"], ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], + "oversized_ask", + [False, True], + ids=["modest_ask", "oversized_ask"], ) +@pytest.mark.parametrize( + "ample_budget", + [False, True], + ids=["starved", "ample"], +) +@pytest.mark.parametrize("tx_value", [1, 0], ids=["v1", "v0"]) @pytest.mark.pre_alloc_mutable def test_revert_depth_create_address_collision( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + oversized_ask: bool, + ample_budget: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create_address_collision.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 + """A CREATE address collision leaves the collided account intact.""" + creator_store = Op.SSTORE( + key=0x2, value=0x8, key_warm=False, original_value=0, new_value=8 ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + create_code = Op.POP( + Op.CREATE( + value=0x0, + offset=0x0, + size=0x0, + init_code_size=0, + new_memory_size=0, + ) ) - - pre[sender] = Account(balance=0xE8D4A51000) - # Source: lll - # { [[2]] 8 (CREATE 0 0 0) [[3]] 12} - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - address=Address(0xB1B49241A4ECF7860872E686090781C906B1B437), # noqa: E501 + creator_tail = Op.SSTORE( + key=0x3, value=0xC, key_warm=False, original_value=0, new_value=0xC ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0 0 0 0 0) [[4]] 12 } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=0xB1B49241A4ECF7860872E686090781C906B1B437, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, - address=Address(0x97E33A176B7C8D61B356D1C170AC2119D28867DF), # noqa: E501 + creator = pre.deploy_contract( + code=creator_store + create_code + creator_tail + Op.STOP ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={0: 1, 4: 12}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - balance=5, - nonce=54, - ), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - target: Account( - storage={}, - code=bytes.fromhex( - "60016000556000600060006000600073b1b49241a4ecf7860872e686090781c906b1b437600035f1600155600c60045500" # noqa: E501 - ), - nonce=54, - ), - addr: Account(storage={}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + head_store = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), address=creator, address_warm=False + ) + # The creator's frame always fails, so the call result is always 0. + call_store = Op.SSTORE( + key=0x1, + value=call_code, + key_warm=False, + original_value=0, + new_value=0, + ) + tail_store = Op.SSTORE( + key=0x4, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + caller_code = head_store + call_store + tail_store + Op.STOP + # The caller sits exactly at the creator's CREATE address: the + # nested creation collides with the very contract that called it. + caller = pre.deploy_contract( + code=caller_code, + address=compute_create_address(address=creator, nonce=1), + ) - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 160000] - tx_value = [1, 0] + intrinsic_calculator = fork.transaction_intrinsic_cost_calculator() + modest_grant = (creator_store + create_code).gas_cost(fork) + BURN_MARGIN + gas_limit = ( + intrinsic_calculator( + calldata=Hash(modest_grant), + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + + head_store.gas_cost(fork) + + call_store.gas_cost(fork) + + modest_grant + + tail_store.gas_cost(fork) + + BUDGET_MARGIN + ) + # An oversized ask is clamped to the EIP-150 63/64 cap, leaving the + # caller only 1/64 of its remaining gas: it can never complete. + grant = gas_limit if oversized_ask else modest_grant + data = Hash(grant) + intrinsic = intrinsic_calculator( + calldata=data, + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + if ample_budget: + available = ( + gas_limit + - intrinsic + - head_store.gas_cost(fork) + - call_code.gas_cost(fork) + ) + if oversized_ask: + store_costs = ( + call_store.gas_cost(fork) + - call_code.gas_cost(fork) + + tail_store.gas_cost(fork) + ) + assert available // 64 < store_costs, "caller must fail" + else: + assert grant <= available - available // 64, "grant is granted" + else: + # The caller reaches its call with only STARVE_MARGIN left: the + # creator halts at its first store and the retained 1/64 cannot + # pass the EIP-2200 stipend check, so everything reverts. + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_code.gas_cost(fork) + + STARVE_MARGIN + ) + assert STARVE_MARGIN - STARVE_MARGIN // 64 <= 2300, "creator halts" + assert STARVE_MARGIN // 64 <= 2300, "caller store must halt" + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + to=caller, + data=data, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + caller_completes = ample_budget and not oversized_ask + post = { + # The collided account survives with its code and nonce intact. + caller: Account( + code=caller_code, + nonce=1, + storage={0: 1, 4: 0xC} if caller_completes else {}, + balance=tx_value if caller_completes else 0, + ), + creator: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py b/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py index 3cd7ee838c..9eca41eee7 100644 --- a/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py +++ b/tests/ported_static/stRevertTest/test_revert_depth_create_oog.py @@ -1,195 +1,180 @@ """ -Test_revert_depth_create_oog. +Verify revert propagation around a nested CREATE that runs out of gas: +a sub-call either funds its CREATE-then-store sequence completely, or +runs out of gas after the CREATE, reverting the created account but not +the caller; a starved outer budget reverts everything. Ported from: state_tests/stRevertTest/RevertDepthCreateOOGFiller.json + +@manually-enhanced: Do not overwrite. The sub-call grants and both +transaction budgets derive from fork composites (EIP-8037 state gas is +tracked instead of pinned), all addresses are dynamic, and every account +including the created one is pinned in each arm. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +# Gas left in the creator frame after its CREATE: enough to reach the +# following store, never enough to pay for it. +PARTIAL_MARGIN = 5_000 +# Head room on top of a derived budget. +BUDGET_MARGIN = 5_000 +# Gas left at the caller's call site in the starved arm: too little for +# any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( ["state_tests/stRevertTest/RevertDepthCreateOOGFiller.json"], ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0-v0", - ), - pytest.param( - 0, - 0, - 1, - id="d0-g0-v1", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1-v0", - ), - pytest.param( - 0, - 1, - 1, - id="d0-g1-v1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0-v0", - ), - pytest.param( - 1, - 0, - 1, - id="d1-g0-v1", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1-v0", - ), - pytest.param( - 1, - 1, - 1, - id="d1-g1-v1", - ), - ], + "full_grant", + [False, True], + ids=["partial_grant", "full_grant"], +) +@pytest.mark.parametrize( + "ample_budget", + [False, True], + ids=["starved", "ample"], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize("tx_value", [1, 0], ids=["v1", "v0"]) def test_revert_depth_create_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + full_grant: bool, + ample_budget: bool, + tx_value: int, ) -> None: - """Test_revert_depth_create_oog.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xA000000000000000000000000000000000000000) - contract_1 = Address(0xB000000000000000000000000000000000000000) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, + """An out-of-gas CREATE frame reverts alone; a starved caller fully.""" + creator_store = Op.SSTORE( + key=0x2, value=0x8, key_warm=False, original_value=0, new_value=8 ) - - # Source: lll - # { [[2]] 8 (CREATE 0 0 0) [[3]] 12} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x8) - + Op.POP(Op.CREATE(value=0x0, offset=0x0, size=0x0)) - + Op.SSTORE(key=0x3, value=0xC) - + Op.STOP, - nonce=0, - ) - # Source: lll - # { [[0]] 1 [[1]] (CALL (CALLDATALOAD 0) 0xb000000000000000000000000000000000000000 0 0 0 0 0) [[4]] 12 } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=0x1) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.CALLDATALOAD(offset=0x0), - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + create_code = Op.POP( + Op.CREATE( + value=0x0, + offset=0x0, + size=0x0, + init_code_size=0, + new_memory_size=0, ) - + Op.SSTORE(key=0x4, value=0xC) - + Op.STOP, - balance=5, - nonce=54, ) + creator_tail = Op.SSTORE( + key=0x3, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + creator_code = creator_store + create_code + creator_tail + Op.STOP + creator = pre.deploy_contract(code=creator_code) + created = compute_create_address(address=creator, nonce=1) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=contract_1, nonce=0): Account( - nonce=1 - ), - contract_0: Account(storage={0: 1, 1: 1, 4: 12}), - contract_1: Account(storage={2: 8, 3: 12}), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, - contract_0: Account(storage={0: 1, 4: 12}), - contract_1: Account(storage={}), - }, - }, - { - "indexes": {"data": [0, 1], "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address( - address=contract_1, nonce=0 - ): Account.NONEXISTENT, - contract_0: Account(storage={}), - contract_1: Account(storage={}), - }, - }, - ] + # The grant covers the creator completely, or only up to and + # including its CREATE, leaving too little for the following store. + if full_grant: + grant = creator_code.gas_cost(fork) + BUDGET_MARGIN + inner_consumed = creator_code.gas_cost(fork) + else: + grant = (creator_store + create_code).gas_cost(fork) + PARTIAL_MARGIN + inner_consumed = grant + inner_succeeds = ample_budget and full_grant + inner_fails_reaching_create = ample_budget and not full_grant - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + head_store = Op.SSTORE( + key=0x0, value=0x1, key_warm=False, original_value=0, new_value=1 + ) + call_code = Op.CALL( + gas=Op.CALLDATALOAD(offset=0x0), address=creator, address_warm=False + ) + call_store = Op.SSTORE( + key=0x1, + value=call_code, + key_warm=False, + original_value=0, + new_value=1 if inner_succeeds else 0, + ) + tail_store = Op.SSTORE( + key=0x4, value=0xC, key_warm=False, original_value=0, new_value=0xC + ) + caller = pre.deploy_contract( + code=head_store + call_store + tail_store + Op.STOP + ) - tx_data = [ - Hash(0xEA60), - Hash(0x1EA60), - ] - tx_gas = [110000, 180000] - tx_value = [1, 0] + data = Hash(grant) + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, + sends_value=tx_value > 0, + return_cost_deducted_prior_execution=True, + ) + if ample_budget: + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_store.gas_cost(fork) + + inner_consumed + + tail_store.gas_cost(fork) + + BUDGET_MARGIN + ) + # The requested grant must fit under the EIP-150 63/64 cap. + available = ( + gas_limit + - intrinsic + - head_store.gas_cost(fork) + - call_code.gas_cost(fork) + ) + assert grant <= available - available // 64, "grant must be granted" + else: + # The caller reaches its call with only STARVE_MARGIN left: the + # creator halts at its first store and the retained 1/64 cannot + # pass the EIP-2200 stipend check, so everything reverts. + gas_limit = ( + intrinsic + + head_store.gas_cost(fork) + + call_code.gas_cost(fork) + + STARVE_MARGIN + ) + assert STARVE_MARGIN - STARVE_MARGIN // 64 <= 2300, "creator halts" + assert STARVE_MARGIN // 64 <= 2300, "caller store must halt" + sender = pre.fund_eoa() tx = Transaction( sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + to=caller, + data=data, + gas_limit=gas_limit, + value=tx_value, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post: dict + if inner_succeeds: + post = { + created: Account(nonce=1), + caller: Account(storage={0: 1, 1: 1, 4: 0xC}, balance=tx_value), + creator: Account(storage={2: 8, 3: 0xC}), + } + elif inner_fails_reaching_create: + post = { + created: Account.NONEXISTENT, + caller: Account(storage={0: 1, 4: 0xC}, balance=tx_value), + creator: Account(storage={}), + } + else: + post = { + created: Account.NONEXISTENT, + caller: Account(storage={}, balance=0), + creator: Account(storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py b/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py index a49cc7b38b..2f6ab5355a 100644 --- a/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py +++ b/tests/ported_static/stRevertTest/test_revert_opcode_in_calls_on_non_empty_return_data.py @@ -1,35 +1,46 @@ """ -Test: this test checks that the returndata buffer is changed when a... +Verify that a reverting callee's return data replaces the empty return +data left by a previously failed call: each prober records the failed +call's result and RETURNDATASIZE, for CALL, CALLCODE, DELEGATECALL and a +nested CALL chain, with an ample and a starved transaction budget. Ported from: state_tests/stRevertTest/RevertOpcodeInCallsOnNonEmptyReturnDataFiller.json -@manually-enhanced: Do not overwrite. Inner-CALL/DELEGATECALL gas -bumped on Amsterdam to cover EIP-8037 state-gas spill into regular gas; -pre-EIP-8037 unchanged. +@manually-enhanced: Do not overwrite. Sub-calls forward all gas and the +ample arm omits the gas limit (maxing the EIP-8037 reservoir), replacing +per-fork gas bumps; the starved budget derives from fork composites; all +addresses are dynamic and every contract is pinned in the post. """ import pytest from execution_testing import ( - EOA, Account, Address, Alloc, - Environment, + Fork, Hash, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) +from execution_testing.vm import Bytecode, Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +RESULT_SLOT = 0x0 +RETURN_DATA_SIZE_SLOT = 0x2 +NESTED_RESULT_SLOT = 0x4 +NESTED_RETURN_DATA_SIZE_SLOT = 0x5 +ENTRY_SLOT = 0xA +ENTRY_SLOT_INITIAL = 255 +# A zero-gas grant: the prelude call must fail without touching the +# return data buffer. +FAILING_CALL_GAS = 0 +# Gas left at the entry frame's call site in the starved arm: enough to +# start the call, too little for any frame to complete. +STARVE_MARGIN = 1_000 + @pytest.mark.ported_from( [ @@ -38,396 +49,146 @@ ) @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="d0-g0", - ), - pytest.param( - 0, - 1, - 0, - id="d0-g1", - ), - pytest.param( - 1, - 0, - 0, - id="d1-g0", - ), - pytest.param( - 1, - 1, - 0, - id="d1-g1", - ), - pytest.param( - 2, - 0, - 0, - id="d2-g0", - ), - pytest.param( - 2, - 1, - 0, - id="d2-g1", - ), - pytest.param( - 3, - 0, - 0, - id="d3-g0", - ), - pytest.param( - 3, - 1, - 0, - id="d3-g1", - ), - ], + "call_op", + [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, None], + ids=["call", "callcode", "delegatecall", "nested_call"], +) +@pytest.mark.parametrize( + "ample_gas", + [True, False], + ids=["ample", "starved"], ) -@pytest.mark.pre_alloc_mutable def test_revert_opcode_in_calls_on_non_empty_return_data( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_op: Op | None, + ample_gas: bool, ) -> None: - """Test: tis test checks that the returndata buffer is changed when a...""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = EOA( - key=0x4F31B3206FBF0E0E598B9B1A7D8AC86302A0FF1D8930738F1BEBAE9B67173E52 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - pre[sender] = Account(balance=0xE8D4A51000) - # EIP-8037 inner-CALL/DELEGATECALL gas bumps: original values - # restored for pre-EIP-8037 forks; bumped for state-gas spill on - # Amsterdam. - inner_call_gas = 50000 - deeper_call_gas = 100000 - deepest_call_gas = 260000 - if fork.is_eip_enabled(8037): - inner_call_gas = 100000 - deeper_call_gas = 1000000 - deepest_call_gas = 1000000 - # Source: lll - # { [[1]] 12 (REVERT 0 1) [[3]] 13 } - addr_6 = pre.deploy_contract( # noqa: F841 + """A revert's return data is observed after a failed call.""" + # Reverts one byte of return data; the store before the REVERT is + # undone and the code after it must never run. + reverter = pre.deploy_contract( code=Op.SSTORE(key=0x1, value=0xC) + Op.REVERT(offset=0x0, size=0x1) + Op.SSTORE(key=0x3, value=0xD) + Op.STOP, - balance=1, - nonce=0, - address=Address(0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B), # noqa: E501 ) - # Source: lll - # { [1] 12 (RETURN 0 64) } - addr_7 = pre.deploy_contract( # noqa: F841 + # Would return 64 bytes, but is only ever called with zero gas. + returner = pre.deploy_contract( code=Op.MSTORE(offset=0x1, value=0xC) - + Op.RETURN(offset=0x0, size=0x40) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0x127EAF7E31D691A8393B7A2F84A6E94372190C01), # noqa: E501 + + Op.RETURN(offset=0x0, size=0x40), ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (DELEGATECALL 50000 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_3 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.DELEGATECALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xF20CCAF271BEAA36E7CF4C9CED2867FAC9558F14), # noqa: E501 + + prelude = Op.POP( + Op.CALL(gas=FAILING_CALL_GAS, address=returner, address_warm=False) ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALLCODE 50000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE( - key=0x0, - value=Op.CALLCODE( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + def prober_code( + op: Op, callee: Address, result_slot: int, rds_slot: int + ) -> Bytecode: + """Call the callee and record its result and RETURNDATASIZE.""" + return ( + prelude + + Op.SSTORE(key=result_slot, value=op(address=callee)) + + Op.SSTORE(key=rds_slot, value=Op.RETURNDATASIZE) + + Op.STOP ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xC9DA6CD8413F64323F12CD44C99671F280F15E1C), # noqa: E501 + + prober_call = pre.deploy_contract( + code=prober_code(Op.CALL, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT) ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[4]] (CALL 50000 0 0 0 0 0) [[5]] (RETURNDATASIZE) } # noqa: E501 - addr_5 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + prober_callcode = pre.deploy_contract( + code=prober_code( + Op.CALLCODE, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE( - key=0x4, - value=Op.CALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x5, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xEA519C47889074E6378B0D83747F2C3EA0B9CBC9), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALL 50000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + prober_delegatecall = pre.deploy_contract( + code=prober_code( + Op.DELEGATECALL, reverter, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=inner_call_gas, - address=0x93A599BDE9A3B6390AFDB06952AA5EC0B8C44F3B, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0xE73611B5B479B30C93AC377AEB3BFB199764F3C3), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[10]] (CALL 260000 (CALLDATALOAD 0) 0 0 0 0 0)} # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + inner_prober = pre.deploy_contract( + code=prober_code( + Op.CALL, + reverter, + NESTED_RESULT_SLOT, + NESTED_RETURN_DATA_SIZE_SLOT, ) - + Op.SSTORE( - key=0xA, - value=Op.CALL( - gas=deepest_call_gas, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - storage={10: 255}, - balance=1, - nonce=0, - address=Address(0x172A8F572404293AA810685DFDC6F740C300CC4B), # noqa: E501 ) - # Source: lll - # { (CALL 0 0 0 0 0 0) [[0]] (CALL 100000 0 0 0 0 0) [[2]] (RETURNDATASIZE) } # noqa: E501 - addr_4 = pre.deploy_contract( # noqa: F841 - code=Op.POP( - Op.CALL( - gas=0x0, - address=0x127EAF7E31D691A8393B7A2F84A6E94372190C01, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) + prober_nested = pre.deploy_contract( + code=prober_code( + Op.CALL, inner_prober, RESULT_SLOT, RETURN_DATA_SIZE_SLOT ) - + Op.SSTORE( - key=0x0, - value=Op.CALL( - gas=deeper_call_gas, - address=0xEA519C47889074E6378B0D83747F2C3EA0B9CBC9, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE(key=0x2, value=Op.RETURNDATASIZE) - + Op.STOP, - balance=1, - nonce=0, - address=Address(0x6BACDFA8216DBB2A09819F8739E57AE3574C9FFF), # noqa: E501 ) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 0, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr: Account(storage={}), - }, - }, - { - "indexes": {"data": 1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_2: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr_2: Account(storage={}), - }, - }, - { - "indexes": {"data": 2, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_3: Account(storage={2: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 2, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - addr_3: Account(storage={}), - }, - }, - { - "indexes": {"data": 3, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 1}), - addr_4: Account(storage={0: 1}, nonce=0), - addr_5: Account(storage={5: 1}, nonce=0), - }, - }, - { - "indexes": {"data": 3, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": { - addr_6: Account(storage={}), - target: Account(storage={10: 255}), - addr_4: Account(storage={0: 0}, nonce=0), - addr_5: Account(storage={5: 0}, nonce=0), - }, - }, - ] + big_call = Op.CALL(address=Op.CALLDATALOAD(offset=0x0), address_warm=False) + entry = pre.deploy_contract( + code=prelude + Op.SSTORE(key=ENTRY_SLOT, value=big_call) + Op.STOP, + storage={ENTRY_SLOT: ENTRY_SLOT_INITIAL}, + ) - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + probers = { + Op.CALL: prober_call, + Op.CALLCODE: prober_callcode, + Op.DELEGATECALL: prober_delegatecall, + None: prober_nested, + } + prober = probers[call_op] + data = Hash(prober, left_padding=True) - tx_data = [ - Hash(addr, left_padding=True), - Hash(addr_2, left_padding=True), - Hash(addr_3, left_padding=True), - Hash(addr_4, left_padding=True), - ] - tx_gas = [860000, 28000] + sender = pre.fund_eoa() + if ample_gas: + tx = Transaction(sender=sender, to=entry, data=data) + else: + # The entry frame reaches its call with only STARVE_MARGIN left: + # the prober cannot even pay its prelude, and the retained 1/64 + # cannot pass the EIP-2200 stipend check, so everything reverts. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + calldata=data, return_cost_deducted_prior_execution=True + ) + starved = ( + intrinsic + + prelude.gas_cost(fork) + + big_call.gas_cost(fork) + + STARVE_MARGIN + ) + forwarded = STARVE_MARGIN - STARVE_MARGIN // 64 + assert forwarded < prelude.gas_cost(fork), "prober must starve" + assert STARVE_MARGIN // 64 <= 2300, "entry store must halt" + tx = Transaction(sender=sender, to=entry, data=data, gas_limit=starved) - tx = Transaction( - sender=sender, - to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, - ) + untouched = { + reverter: Account(storage={}), + prober_call: Account(storage={}), + prober_callcode: Account(storage={}), + prober_delegatecall: Account(storage={}), + inner_prober: Account(storage={}), + prober_nested: Account(storage={}), + } + if not ample_gas: + # The transaction runs out of gas at the entry frame: everything + # reverts. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: ENTRY_SLOT_INITIAL}), + } + elif call_op is None: + # The nested prober's callee completes (its own probe fails), so + # the outer call succeeds and returns no data. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: 1}), + prober_nested: Account(storage={RESULT_SLOT: 1}), + inner_prober: Account(storage={NESTED_RETURN_DATA_SIZE_SLOT: 1}), + } + else: + # The probed call reverts with one byte of return data: result 0, + # RETURNDATASIZE 1. + post = { + **untouched, + entry: Account(storage={ENTRY_SLOT: 1}), + prober: Account(storage={RETURN_DATA_SIZE_SLOT: 1}), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx)