From b5eec529d04ac8ebbd723f1f8f7bd72568e00f7c Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Wed, 11 Mar 2026 11:12:09 +0800 Subject: [PATCH] perf(core/txpool): pre-filter dynamic fees during pending tx retrieval #29005 (#2137) Introduce dynamic-fee pre-filtering in txpool pending retrieval to reduce allocations and downstream processing work during mining and tx propagation. What changed: - Change the `Pending` API from `Pending(enforceTips bool)` to `Pending(minTip *uint256.Int, baseFee *uint256.Int)` in txpool interfaces. - Implement pre-filtering in `legacypool.Pending` by comparing effective tip against the provided `minTip/baseFee` for non-local, non-special txs. - Update call sites to the new API: - miner work assembly (`miner/worker.go`) - tx sync (`eth/sync.go`) - pool transaction retrieval (`eth/api_backend.go`) - protocol/test interfaces (`eth/protocol.go`, `eth/helper_test.go`). Tests: - Add targeted coverage for pending filtering semantics in `core/txpool/legacypool/legacypool_test.go`, including: - minTip threshold boundary behavior - baseFee-aware effective tip filtering - local/special transaction exemption behavior - dynamic-fee boundary behavior when baseFee is nil. Impact: - Preserves existing behavior while making pending selection cheaper for downstream consumers. - Improves confidence in edge-case behavior through dedicated tests. --- core/txpool/legacypool/legacypool.go | 26 +++- core/txpool/legacypool/legacypool_test.go | 179 ++++++++++++++++++++++ core/txpool/subpool.go | 6 +- core/txpool/txpool.go | 8 +- eth/api_backend.go | 2 +- eth/helper_test.go | 3 +- eth/protocol.go | 3 +- eth/sync.go | 2 +- miner/worker.go | 8 +- 9 files changed, 221 insertions(+), 16 deletions(-) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 493caa7201..af20848b6c 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -577,24 +577,34 @@ func (pool *LegacyPool) ContentFrom(addr common.Address) ([]*types.Transaction, } // Pending retrieves all currently processable transactions, grouped by origin -// account and sorted by nonce. The returned transaction set is a copy and can be -// freely modified by calling code. +// account and sorted by nonce. // -// The enforceTips parameter can be used to do an extra filtering on the pending -// transactions and only return those whose **effective** tip is large enough in -// the next pending execution environment. -func (pool *LegacyPool) Pending(enforceTips bool) map[common.Address][]*txpool.LazyTransaction { +// The transactions can also be pre-filtered by the dynamic fee components to +// reduce allocations and load on downstream subsystems. +func (pool *LegacyPool) Pending(minTip *uint256.Int, baseFee *uint256.Int) map[common.Address][]*txpool.LazyTransaction { pool.mu.Lock() defer pool.mu.Unlock() + // Convert the new uint256.Int types to the old big.Int ones used by the legacy pool + var ( + minTipBig *big.Int + baseFeeBig *big.Int + ) + if minTip != nil { + minTipBig = minTip.ToBig() + } + if baseFee != nil { + baseFeeBig = baseFee.ToBig() + } + pending := make(map[common.Address][]*txpool.LazyTransaction, len(pool.pending)) for addr, list := range pool.pending { txs := list.Flatten() // If the miner requests tip enforcement, cap the lists now - if enforceTips && !pool.locals.contains(addr) { + if minTipBig != nil && !pool.locals.contains(addr) { for i, tx := range txs { - if !tx.IsSpecialTransaction() && tx.EffectiveGasTipIntCmp(pool.gasTip.Load().ToBig(), pool.priced.urgent.baseFee) < 0 { + if !tx.IsSpecialTransaction() && tx.EffectiveGasTipIntCmp(minTipBig, baseFeeBig) < 0 { txs = txs[:i] break } diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index f25edfd346..8402ea9301 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -3066,6 +3066,185 @@ func BenchmarkMultiAccountBatchInsert(b *testing.B) { } } +func TestPendingMinTipThreshold(t *testing.T) { + t.Parallel() + + pool, key := setupPool() + defer pool.Close() + + addr := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, addr, big.NewInt(1_000_000_000_000_000)) + threshold := new(big.Int).Add(new(big.Int).Set(common.MinGasPrice), big.NewInt(100)) + aboveThreshold := new(big.Int).Set(threshold) + belowThreshold := new(big.Int).Sub(new(big.Int).Set(threshold), big.NewInt(1)) + + if err := pool.addRemoteSync(pricedTransaction(0, 100000, aboveThreshold, key)); err != nil { + t.Fatalf("failed to add tx nonce 0: %v", err) + } + if err := pool.addRemoteSync(pricedTransaction(1, 100000, belowThreshold, key)); err != nil { + t.Fatalf("failed to add tx nonce 1: %v", err) + } + + filtered := pool.Pending(uint256.MustFromBig(threshold), nil) + txs := filtered[addr] + if len(txs) != 1 { + t.Fatalf("unexpected filtered tx count: have %d, want %d", len(txs), 1) + } + resolved := txs[0].Resolve() + if resolved == nil || resolved.Nonce() != 0 { + have := uint64(math.MaxUint64) + if resolved != nil { + have = resolved.Nonce() + } + t.Fatalf("unexpected surviving tx after tip filter: got nonce %d", have) + } + + all := pool.Pending(nil, nil) + if len(all[addr]) != 2 { + t.Fatalf("unexpected unfiltered tx count: have %d, want %d", len(all[addr]), 2) + } +} + +func TestPendingMinTipWithBaseFee(t *testing.T) { + t.Parallel() + + pool, key := setupPoolWithConfig(eip1559Config) + defer pool.Close() + + addr := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, addr, big.NewInt(1_000_000_000_000_000)) + minGasTip := new(big.Int).Set(common.MinGasPrice) + tipPass := new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(80)) + tipPassWithoutBaseFeeOnly := new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(60)) + + if err := pool.addRemoteSync(dynamicFeeTx(0, 100000, new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(300)), tipPass, key)); err != nil { + t.Fatalf("failed to add tx nonce 0: %v", err) + } + if err := pool.addRemoteSync(dynamicFeeTx(1, 100000, new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(240)), tipPassWithoutBaseFeeOnly, key)); err != nil { + t.Fatalf("failed to add tx nonce 1: %v", err) + } + if err := pool.addRemoteSync(dynamicFeeTx(2, 100000, new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(240)), tipPassWithoutBaseFeeOnly, key)); err != nil { + t.Fatalf("failed to add tx nonce 2: %v", err) + } + + minTipBig := new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(45)) + baseFeeBig := big.NewInt(200) + minTip := uint256.MustFromBig(minTipBig) + baseFee := uint256.MustFromBig(baseFeeBig) + + withBaseFee := pool.Pending(minTip, baseFee) + txs := withBaseFee[addr] + if len(txs) != 1 { + t.Fatalf("unexpected tx count with base fee filter: have %d, want %d", len(txs), 1) + } + resolved := txs[0].Resolve() + if resolved == nil || resolved.Nonce() != 0 { + have := uint64(math.MaxUint64) + if resolved != nil { + have = resolved.Nonce() + } + t.Fatalf("unexpected surviving tx with base fee filter: got nonce %d", have) + } + + withoutBaseFee := pool.Pending(minTip, nil) + if len(withoutBaseFee[addr]) != 3 { + t.Fatalf("unexpected tx count without base fee filter: have %d, want %d", len(withoutBaseFee[addr]), 3) + } +} + +func TestPendingKeepsLocalAndSpecialTransactions(t *testing.T) { + t.Parallel() + + pool, _ := setupPool() + defer pool.Close() + minGasTip := new(big.Int).Set(common.MinGasPrice) + filterTip := new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(100)) + + specialKey, _ := crypto.GenerateKey() + specialAddr := crypto.PubkeyToAddress(specialKey.PublicKey) + testAddBalance(pool, specialAddr, big.NewInt(1_000_000_000_000_000)) + + specialTx, _ := types.SignTx(types.NewTransaction(0, common.BlockSignersBinary, big.NewInt(1), 100000, new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(1)), nil), types.HomesteadSigner{}, specialKey) + if err := pool.addRemoteSync(specialTx); err != nil { + t.Fatalf("failed to add special tx: %v", err) + } + normalTx := pricedTransaction(1, 100000, new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(200)), specialKey) + if err := pool.addRemoteSync(normalTx); err != nil { + t.Fatalf("failed to add normal tx after special tx: %v", err) + } + + localKey, _ := crypto.GenerateKey() + localAddr := crypto.PubkeyToAddress(localKey.PublicKey) + testAddBalance(pool, localAddr, big.NewInt(1_000_000_000_000_000)) + if err := pool.addLocal(pricedTransaction(0, 100000, new(big.Int).Add(new(big.Int).Set(minGasTip), big.NewInt(1)), localKey)); err != nil { + t.Fatalf("failed to add local tx: %v", err) + } + + filtered := pool.Pending(uint256.MustFromBig(filterTip), nil) + + specialPending := filtered[specialAddr] + if len(specialPending) != 2 { + t.Fatalf("unexpected special account tx count: have %d, want %d", len(specialPending), 2) + } + first := specialPending[0].Resolve() + if first == nil || first.Nonce() != 0 || !first.IsSpecialTransaction() { + t.Fatalf("special tx should survive filtering at nonce 0") + } + second := specialPending[1].Resolve() + if second == nil || second.Nonce() != 1 { + t.Fatalf("unexpected tx order for special account") + } + + localPending := filtered[localAddr] + if len(localPending) != 1 { + t.Fatalf("unexpected local account tx count: have %d, want %d", len(localPending), 1) + } + local := localPending[0].Resolve() + if local == nil || local.Nonce() != 0 { + t.Fatalf("unexpected local tx after filtering") + } +} + +func TestPendingDynamicFeeThresholdWithoutBaseFee(t *testing.T) { + t.Parallel() + + pool, key := setupPoolWithConfig(eip1559Config) + defer pool.Close() + + addr := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, addr, big.NewInt(1_000_000_000_000_000)) + + minTipBig := new(big.Int).Add(new(big.Int).Set(common.MinGasPrice), big.NewInt(50)) + equalTip := new(big.Int).Set(minTipBig) + belowTip := new(big.Int).Sub(new(big.Int).Set(minTipBig), big.NewInt(1)) + + if err := pool.addRemoteSync(dynamicFeeTx(0, 100000, new(big.Int).Add(new(big.Int).Set(equalTip), big.NewInt(100)), equalTip, key)); err != nil { + t.Fatalf("failed to add tx nonce 0: %v", err) + } + if err := pool.addRemoteSync(dynamicFeeTx(1, 100000, new(big.Int).Add(new(big.Int).Set(equalTip), big.NewInt(100)), belowTip, key)); err != nil { + t.Fatalf("failed to add tx nonce 1: %v", err) + } + + filtered := pool.Pending(uint256.MustFromBig(minTipBig), nil) + txs := filtered[addr] + if len(txs) != 1 { + t.Fatalf("unexpected filtered tx count: have %d, want %d", len(txs), 1) + } + resolved := txs[0].Resolve() + if resolved == nil || resolved.Nonce() != 0 { + have := uint64(math.MaxUint64) + if resolved != nil { + have = resolved.Nonce() + } + t.Fatalf("unexpected surviving tx without base fee: got nonce %d", have) + } + + all := pool.Pending(nil, nil) + if len(all[addr]) != 2 { + t.Fatalf("unexpected unfiltered tx count: have %d, want %d", len(all[addr]), 2) + } +} + // TestSetGasPrice tests the SetGasPrice validation logic using table-driven tests func TestSetGasPrice(t *testing.T) { testCases := []struct { diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go index c20115ff0e..4ebd72fd25 100644 --- a/core/txpool/subpool.go +++ b/core/txpool/subpool.go @@ -24,6 +24,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/core" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/event" + "github.com/holiman/uint256" ) // LazyTransaction contains a small subset of the transaction properties that is @@ -109,7 +110,10 @@ type SubPool interface { // Pending retrieves all currently processable transactions, grouped by origin // account and sorted by nonce. - Pending(enforceTips bool) map[common.Address][]*LazyTransaction + // + // The transactions can also be pre-filtered by the dynamic fee components to + // reduce allocations and load on downstream subsystems. + Pending(minTip *uint256.Int, baseFee *uint256.Int) map[common.Address][]*LazyTransaction // SubscribeTransactions subscribes to new transaction events. The subscriber // can decide whether to receive notifications only for newly seen transactions diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index f2c4ad3c00..26d5af1ca3 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -25,6 +25,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/core" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/event" + "github.com/holiman/uint256" ) // TxStatus is the current status of a transaction as seen by the pool. @@ -250,10 +251,13 @@ func (p *TxPool) Add(txs []*types.Transaction, local bool, sync bool) []error { // Pending retrieves all currently processable transactions, grouped by origin // account and sorted by nonce. -func (p *TxPool) Pending(enforceTips bool) map[common.Address][]*LazyTransaction { +// +// The transactions can also be pre-filtered by the dynamic fee components to +// reduce allocations and load on downstream subsystems. +func (p *TxPool) Pending(minTip *uint256.Int, baseFee *uint256.Int) map[common.Address][]*LazyTransaction { txs := make(map[common.Address][]*LazyTransaction) for _, subpool := range p.subpools { - maps.Copy(txs, subpool.Pending(enforceTips)) + maps.Copy(txs, subpool.Pending(minTip, baseFee)) } return txs } diff --git a/eth/api_backend.go b/eth/api_backend.go index d3bfefc8b0..cd906846e1 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -305,7 +305,7 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) } func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { - pending := b.eth.txPool.Pending(false) + pending := b.eth.txPool.Pending(nil, nil) var txs types.Transactions for _, batch := range pending { for _, lazy := range batch { diff --git a/eth/helper_test.go b/eth/helper_test.go index 7202aee29e..04f6b0494a 100644 --- a/eth/helper_test.go +++ b/eth/helper_test.go @@ -42,6 +42,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/p2p" "github.com/XinFinOrg/XDPoSChain/p2p/discover" "github.com/XinFinOrg/XDPoSChain/params" + "github.com/holiman/uint256" ) var ( @@ -146,7 +147,7 @@ func (p *testTxPool) Add(txs []*types.Transaction, local bool, sync bool) []erro } // Pending returns all the transactions known to the pool -func (p *testTxPool) Pending(enforceTips bool) map[common.Address][]*txpool.LazyTransaction { +func (p *testTxPool) Pending(minTip *uint256.Int, baseFee *uint256.Int) map[common.Address][]*txpool.LazyTransaction { p.lock.RLock() defer p.lock.RUnlock() diff --git a/eth/protocol.go b/eth/protocol.go index eff0fbdce5..23db4e5165 100644 --- a/eth/protocol.go +++ b/eth/protocol.go @@ -27,6 +27,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/rlp" + "github.com/holiman/uint256" ) // Constants to match up protocol versions and messages @@ -109,7 +110,7 @@ type txPool interface { // Pending should return pending transactions. // The slice should be modifiable by the caller. - Pending(enforceTips bool) map[common.Address][]*txpool.LazyTransaction + Pending(minTip *uint256.Int, baseFee *uint256.Int) map[common.Address][]*txpool.LazyTransaction // SubscribeTransactions subscribes to new transaction events. The subscriber // can decide whether to receive notifications only for newly seen transactions diff --git a/eth/sync.go b/eth/sync.go index 8eadbfa332..0da6bec830 100644 --- a/eth/sync.go +++ b/eth/sync.go @@ -45,7 +45,7 @@ type txsync struct { // syncTransactions starts sending all currently pending transactions to the given peer. func (pm *ProtocolManager) syncTransactions(p *peer) { var txs types.Transactions - pending := pm.txpool.Pending(false) + pending := pm.txpool.Pending(nil, nil) for _, batch := range pending { for _, lazy := range batch { if tx := lazy.Resolve(); tx != nil { diff --git a/miner/worker.go b/miner/worker.go index a23319b6f9..6cfb6167e1 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -46,6 +46,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/trie" mapset "github.com/deckarep/golang-set/v2" + "github.com/holiman/uint256" ) const ( @@ -860,7 +861,12 @@ func (w *worker) commitNewWork() { log.Error("[commitNewWork] fail to check if block is epoch switch block when fetching pending transactions", "BlockNum", header.Number, "Hash", header.Hash()) } if !isEpochSwitchBlock { - pending := w.eth.TxPool().Pending(true) + // Retrieve the pending transactions pre-filtered by the 1559 dynamic fees + var baseFee *uint256.Int + if header.BaseFee != nil { + baseFee = uint256.MustFromBig(header.BaseFee) + } + pending := w.eth.TxPool().Pending(uint256.MustFromBig(w.tip), baseFee) txs, specialTxs = newTransactionsByPriceAndNonce(w.current.signer, pending, feeCapacity, header.BaseFee) } }