Skip to content

Commit 4cfc8ae

Browse files
committed
feat(tests): 8037 nested CREATE fail and deep-recursion reservoir tests
Ports the remaining two tests from the `feat/eip-8037-additional-tests` / `feat/eip-8037-tests-devnet3` branches that were not yet covered. test_nested_create_fail_parent_revert_state_gas Two-layer refund composition: caller CALLs factory, factory does CREATE with failing initcode, factory then REVERTs or STOPs. Parametrized over `child_failure` (revert, halt) x `parent_reverts` x `create_opcode`. Verifies the nonce side effect of factory's CREATE is rolled back when the parent reverts, and preserved (nonce=2) when it STOPs. Complements PR ethereum#2704's single-layer refund tests by exercising the caller→factory→inner chain through `incorporate_child_on_error` at both depths. test_create_stack_depth_state_gas_consumed Deep-recursion robustness check. The contract CALLs itself until gas exhaustion (EIP-150 63/64 rule limits effective depth well below STACK_DEPTH_LIMIT at the current `gas_limit_cap`; reaching depth 1024 is physically infeasible since the cumulative survival factor is `(63/64)**1024 ≈ 1e-7`). As recursion unwinds, frames run an SSTORE; the outermost frame's SSTORE must succeed, proving the reservoir threads through nested CALLs intact. Docstring notes that despite the name (retained for continuity with closed PR ethereum#2639), this exercises CALL's silent-failure branch rather than `generic_create`'s depth-1024 branch (which is unreachable at current gas params — effectively dead code in the spec).
1 parent d9ce45d commit 4cfc8ae

1 file changed

Lines changed: 284 additions & 0 deletions

File tree

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,3 +1928,287 @@ def test_inner_create_succeeds_code_deposit_state_gas(
19281928
],
19291929
post=post,
19301930
)
1931+
1932+
1933+
@pytest.mark.parametrize(
1934+
"parent_reverts",
1935+
[
1936+
pytest.param(True, id="parent_reverts"),
1937+
pytest.param(False, id="parent_succeeds"),
1938+
],
1939+
)
1940+
@pytest.mark.parametrize(
1941+
"child_failure",
1942+
[
1943+
pytest.param("revert", id="child_revert"),
1944+
pytest.param("halt", id="child_halt"),
1945+
],
1946+
)
1947+
@pytest.mark.with_all_create_opcodes()
1948+
@pytest.mark.valid_from("EIP8037")
1949+
def test_nested_create_fail_parent_revert_state_gas(
1950+
blockchain_test: BlockchainTestFiller,
1951+
pre: Alloc,
1952+
fork: Fork,
1953+
parent_reverts: bool,
1954+
child_failure: str,
1955+
create_opcode: Op,
1956+
) -> None:
1957+
"""
1958+
Verify 2-layer refund composition: child CREATE fail + parent revert.
1959+
1960+
A caller CALLs a factory that performs CREATE/CREATE2 whose
1961+
initcode fails via REVERT or INVALID. Under PR #2704 the
1962+
factory's `GAS_NEW_ACCOUNT` for the inner CREATE is refunded
1963+
end-of-frame via `incorporate_child_on_error` in the caller.
1964+
The factory then either REVERTs (caller sees factory as an
1965+
error frame too) or STOPs (caller sees factory as success).
1966+
1967+
Verifies the nonce-mutation side effect:
1968+
* `parent_succeeds`: factory's CREATE attempted, nonce
1969+
incremented to 2 (CREATE always bumps nonce on its frame).
1970+
* `parent_reverts`: factory's state is rolled back, so nonce
1971+
stays at 1.
1972+
1973+
Distinct from single-layer tests added by PR #2704 which verify
1974+
the refund within a single failing frame; this test covers the
1975+
compound caller → factory → child failure flow.
1976+
"""
1977+
gas_limit_cap = fork.transaction_gas_limit_cap()
1978+
assert gas_limit_cap is not None
1979+
gas_costs = fork.gas_costs()
1980+
create_state_gas = gas_costs.GAS_NEW_ACCOUNT
1981+
1982+
if child_failure == "revert":
1983+
init_code = Op.REVERT(0, 0)
1984+
else:
1985+
init_code = Op.INVALID
1986+
1987+
create_call = (
1988+
create_opcode(value=0, offset=0, size=len(init_code), salt=0)
1989+
if create_opcode == Op.CREATE2
1990+
else create_opcode(value=0, offset=0, size=len(init_code))
1991+
)
1992+
1993+
factory = pre.deploy_contract(
1994+
code=(
1995+
Op.MSTORE(
1996+
0,
1997+
int.from_bytes(bytes(init_code), "big")
1998+
<< (256 - 8 * len(init_code)),
1999+
)
2000+
+ Op.POP(create_call)
2001+
+ (Op.REVERT(0, 0) if parent_reverts else Op.STOP)
2002+
),
2003+
)
2004+
2005+
# Nested CALL required: `incorporate_child_on_error` only
2006+
# restores state gas when there is a parent frame to receive it.
2007+
caller = pre.deploy_contract(
2008+
code=Op.POP(Op.CALL(gas=500_000, address=factory)),
2009+
)
2010+
2011+
tx = Transaction(
2012+
to=caller,
2013+
gas_limit=gas_limit_cap + create_state_gas,
2014+
sender=pre.fund_eoa(),
2015+
)
2016+
2017+
if parent_reverts:
2018+
# Factory reverted: state rolled back, nonce unchanged.
2019+
post = {factory: Account(nonce=1)}
2020+
else:
2021+
# Factory succeeded: CREATE bumped the factory nonce to 2.
2022+
post = {factory: Account(nonce=2)}
2023+
2024+
blockchain_test(
2025+
pre=pre,
2026+
blocks=[Block(txs=[tx])],
2027+
post=post,
2028+
)
2029+
2030+
2031+
@pytest.mark.valid_from("EIP8037")
2032+
def test_create_stack_depth_state_gas_consumed(
2033+
state_test: StateTestFiller,
2034+
pre: Alloc,
2035+
fork: Fork,
2036+
) -> None:
2037+
"""
2038+
Deep-recursion robustness for state gas reservoir handling.
2039+
2040+
A contract CALLs itself recursively until gas is exhausted
2041+
(the EIP-150 63/64 rule caps effective recursion depth far
2042+
below `STACK_DEPTH_LIMIT`; reaching depth 1024 with
2043+
`gas_limit_cap = 16.7M` is physically infeasible since the
2044+
cumulative survival factor is `(63/64)**1024` ≈ 1e-7). As the
2045+
recursion unwinds, each frame attempts an SSTORE. The
2046+
outermost frame's SSTORE must succeed, proving the reservoir
2047+
threads through nested CALLs and survives the deepest child's
2048+
silent failure (CALL returns 0 when depth+1 > STACK_DEPTH_LIMIT
2049+
or when gas runs out, preserving `state_gas_reservoir`).
2050+
2051+
Despite the name (retained for continuity with the closed
2052+
PR #2639), this does NOT exercise `generic_create`'s
2053+
depth-1024 silent-failure branch directly, because that branch
2054+
is unreachable at the current gas limit. It instead exercises
2055+
CALL's depth/gas silent-failure branch and the reservoir
2056+
preservation that threads through many levels.
2057+
"""
2058+
gas_limit_cap = fork.transaction_gas_limit_cap()
2059+
assert gas_limit_cap is not None
2060+
sstore_state_gas = fork.sstore_state_gas()
2061+
2062+
storage = Storage()
2063+
recursive = pre.deploy_contract(
2064+
code=(
2065+
# Recursive CALL until gas / depth is exhausted. The
2066+
# child's CALL silently fails either at depth+1 > 1024
2067+
# or when forwarded gas can't afford the next frame's
2068+
# overhead; in either case the reservoir is returned
2069+
# to the caller intact.
2070+
Op.POP(Op.CALL(Op.GAS, Op.ADDRESS, 0, 0, 0, 0, 0))
2071+
# Probe: the outermost (and any frame with enough
2072+
# remaining gas) sets slot 0. Storage check ensures
2073+
# the probe succeeded, i.e. the reservoir remained
2074+
# available after the nested CALLs unwound.
2075+
+ Op.SSTORE(storage.store_next(1, "reservoir_ok"), 1)
2076+
),
2077+
)
2078+
2079+
tx = Transaction(
2080+
to=recursive,
2081+
gas_limit=gas_limit_cap + sstore_state_gas,
2082+
sender=pre.fund_eoa(),
2083+
)
2084+
2085+
post = {recursive: Account(storage=storage)}
2086+
state_test(pre=pre, post=post, tx=tx)
2087+
2088+
2089+
@pytest.mark.parametrize(
2090+
"num_inner_ops",
2091+
[
2092+
pytest.param(1, id="single"),
2093+
pytest.param(3, id="accumulate"),
2094+
],
2095+
)
2096+
@pytest.mark.parametrize(
2097+
"outer_outcome",
2098+
[
2099+
pytest.param("succeeds", id="outer_succeeds"),
2100+
pytest.param("reverts", id="outer_reverts"),
2101+
],
2102+
)
2103+
@pytest.mark.with_all_create_opcodes()
2104+
@pytest.mark.valid_from("EIP8037")
2105+
def test_inner_create_fail_refunds_in_creation_tx(
2106+
blockchain_test: BlockchainTestFiller,
2107+
pre: Alloc,
2108+
fork: Fork,
2109+
create_opcode: Op,
2110+
outer_outcome: str,
2111+
num_inner_ops: int,
2112+
) -> None:
2113+
"""
2114+
Cross-over: PR #2704 CREATE failure refund in a creation-tx context.
2115+
2116+
The original closed PR #2639 test asserted inner CREATE's
2117+
`GAS_NEW_ACCOUNT` PERSISTS on child failure (targeting a
2118+
bal-devnet-3 geth behavior where geth incorrectly refunded).
2119+
PR #2704 later changed the spec to refund the charge, which
2120+
codifies what geth was already doing and inverts the original
2121+
test's premise. This test covers the CURRENT spec behavior in
2122+
a scenario not exercised by PR #2704's own tests (which all
2123+
use non-creation factories):
2124+
2125+
A creation tx (to=None) whose initcode performs `num_inner_ops`
2126+
inner CREATE/CREATE2 calls with REVERT initcode. Each inner
2127+
CREATE charges `GAS_NEW_ACCOUNT` state then refunds it via the
2128+
child-error branch (PR #2704). The outer initcode then
2129+
terminates via RETURN (succeeds) or REVERT.
2130+
2131+
Both outcomes yield `block_state = outer intrinsic` because inner
2132+
refunds net the state gas back to zero; PR #2689's top-level
2133+
refund (applied on revert) is a no-op over an already-zeroed
2134+
`state_gas_used`. A client regressing to the pre-#2704 "persist"
2135+
behavior would inflate `block_state` by
2136+
`num_inner_ops * GAS_NEW_ACCOUNT` and fail both variants.
2137+
2138+
The `outer_halts` variant is omitted: INVALID absorbs remaining
2139+
gas into `regular_gas_used`, making `block_regular` dominate the
2140+
header and dilute the state-dimension signal. PR #2689's
2141+
`test_creation_tx_failure_preserves_intrinsic_state_gas` already
2142+
covers the creation-tx + top-level halt interaction.
2143+
"""
2144+
gas_costs = fork.gas_costs()
2145+
outer_state_gas = fork.create_state_gas(code_size=0)
2146+
2147+
inner_initcode = bytes(Op.REVERT(0, 0))
2148+
2149+
setup = Op.MSTORE(
2150+
0,
2151+
int.from_bytes(inner_initcode, "big")
2152+
<< (256 - 8 * len(inner_initcode)),
2153+
)
2154+
2155+
inner_ops = Bytecode()
2156+
for i in range(num_inner_ops):
2157+
if create_opcode == Op.CREATE2:
2158+
inner_ops += Op.POP(Op.CREATE2(0, 0, len(inner_initcode), i))
2159+
else:
2160+
inner_ops += Op.POP(Op.CREATE(0, 0, len(inner_initcode)))
2161+
2162+
if outer_outcome == "succeeds":
2163+
termination = Op.RETURN(0, 0)
2164+
else:
2165+
termination = Op.REVERT(0, 0)
2166+
2167+
initcode = setup + inner_ops + termination
2168+
2169+
sender = pre.fund_eoa()
2170+
intrinsic_calc = fork.transaction_intrinsic_cost_calculator()
2171+
intrinsic_total = intrinsic_calc(
2172+
calldata=bytes(initcode), contract_creation=True
2173+
)
2174+
2175+
# Gas budget: enough for each inner CREATE to charge
2176+
# GAS_NEW_ACCOUNT (spill into gas_left, then get refunded on
2177+
# child failure) and for the outer termination to run.
2178+
initcode_gas = initcode.gas_cost(fork)
2179+
per_inner_slack = 2_000
2180+
gas_limit = (
2181+
intrinsic_total
2182+
+ initcode_gas
2183+
+ num_inner_ops * (gas_costs.GAS_NEW_ACCOUNT + per_inner_slack)
2184+
)
2185+
2186+
# Expected: only outer intrinsic remains in block_state. Each
2187+
# inner CREATE's charge is refunded by PR #2704; any residue
2188+
# left in state_gas_used is zeroed on outer failure by PR #2689.
2189+
expected_state = outer_state_gas
2190+
2191+
create_address = compute_create_address(address=sender, nonce=0)
2192+
2193+
tx = Transaction(
2194+
sender=sender,
2195+
to=None,
2196+
data=initcode,
2197+
gas_limit=gas_limit,
2198+
)
2199+
2200+
if outer_outcome == "succeeds":
2201+
post: dict = {create_address: Account(code=b"")}
2202+
else:
2203+
post = {create_address: Account.NONEXISTENT}
2204+
2205+
blockchain_test(
2206+
pre=pre,
2207+
blocks=[
2208+
Block(
2209+
txs=[tx],
2210+
header_verify=Header(gas_used=expected_state),
2211+
),
2212+
],
2213+
post=post,
2214+
)

0 commit comments

Comments
 (0)