From e079c9353afd19f19bf40056e7b846a9e17b7632 Mon Sep 17 00:00:00 2001 From: Stefan <22667037+qu0b@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:03:16 +0200 Subject: [PATCH] =?UTF-8?q?core/state:=20avoid=20polluting=20EIP-7928=20re?= =?UTF-8?q?ad=20list=20in=20SelfDestructRefundB=E2=80=A6=20(#34848)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …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. --- core/state/statedb.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index e40916c504..7ecae74225 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -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 } }