init: introduce a new option to enable/disable private broadcast
What changed, and why it matters
This commit adds a new optional Bitcoin Core setting called -privatebroadcast. When enabled, transactions submitted through the sendrawtransaction RPC are broadcast over short-lived Tor or I2P connections instead of being placed in the node's public mempool first. The change is purely additive: it introduces the option, sets its default to off, and adds startup checks that warn or error if the feature is configured incorrectly. There is no bug fix or vulnerability patch here.
No security action required. This is a feature addition. Operators interested in privacy may review the new -privatebroadcast option and its compatibility requirements.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces a new node relay argument -privatebroadcast (default false) in init.cpp, defines DEFAULT_PRIVATE_BROADCAST in net.h, and adds functional tests in feature_config_args.py. It validates that private broadcast is only usable when Tor or I2P is reachable, that it is incompatible with -connect, and that disabling -proxyrandomize produces a privacy warning. The commit also refactors the -listenonion boolean lookup into a single variable. No existing behavior is changed unless the user explicitly opts in.
Changed components
src/init.cppsrc/net.htest/functional/feature_config_args.pyInspect captured patch +64 / −3
diff --git a/src/init.cpp b/src/init.cpp
index 158c4905..9e223704 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -670,6 +670,15 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
OptionsCategory::NODE_RELAY);
argsman.AddArg("-minrelaytxfee=<amt>", strprintf("Fees (in %s/kvB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)",
CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
+ argsman.AddArg("-privatebroadcast",
+ strprintf(
+ "Broadcast transactions submitted via sendrawtransaction RPC using short-lived "
+ "connections through the Tor or I2P networks, without putting them in the mempool first. "
+ "Transactions submitted through the wallet are not affected by this option "
+ "(default: %u)",
+ DEFAULT_PRIVATE_BROADCAST),
+ 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);
@@ -1732,13 +1741,13 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
}
}
+ const bool listenonion{args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)};
if (onion_proxy.IsValid()) {
SetProxy(NET_ONION, onion_proxy);
} else {
// If -listenonion is set, then we will (try to) connect to the Tor control port
// later from the torcontrol thread and may retrieve the onion proxy from there.
- const bool listenonion_disabled{!args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)};
- if (onlynet_used_with_onion && listenonion_disabled) {
+ if (onlynet_used_with_onion && !listenonion) {
return InitError(
_("Outbound connections restricted to Tor (-onlynet=onion) but the proxy for "
"reaching the Tor network is not provided: none of -proxy, -onion or "
@@ -2119,7 +2128,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
connOptions.onion_binds.push_back(onion_service_target);
}
- if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) {
+ if (listenonion) {
if (connOptions.onion_binds.size() > 1) {
InitWarning(strprintf(_("More than one onion bind address is provided. Using %s "
"for the automatically created Tor onion service."),
@@ -2192,6 +2201,32 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
conflict->ToStringAddrPort()));
}
+ if (args.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)) {
+ // If -listenonion is set, then NET_ONION may not be reachable now
+ // but may become reachable later, thus only error here if it is not
+ // reachable and will not become reachable for sure.
+ const bool onion_may_become_reachable{listenonion && (!args.IsArgSet("-onlynet") || onlynet_used_with_onion)};
+ if (!g_reachable_nets.Contains(NET_I2P) &&
+ !g_reachable_nets.Contains(NET_ONION) &&
+ !onion_may_become_reachable) {
+ return InitError(_("Private broadcast of own transactions requested (-privatebroadcast), "
+ "but none of Tor or I2P networks is reachable"));
+ }
+ if (!connOptions.m_use_addrman_outgoing) {
+ return InitError(_("Private broadcast of own transactions requested (-privatebroadcast), "
+ "but -connect is also configured. They are incompatible because the "
+ "private broadcast needs to open new connections to randomly "
+ "chosen Tor or I2P peers. Consider using -maxconnections=0 -addnode=... "
+ "instead"));
+ }
+ if (!proxyRandomize && (g_reachable_nets.Contains(NET_ONION) || onion_may_become_reachable)) {
+ InitWarning(_("Private broadcast of own transactions requested (-privatebroadcast) and "
+ "-proxyrandomize is disabled. Tor circuits for private broadcast connections "
+ "may be correlated to other connections over Tor. For maximum privacy set "
+ "-proxyrandomize=1."));
+ }
+ }
+
if (!node.connman->Start(scheduler, connOptions)) {
return false;
}
diff --git a/src/net.h b/src/net.h
index eedfdfaf..1f8f1771 100644
--- a/src/net.h
+++ b/src/net.h
@@ -83,6 +83,8 @@ static const std::string DEFAULT_MAX_UPLOAD_TARGET{"0M"};
static const bool DEFAULT_BLOCKSONLY = false;
/** -peertimeout default */
static const int64_t DEFAULT_PEER_CONNECT_TIMEOUT = 60;
+/** Default for -privatebroadcast. */
+static constexpr bool DEFAULT_PRIVATE_BROADCAST{false};
/** Number of file descriptors required for message capture **/
static const int NUM_FDS_MESSAGE_CAPTURE = 1;
/** Interval for ASMap Health Check **/
diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py
index d2e001db..cbd68971 100755
--- a/test/functional/feature_config_args.py
+++ b/test/functional/feature_config_args.py
@@ -411,6 +411,29 @@ class ConfArgsTest(BitcoinTestFramework):
self.restart_node(0, extra_args=[connect_arg, '-dnsseed', '-proxy=localhost:1080'])
self.stop_node(0)
+ def test_privatebroadcast(self):
+ self.log.info("Test that an invalid usage of -privatebroadcast throws an init error")
+ self.stop_node(0)
+ # -privatebroadcast init error: Tor/I2P not reachable at startup
+ self.nodes[0].assert_start_raises_init_error(
+ extra_args=["-privatebroadcast"],
+ expected_msg=(
+ "Error: Private broadcast of own transactions requested (-privatebroadcast), "
+ "but none of Tor or I2P networks is reachable"))
+ # -privatebroadcast init error: incompatible with -connect
+ self.nodes[0].assert_start_raises_init_error(
+ extra_args=["-privatebroadcast", "-connect=127.0.0.1:8333", "-onion=127.0.0.1:9050"],
+ expected_msg=(
+ "Error: Private broadcast of own transactions requested (-privatebroadcast), but -connect is also configured. "
+ "They are incompatible because the private broadcast needs to open new connections to randomly "
+ "chosen Tor or I2P peers. Consider using -maxconnections=0 -addnode=... instead"))
+ # Warning case: private broadcast allowed, but -proxyrandomize=0 triggers a privacy warning
+ self.start_node(0, extra_args=["-privatebroadcast", "-onion=127.0.0.1:9050", "-proxyrandomize=0"])
+ self.stop_node(0, expected_stderr=(
+ "Warning: Private broadcast of own transactions requested (-privatebroadcast) and "
+ "-proxyrandomize is disabled. Tor circuits for private broadcast connections may "
+ "be correlated to other connections over Tor. For maximum privacy set -proxyrandomize=1."))
+
def test_ignored_conf(self):
self.log.info('Test error is triggered when the datadir in use contains a bitcoin.conf file that would be ignored '
'because a conflicting -conf file argument is passed.')
@@ -496,6 +519,7 @@ class ConfArgsTest(BitcoinTestFramework):
self.test_seed_peers()
self.test_networkactive()
self.test_connect_with_seednode()
+ self.test_privatebroadcast()
self.test_dir_config()
self.test_negated_config()
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.