erigontech / erigontech/erigon
execution/state: reverting a SELFDESTRUCT deletes an earlier same-tx BalancePath write from the parallel TxOut
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
## 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":
https://github.com/erigontech/erigon/blob/4aad1d88999f55aeefcac64f9bf55faca3a4fc23/execution/state/intra_block_state.go#L1370
- and records `SelfDestructPath`, `IncarnationPath`, and `BalancePath=0`:
https://github.com/erigontech/erigon/blob/4aad1d88999f55aeefcac64f9bf55faca3a4fc23/execution/state/intra_block_state.go#L1382-L1383
- On revert, the `wasCommited` branch deletes the `BalancePath` and `SelfDestructPath` entries outright:
https://github.com/erigontech/erigon/blob/4aad1d88999f55aeefcac64f9bf55faca3a4fc23/execution/state/journal.go#L252-L264
`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 `selfdestructChange` → `DelBalance` 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 @ 4aad1d8899.
Contributor guide
Assessment
This issue has not been assessed yet.