Skip to content

feat(spec-specs, tests): EIP-8037 - SELFDESTRUCT same-tx refunds state gas at end of tx#2707

Merged
spencer-tb merged 2 commits into
ethereum:eips/amsterdam/eip-8037from
spencer-tb:eips/amsterdam/eip-8037-selfdestruct-refund
Apr 19, 2026
Merged

feat(spec-specs, tests): EIP-8037 - SELFDESTRUCT same-tx refunds state gas at end of tx#2707
spencer-tb merged 2 commits into
ethereum:eips/amsterdam/eip-8037from
spencer-tb:eips/amsterdam/eip-8037-selfdestruct-refund

Conversation

@spencer-tb

@spencer-tb spencer-tb commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🗒️ Description

Implements SELFDESTRUCT same tx state gas refund: when an account is created AND self-destructed in the same transaction (EIP-6780), refund its state gas to state_gas_reservoir at the end of the transaction.

EIP text change: ethereum/EIPs#11532

Spec change: 0e1c131

After process_message_call returns, iterate tx_output.accounts_to_delete. For each address also in tx_state.created_accounts, refund:

  • STATE_BYTES_PER_NEW_ACCOUNT * cost_per_state_byte (account)
  • STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte per non-zero final storage slot
  • len(code) * cost_per_state_byte (code deposit)

Clamped to state_gas_used (cumulative across multiple accounts because state_gas_used -= refund each iteration). Applied before tx_gas_used_before_refund is computed so block_state_gas_used reflects the refund.

The refund loop runs only when tx_output.error is None — composes cleanly with the top-level failure refund path from #2689 which zeros state_gas_used on top-level revert/halt (and where accounts_to_delete is already empty per interpreter.py:157).

account.code_hash still points at deployed code here because EIP-6780 defers actual account/storage/code removal to tx-end; a comment in the spec calls this out so readers don't wonder why get_code doesn't return empty for a selfdestructed account.

Tests: 1495372

Four tests in test_state_gas_selfdestruct.py, each isolating one term of the refund formula or one guard condition. All strict header_verify discriminators — reverting the spec change fails every variant.

  • test_create_selfdestruct_refunds_account_and_storage, parametrized num_slots in {0, 1, 5} and create_opcode in {CREATE, CREATE2}. Initcode writes N cold SSTOREs then SELFDESTRUCTs. Asserts refund covers GAS_NEW_ACCOUNT + N * sstore_state_gas.
  • test_create_selfdestruct_refunds_code_deposit_state_gas, parametrized (beneficiary_type, code_size) across {(self, 2), (self, 100), (external, 100)}. Factory CREATEs a contract deploying code_size bytes then CALLs it to trigger SELFDESTRUCT. Uses Op.CALL(gas=Op.GAS, address=Op.CREATE(...)) so the created address flows via the stack — no hard-coded gas or magic memory slot. external variant verifies the refund targets the created account, not the ETH destination.
  • test_create_selfdestruct_no_double_refund_with_sstore_restoration. Initcode does SSTORE(0, 1) then SSTORE(0, 0) then SELFDESTRUCT. The 0 to x to 0 restoration refunds the slot inline (via the mechanism from feat(spec-specs, tests): EIP-8037 - 0 to x to 0 SSTORE refunds to state gas #2698); the end-of-tx refund then skips it because the final value is zero. Asserts the end-of-tx refund is account-only.
  • test_selfdestruct_pre_existing_account_no_refund. A contract deployed in pre (NOT same-tx-created) is destroyed by the tx. accounts_to_delete contains it but created_accounts does not, so the if address in tx_state.created_accounts guard skips the refund. Block gas_used reflects full regular tx cost. Per EIP-6780, the victim account persists post-tx (SELFDESTRUCT only deletes same-tx-created accounts), which the post-state asserts.

Shared init_code_at_high_bytes helper is moved to spec.py so it can also be used by #2704 / future PRs.

🔗 Related Issues or PRs

✅ Checklist

  • All: Ran fast static checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    just static
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Considered updating the online docs in the ./docs/ directory.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).

@spencer-tb spencer-tb added A-spec-specs Area: Specification—The Ethereum specification itself (eg. `src/ethereum/*`) C-feat Category: an improvement or new feature A-tests Area: Consensus tests. labels Apr 17, 2026
@spencer-tb spencer-tb changed the title Eips/amsterdam/eip 8037 selfdestruct refund feat(spec-specs, tests): EIP-8037 - SELFDESTRUCT same-tx refunds state gas at end of tx Apr 17, 2026
@spencer-tb spencer-tb added this to the Pre-interop - BAL milestone Apr 17, 2026
@codecov

codecov Bot commented Apr 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (eips/amsterdam/eip-8037@5450513). Learn more about missing BASE report.

Additional details and impacted files
@@                    Coverage Diff                     @@
##             eips/amsterdam/eip-8037    #2707   +/-   ##
==========================================================
  Coverage                           ?   88.18%           
==========================================================
  Files                              ?      524           
  Lines                              ?    31120           
  Branches                           ?     3036           
==========================================================
  Hits                               ?    27444           
  Misses                             ?     3161           
  Partials                           ?      515           
Flag Coverage Δ
unittests 88.18% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@spencer-tb spencer-tb force-pushed the eips/amsterdam/eip-8037 branch from 3e1d7c4 to 44b47cc Compare April 17, 2026 16:01
@spencer-tb spencer-tb force-pushed the eips/amsterdam/eip-8037-selfdestruct-refund branch 2 times, most recently from 17d34fe to 1ad4edc Compare April 19, 2026 12:52
@spencer-tb spencer-tb force-pushed the eips/amsterdam/eip-8037-selfdestruct-refund branch from 1ad4edc to 1495372 Compare April 19, 2026 12:55
@spencer-tb spencer-tb merged commit b8defe9 into ethereum:eips/amsterdam/eip-8037 Apr 19, 2026
11 of 16 checks passed
@spencer-tb spencer-tb requested a review from kclowes April 19, 2026 13:00
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 19, 2026
…efund

EIP-11532 (merged as PR ethereum#2707) refunds the CREATE's
`GAS_NEW_ACCOUNT` at end-of-tx when the created account is also
destroyed in the same transaction. This broke three tests in
`test_state_gas_call.py` (added by PR ethereum#2646 before ethereum#2707 merged)
that asserted `header.gas_used == new_account_state_gas`; after
the refund `block_state_gas_used` is zero and the header reports
`block_regular` instead.

Affected tests (20 variants):
  test_call_value_to_self_destructed_header_gas_used
  test_call_value_to_self_destructed_burns_value
  test_call_zero_value_to_self_destructed_same_tx_account

The original discriminator intent (no spurious GAS_NEW_ACCOUNT
charge on the CALL) still holds: a buggy extra charge would push
`state_gas_used` to 131,488 and bump the header above
`new_account_state_gas`. Pin the expected `header.gas_used` to
the empirical `block_regular` per variant, with a build-time
`assert expected < new_account_state_gas` so the
spurious-charge discriminator stays sharp.
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 19, 2026
Ports three tests from the closed PR ethereum#2639 that cover reservoir
behavior paths not exercised by the merged ethereum#2689/ethereum#2704/ethereum#2707
tests.

  test_top_level_halt_preserves_restored_reservoir (parametrized
    reservoir_delta in {-1, 0, 1} x child_termination in {revert,
    halt})
    Regression test for the bal-devnet-3 Besu bug (ethereum#2644). Child
    runs an SSTORE then fails, restoring state gas to the parent.
    Parent then INVALIDs, triggering the top-level failure
    refund. Expected `header.gas_used =
    gas_limit_cap + min(reservoir_delta, 0)` so the reservoir
    (including any spill-restore) is preserved across the halt.

  test_callcode_value_no_new_account_state_gas
    CALLCODE transfers value to the caller, not to the target,
    so no new-account state gas is ever charged regardless of
    whether the target exists. The reservoir stays intact for a
    subsequent SSTORE.

  test_create_oog_during_state_gas_charge
    Parent CALLs an inner with only 20k gas forwarded. The
    inner's CREATE charges GAS_NEW_ACCOUNT which exceeds the
    forwarded budget, OOGing before any state gas lands. Per
    PR ethereum#2704 the refund restores the parent's reservoir and the
    parent's subsequent SSTORE succeeds from it.
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 19, 2026
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.
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 19, 2026
The three regression-fix tests in commit 4828ae6 used hardcoded
empirical `block_regular` dicts (per CREATE/CREATE2 x
self/external variant) to discriminate a spurious
`GAS_NEW_ACCOUNT` charge on the CALL. The dicts are brittle to any
regular-gas constant change and the spurious-charge discriminator
is redundant: PR ethereum#2707's own tests (`test_create_selfdestruct_*`)
already exercise the refund path.

Drop `header_verify` from:
  test_call_value_to_self_destructed_header_gas_used
  test_call_value_to_self_destructed_burns_value
  test_call_zero_value_to_self_destructed_same_tx_account

The tests still verify runtime behavior: NONEXISTENT created
address and orchestrator balance burned to zero.

Also adds a cross-over test for the ethereum#2704 + ethereum#2689 refund
composition that PR ethereum#2704 does not exercise directly:

  test_inner_create_fail_refunds_in_creation_tx (parametrized
    `outer_outcome` in {succeeds, reverts}, `num_inner_ops` in
    {1, 3}, `create_opcode` in {CREATE, CREATE2})
    Creation tx with `num_inner_ops` inner CREATE/CREATE2 calls
    whose initcode REVERTs. Each inner CREATE's GAS_NEW_ACCOUNT
    is refunded by PR ethereum#2704. Outer then succeeds or reverts.
    block_state == outer intrinsic in both cases; a client that
    regressed to pre-ethereum#2704 "gas persists" behavior would inflate
    it by `num_inner_ops * GAS_NEW_ACCOUNT`. Rewrites the
    inverted-premise test from the closed PR ethereum#2639.
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 20, 2026
…efund

EIP-11532 (merged as PR ethereum#2707) refunds the CREATE's
`GAS_NEW_ACCOUNT` at end-of-tx when the created account is also
destroyed in the same transaction. This broke three tests in
`test_state_gas_call.py` (added by PR ethereum#2646 before ethereum#2707 merged)
that asserted `header.gas_used == new_account_state_gas`; after
the refund `block_state_gas_used` is zero and the header reports
`block_regular` instead.

Affected tests (20 variants):
  test_call_value_to_self_destructed_header_gas_used
  test_call_value_to_self_destructed_burns_value
  test_call_zero_value_to_self_destructed_same_tx_account

The original discriminator intent (no spurious GAS_NEW_ACCOUNT
charge on the CALL) still holds: a buggy extra charge would push
`state_gas_used` to 131,488 and bump the header above
`new_account_state_gas`. Pin the expected `header.gas_used` to
the empirical `block_regular` per variant, with a build-time
`assert expected < new_account_state_gas` so the
spurious-charge discriminator stays sharp.
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 20, 2026
EIP-11532 (merged as ethereum#2707) refunds the CREATE's `GAS_NEW_ACCOUNT`
at end-of-tx when the created account is also destroyed in the same
transaction. Three tests in `test_state_gas_call.py` (added by ethereum#2646
before ethereum#2707 merged) asserted `header.gas_used == new_account_state_gas`;
after the refund `block_state_gas_used` is zero, so the header reports
`block_regular` instead and those assertions fail.

Drop the brittle `header_verify` from the three tests. Remaining
post-state checks still validate the semantics each test cares about.

Affected (20 variants across `blockchain_test` and `blockchain_test_engine`):
  test_call_value_to_self_destructed_header_gas_used
  test_call_value_to_self_destructed_burns_value
  test_call_zero_value_to_self_destructed_same_tx_account
marioevz pushed a commit that referenced this pull request Apr 20, 2026
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 20, 2026
Ports three tests from the closed PR ethereum#2639 that cover reservoir
behavior paths not exercised by the merged ethereum#2689/ethereum#2704/ethereum#2707
tests.

  test_top_level_halt_preserves_restored_reservoir (parametrized
    reservoir_delta in {-1, 0, 1} x child_termination in {revert,
    halt})
    Regression test for the bal-devnet-3 Besu bug (ethereum#2644). Child
    runs an SSTORE then fails, restoring state gas to the parent.
    Parent then INVALIDs, triggering the top-level failure
    refund. Expected `header.gas_used =
    gas_limit_cap + min(reservoir_delta, 0)` so the reservoir
    (including any spill-restore) is preserved across the halt.

  test_callcode_value_no_new_account_state_gas
    CALLCODE transfers value to the caller, not to the target,
    so no new-account state gas is ever charged regardless of
    whether the target exists. The reservoir stays intact for a
    subsequent SSTORE.

  test_create_oog_during_state_gas_charge
    Parent CALLs an inner with only 20k gas forwarded. The
    inner's CREATE charges GAS_NEW_ACCOUNT which exceeds the
    forwarded budget, OOGing before any state gas lands. Per
    PR ethereum#2704 the refund restores the parent's reservoir and the
    parent's subsequent SSTORE succeeds from it.
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 20, 2026
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.
spencer-tb added a commit that referenced this pull request Apr 20, 2026
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 21, 2026
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 21, 2026
spencer-tb added a commit that referenced this pull request Apr 21, 2026
@jangko jangko mentioned this pull request Apr 21, 2026
9 tasks
fselmo pushed a commit that referenced this pull request May 5, 2026
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-spec-specs Area: Specification—The Ethereum specification itself (eg. `src/ethereum/*`) A-tests Area: Consensus tests. C-feat Category: an improvement or new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant