Skip to content

Commit 4f20f38

Browse files
committed
core/txpool/blobpool: cache encoded GetPooledTransactions responses
Serving a blob tx to a legacy (pre eth/72) peer re-reads it from disk and re-encodes its full blobs on every GetPooledTransactions request, even though the same transaction is typically requested by multiple peers. The fast path removed the KZG cost, but the disk read and blob re-encode remain, and the KZG cost would return for non-data custody. Memoize the encoded wire response in a byte-bounded (16 MiB) LRU keyed by (tx hash, whether full blobs are needed). The encoding is content-addressed and stable while a tx is pooled, so a hit is served only after confirming the tx is still present (a dropped tx must not be resurrected from the cache). eth/72+ requests (blob payload elided) are cached separately from legacy full-blob ones. Adds blobpool/getrlp/{hit,miss} meters.
1 parent 395c61d commit 4f20f38

3 files changed

Lines changed: 107 additions & 0 deletions

File tree

core/txpool/blobpool/blobpool.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"time"
3232

3333
"github.com/ethereum/go-ethereum/common"
34+
"github.com/ethereum/go-ethereum/common/lru"
3435
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
3536
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
3637
"github.com/ethereum/go-ethereum/core"
@@ -83,6 +84,11 @@ const (
8384
// so pushing it down too aggressively might make resurrections non-functional.
8485
maxTxsPerAccount = 16
8586

87+
// getRLPCacheSize is the byte budget of the cache of encoded
88+
// GetPooledTransactions responses. It bounds the memory spent on serving
89+
// the same blob transactions to multiple (legacy) peers.
90+
getRLPCacheSize = 16 * 1024 * 1024
91+
8692
// pendingTransactionStore is the subfolder containing the currently queued
8793
// blob transactions.
8894
pendingTransactionStore = "queue"
@@ -578,9 +584,22 @@ type BlobPool struct {
578584
discoverFeed event.Feed // Event feed to send out new tx events on pool discovery (reorg excluded)
579585
insertFeed event.Feed // Event feed to send out new tx events on pool inclusion (reorg included)
580586

587+
// rlpCache memoizes encoded GetPooledTransactions responses, keyed by
588+
// (tx hash, whether full blobs are needed). It is content-addressed: a
589+
// pooled tx's encoding is stable, so a cached entry is valid as long as the
590+
// tx is still in the pool (checked on lookup before serving a hit).
591+
rlpCache *lru.SizeConstrainedCache[rlpCacheKey, []byte]
592+
581593
lock sync.RWMutex // Mutex protecting the pool during reorg handling
582594
}
583595

596+
// rlpCacheKey keys the encoded-response cache. full is true for pre eth/72
597+
// requests (which carry full blobs) and false for eth/72+ (blob payload elided).
598+
type rlpCacheKey struct {
599+
hash common.Hash
600+
full bool
601+
}
602+
584603
// New creates a new blob transaction pool to gather, sort and filter inbound
585604
// blob transactions from the network.
586605
func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bool) *BlobPool {
@@ -598,6 +617,7 @@ func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bo
598617
spent: make(map[common.Address]*uint256.Int),
599618
gapped: make(map[common.Address][]*BlobTxForPool),
600619
gappedSource: make(map[common.Hash]common.Address),
620+
rlpCache: lru.NewSizeConstrainedCache[rlpCacheKey, []byte](getRLPCacheSize),
601621
}
602622
}
603623

@@ -1756,7 +1776,26 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction {
17561776
}
17571777

17581778
// GetRLP returns an RLP-encoded transaction if it is contained in the pool.
1779+
//
1780+
// The encoded response is memoized: legacy (pre eth/72) peers fetch full blobs
1781+
// via GetPooledTransactions, and the same transaction is typically requested
1782+
// by multiple peers. Without the cache, each request re-reads the tx from disk
1783+
// and re-encodes it, recovering the blobs from the stored cells.
17591784
func (p *BlobPool) GetRLP(hash common.Hash, version uint) []byte {
1785+
key := rlpCacheKey{hash: hash, full: version < 72}
1786+
if enc, ok := p.rlpCache.Get(key); ok {
1787+
// The encoding is content-addressed, but only serve it while the tx is
1788+
// still pooled so a hit cannot resurrect a dropped transaction.
1789+
p.lock.RLock()
1790+
_, pooled := p.lookup.storeidOfTx(hash)
1791+
p.lock.RUnlock()
1792+
if pooled {
1793+
getRLPCacheHitMeter.Mark(1)
1794+
return enc
1795+
}
1796+
}
1797+
getRLPCacheMissMeter.Mark(1)
1798+
17601799
data := p.getRLP(hash)
17611800
if len(data) == 0 {
17621801
// Not in this pool, do not log.
@@ -1767,6 +1806,7 @@ func (p *BlobPool) GetRLP(hash common.Hash, version uint) []byte {
17671806
log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err)
17681807
return nil
17691808
}
1809+
p.rlpCache.Add(key, rlp)
17701810
return rlp
17711811
}
17721812

core/txpool/blobpool/blobpool_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2439,3 +2439,66 @@ func TestGetCells(t *testing.T) {
24392439
})
24402440
}
24412441
}
2442+
2443+
// TestGetRLPCache checks that GetRLP memoizes encoded responses, keys full (pre
2444+
// eth/72) and sparse (eth/72+) encodings separately, and does not serve a cached
2445+
// entry once the transaction has left the pool.
2446+
func TestGetRLPCache(t *testing.T) {
2447+
storage := t.TempDir()
2448+
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
2449+
store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil)
2450+
2451+
var (
2452+
key1, _ = crypto.GenerateKey()
2453+
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
2454+
tx1 = makeMultiBlobTx(0, 1, 1000, 100, 1, 0, key1)
2455+
ptx1, _ = newBlobTxForPool(tx1)
2456+
blob1, _ = rlp.EncodeToBytes(ptx1)
2457+
)
2458+
store.Put(blob1)
2459+
store.Close()
2460+
2461+
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
2462+
statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
2463+
statedb.Commit(params.Rules{IsEIP158: true}, 0)
2464+
chain := &testBlockChain{
2465+
config: params.MainnetChainConfig,
2466+
basefee: uint256.NewInt(params.InitialBaseFee),
2467+
blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
2468+
statedb: statedb,
2469+
}
2470+
pool := New(Config{Datadir: storage}, chain, nil)
2471+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
2472+
t.Fatalf("failed to create blob pool: %v", err)
2473+
}
2474+
defer pool.Close()
2475+
2476+
// Full (legacy) encoding is memoized and stable across requests.
2477+
full1 := pool.GetRLP(tx1.Hash(), 71)
2478+
if len(full1) == 0 {
2479+
t.Fatalf("expected full encoding, got empty")
2480+
}
2481+
full2 := pool.GetRLP(tx1.Hash(), 71)
2482+
if !bytes.Equal(full1, full2) {
2483+
t.Fatalf("cached full encoding differs from first request")
2484+
}
2485+
// Same backing array proves the repeat response was served from the cache
2486+
// rather than re-read from disk and re-encoded.
2487+
if &full1[0] != &full2[0] {
2488+
t.Fatalf("repeat request was re-encoded instead of served from the cache")
2489+
}
2490+
// Sparse (eth/72+) encoding omits blob payloads: smaller and cached separately.
2491+
sparse := pool.GetRLP(tx1.Hash(), 72)
2492+
if len(sparse) == 0 || len(sparse) >= len(full1) {
2493+
t.Fatalf("expected smaller sparse encoding, got %d vs full %d", len(sparse), len(full1))
2494+
}
2495+
// Unknown transaction is not served.
2496+
if got := pool.GetRLP(common.Hash{0xde, 0xad}, 71); got != nil {
2497+
t.Fatalf("expected nil for unknown tx, got %d bytes", len(got))
2498+
}
2499+
// After the tx leaves the pool, a cached entry must not be served.
2500+
pool.Clear()
2501+
if got := pool.GetRLP(tx1.Hash(), 71); got != nil {
2502+
t.Fatalf("expected nil after the tx left the pool, got %d bytes", len(got))
2503+
}
2504+
}

core/txpool/blobpool/metrics.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ var (
3333
limboDatarealGauge = metrics.NewRegisteredGauge("blobpool/limbo/datareal", nil)
3434
limboSlotusedGauge = metrics.NewRegisteredGauge("blobpool/limbo/slotused", nil)
3535

36+
// The below metrics track the encoded GetPooledTransactions response cache.
37+
getRLPCacheHitMeter = metrics.NewRegisteredMeter("blobpool/getrlp/hit", nil)
38+
getRLPCacheMissMeter = metrics.NewRegisteredMeter("blobpool/getrlp/miss", nil)
39+
3640
// The below metrics track the per-shelf metrics for the primary blob store
3741
// and the temporary limbo store.
3842
shelfDatausedGaugeName = "blobpool/shelf_%d/dataused"

0 commit comments

Comments
 (0)