feat: fee-ordered mempool eviction (TrimToSize/Expire) + displacement on admission

This fork never ported Bitcoin's mempool size-limiting: CTxMemPool had no TrimToSize/
Expire and LimitMempoolSize was commented out. An earlier commit added a blunt
DynamicMemoryUsage admission cap that bounded memory but bluntly REJECTED new txs when
full -- so a high-fee tx could not push out a low-fee one. This implements proper
fee-ordered eviction using the per-tx feerate index that already exists (mapTx index 1,
CompareTxMemPoolEntryByFee), with no new index and no descendant-tracking port.

- CTxMemPool::TrimToSize(sizelimit, pvNoSpendsRemaining): while DynamicMemoryUsage() is
  over the limit, evict the lowest-feerate tx (the tail of the feerate index) and its
  in-mempool descendants (recursive remove), re-deriving the tail each iteration.
  Terminates (pool strictly shrinks) and cleans every secondary index via remove().
- CTxMemPool::Expire(time): age-based sweep (entry time older than `time`), for
  LimitMempoolSize's -mempoolexpiry.
- LimitMempoolSize re-enabled (Expire + TrimToSize) and called from ConnectTip on every
  block connect. (No pcoinsTip->Uncache -- CCoinsViewCache has none in this fork; it is
  only a UTXO-cache perf hint.)
- AcceptToMemoryPool now ADDS the tx then TrimToSizes: a higher-fee tx displaces
  lower-fee ones; if this tx was itself the lowest-feerate (evicted), it is rejected
  ("mempool full"). Replaces the blunt reject-when-full cap.
- DEFAULT_MEMPOOL_EXPIRY 1 -> 72 hours (age-Expire is now live; 1h was too aggressive).

Known simplification (documented in code): per-tx feerate, not descendant-aggregate
(CPFP) scoring, and no rollingMinimumFeeRate anti-thrash. Adversarially reviewed
(termination, iterator safety, recursive-lock safety, index cleanup all confirmed) and
runtime-tested on the fleet: pool stays bounded under a 1600-tx flood, verifychain ok,
no hang/crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 09:01:06 +02:00
parent 1ec3dbfee3
commit adf2bacdbd
4 changed files with 64 additions and 14 deletions

View File

@@ -491,6 +491,45 @@ std::vector<uint256> CTxMemPool::removeExpired(unsigned int nBlockHeight)
return ids;
}
// Age-based eviction: remove txs whose entry time is older than `time`. Distinct from
// removeExpired() (which drops txs past their consensus nExpiryHeight); this is the wall-clock
// sweep LimitMempoolSize wants. Collect-then-remove to avoid iterating mapTx while mutating it.
int CTxMemPool::Expire(int64_t time)
{
LOCK(cs);
std::list<CTransaction> toRemove;
for (indexed_transaction_set::const_iterator it = mapTx.begin(); it != mapTx.end(); it++) {
if (it->GetTime() < time)
toRemove.push_back(it->GetTx());
}
for (const CTransaction& tx : toRemove) {
std::list<CTransaction> removed;
remove(tx, removed, true);
}
return (int)toRemove.size();
}
// Fee-ordered eviction: drop the lowest-feerate txs (and their in-mempool descendants, via the
// recursive remove) until DynamicMemoryUsage() is at or below sizelimit. Uses the per-tx feerate
// index (mapTx index 1, sorted feerate DESCENDING, so the worst tx is the tail). NOTE: this is a
// per-tx feerate, not a descendant-aggregate score, so a low-fee parent funded by a high-fee child
// (CPFP) can be evicted -- an accepted simplification (no descendant tracking in this fork). The
// admission cap in AcceptToMemoryPool bounds growth between block connects (no rollingMinFee here).
void CTxMemPool::TrimToSize(size_t sizelimit, std::vector<uint256>* pvNoSpendsRemaining)
{
LOCK(cs);
while (DynamicMemoryUsage() > sizelimit && !mapTx.empty()) {
// Re-derive the tail each iteration: remove() invalidates iterators.
CTransaction tx = std::prev(mapTx.get<1>().end())->GetTx();
std::list<CTransaction> removed;
remove(tx, removed, true);
if (pvNoSpendsRemaining) {
for (const CTransaction& r : removed)
pvNoSpendsRemaining->push_back(r.GetHash());
}
}
}
// Called when a block is connected. Removes from mempool and updates the miner fee estimator.
void CTxMemPool::removeForBlock(const std::vector<CTransaction>& vtx, unsigned int nBlockHeight,
std::list<CTransaction>& conflicts, bool fCurrentEstimate)