Mempool: Do not enforce TRUC checks on reorg
What changed, and why it matters
This commit fixes a bug in Bitcoin Core's transaction memory pool (mempool) handling during blockchain reorganizations (reorgs). A reorg happens when the network briefly has two competing versions of recent history and one wins. The bug caused the mempool to wrongly enforce new TRUC topology rules on old transactions being restored after a reorg. That could reject a chain of historically valid, high-fee transactions and waste CPU checking rules that were never meant to apply in that situation. The fix adds a bypass flag so those checks are skipped during reorg recovery, matching the original design intent.
Apply the patch. Nodes should upgrade to a version containing this commit to avoid the reorg-time mempool policy bug. Operators relying on mempool consistency during reorgs should monitor for the fix in the next maintenance release.
Security signals we found
Denial-of-service vector: unnecessary CPU spent on topology checks during reorg
Transaction censorship / mempool exclusion of historically valid high-feerate transaction chains
Policy-rule misapplication outside intended context (reorg recovery)
Fix matches stated original intent, indicating a logic-gap bug
Evidence from the diff
In src/validation.cpp, PreChecks() now wraps the SingleTRUCChecks() call in if (!args.m_bypass_limits). During reorg-driven mempool reacceptance, m_bypass_limits is set, so TRUC topology and sibling-eviction validation is skipped. The commit message states this was the intended behavior but the bypass argument was not being checked, causing unnecessary policy enforcement and potential rejection of incentive-compatible transaction chains restored from disconnected blocks.
Changed components
src/validation.cppMempool acceptance PreChecksTRUC (Topology Restricted Until Confirmation) policy checksReorg / chain reorganization mempool recovery pathInspect captured patch +22 / −20
diff --git a/src/validation.cpp b/src/validation.cpp
index 8fcc719a..3f77955d 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -1044,26 +1044,28 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
// Even though just checking direct mempool parents for inheritance would be sufficient, we
// check using the full ancestor set here because it's more convenient to use what we have
// already calculated.
- if (const auto err{SingleTRUCChecks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) {
- // Single transaction contexts only.
- if (args.m_allow_sibling_eviction && err->second != nullptr) {
- // We should only be considering where replacement is considered valid as well.
- Assume(args.m_allow_replacement);
-
- // Potential sibling eviction. Add the sibling to our list of mempool conflicts to be
- // included in RBF checks.
- ws.m_conflicts.insert(err->second->GetHash());
- // Adding the sibling to m_iters_conflicting here means that it doesn't count towards
- // RBF Carve Out above. This is correct, since removing to-be-replaced transactions from
- // the descendant count is done separately in SingleTRUCChecks for TRUC transactions.
- ws.m_iters_conflicting.insert(m_pool.GetIter(err->second->GetHash()).value());
- ws.m_sibling_eviction = true;
- // The sibling will be treated as part of the to-be-replaced set in ReplacementChecks.
- // Note that we are not checking whether it opts in to replaceability via BIP125 or TRUC
- // (which is normally done in PreChecks). However, the only way a TRUC transaction can
- // have a non-TRUC and non-BIP125 descendant is due to a reorg.
- } else {
- return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "TRUC-violation", err->first);
+ if (!args.m_bypass_limits) {
+ if (const auto err{SingleTRUCChecks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) {
+ // Single transaction contexts only.
+ if (args.m_allow_sibling_eviction && err->second != nullptr) {
+ // We should only be considering where replacement is considered valid as well.
+ Assume(args.m_allow_replacement);
+
+ // Potential sibling eviction. Add the sibling to our list of mempool conflicts to be
+ // included in RBF checks.
+ ws.m_conflicts.insert(err->second->GetHash());
+ // Adding the sibling to m_iters_conflicting here means that it doesn't count towards
+ // RBF Carve Out above. This is correct, since removing to-be-replaced transactions from
+ // the descendant count is done separately in SingleTRUCChecks for TRUC transactions.
+ ws.m_iters_conflicting.insert(m_pool.GetIter(err->second->GetHash()).value());
+ ws.m_sibling_eviction = true;
+ // The sibling will be treated as part of the to-be-replaced set in ReplacementChecks.
+ // Note that we are not checking whether it opts in to replaceability via BIP125 or TRUC
+ // (which is normally done in PreChecks). However, the only way a TRUC transaction can
+ // have a non-TRUC and non-BIP125 descendant is due to a reorg.
+ } else {
+ return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "TRUC-violation", err->first);
+ }
}
}
Why this scored 44/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.