What changed, and why it matters
This commit adds new read-only ways to inspect Bitcoin's mempool through RPC: it exposes 'cluster' information for transactions, adds chunk-based fee/weight fields to existing mempool entries, and introduces a hidden diagnostic command that prints the mempool's feerate diagram. It is a feature/informational change, not a fix for a known vulnerability. There is no evidence in the commit or supplied references that it addresses a security bug or was triggered by a security report.
Treat as a routine feature commit. Review the new RPCs for information disclosure concerns if mempool privacy is relevant to the deployment, and ensure the hidden getmempoolfeeratediagram command is not unexpectedly exposed. No security patch action is indicated by the supplied materials.
Security signals we found
New RPC surface added (getmempoolcluster, hidden getmempoolfeeratediagram)
Existing RPC output schema extended with chunkweight, fees.chunk, limitclustercount, limitclustersize
No input validation, authorization, or resource-limit changes observed
No memory safety, cryptographic, or consensus-critical code changes observed
No vendor security disclosure or CVE references present in commit or supplied materials
Evidence from the diff
The patch extends mempool RPCs with cluster-aware data. It adds getmempoolcluster and a hidden getmempoolfeeratediagram, enriches getmempoolentry/getrawmempool output with chunkweight and fees.chunk, and exposes limitclustercount/limitclustersize in getmempoolinfo. Supporting CTxMemPool helpers (GetFeerateDiagram, GetMainChunkFeerate, GetCluster) are added. The changes are additive and read-only, protected by mempool.cs. Fuzz and functional tests are updated to account for the new fields. No bug fix, bounds-check change, or vulnerability remediation is visible in the diff.
Changed components
src/rpc/mempool.cppsrc/txmempool.cppsrc/txmempool.hsrc/test/fuzz/rpc.cpptest/functional/mempool_persist.pytest/functional/mining_prioritisetransaction.pyInspect captured patch +171 / −1
diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp
index f07f06f8..2f8a4870 100644
--- a/src/rpc/mempool.cpp
+++ b/src/rpc/mempool.cpp
@@ -258,6 +258,23 @@ static RPCHelpMan testmempoolaccept()
};
}
+static std::vector<RPCResult> ClusterDescription()
+{
+ return {
+ RPCResult{RPCResult::Type::NUM, "weight", "total sigops-adjusted weight (as defined in BIP 141 and modified by '-bytespersigop'"},
+ RPCResult{RPCResult::Type::NUM, "txcount", "number of transactions"},
+ RPCResult{RPCResult::Type::ARR, "txs", "transactions in this cluster in mining order",
+ {RPCResult{RPCResult::Type::OBJ, "txentry", "",
+ {
+ RPCResult{RPCResult::Type::STR_HEX, "txid", "the transaction id"},
+ RPCResult{RPCResult::Type::NUM, "chunkfee", "fee of the chunk containing this tx"},
+ RPCResult{RPCResult::Type::NUM, "chunkweight", "sigops-adjusted weight of the chunk containing this transaction"}
+ }
+ }}
+ }
+ };
+}
+
static std::vector<RPCResult> MempoolEntryDescription()
{
return {
@@ -269,6 +286,7 @@ static std::vector<RPCResult> MempoolEntryDescription()
RPCResult{RPCResult::Type::NUM, "descendantsize", "virtual transaction size of in-mempool descendants (including this one)"},
RPCResult{RPCResult::Type::NUM, "ancestorcount", "number of in-mempool ancestor transactions (including this one)"},
RPCResult{RPCResult::Type::NUM, "ancestorsize", "virtual transaction size of in-mempool ancestors (including this one)"},
+ RPCResult{RPCResult::Type::NUM, "chunkweight", "sigops-adjusted weight (as defined in BIP 141 and modified by '-bytespersigop') of this transaction's chunk"},
RPCResult{RPCResult::Type::STR_HEX, "wtxid", "hash of serialized transaction, including witness data"},
RPCResult{RPCResult::Type::OBJ, "fees", "",
{
@@ -276,6 +294,7 @@ static std::vector<RPCResult> MempoolEntryDescription()
RPCResult{RPCResult::Type::STR_AMOUNT, "modified", "transaction fee with fee deltas used for mining priority, denominated in " + CURRENCY_UNIT},
RPCResult{RPCResult::Type::STR_AMOUNT, "ancestor", "transaction fees of in-mempool ancestors (including this one) with fee deltas used for mining priority, denominated in " + CURRENCY_UNIT},
RPCResult{RPCResult::Type::STR_AMOUNT, "descendant", "transaction fees of in-mempool descendants (including this one) with fee deltas used for mining priority, denominated in " + CURRENCY_UNIT},
+ RPCResult{RPCResult::Type::STR_AMOUNT, "chunk", "transaction fees of chunk, denominated in " + CURRENCY_UNIT},
}},
RPCResult{RPCResult::Type::ARR, "depends", "unconfirmed transactions used as inputs for this transaction",
{RPCResult{RPCResult::Type::STR_HEX, "transactionid", "parent transaction id"}}},
@@ -286,6 +305,27 @@ static std::vector<RPCResult> MempoolEntryDescription()
};
}
+static void clusterToJSON(const CTxMemPool& pool, UniValue& info, std::vector<const CTxMemPoolEntry *> cluster) EXCLUSIVE_LOCKS_REQUIRED(pool.cs)
+{
+ AssertLockHeld(pool.cs);
+ int total_weight{0};
+ for (const auto& tx : cluster) {
+ total_weight += tx->GetAdjustedWeight();
+ }
+ info.pushKV("weight", total_weight);
+ info.pushKV("txcount", (int)cluster.size());
+ UniValue txs(UniValue::VARR);
+ for (const auto& tx : cluster) {
+ UniValue txentry(UniValue::VOBJ);
+ auto feerate = pool.GetMainChunkFeerate(*tx);
+ txentry.pushKV("txid", tx->GetTx().GetHash().ToString());
+ txentry.pushKV("chunkfee", ValueFromAmount((int)feerate.fee));
+ txentry.pushKV("chunkweight", feerate.size);
+ txs.push_back(txentry);
+ }
+ info.pushKV("txs", txs);
+}
+
static void entryToJSON(const CTxMemPool& pool, UniValue& info, const CTxMemPoolEntry& e) EXCLUSIVE_LOCKS_REQUIRED(pool.cs)
{
AssertLockHeld(pool.cs);
@@ -302,12 +342,15 @@ static void entryToJSON(const CTxMemPool& pool, UniValue& info, const CTxMemPool
info.pushKV("ancestorcount", ancestor_count);
info.pushKV("ancestorsize", ancestor_size);
info.pushKV("wtxid", e.GetTx().GetWitnessHash().ToString());
+ auto feerate = pool.GetMainChunkFeerate(e);
+ info.pushKV("chunkweight", feerate.size);
UniValue fees(UniValue::VOBJ);
fees.pushKV("base", ValueFromAmount(e.GetFee()));
fees.pushKV("modified", ValueFromAmount(e.GetModifiedFee()));
fees.pushKV("ancestor", ValueFromAmount(ancestor_fees));
fees.pushKV("descendant", ValueFromAmount(descendant_fees));
+ fees.pushKV("chunk", ValueFromAmount((int)feerate.fee));
info.pushKV("fees", std::move(fees));
const CTransaction& tx = e.GetTx();
@@ -384,6 +427,49 @@ UniValue MempoolToJSON(const CTxMemPool& pool, bool verbose, bool include_mempoo
}
}
+static RPCHelpMan getmempoolfeeratediagram()
+{
+ return RPCHelpMan{"getmempoolfeeratediagram",
+ "Returns the feerate diagram for the whole mempool.",
+ {},
+ {
+ RPCResult{"mempool chunks",
+ RPCResult::Type::ARR, "", "",
+ {
+ {
+ RPCResult::Type::OBJ, "", "",
+ {
+ {RPCResult::Type::NUM, "weight", "cumulative sigops-adjusted weight"},
+ {RPCResult::Type::NUM, "fee", "cumulative fee"}
+ }
+ }
+ }
+ }
+ },
+ RPCExamples{
+ HelpExampleCli("getmempoolfeeratediagram", "")
+ + HelpExampleRpc("getmempoolfeeratediagram", "")
+ },
+ [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
+ {
+ const CTxMemPool& mempool = EnsureAnyMemPool(request.context);
+ LOCK(mempool.cs);
+
+ UniValue result(UniValue::VARR);
+
+ auto diagram = mempool.GetFeerateDiagram();
+
+ for (auto f : diagram) {
+ UniValue o(UniValue::VOBJ);
+ o.pushKV("weight", f.size);
+ o.pushKV("fee", ValueFromAmount(f.fee));
+ result.push_back(o);
+ }
+ return result;
+ }
+ };
+}
+
static RPCHelpMan getrawmempool()
{
return RPCHelpMan{
@@ -561,6 +647,35 @@ static RPCHelpMan getmempooldescendants()
};
}
+static RPCHelpMan getmempoolcluster()
+{
+ return RPCHelpMan{"getmempoolcluster",
+ "Returns mempool data for given cluster\n",
+ {
+ {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The txid of a transaction in the cluster"},
+ },
+ RPCResult{
+ RPCResult::Type::OBJ, "", "", ClusterDescription()},
+ RPCExamples{
+ HelpExampleCli("getmempoolcluster", "txid")
+ + HelpExampleRpc("getmempoolcluster", "txid")
+ },
+ [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
+{
+ uint256 hash = ParseHashV(request.params[0], "parameter 1");
+
+ const CTxMemPool& mempool = EnsureAnyMemPool(request.context);
+ LOCK(mempool.cs);
+
+ auto cluster = mempool.GetCluster(Txid::FromUint256(hash));
+
+ UniValue info(UniValue::VOBJ);
+ clusterToJSON(mempool, info, cluster);
+ return info;
+},
+ };
+}
+
static RPCHelpMan getmempoolentry()
{
return RPCHelpMan{
@@ -694,6 +809,8 @@ UniValue MempoolInfoToJSON(const CTxMemPool& pool)
ret.pushKV("fullrbf", true);
ret.pushKV("permitbaremultisig", pool.m_opts.permit_bare_multisig);
ret.pushKV("maxdatacarriersize", pool.m_opts.max_datacarrier_bytes.value_or(0));
+ ret.pushKV("limitclustercount", pool.m_opts.limits.cluster_count);
+ ret.pushKV("limitclustersize", pool.m_opts.limits.cluster_size_vbytes);
return ret;
}
@@ -718,6 +835,8 @@ static RPCHelpMan getmempoolinfo()
{RPCResult::Type::BOOL, "fullrbf", "True if the mempool accepts RBF without replaceability signaling inspection (DEPRECATED)"},
{RPCResult::Type::BOOL, "permitbaremultisig", "True if the mempool accepts transactions with bare multisig outputs"},
{RPCResult::Type::NUM, "maxdatacarriersize", "Maximum number of bytes that can be used by OP_RETURN outputs in the mempool"},
+ {RPCResult::Type::NUM, "limitclustercount", "Maximum number of transactions that can be in a cluster (configured by -limitclustercount)"},
+ {RPCResult::Type::NUM, "limitclustersize", "Maximum size of a cluster in virtual bytes (configured by -limitclustersize)"},
}},
RPCExamples{
HelpExampleCli("getmempoolinfo", "")
@@ -1145,8 +1264,10 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
{"blockchain", &getmempoolancestors},
{"blockchain", &getmempooldescendants},
{"blockchain", &getmempoolentry},
+ {"blockchain", &getmempoolcluster},
{"blockchain", &gettxspendingprevout},
{"blockchain", &getmempoolinfo},
+ {"hidden", &getmempoolfeeratediagram},
{"blockchain", &getrawmempool},
{"blockchain", &importmempool},
{"blockchain", &savemempool},
diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp
index 580a6338..9fd1021f 100644
--- a/src/test/fuzz/rpc.cpp
+++ b/src/test/fuzz/rpc.cpp
@@ -136,6 +136,8 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"getmempoolancestors",
"getmempooldescendants",
"getmempoolentry",
+ "getmempoolfeeratediagram",
+ "getmempoolcluster",
"getmempoolinfo",
"getmininginfo",
"getnettotals",
diff --git a/src/txmempool.cpp b/src/txmempool.cpp
index d7303cc6..597d4864 100644
--- a/src/txmempool.cpp
+++ b/src/txmempool.cpp
@@ -1010,3 +1010,25 @@ bool CTxMemPool::ChangeSet::CheckMemPoolPolicyLimits()
return !m_pool->m_txgraph->IsOversized(TxGraph::Level::TOP);
}
+
+std::vector<FeePerWeight> CTxMemPool::GetFeerateDiagram() const
+{
+ FeePerWeight zero{};
+ std::vector<FeePerWeight> ret;
+
+ ret.emplace_back(zero);
+
+ StartBlockBuilding();
+
+ std::vector<CTxMemPoolEntry::CTxMemPoolEntryRef> dummy;
+
+ FeePerWeight last_selection = GetBlockBuilderChunk(dummy);
+ while (last_selection != FeePerWeight{}) {
+ last_selection += ret.back();
+ ret.emplace_back(last_selection);
+ IncludeBuilderChunk();
+ last_selection = GetBlockBuilderChunk(dummy);
+ }
+ StopBlockBuilding();
+ return ret;
+}
diff --git a/src/txmempool.h b/src/txmempool.h
index 1891c8d4..e2020b65 100644
--- a/src/txmempool.h
+++ b/src/txmempool.h
@@ -406,6 +406,23 @@ public:
*/
void UpdateTransactionsFromBlock(const std::vector<Txid>& vHashesToUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs, cs_main) LOCKS_EXCLUDED(m_epoch);
+ std::vector<FeePerWeight> GetFeerateDiagram() const EXCLUSIVE_LOCKS_REQUIRED(cs);
+ FeePerWeight GetMainChunkFeerate(const CTxMemPoolEntry& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
+ return m_txgraph->GetMainChunkFeerate(tx);
+ }
+ std::vector<const CTxMemPoolEntry*> GetCluster(Txid txid) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
+ auto tx = GetIter(txid);
+ if (!tx) return {};
+ auto cluster = m_txgraph->GetCluster(**tx, TxGraph::Level::MAIN);
+ std::vector<const CTxMemPoolEntry*> ret;
+ ret.reserve(cluster.size());
+ for (const auto& tx : cluster) {
+ ret.emplace_back(static_cast<const CTxMemPoolEntry*>(tx));
+ }
+ return ret;
+ }
+
+
size_t GetUniqueClusterCount(const setEntries& iters_conflicting) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
std::vector<const TxGraph::Ref *> entries;
entries.reserve(iters_conflicting.size());
diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py
index a9560dd6..ceb69cf1 100755
--- a/test/functional/mempool_persist.py
+++ b/test/functional/mempool_persist.py
@@ -128,7 +128,8 @@ class MempoolPersistTest(BitcoinTestFramework):
assert_equal(fees['base'] + Decimal('0.00001000'), fees['modified'])
self.log.debug('Verify all fields are loaded correctly')
- assert_equal(last_entry, self.nodes[0].getmempoolentry(txid=last_txid))
+ new_entry = self.nodes[0].getmempoolentry(txid=last_txid)
+ assert_equal({**last_entry, "clusterid": None}, {**new_entry, "clusterid": None})
self.nodes[0].sendrawtransaction(tx_prioritised_not_submitted['hex'])
entry_prioritised_before_restart = self.nodes[0].getmempoolentry(txid=tx_prioritised_not_submitted['txid'])
assert_equal(entry_prioritised_before_restart['fees']['base'] + Decimal('0.00009999'), entry_prioritised_before_restart['fees']['modified'])
diff --git a/test/functional/mining_prioritisetransaction.py b/test/functional/mining_prioritisetransaction.py
index a8646d49..c3b56e8d 100755
--- a/test/functional/mining_prioritisetransaction.py
+++ b/test/functional/mining_prioritisetransaction.py
@@ -101,14 +101,21 @@ class PrioritiseTransactionTest(BitcoinTestFramework):
self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_1 * COIN))
self.nodes[0].prioritisetransaction(txid=txid_c, fee_delta=int(fee_delta_c_2 * COIN))
raw_before[txid_a]["fees"]["descendant"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2
+ # We expect tx_a to have a chunk fee that includes tx_b and tx_c.
+ raw_before[txid_a]["fees"]["chunk"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2
raw_before[txid_b]["fees"]["modified"] += fee_delta_b
raw_before[txid_b]["fees"]["ancestor"] += fee_delta_b
raw_before[txid_b]["fees"]["descendant"] += fee_delta_b
+ # We also expect tx_b and tx_c to have their chunk fees modified too,
+ # since they chunk together.
+ raw_before[txid_b]["fees"]["chunk"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2
raw_before[txid_c]["fees"]["modified"] += fee_delta_c_1 + fee_delta_c_2
raw_before[txid_c]["fees"]["ancestor"] += fee_delta_c_1 + fee_delta_c_2
raw_before[txid_c]["fees"]["descendant"] += fee_delta_c_1 + fee_delta_c_2
+ raw_before[txid_c]["fees"]["chunk"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2
raw_before[txid_d]["fees"]["ancestor"] += fee_delta_b + fee_delta_c_1 + fee_delta_c_2
raw_after = self.nodes[0].getrawmempool(verbose=True)
+ # Don't bother comparing cluster ids, which are not meant to be stable.
assert_equal(raw_before[txid_a], raw_after[txid_a])
assert_equal(raw_before, raw_after)
assert_equal(self.nodes[0].getprioritisedtransactions(), {txid_b: {"fee_delta" : fee_delta_b*COIN, "in_mempool" : True, "modified_fee": int(fee_delta_b*COIN + COIN * tx_o_b["fee"])}, txid_c: {"fee_delta" : (fee_delta_c_1 + fee_delta_c_2)*COIN, "in_mempool" : True, "modified_fee": int((fee_delta_c_1 + fee_delta_c_2 ) * COIN + COIN * tx_o_c["fee"])}})
Why this scored 20/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.