Check cluster limits when using -walletrejectlongchains
What changed, and why it matters
This Bitcoin Core patch tightens a wallet safety check. The '-walletrejectlongchains' option is meant to stop a user's own wallet from creating long chains of unconfirmed transactions, which can be hard to get mined. The old code only checked the size of the transaction package, but did not check the newer 'cluster' limits that count related transactions more broadly. The fix adds a cluster-limit check before the package-size check, so the wallet now rejects transactions that would violate current mempool policy. This is a defensive correctness fix; it mainly protects users from creating transactions that the network would currently refuse to relay or mine.
Treat as a routine defensive fix. Users and operators running Bitcoin Core with -walletrejectlongchains will get stronger protection against creating policy-violating transaction clusters. No urgent network-wide action is required, but the fix should be included in the next maintenance release. Reviewers should verify that CheckPolicyLimits() correctly mirrors the mempool's actual acceptance policy and that the temporary ChangeSet does not accidentally mutate mempool state.
Security signals we found
Adds missing cluster-limit enforcement for wallet transaction creation
Prevents wallet from creating transactions that violate current mempool policy
Reduces risk of wallet producing stuck or non-relayable unconfirmed transaction chains
Does not change consensus or P2P network rules; only wallet behavior
Evidence from the diff
The commit modifies the wallet-to-mempool interface in src/node/interfaces.cpp. The checkChainLimits() method previously constructed a temporary CTxMemPoolEntry and called CheckPackageLimits({tx}, entry.GetTxSize()) under the mempool lock. That only enforced package-size limits. The patch adds a new CTxMemPool::CheckPolicyLimits() method (declared in txmempool.h, implemented in txmempool.cpp) which creates a temporary ChangeSet, stages the transaction, and calls CheckMemPoolPolicyLimits() to validate cluster limits before staging. checkChainLimits() now calls CheckPolicyLimits() first and returns an error ‘too many unconfirmed transactions in cluster’ if it fails, then falls back to the existing CheckPackageLimits() using GetVirtualTransactionSize(*tx). The change is local and additive; it does not alter mempool acceptance rules for non-wallet paths.
Changed components
src/node/interfaces.cppsrc/txmempool.cppsrc/txmempool.hWallet RPC / transaction creation path using -walletrejectlongchainsInspect captured patch +17 / −3
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index 0626e2c6..8e38ae12 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -719,10 +719,11 @@ public:
util::Result<void> checkChainLimits(const CTransactionRef& tx) override
{
if (!m_node.mempool) return {};
- LockPoints lp;
- CTxMemPoolEntry entry(TxGraph::Ref(), tx, 0, 0, 0, 0, false, 0, lp);
+ if (!m_node.mempool->CheckPolicyLimits(tx)) {
+ return util::Error{Untranslated("too many unconfirmed transactions in cluster")};
+ }
LOCK(m_node.mempool->cs);
- return m_node.mempool->CheckPackageLimits({tx}, entry.GetTxSize());
+ return m_node.mempool->CheckPackageLimits({tx}, GetVirtualTransactionSize(*tx));
}
CFeeRate estimateSmartFee(int num_blocks, bool conservative, FeeCalculation* calc) override
{
diff --git a/src/txmempool.cpp b/src/txmempool.cpp
index af3a54cc..e5a31852 100644
--- a/src/txmempool.cpp
+++ b/src/txmempool.cpp
@@ -1078,6 +1078,17 @@ void CTxMemPool::RemoveStaged(setEntries &stage, bool updateDescendants, MemPool
}
}
+bool CTxMemPool::CheckPolicyLimits(const CTransactionRef& tx)
+{
+ LOCK(cs);
+ // Use ChangeSet interface to check whether the chain
+ // limits would be violated. Note that the changeset will be destroyed
+ // when it goes out of scope.
+ auto changeset = GetChangeSet();
+ (void) changeset->StageAddition(tx, /*fee=*/0, /*time=*/0, /*entry_height=*/0, /*entry_sequence=*/0, /*spends_coinbase=*/false, /*sigops_cost=*/0, LockPoints{});
+ return changeset->CheckMemPoolPolicyLimits();
+}
+
int CTxMemPool::Expire(std::chrono::seconds time)
{
AssertLockHeld(cs);
diff --git a/src/txmempool.h b/src/txmempool.h
index 60087561..2b5882c8 100644
--- a/src/txmempool.h
+++ b/src/txmempool.h
@@ -681,6 +681,8 @@ public:
if (exists(txid)) m_unbroadcast_txids.insert(txid);
};
+ bool CheckPolicyLimits(const CTransactionRef& tx);
+
/** Removes a transaction from the unbroadcast set */
void RemoveUnbroadcastTx(const Txid& txid, const bool unchecked = false);
Why this scored 42/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.