fix: restore removeExpired() mempool scan (was a no-op -> unbounded mempool DoS)

CTxMemPool::removeExpired() declared `transactionsToRemove` and looped over it
without ever populating it -- the mapTx scan that collects expired txs had been
dropped, so it evicted nothing. Expired txs (past nExpiryHeight) can never be mined
yet were never removed, so a peer could wedge them into every node's mempool
permanently at ~zero cost (never mined -> never pay a fee), growing the mempool
without bound: a memory-exhaustion DoS against every node (incl. pool/payout nodes).

Restore the upstream Zcash/Komodo scan: iterate mapTx, collect txs failing
IsExpiredTx(tx, tipHeight) into a separate list, then remove() them (collect-then-
remove avoids iterator invalidation; recursive=true also evicts the now-unmineable
descendants). Also drops an unused CBlockIndex* local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 04:29:54 +02:00
parent 4238da9bea
commit 2e54d9fb4d

View File

@@ -469,10 +469,17 @@ extern char SMART_CHAIN_SYMBOL[];
std::vector<uint256> CTxMemPool::removeExpired(unsigned int nBlockHeight)
{
CBlockIndex *tipindex;
// Remove expired txs from the mempool
// Remove expired txs from the mempool. (Regression fix: the scan that populates
// transactionsToRemove had been dropped, making this a no-op, so expired txs -- which
// can never be mined -- were never evicted and accumulated without bound.)
LOCK(cs);
list<CTransaction> transactionsToRemove;
for (indexed_transaction_set::const_iterator it = mapTx.begin(); it != mapTx.end(); it++) {
const CTransaction& tx = it->GetTx();
if (IsExpiredTx(tx, nBlockHeight)) {
transactionsToRemove.push_back(tx);
}
}
std::vector<uint256> ids;
for (const CTransaction& tx : transactionsToRemove) {