net_processing: store transactions for private broadcast in PeerManager
What changed, and why it matters
This commit adds infrastructure for a new privacy feature in Bitcoin Core. It creates a storage area for transactions that should be broadcast privately, and a method to request short-lived connections to privacy-network peers to broadcast them. The code is purely additive scaffolding; it does not by itself introduce a vulnerability, nor does it complete the private-broadcast feature. It simply wires a new code path into the existing transaction-broadcast logic.
No immediate security action is required. Treat this as normal feature code review. If auditing the broader private-broadcast feature, verify that: (1) the unsalted transaction hash map cannot be influenced by remote peers, (2) the new connections opened via NumToOpenAdd are rate-limited and authenticated appropriately, (3) the storage does not grow unbounded, and (4) the eventual send path confirms receipt and removes entries to prevent rebroadcast leaks.
Security signals we found
New network-facing code path added for private transaction broadcast
Use of unsalted hash for CTransactionRef in unordered_map (developer comment asserts low risk because only local transactions are stored)
Addition of concurrency primitive (Mutex) around new transaction storage
Partial feature: storage and request for connections exist, but actual broadcast mechanism is not in this commit
Evidence from the diff
The change introduces a PrivateBroadcast class (private_broadcast.h/cpp) that stores CTransactionRef objects keyed by witness transaction hash in a mutex-guarded unordered_map. PeerManagerImpl gains a m_tx_for_private_broadcast member and a new InitiateTxBroadcastPrivate() method. When called, it adds the transaction to the storage and asks CConnman::PrivateBroadcast::NumToOpenAdd() to open NUM_PRIVATE_BROADCAST_PER_TX (3) dedicated privacy connections. The node/transaction.cpp switch case for TxBroadcast::NO_MEMPOOL_PRIVATE_BROADCAST now invokes this new method instead of doing nothing. The commit is a partial feature implementation; the actual connection-opening, picking, and sending logic is not shown here.
Changed components
src/net_processing.cppsrc/net_processing.hsrc/node/transaction.cppsrc/private_broadcast.cppsrc/private_broadcast.hsrc/CMakeLists.txtInspect captured patch +104 / −0
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 47aee937..d655b7a2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -244,6 +244,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
policy/rbf.cpp
policy/settings.cpp
policy/truc_policy.cpp
+ private_broadcast.cpp
rest.cpp
rpc/blockchain.cpp
rpc/external_signer.cpp
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index f8b56d55..54156470 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -44,6 +44,7 @@
#include <policy/policy.h>
#include <primitives/block.h>
#include <primitives/transaction.h>
+#include <private_broadcast.h>
#include <protocol.h>
#include <random.h>
#include <scheduler.h>
@@ -196,6 +197,8 @@ static constexpr double MAX_ADDR_RATE_PER_SECOND{0.1};
static constexpr size_t MAX_ADDR_PROCESSING_TOKEN_BUCKET{MAX_ADDR_TO_SEND};
/** The compactblocks version we support. See BIP 152. */
static constexpr uint64_t CMPCTBLOCKS_VERSION{2};
+/** For private broadcast, send a transaction to this many peers. */
+static constexpr size_t NUM_PRIVATE_BROADCAST_PER_TX{3};
// Internal stuff
namespace {
@@ -538,6 +541,7 @@ public:
PeerManagerInfo GetInfo() const 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);
void SetBestBlock(int height, std::chrono::seconds time) override
{
m_best_height = height;
@@ -1070,6 +1074,9 @@ private:
void PushAddress(Peer& peer, const CAddress& addr) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex);
void LogBlockHeader(const CBlockIndex& index, const CNode& peer, bool via_compact_block);
+
+ /// The transactions to be broadcast privately.
+ PrivateBroadcast m_tx_for_private_broadcast;
};
const CNodeState* PeerManagerImpl::State(NodeId pnode) const
@@ -2147,6 +2154,17 @@ void PeerManagerImpl::InitiateTxBroadcastToAll(const Txid& txid, const Wtxid& wt
}
}
+void PeerManagerImpl::InitiateTxBroadcastPrivate(const CTransactionRef& tx)
+{
+ const auto txstr{strprintf("txid=%s, wtxid=%s", tx->GetHash().ToString(), tx->GetWitnessHash().ToString())};
+ if (m_tx_for_private_broadcast.Add(tx)) {
+ LogDebug(BCLog::PRIVBROADCAST, "Requesting %d new connections due to %s", NUM_PRIVATE_BROADCAST_PER_TX, txstr);
+ m_connman.m_private_broadcast.NumToOpenAdd(NUM_PRIVATE_BROADCAST_PER_TX);
+ } else {
+ LogDebug(BCLog::PRIVBROADCAST, "Ignoring unnecessary request to schedule an already scheduled transaction: %s", txstr);
+ }
+}
+
void PeerManagerImpl::RelayAddress(NodeId originator,
const CAddress& addr,
bool fReachable)
diff --git a/src/net_processing.h b/src/net_processing.h
index 654594aa..ecfb6985 100644
--- a/src/net_processing.h
+++ b/src/net_processing.h
@@ -124,6 +124,12 @@ public:
*/
virtual void InitiateTxBroadcastToAll(const Txid& txid, const Wtxid& wtxid) = 0;
+ /**
+ * Initiate a private transaction broadcast. This is done
+ * asynchronously via short-lived connections to peers on privacy networks.
+ */
+ virtual void InitiateTxBroadcastPrivate(const CTransactionRef& tx) = 0;
+
/** Send ping message to all peers */
virtual void SendPings() = 0;
diff --git a/src/node/transaction.cpp b/src/node/transaction.cpp
index a5a3a9d9..8b9eb80d 100644
--- a/src/node/transaction.cpp
+++ b/src/node/transaction.cpp
@@ -139,6 +139,7 @@ TransactionError BroadcastTransaction(NodeContext& node,
node.peerman->InitiateTxBroadcastToAll(txid, wtxid);
break;
case TxBroadcast::NO_MEMPOOL_PRIVATE_BROADCAST:
+ node.peerman->InitiateTxBroadcastPrivate(tx);
break;
}
diff --git a/src/private_broadcast.cpp b/src/private_broadcast.cpp
new file mode 100644
index 00000000..c14cea84
--- /dev/null
+++ b/src/private_broadcast.cpp
@@ -0,0 +1,13 @@
+// Copyright (c) 2023-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or https://opensource.org/license/mit/.
+
+#include <private_broadcast.h>
+
+bool PrivateBroadcast::Add(const CTransactionRef& tx)
+ EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
+{
+ LOCK(m_mutex);
+ const bool inserted{m_transactions.try_emplace(tx).second};
+ return inserted;
+}
diff --git a/src/private_broadcast.h b/src/private_broadcast.h
new file mode 100644
index 00000000..8e0b5ffb
--- /dev/null
+++ b/src/private_broadcast.h
@@ -0,0 +1,65 @@
+// Copyright (c) 2023-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or https://opensource.org/license/mit/.
+
+#ifndef BITCOIN_PRIVATE_BROADCAST_H
+#define BITCOIN_PRIVATE_BROADCAST_H
+
+#include <net.h>
+#include <primitives/transaction.h>
+#include <sync.h>
+#include <threadsafety.h>
+#include <util/time.h>
+
+#include <optional>
+#include <unordered_map>
+#include <vector>
+
+/**
+ * Store a list of transactions to be broadcast privately. Supports the following operations:
+ * - Add a new transaction
+ */
+class PrivateBroadcast
+{
+public:
+ /**
+ * Add a transaction to the storage.
+ * @param[in] tx The transaction to add.
+ * @retval true The transaction was added.
+ * @retval false The transaction was already present.
+ */
+ bool Add(const CTransactionRef& tx)
+ EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
+
+private:
+ /// Status of a transaction sent to a given node.
+ struct SendStatus {
+ const NodeId nodeid; /// Node to which the transaction will be sent (or was sent).
+ const NodeClock::time_point picked; ///< When was the transaction picked for sending to the node.
+ std::optional<NodeClock::time_point> confirmed; ///< When was the transaction reception confirmed by the node (by PONG).
+
+ SendStatus(const NodeId& nodeid, const NodeClock::time_point& picked) : nodeid{nodeid}, picked{picked} {}
+ };
+
+ // No need for salted hasher because we are going to store just a bunch of locally originating transactions.
+
+ struct CTransactionRefHash {
+ size_t operator()(const CTransactionRef& tx) const
+ {
+ return static_cast<size_t>(tx->GetWitnessHash().ToUint256().GetUint64(0));
+ }
+ };
+
+ struct CTransactionRefComp {
+ bool operator()(const CTransactionRef& a, const CTransactionRef& b) const
+ {
+ return a->GetWitnessHash() == b->GetWitnessHash(); // If wtxid equals, then txid also equals.
+ }
+ };
+
+ mutable Mutex m_mutex;
+ std::unordered_map<CTransactionRef, std::vector<SendStatus>, CTransactionRefHash, CTransactionRefComp>
+ m_transactions GUARDED_BY(m_mutex);
+};
+
+#endif // BITCOIN_PRIVATE_BROADCAST_H
Why this scored 12/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.