net: Add PrivateBroadcast::GetBroadcastInfo
What changed, and why it matters
This commit adds a new read-only method, GetBroadcastInfo(), to Bitcoin Core's PrivateBroadcast class. It simply returns statistics about which transactions are being privately broadcast and to which peers. There is no change to network behavior, no new permissions, and no bug fix. It is a routine code addition with no security relevance.
No security action needed. Review as normal code-quality change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces PrivateBroadcast::GetBroadcastInfo() and two public helper structs (PeerSendInfo, TxBroadcastInfo) that expose a snapshot of currently tracked private-broadcast transactions and their per-peer send/confirm status. The method acquires the existing mutex, copies data into new structs, and returns them. It is purely observational. The unit tests are extended to cover the new accessor. No existing logic is modified.
Changed components
src/private_broadcast.cppsrc/private_broadcast.hsrc/test/private_broadcast_tests.cppInspect captured patch +75 / −3
diff --git a/src/private_broadcast.cpp b/src/private_broadcast.cpp
index fd36a940..3900f10a 100644
--- a/src/private_broadcast.cpp
+++ b/src/private_broadcast.cpp
@@ -104,6 +104,25 @@ std::vector<CTransactionRef> PrivateBroadcast::GetStale() const
return stale;
}
+std::vector<PrivateBroadcast::TxBroadcastInfo> PrivateBroadcast::GetBroadcastInfo() const
+ EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
+{
+ LOCK(m_mutex);
+ std::vector<TxBroadcastInfo> entries;
+ entries.reserve(m_transactions.size());
+
+ for (const auto& [tx, sent_to] : m_transactions) {
+ std::vector<PeerSendInfo> peers;
+ peers.reserve(sent_to.size());
+ for (const auto& status : sent_to) {
+ peers.emplace_back(PeerSendInfo{.address = status.address, .sent = status.picked, .received = status.confirmed});
+ }
+ entries.emplace_back(TxBroadcastInfo{.tx = tx, .peers = std::move(peers)});
+ }
+
+ return entries;
+}
+
PrivateBroadcast::Priority PrivateBroadcast::DerivePriority(const std::vector<SendStatus>& sent_to)
{
Priority p;
diff --git a/src/private_broadcast.h b/src/private_broadcast.h
index 3beed5ee..28634424 100644
--- a/src/private_broadcast.h
+++ b/src/private_broadcast.h
@@ -30,6 +30,17 @@
class PrivateBroadcast
{
public:
+ struct PeerSendInfo {
+ CService address;
+ NodeClock::time_point sent;
+ std::optional<NodeClock::time_point> received;
+ };
+
+ struct TxBroadcastInfo {
+ CTransactionRef tx;
+ std::vector<PeerSendInfo> peers;
+ };
+
/**
* Add a transaction to the storage.
* @param[in] tx The transaction to add.
@@ -97,6 +108,12 @@ public:
std::vector<CTransactionRef> GetStale() const
EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
+ /**
+ * Get stats about all transactions currently being privately broadcast.
+ */
+ std::vector<TxBroadcastInfo> GetBroadcastInfo() const
+ EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
+
private:
/// Status of a transaction sent to a given node.
struct SendStatus {
diff --git a/src/test/private_broadcast_tests.cpp b/src/test/private_broadcast_tests.cpp
index e0ab64cd..73bdbbb5 100644
--- a/src/test/private_broadcast_tests.cpp
+++ b/src/test/private_broadcast_tests.cpp
@@ -7,6 +7,7 @@
#include <test/util/setup_common.h>
#include <util/time.h>
+#include <algorithm>
#include <boost/test/unit_test.hpp>
BOOST_FIXTURE_TEST_SUITE(private_broadcast_tests, BasicTestingSetup)
@@ -29,12 +30,15 @@ BOOST_AUTO_TEST_CASE(basic)
PrivateBroadcast pb;
const NodeId recipient1{1};
- const CService addr1{};
+ in_addr ipv4Addr;
+ ipv4Addr.s_addr = 0xa0b0c001;
+ const CService addr1{ipv4Addr, 1111};
// No transactions initially.
BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).has_value());
BOOST_CHECK_EQUAL(pb.GetStale().size(), 0);
BOOST_CHECK(!pb.HavePendingTransactions());
+ BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), 0);
// Make a transaction and add it.
const auto tx1{MakeDummyTx(/*id=*/1, /*num_witness=*/0)};
@@ -48,17 +52,32 @@ BOOST_AUTO_TEST_CASE(basic)
BOOST_REQUIRE(tx1->GetWitnessHash() != tx2->GetWitnessHash());
BOOST_CHECK(pb.Add(tx2));
+ const auto find_tx_info{[](auto& infos, const CTransactionRef& tx) -> const PrivateBroadcast::TxBroadcastInfo& {
+ const auto it{std::ranges::find(infos, tx->GetWitnessHash(), [](const auto& info) { return info.tx->GetWitnessHash(); })};
+ BOOST_REQUIRE(it != infos.end());
+ return *it;
+ }};
+ const auto check_peer_counts{[&](size_t tx1_peer_count, size_t tx2_peer_count) {
+ const auto infos{pb.GetBroadcastInfo()};
+ BOOST_CHECK_EQUAL(infos.size(), 2);
+ BOOST_CHECK_EQUAL(find_tx_info(infos, tx1).peers.size(), tx1_peer_count);
+ BOOST_CHECK_EQUAL(find_tx_info(infos, tx2).peers.size(), tx2_peer_count);
+ }};
+
+ check_peer_counts(/*tx1_peer_count=*/0, /*tx2_peer_count=*/0);
const auto tx_for_recipient1{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).value()};
BOOST_CHECK(tx_for_recipient1 == tx1 || tx_for_recipient1 == tx2);
// A second pick must return the other transaction.
const NodeId recipient2{2};
- const CService addr2{};
+ const CService addr2{ipv4Addr, 2222};
const auto tx_for_recipient2{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient2, /*will_send_to_address=*/addr2).value()};
BOOST_CHECK(tx_for_recipient2 == tx1 || tx_for_recipient2 == tx2);
BOOST_CHECK_NE(tx_for_recipient1, tx_for_recipient2);
+ check_peer_counts(/*tx1_peer_count=*/1, /*tx2_peer_count=*/1);
+
const NodeId nonexistent_recipient{0};
// Confirm transactions <-> recipients mapping is correct.
@@ -80,6 +99,21 @@ BOOST_AUTO_TEST_CASE(basic)
BOOST_CHECK(pb.DidNodeConfirmReception(recipient1));
BOOST_CHECK(!pb.DidNodeConfirmReception(recipient2));
+ const auto infos{pb.GetBroadcastInfo()};
+ BOOST_CHECK_EQUAL(infos.size(), 2);
+ {
+ const auto& [tx, peers]{find_tx_info(infos, tx_for_recipient1)};
+ BOOST_CHECK_EQUAL(peers.size(), 1);
+ BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr1.ToStringAddrPort());
+ BOOST_CHECK(peers[0].received.has_value());
+ }
+ {
+ const auto& [tx, peers]{find_tx_info(infos, tx_for_recipient2)};
+ BOOST_CHECK_EQUAL(peers.size(), 1);
+ BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr2.ToStringAddrPort());
+ BOOST_CHECK(!peers[0].received.has_value());
+ }
+
BOOST_CHECK_EQUAL(pb.GetStale().size(), 1);
BOOST_CHECK_EQUAL(pb.GetStale()[0], tx_for_recipient2);
@@ -92,7 +126,9 @@ BOOST_AUTO_TEST_CASE(basic)
BOOST_CHECK_EQUAL(pb.Remove(tx_for_recipient2).value(), 0);
BOOST_CHECK(!pb.Remove(tx_for_recipient2).has_value());
- BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/nonexistent_recipient, /*will_send_to_address=*/CService{}).has_value());
+ BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), 0);
+ const CService addr_nonexistent{ipv4Addr, 3333};
+ BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/nonexistent_recipient, /*will_send_to_address=*/addr_nonexistent).has_value());
}
BOOST_AUTO_TEST_SUITE_END()
Why this scored 15/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.