Skip to content

execution/state: reverting a SELFDESTRUCT deletes an earlier same-tx BalancePath write from the parallel TxOut #22419

Description

@yperbasis

Summary

When a SELFDESTRUCT is unwound by an ancestor frame's revert, selfdestructChange.revert unconditionally deletes the account's BalancePath entry from the versioned write set. If an earlier, non-reverted part of the same transaction had written that account's balance — e.g. the CREATE endowment of a prefunded CREATE2 target — that write is deleted along with it and nothing re-records it. The in-memory state object is restored correctly by the journal, so serial execution is unaffected, but the transaction's versioned writes (TxOut) — which feed the parallel executor's version map, write-set validation, and the EIP-7928 Block Access List — permanently lose the balance update.

Mechanism

  • Selfdestruct marks its journal entry wasCommited: !sdb.hasWrite(addr, SelfDestructPath, NilKey) — i.e. "first SELFDESTRUCT of this address in this tx":
    wasCommited: !sdb.hasWrite(addr, SelfDestructPath, accounts.NilKey),
  • and records SelfDestructPath, IncarnationPath, and BalancePath=0:
    sdb.recordWriteSelfDestruct(addr, stateObject.selfdestructed)
    sdb.recordWriteBalance(addr, uint256.Int{})
  • On revert, the wasCommited branch deletes the BalancePath and SelfDestructPath entries outright:
    if ch.wasCommited {
    if trace {
    if v, ok := s.versionedWrites.GetSelfDestruct(ch.account); ok {
    sd := v.Val
    fmt.Printf("%s WRT Revert %x: %v -> %v\n", tracePrefix, ch.account, sd, ch.prev)
    }
    if v, ok := s.versionedWrites.GetBalance(ch.account); ok {
    val := v.Val
    fmt.Printf("%s WRT Revert %x: %d -> %d\n", tracePrefix, ch.account, &val, &ch.prevbalance)
    }
    }
    s.versionedWrites.DelBalance(ch.account)
    s.versionedWrites.DelSelfDestruct(ch.account)

wasCommited only proves the SelfDestructPath entry was freshly authored by this SELFDESTRUCT. The BalancePath entry may have been authored earlier in the transaction (endowment, transfer) and merely overwritten by the SELFDESTRUCT's zero write — deleting it on revert loses the earlier value instead of restoring it. Account-field versioned writes are only recorded during execution via recordWrite*; MakeWriteSet/FinalizeTx do not re-emit them, so the hole is not repaired at end of tx.

Reachability (Cancun+)

Post-EIP-6780 the SELFDESTRUCT must target a same-tx-created contract for the journal entry to matter, and SELFDESTRUCT halts its own frame, so the revert must come from an ancestor frame:

  1. contract A CREATE2s C at a prefunded address with an endowment → BalancePath = prefund + endowment is recorded;
  2. A calls B; B calls C, whose code SELFDESTRUCTs (that frame ends normally);
  3. B REVERTs → the snapshot unwind pops C's selfdestructChangeDelBalance removes the balance write from step one, which predates the reverted snapshot and is never re-recorded.

Post-revert truth: C exists with its code and the endowed balance. TxOut/BAL: C's balance write is missing.

Impact

Parallel-mode only: later transactions reading C through the version map, write-set validation, and the BAL see a stale or missing balance; serial execution and receipts are unaffected, which is why this can go unnoticed.

It becomes spec-load-bearing with EIP-8246 (#22030, implementation in #22136): the reverted-SELFDESTRUCT matrix in the glamsterdam-devnet-6 fixtures exercises exactly this shape, and under the 8246 balance-preserving path the SELFDESTRUCT records no BalancePath write at all, so the revert deletes an entry it demonstrably never authored.

Suggested fix

selfdestructChange should capture at append time whether a BalancePath own-write already existed and what its value was, and the revert should restore the prior entry instead of deleting it — mirroring the !wasCommited branch, which already updates entries back to previous values rather than removing them.

Surfaced by review of #22136 (Codex review pass); confirmed by code inspection on main @ 4aad1d8.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions