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)