Skip to content

Commit cd3c9e7

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 be22eb7 commit cd3c9e7

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,3 +1928,159 @@ 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)

0 commit comments

Comments
 (0)