diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 7efb6e9c01..7d86730c92 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -346,10 +346,10 @@ type BlobPool struct { reserver txpool.Reserver // Address reserver to ensure exclusivity across subpools hasPendingAuth func(common.Address) bool // Determine whether the specified address has a pending 7702-auth - store billy.Database // Persistent data store for the tx metadata and blobs - newSlotter func() billy.SlotSizeFn // Factory to create fresh slotter instances for slot size lookups - stored uint64 // Useful data size of all transactions on disk - limbo *limbo // Persistent data store for the non-finalized blobs + store billy.Database // Persistent data store for the tx metadata and blobs + slotter slotSizer // O(1) slot size calculator matching the active billy shelves + stored uint64 // Useful data size of all transactions on disk + limbo *limbo // Persistent data store for the non-finalized blobs gapped map[common.Address][]*types.Transaction // Transactions that are currently gapped (nonce too high) gappedSource map[common.Hash]common.Address // Source of gapped transactions to allow rechecking on inclusion @@ -437,16 +437,19 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser // Create new slotter for pre-Osaka blob configuration. slotter := newSlotter(params.BlobTxMaxBlobs) - p.newSlotter = func() billy.SlotSizeFn { return newSlotter(params.BlobTxMaxBlobs) } // See if we need to migrate the queue blob store after fusaka slotter, err = tryMigrate(p.chain.Config(), slotter, queuedir) if err != nil { return err } - // Update the slotter factory if Osaka is active + // Build an O(1) slot size calculator from the active slotter configuration. + // We need a fresh slotter instance since tryMigrate may have consumed the + // previous one, and billy.Open below will consume this one. if p.chain.Config().OsakaTime != nil { - p.newSlotter = func() billy.SlotSizeFn { return newSlotterEIP7594(params.BlobTxMaxBlobs) } + p.slotter = newSlotSizer(newSlotterEIP7594(params.BlobTxMaxBlobs)) + } else { + p.slotter = newSlotSizer(newSlotter(params.BlobTxMaxBlobs)) } // Index all transactions on disk and delete anything unprocessable var fails []uint64 @@ -1593,7 +1596,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error // Create meta, in preparation of adding to the pool. // Having the meta simplifies the check below for underpriced transactions. - // Note: the meta will be finalized with storage information after the transaction is stored + // Note: the meta will be finalized with storage information after the transaction is stored. meta := newBlobTxMeta(tx) // Calculate the eviction parameters for the transaction @@ -1627,9 +1630,9 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err) return err } - newStorageSize, err := getSlotSize(p.newSlotter(), uint32(len(blob))) + newStorageSize, err := p.slotter.getSlotSize(uint32(len(blob))) if err != nil { - // This should also not happen at this stage + // This should never happen, but better safe than sorry. log.Warn("Dropping blob transaction due to size", "tx", tx.Hash(), "size", meta.size, "err", err) return err } diff --git a/core/txpool/blobpool/slotter.go b/core/txpool/blobpool/slotter.go index 26e3116657..3760b7c293 100644 --- a/core/txpool/blobpool/slotter.go +++ b/core/txpool/blobpool/slotter.go @@ -23,17 +23,47 @@ import ( "github.com/holiman/billy" ) -// getSlotSize return the storage size for a given transaction size based on the current slotter. -func getSlotSize(slotter billy.SlotSizeFn, size uint32) (uint32, error) { - for { - slotSize, done := slotter() - if size <= slotSize { - return slotSize, nil - } - if done { - return size, errors.New("size exceeds maximum slot size") - } +// slotSizer computes the storage shelf size for a given transaction size using +// O(1) arithmetic. Shelf sizes form an arithmetic sequence: +// +// base, base+step, base+2*step, ... +// +// This mirrors the progression in newSlotter and newSlotterEIP7594, but avoids +// creating and iterating a stateful closure on every lookup. +type slotSizer struct { + base uint32 // Size of the first shelf (txAvgSize) + step uint32 // Size increment per subsequent shelf + max uint32 // Largest valid shelf size +} + +// newSlotSizer creates a slotSizer by consuming a slotter closure once to +// discover its base size, step size, and maximum shelf size. +func newSlotSizer(slotter billy.SlotSizeFn) slotSizer { + first, done := slotter() + if done { + return slotSizer{base: first, step: 0, max: first} } + second, done := slotter() + step := second - first + last := second + for !done { + last, done = slotter() + } + return slotSizer{base: first, step: step, max: last} +} + +// getSlotSize returns the shelf size that can store a transaction of the given +// byte size, or an error if it exceeds the largest shelf. +func (s slotSizer) getSlotSize(size uint32) (uint32, error) { + if size <= s.base { + return s.base, nil + } + // Round up to the nearest shelf: base + ⌈(size-base)/step⌉ * step + slot := s.base + ((size-s.base+s.step-1)/s.step)*s.step + if slot > s.max { + return 0, errors.New("size exceeds maximum slot size") + } + return slot, nil } // tryMigrate checks if the billy needs to be migrated and migrates if needed.