@@ -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