From 393555b0977c97d502fcf8c25685e5f6042142d7 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 13 Aug 2026 13:26:11 +0200 Subject: [PATCH] eth: don't read a blob pool that is still initialising (#35509) `BlobPool.Init` builds the lookup, index and store without holding `p.lock`, and `eth/backend.go` publishes the pool to `blobpool.NewCache` before `txpool.New` runs Init. Cache.update spawns a goroutine calling `getByVhash`, so it can read the pool mid-construction: ``` panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x28] blobpool.(*BlobPool).getByVhash(...) blobpool.go:1303 blobpool.(*Cache).update.func1() cache.go:429 ``` `Init` fills the lookup inside `billy.Open`, whose index callback runs `parseTransaction` -> `trackTransaction`, and assigns `p.store` only once `billy.Open` returns. For that window a lookup hit names a transaction whose store does not exist yet, and `p.store.Get` dereferences nil. Build the cache after `txpool.New` so no reader exists while `Init` runs -- `getByVhash` has exactly one caller -- and have `getByVhash` read the store under the same lock as the lookup, returning nil when it is not set. A benchmark harness that restarts the client once per test hit this on 1-4% of starts; a node that starts once rarely will. --- eth/backend.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eth/backend.go b/eth/backend.go index 99a358af2a..908bfd15b1 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -329,12 +329,14 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { config.BlobPool.Datadir = stack.ResolvePath(config.BlobPool.Datadir) } eth.blobTxPool = blobpool.New(config.BlobPool, eth.blockchain, legacyPool.HasPendingAuth) - eth.blobCache = blobpool.NewCache(eth.blobTxPool) eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, []txpool.SubPool{legacyPool, eth.blobTxPool}) if err != nil { return nil, err } + // Only after txpool.New has run the pool's Init: the cache reads the pool's + // lookup and store, which Init builds without holding the pool lock. + eth.blobCache = blobpool.NewCache(eth.blobTxPool) if !config.TxPool.NoLocals { rejournal := config.TxPool.Rejournal