diff --git a/.claude/commands/enhance-ported-test.md b/.claude/commands/enhance-ported-test.md index f6d3f6852e5..0d0404b2a04 100644 --- a/.claude/commands/enhance-ported-test.md +++ b/.claude/commands/enhance-ported-test.md @@ -42,11 +42,12 @@ so a failure is attributable. - **Checkpoint / done:** fill the whole `valid_from` range (omit `--fork`) so all deployed forks are exercised. - **Probe the future fork:** explicitly `--fork Amsterdam` (or the latest fork - that enables new EIPs). A ported test listed in `amsterdam_skip_list.txt` will - always show `sss` there — to see its *real* behavior, temporarily remove its - entry from that file, fill, then restore (or, once fixed, remove it for good — - see Finishing). A gas/state-cost change there is the most likely future - breakage. + that enables new EIPs). A gas/state-cost change there is the most likely + future breakage. (Historical note: broken tests used to be parked in a + `tests/ported_static/amsterdam_skip_list.txt` consumed by a local conftest; + the list was emptied and both were removed. If a future fork's repricing + breaks tests en masse, the same parking pattern — a substring-matched skip + list plus a `pytest_collection_modifyitems` hook — is in git history.) - **`fill` output:** writes to `./fixtures` (`--clean` resets it), or pass `--output ` for a scratch location. Do **not** use `-o` — that is pytest's `--override-ini`, not the output dir. @@ -77,8 +78,8 @@ gas the tx receives, so the body executes fully. See `write-test.md` "Transactio - **Gas-snapshot tests are gas-sensitive.** If the post asserts a stored `GAS` reading or a `SUB(@gas_before, GAS)` delta (legacy slots `0` / `0x64`), the test *measures gas* — handle it under step 10 (preserve via `CodeGasMeasure`), - do not just strip `gas_limit`. This is the dominant `amsterdam_skip_list.txt` - shape: the stored gas value is exactly what EIP-8037 re-prices and breaks. + do not just strip `gas_limit`. This was the dominant skip-list shape: + the stored gas value is exactly what EIP-8037 re-prices and breaks. - **EIP-8037 caveat:** when you omit `gas_limit` on a test that *measures* an operation incurring **state gas** (account creation, storage writes), add `state_gas_reservoir=0` to the tx, or that state gas is silently dropped from @@ -294,8 +295,8 @@ ties a `CREATE`'s `size` operand to the memory/gas math that depends on it. on `test_make_money`. ### 10. (Gas-subject / gas-snapshot tests) Replace hardcoded gas with dynamic calculation -Covers both tests that *assert* a gas amount and the dominant -`amsterdam_skip_list.txt` shape: a legacy `GAS` snapshot / `SUB(@gas_before, +Covers both tests that *assert* a gas amount and the dominant broken-port +shape: a legacy `GAS` snapshot / `SUB(@gas_before, GAS)` delta stored to slot `0`/`0x64`. That stored value is *why* EIP-8037 breaks the test, but it is real coverage — **preserve and fork-robustify it, do not drop it.** @@ -386,6 +387,20 @@ previous_bytes=)`; EIP-3860 init-code words → `fork.gas_costs().CODE_INIT_PER_ * ceil(size/32)`. You can also call `.gas_cost` / `.regular_cost` / `.state_cost` on exactly the measured bytecode. +**Reservoir-less sub-calls pay state gas from their regular grant.** With +the tx reservoir at 0, a sub-frame's state charges spill from its own +`gas_left` — a delegate that does one first-set SSTORE needs its *whole* +~111k inside the forwarded grant on Amsterdam, not just the ~13k regular +part. Size derived sub-call budgets from the callee composite's full +`gas_cost(fork)`. Corollaries: (a) a *failed* sub-frame contributes its +entire forfeited grant to the parent's measured window, not its "cost"; +(b) `SSTORE(flag, )` silently degrades to a ~3k no-op store when +the call fails — the flag reads 0 and no state gas is charged, which can +mask a broken callee behind a plausible-looking measurement. Validated on +`test_new_gas_price_for_codes` (delegate budget derived; failed value +calls return their stipends: subtract one `CALL_STIPEND` per failed +value-bearing call from window measurements). + **Nested / callee-side measurements.** When the measured op is a `CALL` whose callee does real work, the measured cost = `call_code.gas_cost(fork) + callee_code.gas_cost(fork)` (the CALL's own cost plus what the callee consumed). @@ -402,6 +417,88 @@ transition, not the magnitude — which also breaks the `forward_gas`/`new_value circularity.) Set `state_gas_reservoir=0` so the state gas is captured. Validated on `test_raw_call_gas`. +**An expensive store after a callee that eats all forwarded gas — pre-write +the slot.** When a frame must SSTORE a result *after* a subcall that +deliberately consumes its whole 63/64 grant (an OOG-probe callee), the frame +retains only 1/64 — under EIP-8037 that cannot afford a cold zero→nonzero +store (~111k), and pre-8037 it often couldn't afford the cold 2.2k either +(making the ported `{slot: 0}` expectation vacuous: caller-OOG and +callee-failure were indistinguishable). Fix: write a sentinel to the slot +*before* the call (paying cold + state with the full budget), then store +`BASE + result` after it — now a dirty-warm write (100 gas) the retention +always covers, and the three outcomes (success `BASE+1`, failure `BASE`, +caller OOG `sentinel`) are all distinct. Validated on +`test_static_execute_call_that_ask_fore_gas_then_trabsaction_has`. +**Caveat — EIP-2200's stipend rule caps this trick.** Any SSTORE (even a +100-gas dirty-warm one) exceptionally halts unless `gas_left > 2300` +(Istanbul+), so the 1/64 retention must exceed ~2400, i.e. the pre-call +budget must exceed ~154k. When the scenario *requires* a smaller budget +(e.g. a starved arm whose forwarded gas must undercut the callee's cost), +no post-call SSTORE is possible at all: write the sentinel *before* the +call and put nothing but a `POP` after it — frame completion (the account +persists with the sentinel) plus the callee-side observable already +separate the outcomes. Validated on +`test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided`. + +**Refund-cap derivations need the EIP-7623 kwarg.** The EIP-3529 cap's +base is the gas deducted before execution, which excludes the calldata +floor: pass `return_cost_deducted_prior_execution=True` to the intrinsic +calculator whenever the tx has calldata, or the derived `executed` (and +the cap) overstate. Validated on `test_refund_suicide50procent_cap`. + +**A CREATE address collision burns the child's gas allowance** (the +EIP-684 path): the withheld child grant is consumed, nothing is created, +and under EIP-8037 the new-account state charge is refunded. Useful to +build always-failing creator frames with predictable consumption. +Validated on `test_revert_depth_create_address_collision`. + +**Loop-to-depth-1024 cannot replace loop-to-OOG.** With 63/64 +attenuation, reaching depth 1024 needs ~e^16 × the terminal gas — no +legal budget gets there. For call-loop depth tests the honest shape is a +fixed named budget with per-gas-schedule-era pinned depth counts, each +shift explained (±1 frame ≈ 64·ln(cost ratio)). Validated on +`test_loop_calls_depth_then_revert`. + +**Framework wart: the SSTORE dirty-rewrite composite prices 100 on every +fork**, but Constantinople/Petersburg charge 5,000 for a dirty re-store — +a derived budget that must survive pre-Istanbul forks needs an explicit +headroom constant for it (named, commented). Observed on +`test_revert_depth_create_address_collision`'s ConstantinopleFix sweep. + +**EIP-8037 repriced the code deposit's regular part — boundaries beware.** +On 8037 forks the deposit charges only the keccak word cost +(`OPCODE_KECCAK256_PER_WORD * ceil32(len)/32`, ~6 gas) as regular gas plus +`len * 1530` state; `fork.gas_costs().CODE_DEPOSIT_PER_BYTE` (200) is the +*pre-8037* constant. Using 200/byte in a *sufficiency* budget merely +overshoots (safe); using it in a one-gas-short *boundary* silently funds +the deposit on Amsterdam. Branch on `fork.is_eip_enabled(8037)` for exact +deposit boundaries. Validated on +`test_create_oo_gafter_init_code_returndata_size`. + +**Match the intrinsic calculator's kwargs to the transaction's shape.** +`fork.transaction_intrinsic_cost_calculator()()` defaults to +`sends_value=False`; under EIP-2780 a value-bearing transaction's intrinsic +includes the folded value-transfer cost (~5.9k), so a derived budget or +GAS-observation formula silently skews by that amount on Amsterdam only. +Pass `sends_value=True` when the tx carries value — or drop an incidental +tx `value` entirely (step 5) so the default holds. Validated on +`test_store_gas_on_create`. + +**A creation transaction's top frame pays new-account state gas +(EIP-8037) — but only for a fresh target.** When deriving a create-tx +budget, the intrinsic calculator does not include the created account's +state gas — add +`fork.transaction_top_frame_state_gas(contract_creation=True)` (183,600 on +Amsterdam, 0 before) or the whole creation silently OOGs only on the +future fork. Exception: `prepare_dispatch` charges it only when the +target's *pre-state* account is `EMPTY_ACCOUNT` — a prefunded create +address pays nothing (validated on +`test_out_of_gas_prefunded_contract_creation`, whose budgets omit the +term). A nested CREATE's new-account state is charged to the parent +before the 63/64 withhold and refunded if the child fails, so a derived +budget must cover its *peak* (use the composite `gas_cost(fork)`), even +on paths where the net is zero. + **Measuring forwarded gas / the EIP-150 63/64 rule (the `*_gas_ask` shape).** Ported fillers probe "how much gas does a subcall receive when it asks for more than is available" by pinning an absolute forwarded amount — fork-fragile, @@ -499,11 +596,10 @@ Not yet covered by a validated walkthrough; figure out and append when hit: ## Finishing -**Remove the skip-list entry.** Once the test passes on the future fork, delete -its line from `tests/ported_static/amsterdam_skip_list.txt` and decrement both -its per-directory count header (`# stXxx (N)`) and the `# Total entries:` count. -Confirm with a full-range fill (`--fork` omitted) with the entry gone — that is -the definition of done. +**Confirm with a full-range fill** (`--fork` omitted) — every deployed fork +green is the definition of done. (If a skip list is ever reintroduced for a +future fork, also delete the test's entry and keep its count headers +accurate.) **Final sweep checklist** — each of these has been missed in practice; check them one by one before calling the test done: diff --git a/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py index 128c162cc9d..01a5c2d6eda 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/shanghai/eip_3860.py @@ -7,10 +7,16 @@ https://eips.ethereum.org/EIPS/eip-3860 """ +from typing import List, Sized + +from execution_testing.base_types import AccessList, Bytes +from execution_testing.base_types.conversions import BytesConvertible from execution_testing.vm import OpcodeBase -from ....base_fork import BaseFork +from .....recipient_type import RecipientType +from ....base_fork import BaseFork, TransactionIntrinsicCostCalculator from ....gas_costs import GasCosts +from ...helpers import ceiling_division class EIP3860(BaseFork): @@ -21,6 +27,46 @@ def max_initcode_size(cls) -> int: """Initcode size is limited.""" return 0xC000 + @classmethod + def transaction_intrinsic_cost_calculator( + cls, + ) -> TransactionIntrinsicCostCalculator: + """ + The intrinsic cost of a creation transaction meters its init code. + """ + super_fn = super(EIP3860, cls).transaction_intrinsic_cost_calculator() + gas_costs = cls.gas_costs() + + def fn( + *, + calldata: BytesConvertible = b"", + contract_creation: bool = False, + access_list: List[AccessList] | None = None, + authorization_list_or_count: Sized | int | None = None, + return_cost_deducted_prior_execution: bool = False, + sends_value: bool = False, + recipient_type: RecipientType = RecipientType.CONTRACT, + ) -> int: + intrinsic_cost: int = super_fn( + calldata=calldata, + contract_creation=contract_creation, + access_list=access_list, + authorization_list_or_count=authorization_list_or_count, + return_cost_deducted_prior_execution=( + return_cost_deducted_prior_execution + ), + sends_value=sends_value, + recipient_type=recipient_type, + ) + if contract_creation: + intrinsic_cost += ( + gas_costs.CODE_INIT_PER_WORD + * ceiling_division(len(Bytes(calldata)), 32) + ) + return intrinsic_cost + + return fn + @classmethod def _calculate_create_gas( cls, opcode: OpcodeBase, gas_costs: GasCosts diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index b4f8492993d..d30f8fd376f 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -872,6 +872,7 @@ def fn( ) -> int: del return_cost_deducted_prior_execution del sends_value, recipient_type + del contract_creation assert access_list is None, ( f"Access list is not supported in {cls.name()}" @@ -882,12 +883,6 @@ def fn( intrinsic_cost: int = gas_costs.TX_BASE - if contract_creation: - intrinsic_cost += ( - gas_costs.CODE_INIT_PER_WORD - * ceiling_division(len(Bytes(calldata)), 32) - ) - return intrinsic_cost + calldata_gas_calculator(data=calldata) return fn diff --git a/packages/testing/src/execution_testing/forks/tests/test_forks.py b/packages/testing/src/execution_testing/forks/tests/test_forks.py index c9c06f0a7b4..0a3864a177c 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_forks.py +++ b/packages/testing/src/execution_testing/forks/tests/test_forks.py @@ -402,6 +402,7 @@ def test_tx_types() -> None: # noqa: D103 @pytest.mark.parametrize( "fork", [ + pytest.param(Shanghai, id="Shanghai"), pytest.param(Berlin, id="Berlin"), pytest.param(Istanbul, id="Istanbul"), pytest.param(Homestead, id="Homestead"), @@ -434,7 +435,8 @@ def test_tx_intrinsic_gas_functions( # noqa: D103 if create_tx: if fork >= Homestead: intrinsic_gas += 32000 - intrinsic_gas += 2 + if fork >= Shanghai: + intrinsic_gas += 2 assert ( fork.transaction_intrinsic_cost_calculator()( calldata=calldata, diff --git a/tests/ported_static/amsterdam_skip_list.txt b/tests/ported_static/amsterdam_skip_list.txt index 7aba6f38bb0..92fb465eda3 100644 --- a/tests/ported_static/amsterdam_skip_list.txt +++ b/tests/ported_static/amsterdam_skip_list.txt @@ -8,7 +8,7 @@ # Entries are substring-matched against each pytest nodeid (after # stripping the fixture-format suffix in conftest.py). # -# Total entries: 153 +# Total entries: 130 # stAttackTest (1) stAttackTest/test_crashing_transaction.py::test_crashing_transaction[fork_Amsterdam] @@ -19,20 +19,9 @@ 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] +# stCallCodes (0) + +# stCallCreateCallCodeTest (3) 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] @@ -111,20 +100,12 @@ stCreateTest/test_transaction_collision_to_empty_but_code.py::test_transaction_c 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] +# stDelegatecallTestHomestead (0) -# stEIP150Specific (7) -stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py::test_call_ask_more_gas_on_depth2_then_transaction_has[fork_Amsterdam] +# stEIP150Specific (3) stEIP150Specific/test_create_and_gas_inside_create.py::test_create_and_gas_inside_create[fork_Amsterdam] stEIP150Specific/test_delegate_call_on_eip.py::test_delegate_call_on_eip[fork_Amsterdam] stEIP150Specific/test_new_gas_price_for_codes.py::test_new_gas_price_for_codes[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64e0.py::test_transaction64_rule_d64e0[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64m1.py::test_transaction64_rule_d64m1[fork_Amsterdam] -stEIP150Specific/test_transaction64_rule_d64p1.py::test_transaction64_rule_d64p1[fork_Amsterdam] # stEIP150singleCodeGasPrices (2) stEIP150singleCodeGasPrices/test_gas_cost.py::test_gas_cost[fork_Amsterdam-d40] @@ -145,8 +126,7 @@ stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_p stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g1] stInitCodeTest/test_out_of_gas_prefunded_contract_creation.py::test_out_of_gas_prefunded_contract_creation[fork_Amsterdam--g2] -# stMemExpandingEIP150Calls (4) -stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py::test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls[fork_Amsterdam] +# stMemExpandingEIP150Calls (3) stMemExpandingEIP150Calls/test_call_goes_oog_on_second_level_with_mem_expanding_calls.py::test_call_goes_oog_on_second_level_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_create_and_gas_inside_create_with_mem_expanding_calls.py::test_create_and_gas_inside_create_with_mem_expanding_calls[fork_Amsterdam] stMemExpandingEIP150Calls/test_new_gas_price_for_codes_with_mem_expanding_calls.py::test_new_gas_price_for_codes_with_mem_expanding_calls[fork_Amsterdam] @@ -194,10 +174,7 @@ stStaticCall/test_static_create_empty_contract_and_call_it_0wei.py::test_static_ 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 (2) 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] diff --git a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py index 900c2e78bda..e8abac3f1f2 100644 --- a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py +++ b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract.py @@ -1,199 +1,121 @@ """ -Callcode inside create/create2 contract init to existing contract. +Verify a CALLCODE made from inside init code to an existing contract. + +The existing contract's code runs in the created account's context: its +storage write lands there, while the existing contract keeps its own +storage and receives no value. Parametrized over the endowment and the +CALLCODE value, including an endowment too small for the transfer, so +the CALLCODE fails. Ported from: state_tests/stCallCodes/callcodeInInitcodeToExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The calldata-dispatch entry +contract is collapsed into a direct transaction to the create-runner, +sub-calls forward all gas (EIP-8037-proof), the post pins both the +created and the existing account, and the value cases are parametrized. +Widened down to TangerineWhistle, the EIP-150 floor for forwarding all +gas; CREATE2 rejoins at Constantinople. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, - Hash, + Fork, + Macros, + Op, + Opcodes, 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" +SUCCESS_FLAG_SLOT = 1 +DELEGATE_SLOT = 2 + @pytest.mark.ported_from( [ "state_tests/stCallCodes/callcodeInInitcodeToExistingContractFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("TangerineWhistle") @pytest.mark.parametrize( - "d, g, v", + "create_endowment,callcode_value", [ - pytest.param( - 0, - 0, - 0, - id="d0", - ), - pytest.param( - 1, - 0, - 0, - id="d1", - ), + pytest.param(1, 1, id="1_wei_value"), + pytest.param(0, 0, id="zero_value"), + pytest.param(0, 1, id="1_wei_callcode_value_with_zero_balance"), ], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.with_all_create_opcodes def test_callcode_in_initcode_to_existing_contract( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + create_opcode: Opcodes, + create_endowment: int, + callcode_value: int, ) -> None: - """Callcode inside create/create2 contract init to existing contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1100000000000000000000000000000000000000) - contract_1 = Address(0x1000000000000000000000000000000000000000) - contract_2 = Address(0x2000000000000000000000000000000000000000) - contract_3 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """Verify a CALLCODE in init code runs in the created account.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_SLOT, value=1) + Op.STOP, ) - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (CALL 300000 (CALLDATALOAD 0) 0 0 0 0 0) } - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0x493E0, - address=Op.CALLDATALOAD(offset=0x0), - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0x1100000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (SSTORE 2 1) } - contract_3 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # {(seq (CREATE2 1 0 (lll (seq [[1]] (CALLCODE 50000 0x1000000000000000000000000000000000000001 1 0 0 0 0)) 0) 0) )} # noqa: E501 - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH1[0x0] - + Op.PUSH1[0x27] - + Op.CODECOPY(dest_offset=0x0, offset=0x11, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.CREATE2 - + Op.STOP - + Op.INVALID - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + initcode = ( + Op.SSTORE( + key=SUCCESS_FLAG_SLOT, + value=Op.CALLCODE(address=existing, value=callcode_value), ) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x2000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # {(seq (CREATE 1 0 (lll (seq [[1]] (CALLCODE 50000 0x1000000000000000000000000000000000000001 1 0 0 0 0)) 0) ) )} # noqa: E501 - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.PUSH1[0x27] - + Op.CODECOPY(dest_offset=0x0, offset=0xF, size=Op.DUP1) - + Op.PUSH1[0x0] - + Op.PUSH1[0x1] - + Op.CREATE + Op.STOP - + Op.INVALID - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 ) + initcode_bytes = bytes(initcode) - expect_entries_: list[dict] = [ - { - "indexes": {"data": 0, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=contract_1, nonce=0): Account( - storage={1: 1, 2: 1}, balance=1 - ), - }, - }, - { - "indexes": {"data": 1, "gas": -1, "value": -1}, - "network": [">=Cancun"], - "result": { - Address(0x11B62573BE8F72B4085BAFE5B675B3E7F08ED522): Account( - storage={1: 1, 2: 1}, balance=1 - ), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) + create_call = create_opcode( + value=create_endowment, + offset=0, + size=len(initcode_bytes), + ) + runner_balance = max(create_endowment, callcode_value) + 1 + runner = pre.deploy_contract( + code=Macros.MSTORE(initcode_bytes) + create_call + Op.STOP, + balance=runner_balance, + ) - tx_data = [ - Hash(contract_1, left_padding=True), - Hash(contract_2, left_padding=True), - ] - tx_gas = [1000000] + created = compute_create_address( + address=runner, + nonce=1, + initcode=initcode, + opcode=create_opcode, + ) tx = Transaction( - sender=sender, - to=contract_0, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + sender=pre.fund_eoa(), + to=runner, + protected=fork.supports_protected_txs(), ) - state_test(env=env, pre=pre, post=post, tx=tx) + created_nonce = 1 if fork.is_eip_enabled(161) else 0 + callcode_success = create_endowment >= callcode_value + post = { + created: Account( + code=b"", + nonce=created_nonce, + balance=create_endowment, + storage={SUCCESS_FLAG_SLOT: 1, DELEGATE_SLOT: 1} + if callcode_success + else {SUCCESS_FLAG_SLOT: 0, DELEGATE_SLOT: 0}, + ), + runner: Account( + nonce=2, + balance=runner_balance - create_endowment, + storage={}, + ), + existing: Account(balance=0, storage={}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py b/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py deleted file mode 100644 index 4822486a1ea..00000000000 --- a/tests/ported_static/stCallCodes/test_callcode_in_initcode_to_existing_contract_with_value_transfer.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Callcode inside create/create2 contract init to existing contract. - -Ported from: -state_tests/stCallCodes/callcodeInInitcodeToExistingContractWithValueTransferFiller.json -""" - -import pytest -from execution_testing import ( - EOA, - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, - compute_create_address, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - [ - "state_tests/stCallCodes/callcodeInInitcodeToExistingContractWithValueTransferFiller.json" # noqa: E501 - ], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_callcode_in_initcode_to_existing_contract_with_value_transfer( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Callcode inside create/create2 contract init to existing contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1000000000000000000000000000000000000000) - contract_1 = Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, - ) - - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (MSTORE 0 0x6040600060406000600573945304eb96065b2a98b57a48a06ae28d285a71b562) (MSTORE 32 0x0186a0f260005500000000000000000000000000000000000000000000000000) (CREATE 5 0 64) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0x6040600060406000600573945304EB96065B2A98B57A48A06AE28D285A71B562, # noqa: E501 - ) - + Op.MSTORE( - offset=0x20, - value=0x186A0F260005500000000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.CREATE(value=0x5, offset=0x0, size=0x40) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 - ) - # Source: lll - # { (SSTORE 2 1) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) + Op.STOP, - nonce=0, - address=Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=453081, - ) - - post = { - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 1, 2: 1}, balance=5 - ), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py b/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py index 16729e411a8..eb43f04e24a 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_call1024_oog.py @@ -1,153 +1,277 @@ """ -Calldepth with oog. +Verify self-recursive CALL, CALLCODE and DELEGATECALL chains that +terminate by out-of-gas. + +Each level bumps a shared depth counter, forwards almost all its gas to +a self-call (keeping a 10,000 reserve for its post-call stores), then +records the call's success flag and a depth marker. Levels too deep to +afford their stores halt and roll back, so the surviving storage pins +the exact depth the budget reaches under the EIP-150 63/64 rule. Ported from: state_tests/stCallCreateCallCodeTest/Call1024OOGFiller.json +state_tests/stCallCreateCallCodeTest/Callcode1024OOGFiller.json +state_tests/stDelegatecallTestHomestead/Call1024OOGFiller.json +state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +warm/cold and SSTORE pricing via opcode metadata, EIP-8037 state-gas +spill), validated against the ported Cancun depths; the hardcoded +self-address is replaced by ADDRESS. Four fillers from two legacy +suites are joined into one opcode parametrization, every budget run +against every opcode, so the whole scope is visible in one file. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, ) -from execution_testing.forks import Fork -from execution_testing.vm import Op - -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) +from execution_testing.vm import Op, Opcode REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +MARKER_SLOT = 2 +# Gas each level keeps back for its post-call stores. +GAS_RESERVE = 10_000 +# The ask factor zeroes out at the call-depth limit (never reached here; +# the recursion always dies of out-of-gas first). +DEPTH_CUTOFF = 1025 +# The marker store writes 1 + DEPTH_MARKER * depth. +DEPTH_MARKER = 1000 + + +def recursion_code(call_opcode: Opcode) -> Bytecode: + """Build the self-recursive body for the given call opcode.""" + return ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=call_opcode( + gas=Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, Op.DIV(Op.SLOAD(key=COUNTER_SLOT), DEPTH_CUTOFF) + ), + ), + address=Op.ADDRESS, + ), + ) + + Op.SSTORE( + key=MARKER_SLOT, + value=Op.ADD(1, Op.MUL(Op.SLOAD(key=COUNTER_SLOT), DEPTH_MARKER)), + ) + + Op.STOP + ) + + +def predict_recursion_storage( + fork: Fork, call_opcode: Opcode, tx_gas_limit: int +) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its post-call stores halts + and forfeits its entire grant to its parent, so the deepest level + that completes fixes the surviving depth counter (deeper levels' + writes and warmth all revert). Every cost is derived from the fork + via opcode metadata, including EIP-8037 state gas: with a sub-cap + gas limit the state reservoir is zero, so state charges spill from + the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The SUB and MUL of the ask expression run after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + Op.MUL.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + sstore_cold_set = raw_store_cost(False, 0, 1) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.MUL( + Op.SUB(Op.GAS, GAS_RESERVE), + Op.SUB( + 1, + Op.DIV(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_CUTOFF), + ), + ) + call_upfront = call_opcode(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the ask expression through the GAS opcode. + pre_gas_read = ( + call_opcode( + gas=ask_expr, address=Op.ADDRESS, address_warm=True + ).gas_cost(fork) + - call_upfront + - post_gas_read + ) + + marker_statics = ( + Op.ADD( + 1, + Op.MUL(Op.SLOAD(key=COUNTER_SLOT, key_warm=True), DEPTH_MARKER), + ).gas_cost(fork) + + push_cost + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < DEPTH_CUTOFF, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Result store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own stores find cold slots and zero current values. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + # Marker store: parents rewrite the same surviving marker value. + if ok: + gas -= marker_statics + ok = gas >= 0 and gas > stipend + if ok: + gas -= sstore_warm_noop if child_ok else sstore_cold_set + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + assert child_ok and survivor > 0, "the top level must complete" + return { + COUNTER_SLOT: survivor, + RESULT_SLOT: result_below, + MARKER_SLOT: 1 + DEPTH_MARKER * survivor, + } + @pytest.mark.ported_from( - ["state_tests/stCallCreateCallCodeTest/Call1024OOGFiller.json"], + [ + "state_tests/stCallCreateCallCodeTest/Call1024OOGFiller.json", + "state_tests/stCallCreateCallCodeTest/Callcode1024OOGFiller.json", + "state_tests/stDelegatecallTestHomestead/Call1024OOGFiller.json", + "state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json", # noqa: E501 + ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "call_opcode", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - pytest.param( - 0, - 2, - 0, - id="-g2", - ), - pytest.param( - 0, - 3, - 0, - id="-g3", - ), + pytest.param(Op.CALL, id="call"), + pytest.param(Op.CALLCODE, id="callcode"), + pytest.param(Op.DELEGATECALL, id="delegatecall"), ], ) -@pytest.mark.pre_alloc_mutable +@pytest.mark.parametrize( + # Ported budgets; each pins a distinct OOG-terminated depth. + "tx_gas_limit", + [13_120_826, 9_320_826, 15_720_826, 11_220_826], +) def test_call1024_oog( state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_opcode: Opcode, + tx_gas_limit: int, ) -> None: - """Calldepth with oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (CALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x878BC1C3D660907B056E31C854A309F7EF1B4C4, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x0878BC1C3D660907B056E31C854A309F7EF1B4C4), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 113, 1: 1, 2: 0x1B969})}, - }, - { - "indexes": {"data": -1, "gas": 2, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - { - "indexes": {"data": -1, "gas": 3, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 124, 1: 1, 2: 0x1E461})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [13120826, 9320826, 15720826, 11220826] - tx_value = [10] + """Pin the depth an OOG-terminated self-recursion reaches.""" + target = pre.deploy_contract(code=recursion_code(call_opcode)) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=tx_data[d], - gas_limit=tx_gas[g], - value=tx_value[v], - error=_exc, + gas_limit=tx_gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + post = { + target: Account( + storage=predict_recursion_storage(fork, call_opcode, tx_gas_limit) + ), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py b/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py deleted file mode 100644 index c67dfb77762..00000000000 --- a/tests/ported_static/stCallCreateCallCodeTest/test_callcode1024_oog.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -Calldepth and oog. - -Ported from: -state_tests/stCallCreateCallCodeTest/Callcode1024OOGFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - 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/stCallCreateCallCodeTest/Callcode1024OOGFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_callcode1024_oog( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Calldepth and oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (CALLCODE (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALLCODE( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x1B803058288DC00000F98311B059597434253374, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x1B803058288DC00000F98311B059597434253374), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [15720826, 13120826] - tx_value = [10] - - tx = Transaction( - sender=sender, - to=target, - 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/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py b/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py index 281a5b080b4..c1df2752c79 100644 --- a/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py +++ b/tests/ported_static/stCallCreateCallCodeTest/test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided.py @@ -1,152 +1,138 @@ """ -Test_contract_creation_make_call_that_ask_more_gas_then_transaction_prov... +Verify a CALL made inside a contract-creation transaction's init code that +asks for more gas than the transaction provided: the EIP-150 clamp decides +what the callee receives, and the transaction budget decides whether that +grant covers the callee's work. Ported from: state_tests/stCallCreateCallCodeTest/contractCreationMakeCallThatAskMoreGasThenTransactionProvidedFiller.json + +@manually-enhanced: Do not overwrite. The ask is explicitly oversized (the +ported 50000 was schedule-sized); both transaction budgets are derived from +the fork so the clamped grant lands above/below the callee's cost on every +fork; the init code writes a canary before the call (nothing after it needs +more than a POP — the 1/64 retention cannot afford an SSTORE, whose +EIP-2200 stipend rule would kill the creation), so a failed call and a +failed creation stay distinguishable. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Environment, + Fork, StateTestFiller, Transaction, - compute_create_address, ) -from execution_testing.forks import Fork from execution_testing.vm import Op -from tests.ported_static.post_state_resolution import ( - resolve_expect_post, -) - REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CANARY_SLOT = 0x2 +CANARY = 0xFF + +# Far larger than any gas the init frame can hold: the clamp always +# applies, which is the scenario the ported filler names. +OVERSIZED_GAS_ASK = 2**61 + @pytest.mark.ported_from( [ "state_tests/stCallCreateCallCodeTest/contractCreationMakeCallThatAskMoreGasThenTransactionProvidedFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") +@pytest.mark.valid_from("Berlin") @pytest.mark.parametrize( - "d, g, v", + "call_covered", [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), + pytest.param(True, id="enough_gas"), + pytest.param(False, id="not_enough_gas"), ], ) -@pytest.mark.pre_alloc_mutable def test_contract_creation_make_call_that_ask_more_gas_then_transaction_provided( # noqa: E501 state_test: StateTestFiller, pre: Alloc, fork: Fork, - d: int, - g: int, - v: int, + call_covered: bool, ) -> None: - """Test_contract_creation_make_call_that_ask_more_gas_then_transaction...""" # noqa: E501 - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - contract_1 = Address(0x1000000000000000000000000000000000000001) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 + """An init-code CALL asking above the tx budget gets the 63/64 clamp.""" + # Success indicator: writes one cold fresh slot when called. + writer_store = Op.SSTORE( + key=0x1, + value=0x1, + key_warm=False, + original_value=0, + new_value=1, ) + writer = pre.deploy_contract(code=writer_store + Op.STOP) - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + # The init code writes a completion canary before the call (a failed + # creation persists nothing, so the canary distinguishes it from a + # failed call), makes the oversized ask, and deposits no code. Only a + # POP runs after the call: the 1/64 retention on the starved arm is + # far below the EIP-2200 stipend an SSTORE would require. + initcode = ( + Op.SSTORE( + key=CANARY_SLOT, + value=CANARY, + key_warm=False, + original_value=0, + new_value=CANARY, + ) + + Op.CALL( + gas=OVERSIZED_GAS_ASK, + address=writer, + address_warm=False, + value_transfer=False, + account_new=False, + ) + + Op.STOP ) - pre[sender] = Account(balance=0x10C8E0) - # Source: lll - # {(SSTORE 1 1)} - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0x1) + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 - ) - # Source: lll - # {(CALL 50000 0x1000000000000000000000000000000000000001 0 0 64 0 64)} - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.CALL( - gas=0xC350, - address=0x1000000000000000000000000000000000000001, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, + # Derive the two budgets around the callee's fork-priced cost: the + # clamped grant (63/64 of the base left after the charges made before + # the forward point) lands above it on one arm and below it on the + # other. The post-call flag write runs on the 1/64 retention. + overhead = ( + fork.transaction_intrinsic_cost_calculator()( + calldata=initcode, + contract_creation=True, + return_cost_deducted_prior_execution=True, ) - + Op.STOP, - balance=0x186A0, - nonce=0, - address=Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B), # noqa: E501 + # EIP-8037 charges the created account's state gas to the + # creation transaction's top frame (zero before Amsterdam). + + fork.transaction_top_frame_state_gas(contract_creation=True) + + initcode.gas_cost(fork) ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": [0], "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - balance=0 - ), - contract_1: Account(storage={1: 1}), - }, - }, - { - "indexes": {"data": -1, "gas": [1], "value": -1}, - "network": [">=Cancun"], - "result": { - compute_create_address(address=sender, nonce=0): Account( - balance=0 - ), - contract_1: Account(storage={1: 0}), - }, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Op.CALL( - gas=0xC350, - address=contract_1, - value=0x0, - args_offset=0x0, - args_size=0x40, - ret_offset=0x0, - ret_size=0x40, - ), - ] - tx_gas = [96000, 60000] - + callee_needed = writer_store.gas_cost(fork) + # The grant, base - base // 64, is a step function that repeats at + # every multiple of 64, so one gas less does not always forward less: + # step until the grant really crosses the callee's cost. + base = callee_needed * 64 // 63 + while base - base // 64 < callee_needed: + base += 1 + if not call_covered: + while base - base // 64 >= callee_needed: + base -= 1 + assert base < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + gas_limit = overhead + base + sender = pre.fund_eoa() tx = Transaction( sender=sender, to=None, - data=tx_data[d], - gas_limit=tx_gas[g], - error=_exc, + data=initcode, + gas_limit=gas_limit, ) - state_test(env=env, pre=pre, post=post, tx=tx) + created = tx.created_contract + post = { + created: Account( + nonce=1, + code=b"", + storage={CANARY_SLOT: CANARY}, + ), + writer: Account(storage={1: 1 if call_covered else 0}), + } + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py b/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py deleted file mode 100644 index 0f6b6855f24..00000000000 --- a/tests/ported_static/stDelegatecallTestHomestead/test_call1024_oog.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -Test_call1024_oog. - -Ported from: -state_tests/stDelegatecallTestHomestead/Call1024OOGFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - 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/stDelegatecallTestHomestead/Call1024OOGFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.parametrize( - "d, g, v", - [ - pytest.param( - 0, - 0, - 0, - id="-g0", - ), - pytest.param( - 0, - 1, - 0, - id="-g1", - ), - ], -) -@pytest.mark.pre_alloc_mutable -def test_call1024_oog( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - d: int, - g: int, - v: int, -) -> None: - """Test_call1024_oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (DELEGATECALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B), # noqa: E501 - ) - - expect_entries_: list[dict] = [ - { - "indexes": {"data": -1, "gas": 0, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 134, 1: 1, 2: 0x20B71})}, - }, - { - "indexes": {"data": -1, "gas": 1, "value": -1}, - "network": [">=Cancun"], - "result": {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})}, - }, - ] - - post, _exc = resolve_expect_post(expect_entries_, d, g, v, fork) - - tx_data = [ - Bytes(""), - ] - tx_gas = [13120826, 15720826] - tx_value = [10] - - tx = Transaction( - sender=sender, - to=target, - 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/stDelegatecallTestHomestead/test_delegatecall1024_oog.py b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py deleted file mode 100644 index 42959c58111..00000000000 --- a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall1024_oog.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_delegatecall1024_oog. - -Ported from: -state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stDelegatecallTestHomestead/Delegatecall1024OOGFiller.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_delegatecall1024_oog( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_delegatecall1024_oog.""" - coinbase = Address(0xB94F5374FCE5EDBC8E2A8697C15331677E6EBF0B) - sender = pre.fund_eoa(amount=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=9223372036854775807, - ) - - addr = pre.fund_eoa(amount=7000) # noqa: F841 - # Source: lll - # { [[ 0 ]] (ADD @@0 1) [[ 1 ]] (DELEGATECALL (MUL (SUB (GAS) 10000) (SUB 1 (DIV @@0 1025))) 0 0 0 0) [[ 2 ]] (ADD 1(MUL @@0 1000)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.DELEGATECALL( - gas=Op.MUL( - Op.SUB(Op.GAS, 0x2710), - Op.SUB(0x1, Op.DIV(Op.SLOAD(key=0x0), 0x401)), - ), - address=0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.SSTORE( - key=0x2, value=Op.ADD(0x1, Op.MUL(Op.SLOAD(key=0x0), 0x3E8)) - ) - + Op.STOP, - balance=1024, - nonce=0, - address=Address(0x62C5C9278DA01E6594D6FEDE061838CF5E597F2B), # noqa: E501 - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=15720826, - value=10, - ) - - post = {target: Account(storage={0: 146, 1: 1, 2: 0x23A51})} - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py index e20c162ae59..31e1720c2c8 100644 --- a/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py +++ b/tests/ported_static/stDelegatecallTestHomestead/test_delegatecall_in_initcode_to_existing_contract.py @@ -1,107 +1,120 @@ """ -Test_delegatecall_in_initcode_to_existing_contract. +Verify a DELEGATECALL made from inside init code to an existing +contract. + +The created account's init code DELEGATECALLs an already-deployed +contract, so that contract's code runs in the freshly created account's +context with the init frame's caller preserved: both the delegate and +the init code itself observe the creating contract as CALLER, and every +storage write lands in the created account, never in the delegate. Ported from: state_tests/stDelegatecallTestHomestead/delegatecallInInitcodeToExistingContractFiller.json + +@manually-enhanced: Do not overwrite. The port's unused second creator +contract is deleted, the raw-word init code is composed, the delegate +call forwards all gas (EIP-8037-proof), the transaction budget is +maxed, and the post also pins the created account's code/nonce/balance +and that the delegate's own storage stays untouched. """ import pytest from execution_testing import ( - EOA, Account, - Address, Alloc, - Bytes, - Environment, + Macros, + Op, + Opcodes, StateTestFiller, Transaction, compute_create_address, ) -from execution_testing.vm import Op REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +CREATE_ENDOWMENT = 1 +RUNNER_BALANCE = 10_000 + +# Written by the init code with the DELEGATECALL's success flag. +DELEGATE_RESULT_SLOT = 0 +# Written by the init code with the CALLER it observes (the runner). +INITCODE_CALLER_SLOT = 1 +# Written by the delegate's code, in the created account's context. +DELEGATE_WRITE_SLOT = 2 +# Written by the delegate with the CALLER it observes (still the +# runner: DELEGATECALL preserves the init frame's caller). +DELEGATE_CALLER_SLOT = 0xB +DELEGATE_VALUE_SLOT = 0xC + @pytest.mark.ported_from( [ "state_tests/stDelegatecallTestHomestead/delegatecallInInitcodeToExistingContractFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.with_all_create_opcodes +@pytest.mark.valid_from("SpuriousDragon") def test_delegatecall_in_initcode_to_existing_contract( state_test: StateTestFiller, pre: Alloc, + create_opcode: Opcodes, ) -> None: - """Test_delegatecall_in_initcode_to_existing_contract.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - contract_0 = Address(0x1000000000000000000000000000000000000000) - contract_1 = Address(0x1000000000000000000000000000000000000001) - contract_2 = Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5) - sender = EOA( - key=0x45A915E4D060149EB4365960E6A7A45F334393093061116B197E3240065FF2D8 - ) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=1000000, + """A DELEGATECALL in init code runs in the created account.""" + existing = pre.deploy_contract( + code=Op.SSTORE(key=DELEGATE_WRITE_SLOT, value=1) + + Op.SSTORE(key=DELEGATE_CALLER_SLOT, value=Op.CALLER) + + Op.SSTORE(key=DELEGATE_VALUE_SLOT, value=Op.CALLVALUE) + + Op.STOP, ) - pre[sender] = Account(balance=0x2386F26FC10000) - # Source: lll - # { (MSTORE 0 0x604060006040600073945304eb96065b2a98b57a48a06ae28d285a71b5620186) (MSTORE 32 0xa0f4600055336001550000000000000000000000000000000000000000000000) (CREATE 1 0 64) } # noqa: E501 - contract_0 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE( - offset=0x0, - value=0x604060006040600073945304EB96065B2A98B57A48A06AE28D285A71B5620186, # noqa: E501 + initcode = ( + Op.SSTORE( + key=DELEGATE_RESULT_SLOT, + value=Op.DELEGATECALL(address=existing), ) - + Op.MSTORE( - offset=0x20, - value=0xA0F4600055336001550000000000000000000000000000000000000000000000, # noqa: E501 - ) - + Op.CREATE(value=0x1, offset=0x0, size=0x40) - + Op.STOP, - balance=10000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000000), # noqa: E501 + + Op.SSTORE(key=INITCODE_CALLER_SLOT, value=Op.CALLER) + + Op.STOP ) - # Source: lll - # { (MSTORE 0 0x6001600055) (CREATE 1 27 5) } - contract_1 = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=0x6001600055) - + Op.CREATE(value=0x1, offset=0x1B, size=0x5) + + runner = pre.deploy_contract( + code=Macros.MSTORE(initcode) + + create_opcode(value=CREATE_ENDOWMENT, offset=0, size=len(initcode)) + Op.STOP, - balance=1000, - nonce=0, - address=Address(0x1000000000000000000000000000000000000001), # noqa: E501 + balance=RUNNER_BALANCE, ) - # Source: lll - # { (SSTORE 2 1) [[ 11 ]] (CALLER) } - contract_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x2, value=0x1) - + Op.SSTORE(key=0xB, value=Op.CALLER) - + Op.STOP, - nonce=0, - address=Address(0x945304EB96065B2A98B57A48A06AE28D285A71B5), # noqa: E501 + + # Deployed contracts start at nonce 1. + created = compute_create_address( + address=runner, nonce=1, initcode=initcode, opcode=create_opcode ) tx = Transaction( - sender=sender, - to=contract_0, - data=Bytes(""), - gas_limit=453081, + sender=pre.fund_eoa(), + to=runner, ) post = { - compute_create_address(address=contract_0, nonce=0): Account( - storage={0: 1, 1: contract_0, 2: 1, 11: contract_0}, - balance=1, + created: Account( + # The init code deploys no code but writes its own storage. + code=b"", + nonce=1, + balance=CREATE_ENDOWMENT, + storage={ + DELEGATE_RESULT_SLOT: 1, + INITCODE_CALLER_SLOT: runner, + DELEGATE_WRITE_SLOT: 1, + DELEGATE_CALLER_SLOT: runner, + DELEGATE_VALUE_SLOT: CREATE_ENDOWMENT, + }, + ), + runner: Account( + nonce=2, + balance=RUNNER_BALANCE - CREATE_ENDOWMENT, + storage={}, ), + # The delegate's own storage must stay untouched. + existing: Account(storage={}), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py b/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py index 61fd1052dec..20371a9ce34 100644 --- a/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py +++ b/tests/ported_static/stEIP150Specific/test_call_ask_more_gas_on_depth2_then_transaction_has.py @@ -1,17 +1,23 @@ """ -Test_call_ask_more_gas_on_depth2_then_transaction_has. +Verify the EIP-150 63/64 clamp at call depth 2: a first-level call receives +its exact (affordable) ask, and its own oversized ask is clamped to 63/64 +of what remains in that frame. Ported from: state_tests/stEIP150Specific/CallAskMoreGasOnDepth2ThenTransactionHasFiller.json + +@manually-enhanced: Do not overwrite. The lower frames return their +observed GAS up the stack instead of SSTORE-ing it (the ported lower-frame +gas snapshots are EIP-8037 state-gas traps), and both expectations are +derived from the fork: the depth-1 frame sees exactly its asked budget, +the depth-2 frame sees `base - base // 64` of the depth-1 remainder. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,86 +26,86 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x0 +DEPTH2_GAS_SLOT = 0x1 +DEPTH1_GAS_SLOT = 0x2 + +# The ported depth-1 budget: affordable, so it is forwarded exactly. +CALLER_GAS = 0x30D40 +# The ported depth-2 ask: above anything the depth-1 frame can hold, so +# the 63/64 clamp decides what the depth-2 frame receives. +ASK_GAS = 0x927C0 + @pytest.mark.ported_from( [ "state_tests/stEIP150Specific/CallAskMoreGasOnDepth2ThenTransactionHasFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_ask_more_gas_on_depth2_then_transaction_has( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_ask_more_gas_on_depth2_then_transaction_has.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A depth-2 call asking above the frame budget gets 63/64 of it.""" + # Depth 2: returns the gas it observed on entry. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), ) - # Source: lll - # { (SSTORE 8 (GAS))} - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + Op.STOP, - nonce=0, + # Depth 1: records its own entry gas, then asks depth 2 for more gas + # than this frame holds; both observations return to the top frame. + entry_snapshot = Op.MSTORE(0x20, Op.GAS, new_memory_size=0x40) + depth2_call = Op.CALL( + gas=ASK_GAS, + address=gas_return_contract, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=0x40, + old_memory_size=0x40, ) - # Source: lll - # { (SSTORE 8 (GAS)) (SSTORE 9 (CALL 600000 0 0 0 0 0)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr_2, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - nonce=0, + caller = pre.deploy_contract( + code=entry_snapshot + depth2_call + Op.RETURN(0, 0x40), ) - # Source: lll - # { (SSTORE 8 (GAS)) (SSTORE 9 (CALL 200000 0 0 0 0 0)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x30D40, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), + + # Top frame: forwards the exact (affordable) depth-1 budget and stores + # the success flag plus both returned observations. + entry = pre.deploy_contract( + code=Op.SSTORE( + key=FLAG_SLOT, + value=Op.CALL(gas=CALLER_GAS, address=caller, ret_size=0x40), ) - + Op.STOP, - nonce=0, + + Op.SSTORE(key=DEPTH2_GAS_SLOT, value=Op.MLOAD(0)) + + Op.SSTORE(key=DEPTH1_GAS_SLOT, value=Op.MLOAD(0x20)), ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + state_gas_reservoir=0, + ) + + # Depth 1 received exactly CALLER_GAS; its snapshot reads it minus the + # GAS opcode itself. The depth-2 base is what remains after the + # snapshot and the call's own costs, clamped by EIP-150. + depth1_observed = CALLER_GAS - Op.GAS.gas_cost(fork) + base = ( + CALLER_GAS - entry_snapshot.gas_cost(fork) - depth2_call.gas_cost(fork) ) + assert 0 < base < ASK_GAS, "the 63/64 clamp must apply at depth 2" + forwarded = base - base // 64 + depth2_observed = forwarded - Op.GAS.gas_cost(fork) post = { - addr: Account(storage={8: 0x30D3E, 9: 1}), - addr_2: Account(storage={8: 0x2A1F6}), + entry: Account( + storage={ + FLAG_SLOT: 1, + DEPTH2_GAS_SLOT: depth2_observed, + DEPTH1_GAS_SLOT: depth1_observed, + }, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule.py new file mode 100644 index 00000000000..ed0feb3e4fe --- /dev/null +++ b/tests/ported_static/stEIP150Specific/test_transaction64_rule.py @@ -0,0 +1,111 @@ +""" +Verify the EIP-150 "all but one 64th" rounding at the transaction level: the +gas available when a subcall asks for more than the transaction provided is +floored as `base - base // 64`, probed with the base exactly divisible by +64 and one gas below/above it. + +Ported from: +state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json +state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json +state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json + +@manually-enhanced: Do not overwrite. Three fillers folded into one +parametrize; the callee reports its observed GAS so the exact forwarded +amount is asserted (`base - base // 64` differs from `base * 63 // 64` by +one whenever the base is not a multiple of 64 — the ported posts could not +see that difference); the tx gas limit is derived from the fork so the +divisibility residue holds on every fork. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Fork, + StateTestFiller, + Transaction, +) +from execution_testing.vm import Op + +REFERENCE_SPEC_GIT_PATH = "N/A" +REFERENCE_SPEC_VERSION = "N/A" + +GAS_SLOT = 0x1 +# Far larger than any gas the frame can hold: the clamp always applies. +OVERSIZED_GAS_ASK = 2**61 + + +@pytest.mark.ported_from( + [ + "state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json", + "state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json", + "state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json", + ], +) +@pytest.mark.valid_from("Berlin") +@pytest.mark.parametrize( + "residue", + [ + pytest.param(0, id="d64e0"), + pytest.param(-1, id="d64m1"), + pytest.param(1, id="d64p1"), + ], +) +def test_transaction64_rule( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + residue: int, +) -> None: + """A subcall asking above the tx budget receives `base - base // 64`.""" + # Callee returns the gas it observed on entry back to the caller. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), + ) + + call_code = Op.CALL( + gas=OVERSIZED_GAS_ASK, + address=gas_return_contract, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=0x20, + ) + # The observed-gas store is the only op after the call; the callee's + # returned surplus always covers it. + store_code = Op.SSTORE( + key=GAS_SLOT, + value=Op.MLOAD(0), + key_warm=False, + original_value=0, + new_value=1, + ) + caller = pre.deploy_contract(code=call_code + store_code + Op.STOP) + + # Choose the 63/64 rounding base: large enough that the frame can + # afford the trailing store from what the callee hands back, shaped to + # the parametrized residue mod 64. The +1024 margin absorbs the ops + # around the store. + intrinsic = fork.transaction_intrinsic_cost_calculator()( + return_cost_deducted_prior_execution=True + ) + min_base = store_code.gas_cost(fork) + 1024 + base = -(-min_base // 64) * 64 + residue + assert base < OVERSIZED_GAS_ASK, "the 63/64 clamp must apply" + gas_limit = intrinsic + call_code.gas_cost(fork) + base + + tx = Transaction( + sender=pre.fund_eoa(), + to=caller, + gas_limit=gas_limit, + ) + + # The EVM floors the forwarded gas as `base - base // 64`; the callee + # observes it minus its own GAS opcode. An implementation using + # `base * 63 // 64` is exactly one gas short on the m1/p1 residues. + forwarded = base - base // 64 + expected_gas = forwarded - Op.GAS.gas_cost(fork) + + post = {caller: Account(storage={GAS_SLOT: expected_gas})} + + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py deleted file mode 100644 index 256cf7ea0bb..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64e0.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64e0. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64e0Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64e0( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64e0.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160062, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py deleted file mode 100644 index dd89bd167ec..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64m1.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64m1. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64m1Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64m1( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64m1.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160061, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py b/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py deleted file mode 100644 index 2dead5d9e1a..00000000000 --- a/tests/ported_static/stEIP150Specific/test_transaction64_rule_d64p1.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Test_transaction64_rule_d64p1. - -Ported from: -state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json -""" - -import pytest -from execution_testing import ( - Account, - Address, - Alloc, - Bytes, - Environment, - StateTestFiller, - Transaction, -) -from execution_testing.vm import Op - -REFERENCE_SPEC_GIT_PATH = "N/A" -REFERENCE_SPEC_VERSION = "N/A" - - -@pytest.mark.ported_from( - ["state_tests/stEIP150Specific/Transaction64Rule_d64p1Filler.json"], -) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable -def test_transaction64_rule_d64p1( - state_test: StateTestFiller, - pre: Alloc, -) -> None: - """Test_transaction64_rule_d64p1.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[1]] 12 } - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x1, value=0xC) + Op.STOP, - nonce=0, - ) - # Source: lll - # { [0] (GAS) (CALL 160000 0 0 0 0 0) [[2]] (SUB @0 (GAS)) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.MSTORE(offset=0x0, value=Op.GAS) - + Op.POP( - Op.CALL( - gas=0x27100, - address=addr, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - ) - + Op.SSTORE(key=0x2, value=Op.SUB(Op.MLOAD(offset=0x0), Op.GAS)) - + Op.STOP, - nonce=0, - ) - - tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=160063, - ) - - post = { - addr: Account(storage={1: 12}), - target: Account(storage={2: 24740}), - } - - state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py b/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py index e181c5c7f03..f080fc4a5db 100644 --- a/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py +++ b/tests/ported_static/stMemExpandingEIP150Calls/test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls.py @@ -1,17 +1,24 @@ """ -Test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding... +Verify the EIP-150 63/64 clamp at call depth 2 when the calls also expand +memory: a first-level call receives its exact (affordable) ask, and its own +oversized ask is clamped to 63/64 of what remains after the memory +expansion. Ported from: state_tests/stMemExpandingEIP150Calls/CallAskMoreGasOnDepth2ThenTransactionHasWithMemExpandingCallsFiller.json + +@manually-enhanced: Do not overwrite. The lower frames return their +observed GAS up the stack instead of SSTORE-ing it (the ported lower-frame +gas snapshots are EIP-8037 state-gas traps); every expectation is derived +from the fork, including the top frame's entry snapshot, which pins the +transaction intrinsic cost. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,86 +27,111 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +FLAG_SLOT = 0x0 +DEPTH2_GAS_SLOT = 0x1 +DEPTH1_GAS_SLOT = 0x2 +ENTRY_GAS_SLOT = 0x3 + +# The ported depth-1 budget: affordable, so it is forwarded exactly. +CALLER_GAS = 0x30D40 +# The ported depth-2 ask: above anything the depth-1 frame can hold, so +# the 63/64 clamp decides what the depth-2 frame receives. +ASK_GAS = 0x927C0 +# The ported calls' argument window, driving the memory expansion. +MEM_OFFSET = 0xFF +MEM_SIZE = 0xFF + @pytest.mark.ported_from( [ "state_tests/stMemExpandingEIP150Calls/CallAskMoreGasOnDepth2ThenTransactionHasWithMemExpandingCallsFiller.json" # noqa: E501 ], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expanding_calls( # noqa: E501 state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_ask_more_gas_on_depth2_then_transaction_has_with_mem_expa...""" # noqa: E501 - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xE8D4A51000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """A depth-2 memory-expanding call is clamped to 63/64 of its frame.""" + # Depth 2: returns the gas it observed on entry. + gas_return_contract = pre.deploy_contract( + code=Op.MSTORE(0, Op.GAS, new_memory_size=0x20) + Op.RETURN(0, 0x20), ) - # Source: hex - # 0x5a600855 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS), - nonce=0, + # Depth 1: records its own entry gas, then asks depth 2 for more gas + # than this frame holds, expanding memory through the args window; + # both observations return to the top frame. + entry_snapshot = Op.MSTORE(0x20, Op.GAS, new_memory_size=0x40) + depth2_call = Op.CALL( + gas=ASK_GAS, + address=gas_return_contract, + args_offset=MEM_OFFSET, + args_size=MEM_SIZE, + ret_size=0x20, + address_warm=False, + account_new=False, + new_memory_size=MEM_OFFSET + MEM_SIZE, + old_memory_size=0x40, ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff600073620927c0f1600955 # noqa: E501 - addr_2 = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) - + Op.SSTORE( - key=0x9, - value=Op.CALL( - gas=0x927C0, - address=addr, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, - ), - ), - nonce=0, + caller = pre.deploy_contract( + code=entry_snapshot + depth2_call + Op.RETURN(0, 0x40), ) - # Source: hex - # 0x5a60085560ff60ff60ff60ff60007362030d40f1600955 # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x8, value=Op.GAS) + + # Top frame: snapshots its entry gas (pinning the tx intrinsic), then + # forwards the exact depth-1 budget and stores the success flag plus + # both returned observations. + entry_code = ( + Op.SSTORE(key=ENTRY_GAS_SLOT, value=Op.GAS) + Op.SSTORE( - key=0x9, + key=FLAG_SLOT, value=Op.CALL( - gas=0x30D40, - address=addr_2, - value=0x0, - args_offset=0xFF, - args_size=0xFF, - ret_offset=0xFF, - ret_size=0xFF, + gas=CALLER_GAS, + address=caller, + ret_size=0x40, + address_warm=False, + account_new=False, + new_memory_size=0x40, ), - ), - nonce=0, + ) + + Op.SSTORE(key=DEPTH2_GAS_SLOT, value=Op.MLOAD(0)) + + Op.SSTORE(key=DEPTH1_GAS_SLOT, value=Op.MLOAD(0x20)) ) + entry = pre.deploy_contract(code=entry_code + Op.STOP) + + # Conservative fork-derived budget: the entry's own costs (incl. the + # trailing state-priced stores) plus the full depth-1 grant. + intrinsic = fork.transaction_intrinsic_cost_calculator()() + gas_limit = intrinsic + entry_code.gas_cost(fork) + CALLER_GAS tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=600000, + sender=pre.fund_eoa(), + to=entry, + gas_limit=gas_limit, ) + # The entry snapshot observes everything after the intrinsic; depth 1 + # received exactly CALLER_GAS; the depth-2 base is what remains after + # the snapshot and the call's own costs (incl. memory expansion), + # clamped by EIP-150. + entry_observed = gas_limit - intrinsic - Op.GAS.gas_cost(fork) + depth1_observed = CALLER_GAS - Op.GAS.gas_cost(fork) + base = ( + CALLER_GAS - entry_snapshot.gas_cost(fork) - depth2_call.gas_cost(fork) + ) + assert 0 < base < ASK_GAS, "the 63/64 clamp must apply at depth 2" + forwarded = base - base // 64 + depth2_observed = forwarded - Op.GAS.gas_cost(fork) + post = { - sender: Account(nonce=1), - target: Account(storage={8: 0x8D5B6, 9: 1}), - addr: Account(storage={8: 0x2A1C7}), - addr_2: Account(storage={8: 0x30D3E, 9: 1}), + entry: Account( + storage={ + ENTRY_GAS_SLOT: entry_observed, + FLAG_SLOT: 1, + DEPTH2_GAS_SLOT: depth2_observed, + DEPTH1_GAS_SLOT: depth1_observed, + }, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py index 863e937e7bb..c87ef000bf1 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls0.py @@ -1,8 +1,24 @@ """ -Test_ab_acalls0. +Verify mutual A<->B recursion with value transfers and fixed gas asks. + +Contract A calls B forwarding a fixed 100,000-gas ask with 24 wei; B +calls its caller back with a 50,000 ask and 23 wei, storing one plus +the result. Both store into a PC-derived slot only after their call +returns, so every level's store competes with what the descent left +behind: levels too deep to afford it halt and forfeit, rolling back +their stores and transfers, and the surviving storage and balances pin +exactly how far the budget reaches. Ported from: state_tests/stSystemOperationsTest/ABAcalls0Filler.json + +@manually-enhanced: Do not overwrite. The post state (stores and +balances) is predicted by an exact fork-derived replay of the gas flow +(EIP-150 grants, stipend gifting and return, warm/cold and SSTORE +pricing via opcode metadata, EIP-8037 state-gas spill), validated +against the ported Cancun stores. B reaches A as its CALLER instead of +a hardcoded address, which shifts B's PC-derived slot; both slots are +computed from the assembled code. """ import pytest @@ -10,8 +26,7 @@ Account, Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,84 +35,198 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +A_CALL_GAS = 100_000 +A_CALL_VALUE = 0x18 +B_CALL_GAS = 50_000 +B_CALL_VALUE = 0x17 +# One transfer per level up to the call-depth limit can never run dry. +A_INITIAL_BALANCE = A_CALL_VALUE * 1024 +# Exactly one return payment before any income (the ported balance). +B_INITIAL_BALANCE = B_CALL_VALUE +# Ported budget; pins how deep the mutual recursion reaches. +TX_GAS_LIMIT = 1_000_000 + + +def predict_final_state( + fork: Fork, tx_gas_limit: int, b_address: Address +) -> tuple[int, int, int, int]: + """ + Replay the mutual recursion's gas flow. + + Return A's stored value, B's stored value, and the committed + balance deltas of A and B. Descend the alternating call chain + computing each level's EIP-150 grant (both asks are pushed + constants; a value-bearing call gifts the callee the stipend and + gets any unused part back), then unwind: a level that cannot afford + its post-call store (EIP-2200's stipend rule included) halts and + forfeits its grant, reverting its own store and the transfer that + funded it. Every cost is derived from the fork via opcode metadata, + including EIP-8037 state gas: with a sub-cap gas limit the state + reservoir is zero, so state charges spill from the charging frame's + own gas. + """ + stipend = fork.gas_costs().CALL_STIPEND + pc_cost = Op.PC.gas_cost(fork) + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + # A's charges before forwarding: argument pushes plus the call's + # upfront costs (B is cold only in the top level). The ask is a + # pushed constant, so the whole call expression charges up front. + def a_charges(b_warm: bool) -> int: + return Op.CALL( + gas=A_CALL_GAS, + address=b_address, + value=A_CALL_VALUE, + address_warm=b_warm, + value_transfer=True, + ).gas_cost(fork) + + b_value_expr = Op.ADD( + 1, + Op.CALL( + gas=B_CALL_GAS, + address=Op.CALLER, + value=B_CALL_VALUE, + # A is the transaction target: always warm. + address_warm=True, + value_transfer=True, + ), + ) + # B's ADD and its constant push run only after the call returns. + b_post_call = Op.PUSH1[0].gas_cost(fork) + Op.ADD.gas_cost(fork) + b_charges = b_value_expr.gas_cost(fork) - b_post_call + + # Descend: alternate A and B levels until one dies mid-charges. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + balance = {"A": A_INITIAL_BALANCE, "B": B_INITIAL_BALANCE} + while True: + level += 1 + is_a = level % 2 == 1 + if is_a: + gas -= a_charges(b_warm=level > 1) + ask, value = A_CALL_GAS, A_CALL_VALUE + else: + gas -= b_charges + ask, value = B_CALL_GAS, B_CALL_VALUE + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + payer = "A" if is_a else "B" + assert balance[payer] >= value, "value transfer must be funded" + balance[payer] -= value + balance["B" if is_a else "A"] += value + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + stipend + + # Unwind: a failed level forfeits its grant and reverts the whole + # committed state below it (stores, warmth, and transfers). + child_ok = False + leftover = 0 + a_val, a_warm, b_val, b_warm = 0, False, 0, False + a_delta, b_delta = 0, 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + is_a = lvl % 2 == 1 + gas = available - forwarded + (leftover if child_ok else 0) + result = 1 if child_ok else 0 + if is_a: + gas -= pc_cost + store_value, current, warm = result, a_val, a_warm + else: + gas -= b_post_call + pc_cost + store_value, current, warm = 1 + result, b_val, b_warm + ok = gas >= 0 and gas > stipend + if ok: + gas -= raw_store_cost(warm, current, store_value) + ok = gas >= 0 + if ok: + # Commit this level: its store and the transfer into it. + if is_a: + a_val, a_warm = store_value, True + if lvl > 1: + a_delta += B_CALL_VALUE + b_delta -= B_CALL_VALUE + else: + b_val, b_warm = store_value, True + a_delta -= A_CALL_VALUE + b_delta += A_CALL_VALUE + leftover = gas + child_ok = True + else: + child_ok = False + leftover = 0 + a_val, a_warm, b_val, b_warm = 0, False, 0, False + a_delta, b_delta = 0, 0 + assert child_ok, "the top level must complete" + return a_val, b_val, a_delta, b_delta + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/ABAcalls0Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_ab_acalls0( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_ab_acalls0.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, + """Pin how deep a value-bearing A<->B recursion reaches.""" + # B calls whoever called it, so it needs no embedded address. + b_value_expr = Op.ADD( + 1, + Op.CALL(gas=B_CALL_GAS, address=Op.CALLER, value=B_CALL_VALUE), + ) + contract_b = pre.deploy_contract( + code=Op.SSTORE(key=Op.PC, value=b_value_expr) + Op.STOP, + balance=B_INITIAL_BALANCE, ) - # Source: lll - # { [[ (PC) ]] (CALL 100000 24 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=Op.PC, - value=Op.CALL( - gas=0x186A0, - address=0x44EB1162303B6A60F2F8882D43D661787B3011E6, - value=0x18, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0xDE0B6B3A7640000, - nonce=0, - address=Address(0xD6CD6EC9ADCA299F2BBFD754FF8BCF6A4B9AAE40), # noqa: E501 + a_value_expr = Op.CALL( + gas=A_CALL_GAS, address=contract_b, value=A_CALL_VALUE ) - # Source: lll - # { [[ (PC) ]] (ADD 1 (CALL 50000 23 0 0 0 0)) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE( - key=Op.PC, - value=Op.ADD( - 0x1, - Op.CALL( - gas=0xC350, - address=0xD6CD6EC9ADCA299F2BBFD754FF8BCF6A4B9AAE40, - value=0x17, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ), - ) - + Op.STOP, - balance=23, - nonce=0, - address=Address(0x44EB1162303B6A60F2F8882D43D661787B3011E6), # noqa: E501 + contract_a = pre.deploy_contract( + code=Op.SSTORE(key=Op.PC, value=a_value_expr) + Op.STOP, + balance=A_INITIAL_BALANCE, ) + # PC keys: each store's key is the code offset of its PC opcode, + # which sits right after the assembled value expression. + a_key = len(bytes(a_value_expr)) + b_key = len(bytes(b_value_expr)) + tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=1000000, - value=0x186A0, + sender=pre.fund_eoa(), + to=contract_a, + gas_limit=TX_GAS_LIMIT, ) + a_val, b_val, a_delta, b_delta = predict_final_state( + fork, TX_GAS_LIMIT, contract_b + ) post = { - target: Account(storage={36: 1}), - addr: Account(storage={38: 1}), + contract_a: Account( + storage={a_key: a_val}, + balance=A_INITIAL_BALANCE + a_delta, + ), + contract_b: Account( + storage={b_key: b_val}, + balance=B_INITIAL_BALANCE + b_delta, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py index 8f2b77d0966..5647ebd2c95 100644 --- a/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py +++ b/tests/ported_static/stSystemOperationsTest/test_ab_acalls3.py @@ -1,8 +1,22 @@ """ -Test_ab_acalls3. +Verify mutual A<->B recursion where each side reserves 100,000 gas. + +Both contracts bump their own depth counter during descent, then call +the other side forwarding everything but a 100,000-gas reserve (A sends +one wei each level; B sends nothing back). Nothing runs after the call, +so only the single deepest level dies of gas and every completed +level's counter bump and transfer persist: the counters and balances +pin exactly how many rounds the budget sustains. Ported from: state_tests/stSystemOperationsTest/ABAcalls3Filler.json + +@manually-enhanced: Do not overwrite. The post state (counters and +balances) is predicted by an exact fork-derived replay of the gas flow +(EIP-150 grants, stipend gifting, warm/cold and SSTORE pricing via +opcode metadata, EIP-8037 state-gas spill), validated against the +ported Cancun counters. B reaches A as its CALLER instead of a +hardcoded address. """ import pytest @@ -10,8 +24,8 @@ Account, Address, Alloc, - Bytes, - Environment, + Bytecode, + Fork, StateTestFiller, Transaction, ) @@ -20,76 +34,184 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +# Gas each level keeps back for itself before forwarding the rest. +GAS_RESERVE = 100_000 +A_CALL_VALUE = 1 +# One transfer per level up to the call-depth limit can never run dry. +A_INITIAL_BALANCE = A_CALL_VALUE * 1024 +# Ported budget; pins how many rounds the recursion sustains. +TX_GAS_LIMIT = 10_000_000 + + +def predict_depths( + fork: Fork, tx_gas_limit: int, b_address: Address +) -> tuple[int, int]: + """ + Replay the mutual recursion's gas flow. + + Return how many A and B levels complete. Descend the alternating + call chain: each level bumps its own counter (one cold set per + contract, then dirty rewrites), pays its call charges, and forwards + everything but the reserve under the EIP-150 63/64 rule; once the + reserve underflows, the wrapped ask forwards the 63/64 maximum. + Nothing runs after a call, so only the single deepest level dies + and its bump and incoming transfer revert. Every cost is derived + from the fork via opcode metadata, including EIP-8037 state gas: + with a sub-cap gas limit the state reservoir is zero, so state + charges spill from the charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The ask expression's SUB runs after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + def call_split( + address: Address | Op, warm: bool, value: int + ) -> tuple[int, int]: + """Pre-GAS-read and upfront charges of one side's call.""" + upfront = Op.CALL( + address_warm=warm, value_transfer=value > 0 + ).gas_cost(fork) + composite = Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=address, + value=value, + address_warm=warm, + value_transfer=value > 0, + ).gas_cost(fork) + return composite - upfront - post_gas_read, upfront + + a_pre, a_upfront_cold = call_split(b_address, False, A_CALL_VALUE) + _, a_upfront_warm = call_split(b_address, True, A_CALL_VALUE) + # A is the transaction target: always warm for B's call back. + b_pre, b_upfront = call_split(Op.CALLER, True, 0) + + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + level = 0 + a_balance = A_INITIAL_BALANCE + while True: + level += 1 + is_a = level % 2 == 1 + # Each contract's first level pays the cold counter set. + first = level <= 2 + gas -= bump_statics(key_warm=not first) + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= a_pre if is_a else b_pre + if gas < 0: + break + gas_read = gas + if is_a: + gas -= post_gas_read + ( + a_upfront_cold if level == 1 else a_upfront_warm + ) + else: + gas -= post_gas_read + b_upfront + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + if is_a: + assert a_balance >= A_CALL_VALUE, "transfer must be funded" + a_balance -= A_CALL_VALUE + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + gas = forwarded + (stipend if is_a else 0) + + completed = level - 1 + assert completed >= 2, "both sides must run at least once" + a_count = (completed + 1) // 2 + b_count = completed // 2 + return a_count, b_count + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/ABAcalls3Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_ab_acalls3( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_ab_acalls3.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=100000000, - ) + """Pin how many rounds a reserve-throttled A<->B recursion runs.""" - # Source: lll - # { [[ 0 ]] (ADD (SLOAD 0) 1) (CALL (- (GAS) 100000) 1 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.SUB(Op.GAS, 0x186A0), - address=0xA890CEB693666313E0A5A1BE4F59F06C1E33F5C9, - value=0x1, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, + def bounce_code(call: Bytecode) -> Bytecode: + """Bump the own-depth counter, then call the other side.""" + return ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + call + + Op.STOP ) - + Op.STOP, - balance=0xFA3E8, - nonce=0, - address=Address(0x4776B53DEB22F16581088F679DBA75E205B65D34), # noqa: E501 + + # B calls whoever called it, so it needs no embedded address. + contract_b = pre.deploy_contract( + code=bounce_code( + Op.CALL(gas=Op.SUB(Op.GAS, GAS_RESERVE), address=Op.CALLER) + ), ) - # Source: lll - # { [[ 0 ]] (ADD (SLOAD 0) 1) (CALL (- (GAS) 100000) 0 0 0 0 0) } # noqa: E501 - addr = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.CALL( - gas=Op.SUB(Op.GAS, 0x186A0), - address=0x4776B53DEB22F16581088F679DBA75E205B65D34, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ) - + Op.STOP, - nonce=0, - address=Address(0xA890CEB693666313E0A5A1BE4F59F06C1E33F5C9), # noqa: E501 + contract_a = pre.deploy_contract( + code=bounce_code( + Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=contract_b, + value=A_CALL_VALUE, + ) + ), + balance=A_INITIAL_BALANCE, ) tx = Transaction( - sender=sender, - to=target, - data=Bytes(""), - gas_limit=10000000, - value=0x186A0, + sender=pre.fund_eoa(), + to=contract_a, + gas_limit=TX_GAS_LIMIT, ) + a_count, b_count = predict_depths(fork, TX_GAS_LIMIT, contract_b) + # Each completed B level keeps the wei its calling A level sent. post = { - target: Account(storage={0: 52}), - addr: Account(storage={0: 52}), + contract_a: Account( + storage={COUNTER_SLOT: a_count}, + balance=A_INITIAL_BALANCE - b_count * A_CALL_VALUE, + ), + contract_b: Account( + storage={COUNTER_SLOT: b_count}, + balance=b_count * A_CALL_VALUE, + ), } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx) diff --git a/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py b/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py index ee666d8bf61..445e782f2d2 100644 --- a/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py +++ b/tests/ported_static/stSystemOperationsTest/test_call_recursive_bomb3.py @@ -1,17 +1,32 @@ """ -Test_call_recursive_bomb3. +Verify a self-recursive CALL bomb that keeps only a 224-gas reserve. + +Each level bumps a shared depth counter and forwards everything but a +tiny reserve to a call to itself, so descent is throttled only by the +EIP-150 63/64 withhold. On the way back up a level must afford its +success-flag store from its 1/64 retention plus whatever its child +returned; levels that cannot (EIP-2200's stipend rule included) halt +and forfeit, so the surviving storage pins the exact depth the budget +sustains. Ported from: state_tests/stSystemOperationsTest/CallRecursiveBomb3Filler.json + +@manually-enhanced: Do not overwrite. The post state is predicted by an +exact fork-derived replay of the recursion's gas flow (EIP-150 grants, +returned-leftover propagation, warm/cold and SSTORE pricing via opcode +metadata, EIP-8037 state-gas spill), validated against the ported +Cancun depth. Under Amsterdam's revised storage-growth pricing even the +top level cannot afford its zero-to-one flag store at the ported +budget, so the whole transaction reverts and the post pins empty +storage. """ import pytest from execution_testing import ( Account, - Address, Alloc, - Bytes, - Environment, + Fork, StateTestFiller, Transaction, ) @@ -20,58 +35,185 @@ REFERENCE_SPEC_GIT_PATH = "N/A" REFERENCE_SPEC_VERSION = "N/A" +COUNTER_SLOT = 0 +RESULT_SLOT = 1 +# Gas each level keeps back; far below a cold store, so completing the +# post-call flag store depends on the 1/64 retention and the child's +# returned leftover. +GAS_RESERVE = 224 +# Ported budget; pins the OOG-terminated depth. +TX_GAS_LIMIT = 1_000_000 + +RECURSION_CODE = ( + Op.SSTORE( + key=COUNTER_SLOT, + value=Op.ADD(Op.SLOAD(key=COUNTER_SLOT), 1), + ) + + Op.SSTORE( + key=RESULT_SLOT, + value=Op.CALL( + gas=Op.SUB(Op.GAS, GAS_RESERVE), + address=Op.ADDRESS, + ), + ) + + Op.STOP +) + + +def predict_recursion_storage(fork: Fork, tx_gas_limit: int) -> dict[int, int]: + """ + Replay the recursion's gas flow and return the surviving storage. + + Descend the self-call chain computing each level's EIP-150 grant, + then unwind: a level that cannot afford its flag store halts and + forfeits its entire grant to its parent, so the deepest level that + completes fixes the surviving depth counter (deeper levels' writes + and warmth all revert). The level above the deepest survivor funds + its more expensive zero-to-one flag set partly from the survivor's + returned leftover. Every cost is derived from the fork via opcode + metadata, including EIP-8037 state gas: with a sub-cap gas limit + the state reservoir is zero, so state charges spill from the + charging frame's own gas. + """ + push_cost = Op.PUSH1[0].gas_cost(fork) + # The ask expression's SUB runs after GAS reads gas_left. + post_gas_read = Op.SUB.gas_cost(fork) + # EIP-2200: any SSTORE with gas_left <= stipend halts exceptionally. + stipend = fork.gas_costs().CALL_STIPEND + + def raw_store_cost(key_warm: bool, current: int, new: int) -> int: + """Cost of a bare SSTORE; original value is always zero here.""" + return Op.SSTORE( + key_warm=key_warm, + original_value=0, + current_value=current, + new_value=new, + ).gas_cost(fork) + + sstore_warm_set = raw_store_cost(True, 0, 1) + sstore_warm_dirty = raw_store_cost(True, 1, 2) + sstore_warm_noop = raw_store_cost(True, 1, 1) + sstore_cold_noop = raw_store_cost(False, 0, 0) + + def bump_statics(key_warm: bool) -> int: + """Counter-bump costs before its SSTORE (value expr plus key).""" + return ( + Op.ADD(Op.SLOAD(key=COUNTER_SLOT, key_warm=key_warm), 1).gas_cost( + fork + ) + + push_cost + ) + + bump_statics_cold = bump_statics(False) + bump_statics_warm = bump_statics(True) + + ask_expr = Op.SUB(Op.GAS, GAS_RESERVE) + call_upfront = Op.CALL(address_warm=True).gas_cost(fork) + # Everything charged before GAS reads gas_left: the call's argument + # pushes, ADDRESS, and the reserve push plus the GAS opcode itself. + pre_gas_read = ( + Op.CALL(gas=ask_expr, address=Op.ADDRESS, address_warm=True).gas_cost( + fork + ) + - call_upfront + - post_gas_read + ) + + # Descend: compute each level's grant until a level dies mid-frame. + gas = ( + tx_gas_limit + - fork.transaction_intrinsic_cost_calculator()() + - fork.transaction_top_frame_state_gas() + ) + levels: list[tuple[int, int]] = [] + level = 0 + while True: + level += 1 + first = level == 1 + gas -= bump_statics_cold if first else bump_statics_warm + if gas < 0 or gas <= stipend: + break + gas -= sstore_warm_set if first else sstore_warm_dirty + if gas < 0: + break + gas -= pre_gas_read + if gas < 0: + break + gas_read = gas + gas -= post_gas_read + call_upfront + if gas < 0: + break + assert level < 1024, "recursion must die of gas, not depth" + # A reserve underflow wraps mod 2**256: an effectively infinite + # ask, clamped to the 63/64 forwardable maximum. + ask = gas_read - GAS_RESERVE if gas_read >= GAS_RESERVE else 1 << 256 + forwarded = min(ask, gas - gas // 64) + levels.append((gas, forwarded)) + gas = forwarded + + # Unwind: a failed level forfeits its whole grant to its parent. + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + for lvl in range(len(levels), 0, -1): + available, forwarded = levels[lvl - 1] + gas = available - forwarded + (leftover if child_ok else 0) + # Flag store: push the slot key, then store the success flag. + # Below the deepest completing level everything reverts, so its + # own store finds a cold slot and a zero current value. + gas -= push_cost + ok = gas >= 0 and gas > stipend + if ok: + if not child_ok: + result_store = sstore_cold_noop + elif result_below == 0: + result_store = sstore_warm_set + else: + result_store = sstore_warm_noop + gas -= result_store + ok = gas >= 0 + if ok: + if not child_ok: + survivor = lvl + result_below = 1 if child_ok else 0 + leftover = gas + child_ok = True + else: + child_ok = False + result_below = 0 + leftover = 0 + survivor = 0 + if not child_ok: + # The top level itself cannot afford its flag store (its + # retention plus the child's leftover falls short of the + # storage-growth cost), so the transaction halts and every + # write reverts. + return {COUNTER_SLOT: 0, RESULT_SLOT: 0} + assert survivor > 0, "a completing top level must record a depth" + return {COUNTER_SLOT: survivor, RESULT_SLOT: result_below} + @pytest.mark.ported_from( ["state_tests/stSystemOperationsTest/CallRecursiveBomb3Filler.json"], ) -@pytest.mark.valid_from("Cancun") -@pytest.mark.pre_alloc_mutable +@pytest.mark.valid_from("Berlin") def test_call_recursive_bomb3( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: - """Test_call_recursive_bomb3.""" - coinbase = Address(0x2ADC25665018AA1FE0E6BC666DAC8FC2697FF9BA) - sender = pre.fund_eoa(amount=0xDE0B6B3A7640000) - - env = Environment( - fee_recipient=coinbase, - number=1, - timestamp=1000, - prev_randao=0x20000, - base_fee_per_gas=10, - gas_limit=10000000, - ) - - # Source: lll - # { [[ 0 ]] (+ (SLOAD 0) 1) [[ 1 ]] (CALL (- (GAS) 224) (ADDRESS) 0 0 0 0 0) } # noqa: E501 - target = pre.deploy_contract( # noqa: F841 - code=Op.SSTORE(key=0x0, value=Op.ADD(Op.SLOAD(key=0x0), 0x1)) - + Op.SSTORE( - key=0x1, - value=Op.CALL( - gas=Op.SUB(Op.GAS, 0xE0), - address=Op.ADDRESS, - value=0x0, - args_offset=0x0, - args_size=0x0, - ret_offset=0x0, - ret_size=0x0, - ), - ) - + Op.STOP, - balance=0x1312D00, - nonce=0, - ) + """Pin the depth a thin-reserve CALL self-recursion sustains.""" + target = pre.deploy_contract(code=RECURSION_CODE) tx = Transaction( - sender=sender, + sender=pre.fund_eoa(), to=target, - data=Bytes(""), - gas_limit=1000000, - value=0x186A0, + gas_limit=TX_GAS_LIMIT, ) - post = {target: Account(storage={0: 18, 1: 1})} + post = { + target: Account(storage=predict_recursion_storage(fork, TX_GAS_LIMIT)), + } - state_test(env=env, pre=pre, post=post, tx=tx) + state_test(pre=pre, post=post, tx=tx)