Limit mempool size based on chunk feerate
What changed, and why it matters
This Bitcoin Core commit changes how the mempool (the waiting area for unconfirmed transactions) decides which transactions to kick out when it gets too full. Previously it used 'descendant feerate'; now it uses 'chunk feerate' so that the transactions a miner would pick last are the same ones the mempool evicts first. This is a consistency improvement in transaction selection policy, not a fix for a clear exploit. The change could affect transaction propagation, fee estimation, and mempool behavior under load, but the commit itself does not describe any security vulnerability.
Review the interaction between the new chunk graph (m_txgraph) and the legacy mapTx index to ensure the staged entries always correspond to live mempool entries. Verify that UpdateForRemoveFromMempool plus removeUnchecked correctly maintains mempool invariants (ancestors/descendants, cached fees/sizes) and that no entries are double-removed or leaked. Run the updated MempoolSizeLimitTest and fuzz/stress tests for mempool trimming.
Security signals we found
Change in mempool eviction policy from descendant feerate to chunk feerate
Manual staging and removal logic replacing RemoveStaged
Potential for inconsistent mempool state if GetWorstMainChunk and mapTx drift
Behavioral change in which transactions are evicted under memory pressure
No explicit security claim or CVE in commit message
Evidence from the diff
CTxMemPool::TrimToSize now queries the new transaction graph’s GetWorstMainChunk() to find the lowest-feerate chunk and evicts that chunk, instead of using the descendant_score index. The test is updated to assert that tx7 (the lowest-chunk-feerate transaction) is evicted first and tx4 (higher feerate, its own chunk) is retained. The eviction loop now manually builds a setEntries stage, calls UpdateForRemoveFromMempool, then removeUnchecked, rather than RemoveStaged. This aligns mempool eviction with the chunk-based mining selection introduced in a following commit.
Changed components
src/txmempool.cpp CTxMemPool::TrimToSizesrc/test/mempool_tests.cpp MempoolSizeLimitTestBitcoin Core mempool eviction policyInspect captured patch +32 / −13
diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp
index baee31e8..ec3bbd15 100644
--- a/src/test/mempool_tests.cpp
+++ b/src/test/mempool_tests.cpp
@@ -533,27 +533,37 @@ BOOST_AUTO_TEST_CASE(MempoolSizeLimitTest)
tx7.vout[1].nValue = 10 * COIN;
AddToMempool(pool, entry.Fee(700LL).FromTx(tx4));
+ auto usage_with_tx4_only = pool.DynamicMemoryUsage();
AddToMempool(pool, entry.Fee(100LL).FromTx(tx5));
AddToMempool(pool, entry.Fee(110LL).FromTx(tx6));
AddToMempool(pool, entry.Fee(900LL).FromTx(tx7));
- // we only require this to remove, at max, 2 txn, because it's not clear what we're really optimizing for aside from that
+ // From the topology above, tx7 must be sorted last, so it should
+ // definitely evicted first if we must trim. tx4 should definitely remain
+ // in the mempool since it has a higher feerate than its descendants and
+ // should be in its own chunk.
pool.TrimToSize(pool.DynamicMemoryUsage() - 1);
BOOST_CHECK(pool.exists(tx4.GetHash()));
- BOOST_CHECK(pool.exists(tx6.GetHash()));
BOOST_CHECK(!pool.exists(tx7.GetHash()));
+ // Tx5 and Tx6 may be removed as well because they're in the same chunk as
+ // tx7, but this behavior need not be guaranteed.
+
if (!pool.exists(tx5.GetHash()))
AddToMempool(pool, entry.Fee(100LL).FromTx(tx5));
+ if (!pool.exists(tx6.GetHash()))
+ AddToMempool(pool, entry.Fee(110LL).FromTx(tx6));
AddToMempool(pool, entry.Fee(900LL).FromTx(tx7));
- pool.TrimToSize(pool.DynamicMemoryUsage() * 0.75); // should maximize mempool size by only removing 5/7
+ // If we trim sufficiently, everything but tx4 should be removed.
+ pool.TrimToSize(usage_with_tx4_only + 1);
BOOST_CHECK(pool.exists(tx4.GetHash()));
BOOST_CHECK(!pool.exists(tx5.GetHash()));
- BOOST_CHECK(pool.exists(tx6.GetHash()));
+ BOOST_CHECK(!pool.exists(tx6.GetHash()));
BOOST_CHECK(!pool.exists(tx7.GetHash()));
AddToMempool(pool, entry.Fee(100LL).FromTx(tx5));
+ AddToMempool(pool, entry.Fee(110LL).FromTx(tx6));
AddToMempool(pool, entry.Fee(900LL).FromTx(tx7));
std::vector<CTransactionRef> vtx;
diff --git a/src/txmempool.cpp b/src/txmempool.cpp
index e5a31852..672198ec 100644
--- a/src/txmempool.cpp
+++ b/src/txmempool.cpp
@@ -1167,29 +1167,38 @@ void CTxMemPool::TrimToSize(size_t sizelimit, std::vector<COutPoint>* pvNoSpends
unsigned nTxnRemoved = 0;
CFeeRate maxFeeRateRemoved(0);
+
while (!mapTx.empty() && DynamicMemoryUsage() > sizelimit) {
- indexed_transaction_set::index<descendant_score>::type::iterator it = mapTx.get<descendant_score>().begin();
+ const auto &[worst_chunk, feeperweight] = m_txgraph->GetWorstMainChunk();
+ FeePerVSize feerate = ToFeePerVSize(feeperweight);
+ CFeeRate removed{feerate.fee, feerate.size};
// We set the new mempool min fee to the feerate of the removed set, plus the
// "minimum reasonable fee rate" (ie some value under which we consider txn
// to have 0 fee). This way, we don't allow txn to enter mempool with feerate
// equal to txn which were removed with no block in between.
- CFeeRate removed(it->GetModFeesWithDescendants(), it->GetSizeWithDescendants());
removed += m_opts.incremental_relay_feerate;
trackPackageRemoved(removed);
maxFeeRateRemoved = std::max(maxFeeRateRemoved, removed);
- setEntries stage;
- CalculateDescendants(mapTx.project<0>(it), stage);
- nTxnRemoved += stage.size();
+ nTxnRemoved += worst_chunk.size();
std::vector<CTransaction> txn;
if (pvNoSpendsRemaining) {
- txn.reserve(stage.size());
- for (txiter iter : stage)
- txn.push_back(iter->GetTx());
+ txn.reserve(worst_chunk.size());
+ for (auto ref : worst_chunk) {
+ txn.emplace_back(static_cast<const CTxMemPoolEntry&>(*ref).GetTx());
+ }
+ }
+
+ setEntries stage;
+ for (auto ref : worst_chunk) {
+ stage.insert(mapTx.iterator_to(static_cast<const CTxMemPoolEntry&>(*ref)));
+ }
+ UpdateForRemoveFromMempool(stage, false);
+ for (auto e : stage) {
+ removeUnchecked(e, MemPoolRemovalReason::SIZELIMIT);
}
- RemoveStaged(stage, false, MemPoolRemovalReason::SIZELIMIT);
if (pvNoSpendsRemaining) {
for (const CTransaction& tx : txn) {
for (const CTxIn& txin : tx.vin) {
Why this scored 38/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.