Description:
BlobPool.Cache can panic with a nil pointer dereference on p.store due to a race between NewCache() starting its background goroutine and BlobPool.Init() setting p.store.
Root Cause:
In eth/backend.go, the initialization order is:
eth.blobTxPool = blobpool.New(...) // line 332: creates BlobPool, store is nil
eth.blobCache = blobpool.NewCache(...) // line 333: starts cache loop goroutine immediately
eth.txPool, err = txpool.New(...) // line 335: calls BlobPool.Init(), which sets p.store
Inside Init(), p.head is set (line 641) before p.store (line 673):
p.head.Store(head) // line 641: head is now non-nil
// ...
store, err := billy.Open(..., index) // line 669: scans disk, populates p.lookup/p.index via parseTransaction
p.store = store // line 673: store finally assigned
The cache goroutine started at line 333 calls triggerTopK() → selectTopTxs(), which has a guard:
head := p.head.Load()
if head == nil {
return nil // early exit
}
This guard fails to protect once p.head is set at line 641. During billy.Open() (line 669), p.lookup and p.index are populated via the index callback → parseTransaction() → trackTransaction(), but p.store is
still nil. If selectTopTxs() runs in this window, it reads from p.index/p.lookup, returns vhashes, and update() spawns a goroutine that calls getByVhash() → p.store.Get() → panic.
Stack Trace:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x1349b32]
goroutine 11789 [running]:
github.com/ethereum/go-ethereum/core/txpool/blobpool.(*BlobPool).getByVhash(...)
core/txpool/blobpool/blobpool.go:1303 +0x192
github.com/ethereum/go-ethereum/core/txpool/blobpool.(*Cache).update.func1()
core/txpool/blobpool/cache.go:429 +0x1ca
created by (*Cache).update in goroutine 11786
core/txpool/blobpool/cache.go:415 +0x51e
Impact:
On nodes with existing blobpool data on disk, this race is highly reproducible. In our case, the node crashed 81 times in CrashLoopBackOff before one startup got lucky (empty Pending() result in the race
window). The more data on disk, the longer billy.Open() takes, and the wider the race window.
Possible Fix:
Move NewCache() to after txpool.New() returns (i.e., after Init() completes), or defer starting the cache loop until Init() signals readiness (e.g., add a Ready() channel that Init() closes after p.store is
set).
Affected Versions:
Confirmed present on glamsterdam-devnet-7 (HEAD bdf6e17) and glamsterdam-devnet-8. Also present on master.
Description:
BlobPool.Cache can panic with a nil pointer dereference on p.store due to a race between NewCache() starting its background goroutine and BlobPool.Init() setting p.store.
Root Cause:
In eth/backend.go, the initialization order is:
Inside Init(), p.head is set (line 641) before p.store (line 673):
The cache goroutine started at line 333 calls triggerTopK() → selectTopTxs(), which has a guard:
This guard fails to protect once p.head is set at line 641. During billy.Open() (line 669), p.lookup and p.index are populated via the index callback → parseTransaction() → trackTransaction(), but p.store is
still nil. If selectTopTxs() runs in this window, it reads from p.index/p.lookup, returns vhashes, and update() spawns a goroutine that calls getByVhash() → p.store.Get() → panic.
Stack Trace:
Impact:
On nodes with existing blobpool data on disk, this race is highly reproducible. In our case, the node crashed 81 times in CrashLoopBackOff before one startup got lucky (empty Pending() result in the race
window). The more data on disk, the longer billy.Open() takes, and the wider the race window.
Possible Fix:
Move NewCache() to after txpool.New() returns (i.e., after Init() completes), or defer starting the cache loop until Init() signals readiness (e.g., add a Ready() channel that Init() closes after p.store is
set).
Affected Versions:
Confirmed present on glamsterdam-devnet-7 (HEAD bdf6e17) and glamsterdam-devnet-8. Also present on master.