core/state: avoid polluting EIP-7928 read list in SelfDestructRefundB… (#34848)

…ytes

SelfDestructRefundBytes iterates s.stateObjects and previously called
IsNewContract / HasSelfDestructed on each entry to filter for accounts
that were both created and selfdestructed in the current transaction.
Both helpers go through StateDB.getStateObject, which unconditionally
records an EIP-7928 account read via s.stateReadList.AddAccount(addr).

The effect is that *every* live entry in the stateObjects cache gets
added to the per-tx read list whenever a tx triggers the EIP-8037
selfdestruct refund check (i.e. on every successful tx post-Amsterdam).
This pollutes the BAL with addresses the tx never actually touched.

In the miner, the cache may contain accounts loaded by earlier failed-tx
attempts (preCheck via getStateObject loaded them; RevertToSnapshot does
not evict the cache because reads aren't journaled). Validators
re-executing the sealed block don't have those cached entries, so they
don't reproduce the reads. Result: BAL hash mismatch on otherwise
deterministic execution.

Use direct field access on the already-iterated *stateObject so the
filter doesn't go through getStateObject. Functionally equivalent --
newContract and selfDestructed are the same fields the helpers consult
-- but with no read-list side effect.
This commit is contained in:
Stefan 2026-04-29 14:03:16 +02:00 committed by GitHub
parent f59ec20794
commit e079c9353a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -810,10 +810,12 @@ func (s *StateDB) StateChangedBytes(revid int, excludeSubcalls bool) int64 {
// for accounts that were both created and selfdestructed during this
// transaction.
func (s *StateDB) SelfDestructRefundBytes() int64 {
// Collect addresses created and selfdestructed in this tx.
// Read fields directly off the iterated obj. Routing through
// IsNewContract / HasSelfDestructed would call getStateObject for
// every cached entry, adding it to the EIP-7928 read list.
targets := make(map[common.Address]*stateObject)
for addr, obj := range s.stateObjects {
if s.IsNewContract(addr) && s.HasSelfDestructed(addr) {
if obj.newContract && obj.selfDestructed {
targets[addr] = obj
}
}