What changed, and why it matters
This commit adds a new Bitcoin Core RPC command called abortprivatebroadcast. It lets a node operator stop a transaction that is currently being sent out privately (a privacy feature) and remove it from the outgoing queue. The change is purely additive: it exposes a way to cancel an in-progress private broadcast, but does not by itself create or fix a security vulnerability. It is a user-facing control feature, not a security patch.
No immediate security action required. Treat as a normal feature addition. Operators should ensure RPC access remains authenticated and restricted as with all Bitcoin Core RPC commands. Review whether exposing the ability to abort private broadcasts has any operational or privacy implications for users relying on the private broadcast feature.
Security signals we found
New RPC surface added (abortprivatebroadcast) under rawtransactions category
Function matches user-supplied id against both txid and wtxid of queued private broadcasts
Removes entries from m_tx_for_private_broadcast and adjusts connection accounting
Returns full serialized transaction hex for removed entries
Added to RPC_COMMANDS_SAFE_FOR_FUZZING list, indicating developer view of low risk
No input validation, memory safety, or consensus-critical code changes observed
Evidence from the diff
The patch introduces AbortPrivateBroadcast in net_processing and an RPC wrapper abortprivatebroadcast in rpc/mempool. The RPC takes a txid/wtxid, asks PeerManager to remove matching transactions from the private-broadcast queue, decrements the count of expected private broadcast connections, and returns the removed transactions. It also adds the command to the fuzz-testing allowlist. There is no change to validation rules, network protocol parsing, mempool policy, or wallet logic. The function is guarded by the existing RPC authentication model.
Changed components
src/net_processing.cppsrc/net_processing.hsrc/rpc/mempool.cppsrc/test/fuzz/rpc.cppInspect captured patch +91 / −0
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index 7ab05cd0..80622d06 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -543,6 +543,7 @@ public:
std::vector<node::TxOrphanage::OrphanInfo> GetOrphanTransactions() override EXCLUSIVE_LOCKS_REQUIRED(!m_tx_download_mutex);
PeerManagerInfo GetInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
std::vector<PrivateBroadcast::TxBroadcastInfo> GetPrivateBroadcastInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
+ std::vector<CTransactionRef> AbortPrivateBroadcast(const uint256& id) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void SendPings() override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void InitiateTxBroadcastToAll(const Txid& txid, const Wtxid& wtxid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void InitiateTxBroadcastPrivate(const CTransactionRef& tx) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
@@ -1861,6 +1862,26 @@ std::vector<PrivateBroadcast::TxBroadcastInfo> PeerManagerImpl::GetPrivateBroadc
return m_tx_for_private_broadcast.GetBroadcastInfo();
}
+std::vector<CTransactionRef> PeerManagerImpl::AbortPrivateBroadcast(const uint256& id)
+{
+ const auto snapshot{m_tx_for_private_broadcast.GetBroadcastInfo()};
+ std::vector<CTransactionRef> removed_txs;
+
+ size_t connections_cancelled{0};
+ for (const auto& [tx, _] : snapshot) {
+ if (tx->GetHash().ToUint256() != id && tx->GetWitnessHash().ToUint256() != id) continue;
+ if (const auto peer_acks{m_tx_for_private_broadcast.Remove(tx)}) {
+ removed_txs.push_back(tx);
+ if (NUM_PRIVATE_BROADCAST_PER_TX > *peer_acks) {
+ connections_cancelled += (NUM_PRIVATE_BROADCAST_PER_TX - *peer_acks);
+ }
+ }
+ }
+ m_connman.m_private_broadcast.NumToOpenSub(connections_cancelled);
+
+ return removed_txs;
+}
+
void PeerManagerImpl::AddToCompactExtraTransactions(const CTransactionRef& tx)
{
if (m_opts.max_extra_txs <= 0)
diff --git a/src/net_processing.h b/src/net_processing.h
index 4aac8daa..36ae021f 100644
--- a/src/net_processing.h
+++ b/src/net_processing.h
@@ -12,6 +12,7 @@
#include <private_broadcast.h>
#include <protocol.h>
#include <threadsafety.h>
+#include <uint256.h>
#include <util/expected.h>
#include <validationinterface.h>
@@ -122,6 +123,18 @@ public:
/** Get info about transactions currently being privately broadcast. */
virtual std::vector<PrivateBroadcast::TxBroadcastInfo> GetPrivateBroadcastInfo() const = 0;
+ /**
+ * Abort private broadcast attempts for transactions currently being privately broadcast.
+ *
+ * @param[in] id A transaction identifier. It will be matched against both txid and wtxid for
+ * all transactions in the private broadcast queue.
+ *
+ * @return Transactions removed from the private broadcast queue. If the provided id matches a
+ * txid that corresponds to multiple transactions with different wtxids, multiple
+ * transactions may be returned.
+ */
+ virtual std::vector<CTransactionRef> AbortPrivateBroadcast(const uint256& id) = 0;
+
/**
* Initiate a transaction broadcast to eligible peers.
* Queue the witness transaction id to `Peer::TxRelay::m_tx_inventory_to_send`
diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp
index 35870368..12eef3ce 100644
--- a/src/rpc/mempool.cpp
+++ b/src/rpc/mempool.cpp
@@ -202,6 +202,61 @@ static RPCHelpMan getprivatebroadcastinfo()
};
}
+static RPCHelpMan abortprivatebroadcast()
+{
+ return RPCHelpMan{
+ "abortprivatebroadcast",
+ "Abort private broadcast attempts for a transaction currently being privately broadcast.\n"
+ "The transaction will be removed from the private broadcast queue.\n",
+ {
+ {"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "A transaction identifier to abort. It will be matched against both txid and wtxid for all transactions in the private broadcast queue.\n"
+ "If the provided id matches a txid that corresponds to multiple transactions with different wtxids, multiple transactions will be removed and returned."},
+ },
+ RPCResult{
+ RPCResult::Type::OBJ, "", "",
+ {
+ {RPCResult::Type::ARR, "removed_transactions", "Transactions removed from the private broadcast queue",
+ {
+ {RPCResult::Type::OBJ, "", "",
+ {
+ {RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
+ {RPCResult::Type::STR_HEX, "wtxid", "The transaction witness hash in hex"},
+ {RPCResult::Type::STR_HEX, "hex", "The serialized, hex-encoded transaction data"},
+ }},
+ }},
+ }
+ },
+ RPCExamples{
+ HelpExampleCli("abortprivatebroadcast", "\"id\"")
+ + HelpExampleRpc("abortprivatebroadcast", "\"id\"")
+ },
+ [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
+ {
+ const uint256 id{ParseHashV(self.Arg<UniValue>("id"), "id")};
+
+ const NodeContext& node{EnsureAnyNodeContext(request.context)};
+ PeerManager& peerman{EnsurePeerman(node)};
+
+ const auto removed_txs{peerman.AbortPrivateBroadcast(id)};
+ if (removed_txs.empty()) {
+ throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not in private broadcast queue. Check getprivatebroadcastinfo.");
+ }
+
+ UniValue removed_transactions(UniValue::VARR);
+ for (const auto& tx : removed_txs) {
+ UniValue o(UniValue::VOBJ);
+ o.pushKV("txid", tx->GetHash().ToString());
+ o.pushKV("wtxid", tx->GetWitnessHash().ToString());
+ o.pushKV("hex", EncodeHexTx(*tx));
+ removed_transactions.push_back(std::move(o));
+ }
+ UniValue ret(UniValue::VOBJ);
+ ret.pushKV("removed_transactions", std::move(removed_transactions));
+ return ret;
+ },
+ };
+}
+
static RPCHelpMan testmempoolaccept()
{
return RPCHelpMan{
@@ -1395,6 +1450,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
static const CRPCCommand commands[]{
{"rawtransactions", &sendrawtransaction},
{"rawtransactions", &getprivatebroadcastinfo},
+ {"rawtransactions", &abortprivatebroadcast},
{"rawtransactions", &testmempoolaccept},
{"blockchain", &getmempoolancestors},
{"blockchain", &getmempooldescendants},
diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp
index cfb36afd..e8daf9d3 100644
--- a/src/test/fuzz/rpc.cpp
+++ b/src/test/fuzz/rpc.cpp
@@ -90,6 +90,7 @@ const std::vector<std::string> RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{
// RPC commands which are safe for fuzzing.
const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
+ "abortprivatebroadcast",
"analyzepsbt",
"clearbanned",
"combinepsbt",
Why this scored 23/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.