Skip to content

Commit d9ce45d

Browse files
committed
feat(tests): 8037 creation-tx SELFDESTRUCT and inner CREATE compositions
Two tests that exercise state-gas paths the merged PRs don't cover directly: both involve a CREATION tx (to=None) whose initcode interacts with nested CREATE / SELFDESTRUCT semantics. test_selfdestruct_in_create_tx_initcode Creation tx whose initcode SELFDESTRUCTs to a new beneficiary. The outer contract is in `tx_state.created_accounts` and `accounts_to_delete`, so PR ethereum#2707 refunds its GAS_NEW_ACCOUNT end-of-tx. The beneficiary's new-account charge is NOT refunded (beneficiary is not in `created_accounts`), but it equals the refund amount, so `state_gas_used` nets to zero. Only the outer intrinsic_state remains in the header. test_inner_create_succeeds_code_deposit_state_gas (parametrized `outer_outcome` in {succeeds, reverts, halts} x `create_opcode` in {CREATE, CREATE2}) Creation tx whose initcode does an inner CREATE that succeeds and deploys 1 byte of code. The outer then terminates normally, reverts, or halts. * outer_succeeds: inner GAS_NEW_ACCOUNT + code-deposit accumulate via `incorporate_child_on_success`. Block state = 2 * GAS_NEW_ACCOUNT + inner code deposit. * outer_reverts / outer_halts: top-level failure refund (PR ethereum#2689) zeroes execution state gas. Only the outer intrinsic remains. Both tests complete the coverage gap between ethereum#2707/ethereum#2704/ethereum#2689 single-scenario tests for creation-tx initcode compositions.
1 parent 2a22e1f commit d9ce45d

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,3 +1746,185 @@ def test_oversized_initcode_opcode_no_state_gas(
17461746
blocks=[Block(txs=[tx])],
17471747
post=post,
17481748
)
1749+
1750+
1751+
@pytest.mark.valid_from("EIP8037")
1752+
def test_selfdestruct_in_create_tx_initcode(
1753+
blockchain_test: BlockchainTestFiller,
1754+
pre: Alloc,
1755+
fork: Fork,
1756+
) -> None:
1757+
"""
1758+
Verify state gas for a creation tx whose initcode SELFDESTRUCTs.
1759+
1760+
A creation tx (to=None) with value=1 whose initcode immediately
1761+
SELFDESTRUCTs to a new beneficiary. Under EIP-6780 the outer
1762+
contract is marked for deletion (created in the same tx). The
1763+
SELFDESTRUCT charges `GAS_NEW_ACCOUNT` for the beneficiary.
1764+
1765+
Post-PR #2707: the outer account is in `created_accounts` AND
1766+
`accounts_to_delete`, so its `GAS_NEW_ACCOUNT` is refunded
1767+
end-of-tx. The beneficiary charge (new, live) is NOT refunded
1768+
because the beneficiary itself is not in `created_accounts`.
1769+
After the refund, `state_gas_used = 0` (refund == beneficiary
1770+
charge amount). Intrinsic state gas for the outer account is
1771+
preserved (tracked separately from execution state gas).
1772+
1773+
Expected block state gas = intrinsic_state = `GAS_NEW_ACCOUNT`.
1774+
A client that skips the end-of-tx refund for the creation-tx
1775+
path would report `2 * GAS_NEW_ACCOUNT`.
1776+
"""
1777+
gas_costs = fork.gas_costs()
1778+
create_state_gas = fork.create_state_gas(code_size=0)
1779+
1780+
beneficiary = 0xDEAD
1781+
initcode = Op.SELFDESTRUCT(beneficiary)
1782+
1783+
sender = pre.fund_eoa()
1784+
intrinsic_calc = fork.transaction_intrinsic_cost_calculator()
1785+
intrinsic_total = intrinsic_calc(
1786+
calldata=bytes(initcode), contract_creation=True
1787+
)
1788+
1789+
# Refund zeroes state_gas_used from the beneficiary charge;
1790+
# only intrinsic state (outer account) remains in the header.
1791+
expected_state = create_state_gas
1792+
1793+
# Gas budget: enough for initcode + beneficiary state charge
1794+
# so the SELFDESTRUCT reaches completion.
1795+
initcode_gas = initcode.gas_cost(fork)
1796+
gas_limit = (
1797+
intrinsic_total + initcode_gas + gas_costs.GAS_NEW_ACCOUNT + 1000
1798+
)
1799+
1800+
tx = Transaction(
1801+
sender=sender,
1802+
to=None,
1803+
data=initcode,
1804+
value=1,
1805+
gas_limit=gas_limit,
1806+
)
1807+
1808+
blockchain_test(
1809+
pre=pre,
1810+
blocks=[
1811+
Block(
1812+
txs=[tx],
1813+
header_verify=Header(gas_used=expected_state),
1814+
),
1815+
],
1816+
post={},
1817+
)
1818+
1819+
1820+
@pytest.mark.parametrize(
1821+
"outer_outcome",
1822+
[
1823+
pytest.param("succeeds", id="outer_succeeds"),
1824+
pytest.param("reverts", id="outer_reverts"),
1825+
pytest.param("halts", id="outer_halts"),
1826+
],
1827+
)
1828+
@pytest.mark.with_all_create_opcodes()
1829+
@pytest.mark.valid_from("EIP8037")
1830+
def test_inner_create_succeeds_code_deposit_state_gas(
1831+
blockchain_test: BlockchainTestFiller,
1832+
pre: Alloc,
1833+
fork: Fork,
1834+
create_opcode: Op,
1835+
outer_outcome: str,
1836+
) -> None:
1837+
"""
1838+
Verify state gas for creation tx with a successful inner CREATE.
1839+
1840+
A creation tx (to=None) whose initcode runs an inner
1841+
CREATE/CREATE2 that succeeds and deploys 1 byte of code. The
1842+
outer initcode then terminates via RETURN, REVERT, or INVALID.
1843+
1844+
Post-PR #2704/#2689:
1845+
* outer_succeeds: the inner's `GAS_NEW_ACCOUNT` plus 1-byte
1846+
code deposit accumulate into the outer's `state_gas_used`
1847+
(via `incorporate_child_on_success`). No refund applies.
1848+
Block state = outer intrinsic + inner account + inner code
1849+
deposit.
1850+
* outer_reverts / outer_halts: top-level failure refund zeroes
1851+
`state_gas_used` (PR #2689). Only the outer's intrinsic
1852+
state gas remains. Block state = outer intrinsic only.
1853+
1854+
A client that fails to accumulate inner state gas on success,
1855+
or fails to refund on top-level failure, will produce the wrong
1856+
header.
1857+
"""
1858+
gas_costs = fork.gas_costs()
1859+
outer_state_gas = fork.create_state_gas(code_size=0)
1860+
inner_code_deposit = fork.code_deposit_state_gas(code_size=1)
1861+
inner_state_gas = gas_costs.GAS_NEW_ACCOUNT + inner_code_deposit
1862+
1863+
# Inner initcode: MSTORE one byte of 0x00, RETURN(31, 1).
1864+
deploy_code = Op.STOP
1865+
inner_initcode = Op.MSTORE(
1866+
0,
1867+
int.from_bytes(bytes(deploy_code), "big") << 248,
1868+
) + Op.RETURN(31, 1)
1869+
inner_bytes = bytes(inner_initcode)
1870+
1871+
setup = Op.MSTORE(
1872+
0,
1873+
int.from_bytes(inner_bytes, "big") << (256 - 8 * len(inner_bytes)),
1874+
)
1875+
if create_opcode == Op.CREATE2:
1876+
inner_create = Op.POP(Op.CREATE2(0, 0, len(inner_bytes), 0))
1877+
else:
1878+
inner_create = Op.POP(Op.CREATE(0, 0, len(inner_bytes)))
1879+
1880+
if outer_outcome == "succeeds":
1881+
termination = Op.RETURN(0, 0)
1882+
elif outer_outcome == "reverts":
1883+
termination = Op.REVERT(0, 0)
1884+
else:
1885+
termination = Op.INVALID
1886+
1887+
initcode = setup + inner_create + termination
1888+
1889+
sender = pre.fund_eoa()
1890+
intrinsic_calc = fork.transaction_intrinsic_cost_calculator()
1891+
intrinsic_total = intrinsic_calc(
1892+
calldata=bytes(initcode), contract_creation=True
1893+
)
1894+
1895+
# Static cost excludes inner code-deposit, so add it to give
1896+
# the initcode enough to reach RETURN in the child frame.
1897+
initcode_gas = initcode.gas_cost(fork)
1898+
gas_limit = intrinsic_total + initcode_gas + inner_code_deposit + 1000
1899+
1900+
if outer_outcome == "succeeds":
1901+
expected_state = outer_state_gas + inner_state_gas
1902+
else:
1903+
# Top-level failure refund (PR #2689) zeroes execution
1904+
# state gas, so only the outer intrinsic charge remains.
1905+
expected_state = outer_state_gas
1906+
1907+
create_address = compute_create_address(address=sender, nonce=0)
1908+
1909+
tx = Transaction(
1910+
sender=sender,
1911+
to=None,
1912+
data=initcode,
1913+
gas_limit=gas_limit,
1914+
)
1915+
1916+
if outer_outcome == "succeeds":
1917+
post: dict = {create_address: Account(code=b"")}
1918+
else:
1919+
post = {create_address: Account.NONEXISTENT}
1920+
1921+
blockchain_test(
1922+
pre=pre,
1923+
blocks=[
1924+
Block(
1925+
txs=[tx],
1926+
header_verify=Header(gas_used=expected_state),
1927+
),
1928+
],
1929+
post=post,
1930+
)

0 commit comments

Comments
 (0)