From 65de28ad32733f3ec44e243eb472ea938f68be47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Thu, 22 Jan 2026 11:36:41 +0100 Subject: [PATCH 1/3] test: add unit test for private broadcast connection requests --- src/test/peerman_tests.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/peerman_tests.cpp b/src/test/peerman_tests.cpp index 64b13fa3cc1a..6aba4c871be1 100644 --- a/src/test/peerman_tests.cpp +++ b/src/test/peerman_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -73,4 +74,15 @@ BOOST_AUTO_TEST_CASE(connections_desirable_service_flags) BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK | NODE_WITNESS)); } +BOOST_AUTO_TEST_CASE(private_broadcast_requests_connections_immediately) +{ + std::unique_ptr peerman = PeerManager::make(*m_node.connman, *m_node.addrman, nullptr, *m_node.chainman, *m_node.mempool, *m_node.warnings, {}); + + const CTransactionRef tx{MakeTransactionRef(CMutableTransaction{})}; + + BOOST_CHECK_EQUAL(m_node.connman->m_private_broadcast.NumToOpen(), 0U); + peerman->InitiateTxBroadcastPrivate(tx); + BOOST_CHECK_EQUAL(m_node.connman->m_private_broadcast.NumToOpen(), 3U); +} + BOOST_AUTO_TEST_SUITE_END() From 73d18b766a03517b6ee94f7f1a483a15e0d3c9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Thu, 22 Jan 2026 11:38:37 +0100 Subject: [PATCH 2/3] net_processing: optionally delay private broadcast start --- src/net_processing.cpp | 22 ++++++++++++++++++++-- src/net_processing.h | 2 ++ src/test/peerman_tests.cpp | 23 +++++++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index f4c4ae284a1a..61b53bcfce73 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1089,6 +1089,8 @@ class PeerManagerImpl final : public PeerManager void LogBlockHeader(const CBlockIndex& index, const CNode& peer, bool via_compact_block); + CScheduler* m_scheduler{nullptr}; + /// The transactions to be broadcast privately. PrivateBroadcast m_tx_for_private_broadcast; }; @@ -1996,6 +1998,8 @@ PeerManagerImpl::PeerManagerImpl(CConnman& connman, AddrMan& addrman, void PeerManagerImpl::StartScheduledTasks(CScheduler& scheduler) { + m_scheduler = &scheduler; + // Stale tip checking and peer eviction are on two different timers, but we // don't want them to get out of sync due to drift in the scheduler, so we // combine them in one function and schedule at the quicker (peer-eviction) @@ -2239,8 +2243,22 @@ 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); + if (m_opts.private_broadcast_delay_max > 0ms && m_scheduler != nullptr) { + const auto delay{FastRandomContext{m_opts.deterministic_rng}.randrange(m_opts.private_broadcast_delay_max + 1ms)}; + LogDebug(BCLog::PRIVBROADCAST, + "Scheduling %d new connections in %dms due to %s", + NUM_PRIVATE_BROADCAST_PER_TX, count_milliseconds(delay), txstr); + m_scheduler->scheduleFromNow( + [this] { + if (m_tx_for_private_broadcast.HavePendingTransactions()) { + m_connman.m_private_broadcast.NumToOpenAdd(NUM_PRIVATE_BROADCAST_PER_TX); + } + }, + delay); + } else { + 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); } diff --git a/src/net_processing.h b/src/net_processing.h index 4b221c5d9795..607b4a339c23 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -92,6 +92,8 @@ class PeerManager : public CValidationInterface, public NetEventsInterface uint32_t max_headers_result{MAX_HEADERS_RESULTS}; //! Whether private broadcast is used for sending transactions. bool private_broadcast{DEFAULT_PRIVATE_BROADCAST}; + //! Maximum random delay before starting private broadcast for transactions submitted via sendrawtransaction. + std::chrono::milliseconds private_broadcast_delay_max{0}; }; static std::unique_ptr make(CConnman& connman, AddrMan& addrman, diff --git a/src/test/peerman_tests.cpp b/src/test/peerman_tests.cpp index 6aba4c871be1..b71e293c9074 100644 --- a/src/test/peerman_tests.cpp +++ b/src/test/peerman_tests.cpp @@ -7,11 +7,14 @@ #include #include #include +#include #include #include #include +#include + BOOST_FIXTURE_TEST_SUITE(peerman_tests, RegTestingSetup) /** Window, in blocks, for connecting to NODE_NETWORK_LIMITED peers */ @@ -74,14 +77,30 @@ BOOST_AUTO_TEST_CASE(connections_desirable_service_flags) BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK | NODE_WITNESS)); } -BOOST_AUTO_TEST_CASE(private_broadcast_requests_connections_immediately) +BOOST_AUTO_TEST_CASE(private_broadcast_delays_connection_requests) { - std::unique_ptr peerman = PeerManager::make(*m_node.connman, *m_node.addrman, nullptr, *m_node.chainman, *m_node.mempool, *m_node.warnings, {}); + PeerManager::Options opts; + opts.deterministic_rng = true; + opts.private_broadcast = false; + opts.private_broadcast_delay_max = std::chrono::milliseconds{5000}; + + std::unique_ptr peerman = PeerManager::make(*m_node.connman, *m_node.addrman, nullptr, *m_node.chainman, *m_node.mempool, *m_node.warnings, opts); + + CScheduler scheduler; + peerman->StartScheduledTasks(scheduler); const CTransactionRef tx{MakeTransactionRef(CMutableTransaction{})}; BOOST_CHECK_EQUAL(m_node.connman->m_private_broadcast.NumToOpen(), 0U); peerman->InitiateTxBroadcastPrivate(tx); + BOOST_CHECK_EQUAL(m_node.connman->m_private_broadcast.NumToOpen(), 0U); + + std::thread scheduler_thread([&] { scheduler.serviceQueue(); }); + scheduler.MockForward(std::chrono::duration_cast(opts.private_broadcast_delay_max)); + // Ensure the scheduler has time to process all tasks queued before now. + scheduler.scheduleFromNow([&scheduler] { scheduler.stop(); }, std::chrono::milliseconds{1}); + scheduler_thread.join(); + BOOST_CHECK_EQUAL(m_node.connman->m_private_broadcast.NumToOpen(), 3U); } From 9707aae0da61cc7a8c992066b7d92edd998378de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Thu, 22 Jan 2026 11:39:33 +0100 Subject: [PATCH 3/3] init: add -privatebroadcastdelay and default 5s --- src/init.cpp | 5 +++++ src/net.h | 2 ++ src/net_processing.h | 2 +- src/node/peerman_args.cpp | 5 ++++- test/functional/p2p_private_broadcast.py | 1 + 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 571d8b9c02d1..918dc6c61279 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -679,6 +679,11 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) DEFAULT_PRIVATE_BROADCAST), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-privatebroadcastdelay=", + strprintf("Maximum random delay in milliseconds before starting private broadcast for transactions submitted via sendrawtransaction (default: %u). Set to 0 to disable.", + Ticks(DEFAULT_PRIVATE_BROADCAST_DELAY_MAX)), + ArgsManager::ALLOW_ANY, + OptionsCategory::NODE_RELAY); argsman.AddArg("-whitelistforcerelay", strprintf("Add 'forcerelay' permission to whitelisted peers with default permissions. This will relay transactions even if the transactions were already in the mempool. (default: %d)", DEFAULT_WHITELISTFORCERELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-whitelistrelay", strprintf("Add 'relay' permission to whitelisted peers with default permissions. This will accept relayed transactions even when not relaying transactions (default: %d)", DEFAULT_WHITELISTRELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); diff --git a/src/net.h b/src/net.h index 1e4e9124e9f4..feefefda5987 100644 --- a/src/net.h +++ b/src/net.h @@ -87,6 +87,8 @@ static const bool DEFAULT_BLOCKSONLY = false; static const int64_t DEFAULT_PEER_CONNECT_TIMEOUT = 60; /** Default for -privatebroadcast. */ static constexpr bool DEFAULT_PRIVATE_BROADCAST{false}; +/** Default for -privatebroadcastdelay. */ +static constexpr auto DEFAULT_PRIVATE_BROADCAST_DELAY_MAX{5s}; /** Number of file descriptors required for message capture **/ static const int NUM_FDS_MESSAGE_CAPTURE = 1; /** Interval for ASMap Health Check **/ diff --git a/src/net_processing.h b/src/net_processing.h index 607b4a339c23..3e26c17134d7 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -93,7 +93,7 @@ class PeerManager : public CValidationInterface, public NetEventsInterface //! Whether private broadcast is used for sending transactions. bool private_broadcast{DEFAULT_PRIVATE_BROADCAST}; //! Maximum random delay before starting private broadcast for transactions submitted via sendrawtransaction. - std::chrono::milliseconds private_broadcast_delay_max{0}; + std::chrono::milliseconds private_broadcast_delay_max{DEFAULT_PRIVATE_BROADCAST_DELAY_MAX}; }; static std::unique_ptr make(CConnman& connman, AddrMan& addrman, diff --git a/src/node/peerman_args.cpp b/src/node/peerman_args.cpp index 9745d69d5ae3..606ef0c6f2b9 100644 --- a/src/node/peerman_args.cpp +++ b/src/node/peerman_args.cpp @@ -25,7 +25,10 @@ void ApplyArgsManOptions(const ArgsManager& argsman, PeerManager::Options& optio if (auto value{argsman.GetBoolArg("-blocksonly")}) options.ignore_incoming_txs = *value; if (auto value{argsman.GetBoolArg("-privatebroadcast")}) options.private_broadcast = *value; + + if (auto value{argsman.GetIntArg("-privatebroadcastdelay")}) { + options.private_broadcast_delay_max = std::chrono::milliseconds{std::max(*value, 0)}; + } } } // namespace node - diff --git a/test/functional/p2p_private_broadcast.py b/test/functional/p2p_private_broadcast.py index 803444ccbabb..568cb7bb54b7 100755 --- a/test/functional/p2p_private_broadcast.py +++ b/test/functional/p2p_private_broadcast.py @@ -246,6 +246,7 @@ def on_listen_done(addr, port): "-v2transport=0", "-test=addrman", "-privatebroadcast", + "-privatebroadcastdelay=0", f"-proxy={socks5_server_config.addr[0]}:{socks5_server_config.addr[1]}", # To increase coverage, make it think that the I2P network is reachable so that it # selects such addresses as well. Pick a proxy address where nobody is listening