Skip to content

Commit 7da8016

Browse files
authored
core/txpool/blobpool: speed up serving pre eth/72 peers (#35543)
Add cache for recovered cells. 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 would remain without this. Enhance, a dedicated cache is added for this purpose.
1 parent 02b73d4 commit 7da8016

3 files changed

Lines changed: 109 additions & 0 deletions

File tree

core/txpool/blobpool/blobpool.go

Lines changed: 42 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"
@@ -575,9 +581,22 @@ type BlobPool struct {
575581
discoverFeed event.Feed // Event feed to send out new tx events on pool discovery (reorg excluded)
576582
insertFeed event.Feed // Event feed to send out new tx events on pool inclusion (reorg included)
577583

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

593+
// rlpCacheKey keys the encoded-response cache. full is true for pre eth/72
594+
// requests (which carry full blobs) and false for eth/72+ (blob payload elided).
595+
type rlpCacheKey struct {
596+
hash common.Hash
597+
full bool
598+
}
599+
581600
// New creates a new blob transaction pool to gather, sort and filter inbound
582601
// blob transactions from the network.
583602
func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bool) *BlobPool {
@@ -595,6 +614,7 @@ func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bo
595614
spent: make(map[common.Address]*uint256.Int),
596615
gapped: make(map[common.Address][]*BlobTxForPool),
597616
gappedSource: make(map[common.Hash]common.Address),
617+
rlpCache: lru.NewSizeConstrainedCache[rlpCacheKey, []byte](getRLPCacheSize),
598618
}
599619
}
600620

@@ -1753,17 +1773,39 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction {
17531773
}
17541774

17551775
// GetRLP returns an RLP-encoded transaction if it is contained in the pool.
1776+
//
1777+
// The encoded response is memoized: legacy (pre eth/72) peers fetch full blobs
1778+
// via GetPooledTransactions, and the same transaction is typically requested
1779+
// by multiple peers. Without the cache, each request re-reads the tx from disk
1780+
// and re-encodes it, recovering the blobs from the stored cells.
17561781
func (p *BlobPool) GetRLP(hash common.Hash, version uint) []byte {
1782+
key := rlpCacheKey{hash: hash, full: version < 72}
1783+
if enc, ok := p.rlpCache.Get(key); ok {
1784+
// The encoding is content-addressed, but only serve it while the tx is
1785+
// still pooled so a hit cannot resurrect a dropped transaction.
1786+
p.lock.RLock()
1787+
_, pooled := p.lookup.storeidOfTx(hash)
1788+
p.lock.RUnlock()
1789+
if pooled {
1790+
getRLPCacheHitMeter.Mark(1)
1791+
return enc
1792+
}
1793+
}
17571794
data := p.getRLP(hash)
17581795
if len(data) == 0 {
17591796
// Not in this pool, do not log.
17601797
return nil
17611798
}
1799+
// Count misses only for transactions the pool actually holds, so the
1800+
// hit/miss ratio reflects cache effectiveness rather than unknown-tx
1801+
// requests.
1802+
getRLPCacheMissMeter.Mark(1)
17621803
rlp, err := encodeForNetwork(data, version)
17631804
if err != nil {
17641805
log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err)
17651806
return nil
17661807
}
1808+
p.rlpCache.Add(key, rlp)
17671809
return rlp
17681810
}
17691811

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)