diff --git a/src/init.cpp b/src/init.cpp index 158c4905638..9e223704b03 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=", 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 eedfdfaf483..1f8f1771403 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 d2e001db278..cbd68971d6a 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()