Here is a change that does that, on top of this PR. If you do not want to bloat this PR with it, I will submit as a followup:
0diff --git i/src/net_processing.cpp w/src/net_processing.cpp
1index ed3e5ebf98..db8a007a04 100644
2--- i/src/net_processing.cpp
3+++ w/src/net_processing.cpp
4@@ -539,13 +539,13 @@ public:
5 void CheckForStaleTipAndEvictPeers() override;
6 util::Expected<void, std::string> FetchBlock(NodeId peer_id, const CBlockIndex& block_index) override
7 EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
8 bool GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
9 std::vector<node::TxOrphanage::OrphanInfo> GetOrphanTransactions() override EXCLUSIVE_LOCKS_REQUIRED(!m_tx_download_mutex);
10 PeerManagerInfo GetInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
11- std::vector<PrivateBroadcast::TxBroadcastInfo> GetPrivateBroadcastInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
12+ PrivateBroadcast::Transactions GetPrivateBroadcastInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
13 std::vector<CTransactionRef> AbortPrivateBroadcast(const uint256& id) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
14 void SendPings() override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
15 void InitiateTxBroadcastToAll(const Txid& txid, const Wtxid& wtxid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
16 void InitiateTxBroadcastPrivate(const CTransactionRef& tx) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
17 void SetBestBlock(int height, std::chrono::seconds time) override
18 {
19@@ -1854,25 +1854,24 @@ PeerManagerInfo PeerManagerImpl::GetInfo() const
20 return PeerManagerInfo{
21 .median_outbound_time_offset = m_outbound_time_offsets.Median(),
22 .ignores_incoming_txs = m_opts.ignore_incoming_txs,
23 };
24 }
25
26-std::vector<PrivateBroadcast::TxBroadcastInfo> PeerManagerImpl::GetPrivateBroadcastInfo() const
27+PrivateBroadcast::Transactions PeerManagerImpl::GetPrivateBroadcastInfo() const
28 {
29 return m_tx_for_private_broadcast.GetBroadcastInfo();
30 }
31
32 std::vector<CTransactionRef> PeerManagerImpl::AbortPrivateBroadcast(const uint256& id)
33 {
34 const auto snapshot{m_tx_for_private_broadcast.GetBroadcastInfo()};
35 std::vector<CTransactionRef> removed_txs;
36
37 size_t connections_cancelled{0};
38- for (const auto& tx_info : snapshot) {
39- const auto& tx{tx_info.tx};
40+ for (const auto& [tx, status] : snapshot) {
41 if (tx->GetHash().ToUint256() != id && tx->GetWitnessHash().ToUint256() != id) continue;
42 if (const auto peer_acks{m_tx_for_private_broadcast.Remove(tx)}) {
43 removed_txs.push_back(tx);
44 if (NUM_PRIVATE_BROADCAST_PER_TX > *peer_acks) {
45 connections_cancelled += (NUM_PRIVATE_BROADCAST_PER_TX - *peer_acks);
46 }
47diff --git i/src/net_processing.h w/src/net_processing.h
48index 36ae021f67..9472aa7735 100644
49--- i/src/net_processing.h
50+++ w/src/net_processing.h
51@@ -118,13 +118,13 @@ public:
52 virtual std::vector<node::TxOrphanage::OrphanInfo> GetOrphanTransactions() = 0;
53
54 /** Get peer manager info. */
55 virtual PeerManagerInfo GetInfo() const = 0;
56
57 /** Get info about transactions currently being privately broadcast. */
58- virtual std::vector<PrivateBroadcast::TxBroadcastInfo> GetPrivateBroadcastInfo() const = 0;
59+ virtual PrivateBroadcast::Transactions GetPrivateBroadcastInfo() const = 0;
60
61 /**
62 * Abort private broadcast attempts for transactions currently being privately broadcast.
63 *
64 * [@param](/bitcoin-bitcoin/contributor/param/)[in] id A transaction identifier. It will be matched against both txid and wtxid for
65 * all transactions in the private broadcast queue.
66diff --git i/src/private_broadcast.cpp w/src/private_broadcast.cpp
67index 0c70fff7d8..e0349032be 100644
68--- i/src/private_broadcast.cpp
69+++ w/src/private_broadcast.cpp
70@@ -124,37 +124,20 @@ std::vector<CTransactionRef> PrivateBroadcast::GetStale() const
71 stale.push_back(tx);
72 }
73 }
74 return stale;
75 }
76
77-std::vector<PrivateBroadcast::TxBroadcastInfo> PrivateBroadcast::GetBroadcastInfo() const
78+PrivateBroadcast::Transactions PrivateBroadcast::GetBroadcastInfo() const
79 EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
80 {
81 LOCK(m_mutex);
82- std::vector<TxBroadcastInfo> entries;
83- entries.reserve(m_transactions.size());
84-
85- for (const auto& [tx, state] : m_transactions) {
86- std::vector<PeerSendInfo> peers;
87- peers.reserve(state.send_statuses.size());
88- for (const auto& status : state.send_statuses) {
89- peers.emplace_back(PeerSendInfo{.address = status.address, .sent = status.picked, .received = status.confirmed});
90- }
91- entries.emplace_back(TxBroadcastInfo{
92- .tx = tx,
93- .peers = std::move(peers),
94- .received_from = state.received_from,
95- .received_time = state.received_time,
96- });
97- }
98-
99- return entries;
100+ return m_transactions;
101 }
102
103-PrivateBroadcast::Priority PrivateBroadcast::DerivePriority(const TxSendStatus& tx_status)
104+PrivateBroadcast::Priority PrivateBroadcast::DerivePriority(const Status& tx_status)
105 {
106 Priority p;
107 p.num_picked = tx_status.send_statuses.size();
108 for (const auto& send_status : tx_status.send_statuses) {
109 p.last_picked = std::max(p.last_picked, send_status.picked);
110 if (send_status.confirmed.has_value()) {
111@@ -176,17 +159,17 @@ std::optional<PrivateBroadcast::TxAndSendStatusForNode> PrivateBroadcast::GetSen
112 }
113 }
114 }
115 return std::nullopt;
116 }
117
118-std::vector<PrivateBroadcast::TransactionMap::iterator> PrivateBroadcast::GetPendingTransactions()
119+std::vector<PrivateBroadcast::Transactions::iterator> PrivateBroadcast::GetPendingTransactions()
120 EXCLUSIVE_LOCKS_REQUIRED(m_mutex)
121 {
122 AssertLockHeld(m_mutex);
123- std::vector<TransactionMap::iterator> result;
124+ std::vector<Transactions::iterator> result;
125 for (auto it{m_transactions.begin()}; it != m_transactions.end(); ++it) {
126 if (!it->second.received_from.has_value()) {
127 result.push_back(it);
128 }
129 }
130 return result;
131diff --git i/src/private_broadcast.h w/src/private_broadcast.h
132index 5c70754df2..3c39ad298a 100644
133--- i/src/private_broadcast.h
134+++ w/src/private_broadcast.h
135@@ -28,25 +28,47 @@
136 * - Query whether a given recipient node has confirmed reception
137 * - Query whether any transactions that need sending are currently on the list
138 */
139 class PrivateBroadcast
140 {
141 public:
142- struct PeerSendInfo {
143- CService address;
144- NodeClock::time_point sent;
145- std::optional<NodeClock::time_point> received;
146+ /// Status of a transaction sent to a given node.
147+ struct SendStatus {
148+ const NodeId nodeid; /// Node to which the transaction will be sent (or was sent).
149+ const CService address; /// Address of the node.
150+ const NodeClock::time_point picked; ///< When was the transaction picked for sending to the node.
151+ std::optional<NodeClock::time_point> confirmed; ///< When was the transaction reception confirmed by the node (by PONG).
152+
153+ SendStatus(const NodeId& nodeid, const CService& address, const NodeClock::time_point& picked) : nodeid{nodeid}, address{address}, picked{picked} {}
154 };
155
156- struct TxBroadcastInfo {
157- CTransactionRef tx;
158- std::vector<PeerSendInfo> peers;
159- std::optional<CService> received_from;
160- std::optional<NodeClock::time_point> received_time;
161+ /// Status of a transaction, including all send attempts and a possible reception info.
162+ struct Status {
163+ std::vector<SendStatus> send_statuses; /// All send attempts.
164+ std::optional<CService> received_from; /// When we receive back the transaction from the network, this is the peer we got it from.
165+ std::optional<NodeClock::time_point> received_time; /// Time of receiving back the transaction.
166 };
167
168+ // No need for salted hasher because we are going to store just a bunch of locally originating transactions.
169+
170+ struct CTransactionRefHash {
171+ size_t operator()(const CTransactionRef& tx) const
172+ {
173+ return static_cast<size_t>(tx->GetWitnessHash().ToUint256().GetUint64(0));
174+ }
175+ };
176+
177+ struct CTransactionRefComp {
178+ bool operator()(const CTransactionRef& a, const CTransactionRef& b) const
179+ {
180+ return a->GetWitnessHash() == b->GetWitnessHash(); // If wtxid equals, then txid also equals.
181+ }
182+ };
183+
184+ using Transactions = std::unordered_map<CTransactionRef, Status, CTransactionRefHash, CTransactionRefComp>;
185+
186 /**
187 * Add a transaction to the storage, or reset a transaction's state if the
188 * transaction has been marked received.
189 * [@param](/bitcoin-bitcoin/contributor/param/)[in] tx The transaction to add.
190 * [@retval](/bitcoin-bitcoin/contributor/retval/) true The transaction was added or reset.
191 * [@retval](/bitcoin-bitcoin/contributor/retval/) false The transaction was already present and not marked received.
192@@ -124,32 +146,16 @@ public:
193 std::vector<CTransactionRef> GetStale() const
194 EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
195
196 /**
197 * Get stats about all transactions currently being privately broadcast.
198 */
199- std::vector<TxBroadcastInfo> GetBroadcastInfo() const
200+ Transactions GetBroadcastInfo() const
201 EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
202
203 private:
204- /// Status of a transaction sent to a given node.
205- struct SendStatus {
206- const NodeId nodeid; /// Node to which the transaction will be sent (or was sent).
207- const CService address; /// Address of the node.
208- const NodeClock::time_point picked; ///< When was the transaction picked for sending to the node.
209- std::optional<NodeClock::time_point> confirmed; ///< When was the transaction reception confirmed by the node (by PONG).
210-
211- SendStatus(const NodeId& nodeid, const CService& address, const NodeClock::time_point& picked) : nodeid{nodeid}, address{address}, picked{picked} {}
212- };
213-
214- struct TxSendStatus {
215- std::vector<SendStatus> send_statuses;
216- std::optional<CService> received_from;
217- std::optional<NodeClock::time_point> received_time;
218- };
219-
220 /// Cumulative stats from all the send attempts for a transaction. Used to prioritize transactions.
221 struct Priority {
222 size_t num_picked{0}; ///< Number of times the transaction was picked for sending.
223 NodeClock::time_point last_picked{}; ///< The most recent time when the transaction was picked for sending.
224 size_t num_confirmed{0}; ///< Number of nodes that have confirmed reception of a transaction (by PONG).
225 NodeClock::time_point last_confirmed{}; ///< The most recent time when the transaction was confirmed.
226@@ -167,46 +173,28 @@ private:
227 /// A pair of a transaction and a sent status for a given node. Convenience return type of GetSendStatusByNode().
228 struct TxAndSendStatusForNode {
229 const CTransactionRef& tx;
230 SendStatus& send_status;
231 };
232
233- // No need for salted hasher because we are going to store just a bunch of locally originating transactions.
234-
235- struct CTransactionRefHash {
236- size_t operator()(const CTransactionRef& tx) const
237- {
238- return static_cast<size_t>(tx->GetWitnessHash().ToUint256().GetUint64(0));
239- }
240- };
241-
242- struct CTransactionRefComp {
243- bool operator()(const CTransactionRef& a, const CTransactionRef& b) const
244- {
245- return a->GetWitnessHash() == b->GetWitnessHash(); // If wtxid equals, then txid also equals.
246- }
247- };
248-
249- using TransactionMap = std::unordered_map<CTransactionRef, TxSendStatus, CTransactionRefHash, CTransactionRefComp>;
250-
251 /**
252 * Derive the sending priority of a transaction.
253 * [@param](/bitcoin-bitcoin/contributor/param/)[in] status The send status of the transaction.
254 */
255- static Priority DerivePriority(const TxSendStatus& status);
256+ static Priority DerivePriority(const Status& status);
257
258 /**
259 * Find which transaction we sent to a given node (marked by PickTxForSend()).
260 * [@return](/bitcoin-bitcoin/contributor/return/) That transaction together with the send status or nullopt if we did not
261 * send any transaction to the given node.
262 */
263 std::optional<TxAndSendStatusForNode> GetSendStatusByNode(const NodeId& nodeid)
264 EXCLUSIVE_LOCKS_REQUIRED(m_mutex);
265
266- std::vector<TransactionMap::iterator> GetPendingTransactions()
267+ std::vector<Transactions::iterator> GetPendingTransactions()
268 EXCLUSIVE_LOCKS_REQUIRED(m_mutex);
269
270 mutable Mutex m_mutex;
271- TransactionMap m_transactions GUARDED_BY(m_mutex);
272+ Transactions m_transactions GUARDED_BY(m_mutex);
273 };
274
275 #endif // BITCOIN_PRIVATE_BROADCAST_H
276diff --git i/src/rpc/mempool.cpp w/src/rpc/mempool.cpp
277index caa0c9e636..bdc61a3e8c 100644
278--- i/src/rpc/mempool.cpp
279+++ w/src/rpc/mempool.cpp
280@@ -174,34 +174,36 @@ static RPCHelpMan getprivatebroadcastinfo()
281 + HelpExampleRpc("getprivatebroadcastinfo", "")
282 },
283 [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
284 {
285 const NodeContext& node{EnsureAnyNodeContext(request.context)};
286 const PeerManager& peerman{EnsurePeerman(node)};
287- const auto txs{peerman.GetPrivateBroadcastInfo()};
288+ const auto pbinfo{peerman.GetPrivateBroadcastInfo()};
289
290 UniValue transactions(UniValue::VARR);
291- for (const auto& tx_info : txs) {
292+ for (const auto& [tx, status] : pbinfo) {
293 UniValue o(UniValue::VOBJ);
294- o.pushKV("txid", tx_info.tx->GetHash().ToString());
295- o.pushKV("wtxid", tx_info.tx->GetWitnessHash().ToString());
296- o.pushKV("hex", EncodeHexTx(*tx_info.tx));
297+ o.pushKV("txid", tx->GetHash().ToString());
298+ o.pushKV("wtxid", tx->GetWitnessHash().ToString());
299+ o.pushKV("hex", EncodeHexTx(*tx));
300 UniValue peers(UniValue::VARR);
301- for (const auto& peer : tx_info.peers) {
302+ for (const auto& peer : status.send_statuses) {
303 UniValue p(UniValue::VOBJ);
304 p.pushKV("address", peer.address.ToStringAddrPort());
305- p.pushKV("sent", TicksSinceEpoch<std::chrono::seconds>(peer.sent));
306- if (peer.received.has_value()) {
307- p.pushKV("received", TicksSinceEpoch<std::chrono::seconds>(*peer.received));
308+ p.pushKV("sent", TicksSinceEpoch<std::chrono::seconds>(peer.picked));
309+ if (peer.confirmed.has_value()) {
310+ p.pushKV("received", TicksSinceEpoch<std::chrono::seconds>(*peer.confirmed));
311 }
312 peers.push_back(std::move(p));
313 }
314 o.pushKV("peers", std::move(peers));
315- if (tx_info.received_from.has_value()) {
316- o.pushKV("received_from", tx_info.received_from->ToStringAddrPort());
317- o.pushKV("received_time", TicksSinceEpoch<std::chrono::seconds>(*tx_info.received_time));
318+ if (status.received_from.has_value()) {
319+ o.pushKV("received_from", status.received_from->ToStringAddrPort());
320+ }
321+ if (status.received_time.has_value()) {
322+ o.pushKV("received_time", TicksSinceEpoch<std::chrono::seconds>(status.received_time.value()));
323 }
324 transactions.push_back(std::move(o));
325 }
326
327 UniValue ret(UniValue::VOBJ);
328 ret.pushKV("transactions", std::move(transactions));
329diff --git i/src/test/private_broadcast_tests.cpp w/src/test/private_broadcast_tests.cpp
330index 715cafb190..0cf58e5d0e 100644
331--- i/src/test/private_broadcast_tests.cpp
332+++ w/src/test/private_broadcast_tests.cpp
333@@ -21,19 +21,12 @@ static CTransactionRef MakeDummyTx(uint32_t id, size_t num_witness)
334 mtx.vin[0].scriptWitness = CScriptWitness{};
335 mtx.vin[0].scriptWitness.stack.resize(num_witness);
336 }
337 return MakeTransactionRef(mtx);
338 }
339
340-static auto FindTxInfo(const std::vector<PrivateBroadcast::TxBroadcastInfo>& infos, const CTransactionRef& tx)
341-{
342- const auto it{std::ranges::find(infos, tx->GetWitnessHash(), [](const auto& info) { return info.tx->GetWitnessHash(); })};
343- BOOST_REQUIRE(it != infos.end());
344- return it;
345-}
346-
347 } // namespace
348
349 BOOST_FIXTURE_TEST_SUITE(private_broadcast_tests, BasicTestingSetup)
350
351 BOOST_AUTO_TEST_CASE(basic)
352 {
353@@ -63,14 +56,14 @@ BOOST_AUTO_TEST_CASE(basic)
354 BOOST_REQUIRE(tx1->GetWitnessHash() != tx2->GetWitnessHash());
355
356 BOOST_CHECK(pb.Add(tx2));
357 const auto check_peer_counts{[&](size_t tx1_peer_count, size_t tx2_peer_count) {
358 const auto infos{pb.GetBroadcastInfo()};
359 BOOST_CHECK_EQUAL(infos.size(), 2);
360- BOOST_CHECK_EQUAL(FindTxInfo(infos, tx1)->peers.size(), tx1_peer_count);
361- BOOST_CHECK_EQUAL(FindTxInfo(infos, tx2)->peers.size(), tx2_peer_count);
362+ BOOST_CHECK_EQUAL(infos.at(tx1).send_statuses.size(), tx1_peer_count);
363+ BOOST_CHECK_EQUAL(infos.at(tx2).send_statuses.size(), tx2_peer_count);
364 }};
365
366 check_peer_counts(/*tx1_peer_count=*/0, /*tx2_peer_count=*/0);
367
368 const auto tx_for_recipient1{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).value()};
369 BOOST_CHECK(tx_for_recipient1 == tx1 || tx_for_recipient1 == tx2);
370@@ -105,24 +98,22 @@ BOOST_AUTO_TEST_CASE(basic)
371 BOOST_CHECK(pb.DidNodeConfirmReception(recipient1));
372 BOOST_CHECK(!pb.DidNodeConfirmReception(recipient2));
373
374 const auto infos{pb.GetBroadcastInfo()};
375 BOOST_CHECK_EQUAL(infos.size(), 2);
376 {
377- const auto info_it{FindTxInfo(infos, tx_for_recipient1)};
378- const auto& peers{info_it->peers};
379+ const auto& peers{infos.at(tx_for_recipient1).send_statuses};
380 BOOST_CHECK_EQUAL(peers.size(), 1);
381 BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr1.ToStringAddrPort());
382- BOOST_CHECK(peers[0].received.has_value());
383+ BOOST_CHECK(peers[0].confirmed.has_value());
384 }
385 {
386- const auto info_it{FindTxInfo(infos, tx_for_recipient2)};
387- const auto& peers{info_it->peers};
388+ const auto& peers{infos.at(tx_for_recipient2).send_statuses};
389 BOOST_CHECK_EQUAL(peers.size(), 1);
390 BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr2.ToStringAddrPort());
391- BOOST_CHECK(!peers[0].received.has_value());
392+ BOOST_CHECK(!peers[0].confirmed.has_value());
393 }
394
395 BOOST_CHECK_EQUAL(pb.GetStale().size(), 1);
396 BOOST_CHECK_EQUAL(pb.GetStale()[0], tx_for_recipient2);
397
398 SetMockTime(Now<NodeSeconds>() + 10h);
399@@ -164,36 +155,35 @@ BOOST_AUTO_TEST_CASE(mark_received)
400
401 // MarkReceived succeeds and returns the number of confirmed sends.
402 BOOST_CHECK_EQUAL(pb.MarkReceived(tx, received_from).value(), 1);
403
404 {
405 const auto infos{pb.GetBroadcastInfo()};
406- const auto info{FindTxInfo(infos, tx)};
407- BOOST_CHECK_EQUAL(info->received_from->ToStringAddrPort(), received_from.ToStringAddrPort());
408- BOOST_CHECK(info->received_time.has_value());
409+ const auto& info{infos.at(tx)};
410+ BOOST_CHECK_EQUAL(info.received_from->ToStringAddrPort(), received_from.ToStringAddrPort());
411+ BOOST_CHECK(info.received_time.has_value());
412 }
413 BOOST_CHECK(!pb.HavePendingTransactions());
414 BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/recipient2, /*will_send_to_address=*/addr2).has_value());
415
416 // Subsequent MarkReceived returns nullopt and does not overwrite.
417 in_addr ipv4Addr2;
418 ipv4Addr2.s_addr = 0xa0b0c099;
419 const CService received_from2{ipv4Addr2, 4444};
420 BOOST_CHECK(!pb.MarkReceived(tx, received_from2).has_value());
421
422 {
423 const auto infos{pb.GetBroadcastInfo()};
424- const auto info{FindTxInfo(infos, tx)};
425- BOOST_CHECK_EQUAL(info->received_from->ToStringAddrPort(), received_from.ToStringAddrPort());
426+ BOOST_CHECK_EQUAL(infos.at(tx).received_from->ToStringAddrPort(), received_from.ToStringAddrPort());
427 BOOST_CHECK(!pb.HavePendingTransactions());
428 }
429
430 // Re-adding after received clears the received state and makes it pending again.
431 BOOST_CHECK(pb.Add(tx));
432 BOOST_CHECK(pb.HavePendingTransactions());
433 const auto infos{pb.GetBroadcastInfo()};
434- const auto info{FindTxInfo(infos, tx)};
435- BOOST_CHECK(!info->received_from.has_value());
436- BOOST_CHECK(!info->received_time.has_value());
437+ const auto& info{infos.at(tx)};
438+ BOOST_CHECK(!info.received_from.has_value());
439+ BOOST_CHECK(!info.received_time.has_value());
440 }
441
442 BOOST_AUTO_TEST_SUITE_END()