From 04bf0453036dffc7710684354efabdbe3970ffe0 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Mon, 29 Jun 2026 22:03:12 -0300 Subject: [PATCH] core/state: fix account prefetching for absent accounts (#35256) --- core/state/statedb.go | 11 ++++++----- core/state/statedb_test.go | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 176445a575..09a896a89a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -614,16 +614,17 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { } s.AccountReads += time.Since(start) - // Short circuit if the account is not found - if acct == nil { - return nil - } - // Schedule the resolved account for prefetching if it's enabled. + // Schedule the account path for prefetching if it's enabled. Even if the + // account is absent, the trie path proves its non-existence for witnesses. if s.prefetcher != nil { if err = s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, []common.Address{addr}, nil, true); err != nil { log.Error("Failed to prefetch account", "addr", addr, "err", err) } } + // Short circuit if the account is not found + if acct == nil { + return nil + } // Insert into the live set obj := newObject(s, addr, acct) s.setStateObject(obj) diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 0bf9b50e7b..6f4282054e 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -998,6 +999,45 @@ func TestDeleteCreateRevert(t *testing.T) { } } +func TestWitnessIncludesAbsentAccountReads(t *testing.T) { + db := NewDatabaseForTesting() + state, _ := New(types.EmptyRootHash, db) + for i := byte(0); i < 3; i++ { + addr := common.Address{i + 1} + state.SetBalance(addr, uint256.NewInt(uint64(i+1)), tracing.BalanceChangeUnspecified) + } + root, err := state.Commit(0, false, false) + if err != nil { + t.Fatalf("failed to commit initial state: %v", err) + } + state, err = New(root, db) + if err != nil { + t.Fatalf("failed to reopen state: %v", err) + } + + witness := &stateless.Witness{ + Codes: make(map[string]struct{}), + State: make(map[string]struct{}), + } + state.StartPrefetcher("test", witness) + missing := common.HexToAddress("0x017655eac00c837122cabbbc0dd604a196906648") + if balance := state.GetBalance(missing); balance.Sign() != 0 { + t.Fatalf("unexpected balance for absent account: %v", balance) + } + if err := state.Error(); err != nil { + t.Fatalf("unexpected state error after read: %v", err) + } + if got := state.IntermediateRoot(false); got != root { + t.Fatalf("unexpected root after read-only access: have %x want %x", got, root) + } + if err := state.Error(); err != nil { + t.Fatalf("unexpected state error after root calculation: %v", err) + } + if len(witness.State) == 0 { + t.Fatal("missing witness nodes for absent account read") + } +} + // TestMissingTrieNodes tests that if the StateDB fails to load parts of the trie, // the Commit operation fails with an error // If we are missing trie nodes, we should not continue writing to the trie