wallet: Replace max descendant count with cluster_count
What changed, and why it matters
This commit is a code cleanup in Bitcoin Core's transaction memory pool (mempool). It swaps one internal measurement—'maximum descendants'—for a newer equivalent called 'cluster count' after an older size limit was removed. The change is mostly renaming and updating tests to match; it does not appear to fix a security bug or introduce an obvious vulnerability.
No immediate action required. Treat as a normal refactor. If deploying, verify that wallet bump-fee/replace-by-fee logic relying on `getTransactionAncestry` tolerates the new cluster_count semantics, especially for diamond-shaped or merged transaction groups where cluster size can differ from the old per-ancestor descendant maximum.
Security signals we found
No security framing in commit message or diff
No bounds checks, memory allocations, or cryptographic operations changed
Behavioral change is limited to a single numeric return value used by wallet fee estimation
Unit tests updated to expect cluster size instead of per-transaction descendant maximum
Evidence from the diff
The patch renames the descendants output of CTxMemPool::GetTransactionAncestry() to cluster_count and changes its computation from CalculateDescendantMaximum(it) to m_txgraph->GetCluster(*it, TxGraph::Level::MAIN).size(). The wallet-facing Chain::getTransactionAncestry interface and unit tests are updated accordingly. The commit message frames this as a follow-up to the removal of descendant size limits, replacing a now-obsolete metric with the cluster count of the transaction’s connected component in the mempool graph.
Changed components
src/txmempool.cppsrc/txmempool.hsrc/node/interfaces.cppsrc/test/mempool_tests.cppInspect captured patch +70 / −70
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index c3e57291..65997267 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -678,11 +678,11 @@ public:
// that Chain clients do not need to know about.
return TransactionError::OK == err;
}
- void getTransactionAncestry(const Txid& txid, size_t& ancestors, size_t& descendants, size_t* ancestorsize, CAmount* ancestorfees) override
+ void getTransactionAncestry(const Txid& txid, size_t& ancestors, size_t& cluster_count, size_t* ancestorsize, CAmount* ancestorfees) override
{
- ancestors = descendants = 0;
+ ancestors = cluster_count = 0;
if (!m_node.mempool) return;
- m_node.mempool->GetTransactionAncestry(txid, ancestors, descendants, ancestorsize, ancestorfees);
+ m_node.mempool->GetTransactionAncestry(txid, ancestors, cluster_count, ancestorsize, ancestorfees);
}
std::map<COutPoint, CAmount> calculateIndividualBumpFees(const std::vector<COutPoint>& outpoints, const CFeeRate& target_feerate) override
diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp
index 2271e7b8..d0cbdea7 100644
--- a/src/test/mempool_tests.cpp
+++ b/src/test/mempool_tests.cpp
@@ -296,7 +296,7 @@ inline CTransactionRef make_tx(std::vector<CAmount>&& output_values, std::vector
BOOST_AUTO_TEST_CASE(MempoolAncestryTests)
{
- size_t ancestors, descendants;
+ size_t ancestors, clustersize;
CTxMemPool& pool = *Assert(m_node.mempool);
LOCK2(cs_main, pool.cs);
@@ -309,10 +309,10 @@ BOOST_AUTO_TEST_CASE(MempoolAncestryTests)
CTransactionRef tx1 = make_tx(/*output_values=*/{10 * COIN});
AddToMempool(pool, entry.Fee(10000LL).FromTx(tx1));
- // Ancestors / descendants should be 1 / 1 (itself / itself)
- pool.GetTransactionAncestry(tx1->GetHash(), ancestors, descendants);
+ // Ancestors / clustersize should be 1 / 1 (itself / itself)
+ pool.GetTransactionAncestry(tx1->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 1ULL);
- BOOST_CHECK_EQUAL(descendants, 1ULL);
+ BOOST_CHECK_EQUAL(clustersize, 1ULL);
/* Child transaction */
//
@@ -321,17 +321,17 @@ BOOST_AUTO_TEST_CASE(MempoolAncestryTests)
CTransactionRef tx2 = make_tx(/*output_values=*/{495 * CENT, 5 * COIN}, /*inputs=*/{tx1});
AddToMempool(pool, entry.Fee(10000LL).FromTx(tx2));
- // Ancestors / descendants should be:
- // transaction ancestors descendants
+ // Ancestors / clustersize should be:
+ // transaction ancestors clustersize
// ============ =========== ===========
// tx1 1 (tx1) 2 (tx1,2)
// tx2 2 (tx1,2) 2 (tx1,2)
- pool.GetTransactionAncestry(tx1->GetHash(), ancestors, descendants);
+ pool.GetTransactionAncestry(tx1->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 1ULL);
- BOOST_CHECK_EQUAL(descendants, 2ULL);
- pool.GetTransactionAncestry(tx2->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 2ULL);
+ pool.GetTransactionAncestry(tx2->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 2ULL);
- BOOST_CHECK_EQUAL(descendants, 2ULL);
+ BOOST_CHECK_EQUAL(clustersize, 2ULL);
/* Grand-child 1 */
//
@@ -340,21 +340,21 @@ BOOST_AUTO_TEST_CASE(MempoolAncestryTests)
CTransactionRef tx3 = make_tx(/*output_values=*/{290 * CENT, 200 * CENT}, /*inputs=*/{tx2});
AddToMempool(pool, entry.Fee(10000LL).FromTx(tx3));
- // Ancestors / descendants should be:
- // transaction ancestors descendants
+ // Ancestors / clustersize should be:
+ // transaction ancestors clustersize
// ============ =========== ===========
// tx1 1 (tx1) 3 (tx1,2,3)
// tx2 2 (tx1,2) 3 (tx1,2,3)
// tx3 3 (tx1,2,3) 3 (tx1,2,3)
- pool.GetTransactionAncestry(tx1->GetHash(), ancestors, descendants);
+ pool.GetTransactionAncestry(tx1->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 1ULL);
- BOOST_CHECK_EQUAL(descendants, 3ULL);
- pool.GetTransactionAncestry(tx2->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 3ULL);
+ pool.GetTransactionAncestry(tx2->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 2ULL);
- BOOST_CHECK_EQUAL(descendants, 3ULL);
- pool.GetTransactionAncestry(tx3->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 3ULL);
+ pool.GetTransactionAncestry(tx3->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 3ULL);
- BOOST_CHECK_EQUAL(descendants, 3ULL);
+ BOOST_CHECK_EQUAL(clustersize, 3ULL);
/* Grand-child 2 */
//
@@ -365,25 +365,25 @@ BOOST_AUTO_TEST_CASE(MempoolAncestryTests)
CTransactionRef tx4 = make_tx(/*output_values=*/{290 * CENT, 250 * CENT}, /*inputs=*/{tx2}, /*input_indices=*/{1});
AddToMempool(pool, entry.Fee(10000LL).FromTx(tx4));
- // Ancestors / descendants should be:
- // transaction ancestors descendants
+ // Ancestors / clustersize should be:
+ // transaction ancestors clustersize
// ============ =========== ===========
// tx1 1 (tx1) 4 (tx1,2,3,4)
// tx2 2 (tx1,2) 4 (tx1,2,3,4)
// tx3 3 (tx1,2,3) 4 (tx1,2,3,4)
// tx4 3 (tx1,2,4) 4 (tx1,2,3,4)
- pool.GetTransactionAncestry(tx1->GetHash(), ancestors, descendants);
+ pool.GetTransactionAncestry(tx1->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 1ULL);
- BOOST_CHECK_EQUAL(descendants, 4ULL);
- pool.GetTransactionAncestry(tx2->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 4ULL);
+ pool.GetTransactionAncestry(tx2->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 2ULL);
- BOOST_CHECK_EQUAL(descendants, 4ULL);
- pool.GetTransactionAncestry(tx3->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 4ULL);
+ pool.GetTransactionAncestry(tx3->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 3ULL);
- BOOST_CHECK_EQUAL(descendants, 4ULL);
- pool.GetTransactionAncestry(tx4->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 4ULL);
+ pool.GetTransactionAncestry(tx4->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 3ULL);
- BOOST_CHECK_EQUAL(descendants, 4ULL);
+ BOOST_CHECK_EQUAL(clustersize, 4ULL);
/* Make an alternate branch that is longer and connect it to tx3 */
//
@@ -401,56 +401,56 @@ BOOST_AUTO_TEST_CASE(MempoolAncestryTests)
tyi = make_tx(/*output_values=*/{v}, /*inputs=*/i > 0 ? std::vector<CTransactionRef>{*ty[i - 1]} : std::vector<CTransactionRef>{});
v -= 50 * CENT;
AddToMempool(pool, entry.Fee(10000LL).FromTx(tyi));
- pool.GetTransactionAncestry(tyi->GetHash(), ancestors, descendants);
+ pool.GetTransactionAncestry(tyi->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, i+1);
- BOOST_CHECK_EQUAL(descendants, i+1);
+ BOOST_CHECK_EQUAL(clustersize, i+1);
}
CTransactionRef ty6 = make_tx(/*output_values=*/{5 * COIN}, /*inputs=*/{tx3, ty5});
AddToMempool(pool, entry.Fee(10000LL).FromTx(ty6));
- // Ancestors / descendants should be:
- // transaction ancestors descendants
+ // Ancestors / clustersize should be:
+ // transaction ancestors clustersize
// ============ =================== ===========
- // tx1 1 (tx1) 5 (tx1,2,3,4, ty6)
- // tx2 2 (tx1,2) 5 (tx1,2,3,4, ty6)
- // tx3 3 (tx1,2,3) 5 (tx1,2,3,4, ty6)
- // tx4 3 (tx1,2,4) 5 (tx1,2,3,4, ty6)
- // ty1 1 (ty1) 6 (ty1,2,3,4,5,6)
- // ty2 2 (ty1,2) 6 (ty1,2,3,4,5,6)
- // ty3 3 (ty1,2,3) 6 (ty1,2,3,4,5,6)
- // ty4 4 (y1234) 6 (ty1,2,3,4,5,6)
- // ty5 5 (y12345) 6 (ty1,2,3,4,5,6)
- // ty6 9 (tx123, ty123456) 6 (ty1,2,3,4,5,6)
- pool.GetTransactionAncestry(tx1->GetHash(), ancestors, descendants);
+ // tx1 1 (tx1) 10 (tx1-5, ty1-5)
+ // tx2 2 (tx1,2) 10
+ // tx3 3 (tx1,2,3) 10
+ // tx4 3 (tx1,2,4) 10
+ // ty1 1 (ty1) 10
+ // ty2 2 (ty1,2) 10
+ // ty3 3 (ty1,2,3) 10
+ // ty4 4 (y1234) 10
+ // ty5 5 (y12345) 10
+ // ty6 9 (tx123, ty123456) 10
+ pool.GetTransactionAncestry(tx1->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 1ULL);
- BOOST_CHECK_EQUAL(descendants, 5ULL);
- pool.GetTransactionAncestry(tx2->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(tx2->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 2ULL);
- BOOST_CHECK_EQUAL(descendants, 5ULL);
- pool.GetTransactionAncestry(tx3->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(tx3->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 3ULL);
- BOOST_CHECK_EQUAL(descendants, 5ULL);
- pool.GetTransactionAncestry(tx4->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(tx4->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 3ULL);
- BOOST_CHECK_EQUAL(descendants, 5ULL);
- pool.GetTransactionAncestry(ty1->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(ty1->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 1ULL);
- BOOST_CHECK_EQUAL(descendants, 6ULL);
- pool.GetTransactionAncestry(ty2->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(ty2->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 2ULL);
- BOOST_CHECK_EQUAL(descendants, 6ULL);
- pool.GetTransactionAncestry(ty3->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(ty3->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 3ULL);
- BOOST_CHECK_EQUAL(descendants, 6ULL);
- pool.GetTransactionAncestry(ty4->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(ty4->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 4ULL);
- BOOST_CHECK_EQUAL(descendants, 6ULL);
- pool.GetTransactionAncestry(ty5->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(ty5->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 5ULL);
- BOOST_CHECK_EQUAL(descendants, 6ULL);
- pool.GetTransactionAncestry(ty6->GetHash(), ancestors, descendants);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
+ pool.GetTransactionAncestry(ty6->GetHash(), ancestors, clustersize);
BOOST_CHECK_EQUAL(ancestors, 9ULL);
- BOOST_CHECK_EQUAL(descendants, 6ULL);
+ BOOST_CHECK_EQUAL(clustersize, 10ULL);
}
BOOST_AUTO_TEST_CASE(MempoolAncestryTestsDiamond)
diff --git a/src/txmempool.cpp b/src/txmempool.cpp
index ab6d3ed2..e268fd61 100644
--- a/src/txmempool.cpp
+++ b/src/txmempool.cpp
@@ -1217,16 +1217,16 @@ std::tuple<size_t, size_t, CAmount> CTxMemPool::CalculateDescendantData(const CT
return {descendant_count, descendant_size, descendant_fees};
}
-void CTxMemPool::GetTransactionAncestry(const Txid& txid, size_t& ancestors, size_t& descendants, size_t* const ancestorsize, CAmount* const ancestorfees) const {
+void CTxMemPool::GetTransactionAncestry(const Txid& txid, size_t& ancestors, size_t& cluster_count, size_t* const ancestorsize, CAmount* const ancestorfees) const {
LOCK(cs);
auto it = mapTx.find(txid);
- ancestors = descendants = 0;
+ ancestors = cluster_count = 0;
if (it != mapTx.end()) {
auto [ancestor_count, ancestor_size, ancestor_fees] = CalculateAncestorData(*it);
- descendants = CalculateDescendantMaximum(it);
ancestors = ancestor_count;
if (ancestorsize) *ancestorsize = ancestor_size;
if (ancestorfees) *ancestorfees = ancestor_fees;
+ cluster_count = m_txgraph->GetCluster(*it, TxGraph::Level::MAIN).size();
}
}
diff --git a/src/txmempool.h b/src/txmempool.h
index 49ee510a..520410b6 100644
--- a/src/txmempool.h
+++ b/src/txmempool.h
@@ -516,12 +516,12 @@ public:
int Expire(std::chrono::seconds time) EXCLUSIVE_LOCKS_REQUIRED(cs);
/**
- * Calculate the ancestor and descendant count for the given transaction.
+ * Calculate the ancestor and cluster count for the given transaction.
* The counts include the transaction itself.
* When ancestors is non-zero (ie, the transaction itself is in the mempool),
* ancestorsize and ancestorfees will also be set to the appropriate values.
*/
- void GetTransactionAncestry(const Txid& txid, size_t& ancestors, size_t& descendants, size_t* ancestorsize = nullptr, CAmount* ancestorfees = nullptr) const;
+ void GetTransactionAncestry(const Txid& txid, size_t& ancestors, size_t& cluster_count, size_t* ancestorsize = nullptr, CAmount* ancestorfees = nullptr) const;
/**
* @returns true if an initial attempt to load the persisted mempool was made, regardless of
Why this scored 18/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.