Skip to content

Commit 796f18e

Browse files
committed
Merge bitcoin#29415: Broadcast own transactions only via short-lived Tor or I2P connections
8937221 doc: add release notes for 29415 (Vasil Dimov) 582016f test: add unit test for the private broadcast storage (Vasil Dimov) e74d54e test: add functional test for private broadcast (Vasil Dimov) 818b780 rpc: use private broadcast from sendrawtransaction RPC if -privatebroadcast is ON (Vasil Dimov) eab595f net_processing: retry private broadcast (Vasil Dimov) 37b79f9 net_processing: stop private broadcast of a transaction after round-trip (Vasil Dimov) 2de53ee net_processing: handle ConnectionType::PRIVATE_BROADCAST connections (Vasil Dimov) 30a9853 net_processing: move a debug check in VERACK processing earlier (Vasil Dimov) d1092e5 net_processing: modernize PushNodeVersion() (Vasil Dimov) 9937a12 net_processing: move the debug log about receiving VERSION earlier (Vasil Dimov) a098f37 net_processing: reorder the code that handles the VERSION message (Vasil Dimov) 679ce3a net_processing: store transactions for private broadcast in PeerManager (Vasil Dimov) a3faa6f node: extend node::TxBroadcast with a 3rd option (Vasil Dimov) 95c051e net_processing: rename RelayTransaction() to better describe what it does (Vasil Dimov) bb49d26 net: implement opening PRIVATE_BROADCAST connections (Vasil Dimov) 01dad4e net: introduce a new connection type for private broadcast (Vasil Dimov) 94aaa5d init: introduce a new option to enable/disable private broadcast (Vasil Dimov) d6ee490 log: introduce a new category for private broadcast (Vasil Dimov) Pull request description: _Parts of this PR are isolated in independent smaller PRs to ease review:_ * [x] _https://github.com/bitcoin/bitcoin/pull/29420_ * [x] _https://github.com/bitcoin/bitcoin/pull/33454_ * [x] _https://github.com/bitcoin/bitcoin/pull/33567_ * [x] _https://github.com/bitcoin/bitcoin/pull/33793_ --- To improve privacy, broadcast locally submitted transactions (from the `sendrawtransaction` RPC) to the P2P network only via Tor or I2P short-lived connections, or to IPv4/IPv6 peers but through the Tor network. * Introduce a new connection type for private broadcast of transactions with the following properties: * started whenever there are local transactions to be sent * opened to Tor or I2P peers or IPv4/IPv6 via the Tor proxy * opened regardless of max connections limits * after handshake is completed one local transaction is pushed to the peer, `PING` is sent and after receiving `PONG` the connection is closed * ignore all incoming messages after handshake is completed (except `PONG`) * Broadcast transactions submitted via `sendrawtransaction` using this new mechanism, to a few peers. Keep doing this until we receive back this transaction from one of our ordinary peers (this takes about 1 second on mainnet). * The transaction is stored in peerman and does not enter the mempool. * Once we get an `INV` from one of our ordinary peers, then the normal flow executes: we request the transaction with `GETDATA`, receive it with a `TX` message, put it in our mempool and broadcast it to all our existent connections (as if we see it for the first time). * After we receive the full transaction as a `TX` message, in reply to our `GETDATA` request, only then consider the transaction has propagated through the network and remove it from the storage in peerman, ending the private broadcast attempts. The messages exchange should look like this: ``` tx-sender >--- connect -------> tx-recipient tx-sender >--- VERSION -------> tx-recipient (dummy VERSION with no revealing data) tx-sender <--- VERSION -------< tx-recipient tx-sender <--- WTXIDRELAY ----< tx-recipient (maybe) tx-sender <--- SENDADDRV2 ----< tx-recipient (maybe) tx-sender <--- SENDTXRCNCL ---< tx-recipient (maybe) tx-sender <--- VERACK --------< tx-recipient tx-sender >--- VERACK --------> tx-recipient tx-sender >--- INV/TX --------> tx-recipient tx-sender <--- GETDATA/TX ----< tx-recipient tx-sender >--- TX ------------> tx-recipient tx-sender >--- PING ----------> tx-recipient tx-sender <--- PONG ----------< tx-recipient tx-sender disconnects ``` Whenever a new transaction is received from `sendrawtransaction` RPC, the node will send it to a few (`NUM_PRIVATE_BROADCAST_PER_TX`) recipients right away. If after some time we still have not heard anything about the transaction from the network, then it will be sent to 1 more peer (see `PeerManagerImpl::ReattemptPrivateBroadcast()`). A few considerations: * The short-lived private broadcast connections are very cheap and fast wrt network traffic. It is expected that some of those peers could blackhole the transaction. Just one honest/proper peer is enough for successful propagation. * The peers that receive the transaction could deduce that this is initial transaction broadcast from the transaction originator. This is ok, they can't identify the sender. --- <details> <summary>How to test this?</summary> Thank you, @stratospher and @andrewtoth! Start `bitcoind` with `-privatebroadcast=1 -debug=privatebroadcast`. Create a wallet and get a new address, go to the Signet faucet and request some coins to that address: ```bash build/bin/bitcoin-cli -chain="signet" createwallet test build/bin/bitcoin-cli -chain="signet" getnewaddress ``` Get a new address for the test transaction recipient: ```bash build/bin/bitcoin-cli -chain="signet" loadwallet test new_address=$(build/bin/bitcoin-cli -chain="signet" getnewaddress) ``` Create the transaction: ```bash # Option 1: `createrawtransaction` and `signrawtransactionwithwallet`: txid=$(build/bin/bitcoin-cli -chain="signet" listunspent | jq -r '.[0] | .txid') vout=$(build/bin/bitcoin-cli -chain="signet" listunspent | jq -r '.[0] | .vout') echo "txid: $txid" echo "vout: $vout" tx=$(build/bin/bitcoin-cli -chain="signet" createrawtransaction "[{\"txid\": \"$txid\", \"vout\": $vout}]" "[{\"$new_address\": 0.00001000}]" 0 false) echo "tx: $tx" signed_tx=$(build/bin/bitcoin-cli -chain="signet" signrawtransactionwithwallet "$tx" | jq -r '.hex') echo "signed_tx: $signed_tx" # OR Option 2: `walletcreatefundedpsbt` and `walletprocesspsbt`: # This makes it not have to worry about inputs and also automatically sends back change to the wallet. # Start `bitcoind` with `-fallbackfee=0.00003000` for instance for 3 sat/vbyte fee. psbt=$(build/bin/bitcoin-cli -chain="signet" walletcreatefundedpsbt "[]" "[{\"$new_address\": 0.00001000}]" | jq -r '.psbt') echo "psbt: $psbt" signed_tx=$(build/bin/bitcoin-cli -chain="signet" walletprocesspsbt "$psbt" | jq -r '.hex') echo "signed_tx: $signed_tx" ``` Finally, send the transaction: ```bash raw_tx=$(build/bin/bitcoin-cli -chain="signet" sendrawtransaction "$signed_tx") echo "raw_tx: $raw_tx" ``` </details> --- <details> <summary>High-level explanation of the commits</summary> * New logging category and config option to enable private broadcast * `log: introduce a new category for private broadcast` * `init: introduce a new option to enable/disable private broadcast` * Implement the private broadcast connection handling on the `CConnman` side: * `net: introduce a new connection type for private broadcast` * `net: implement opening PRIVATE_BROADCAST connections` * Prepare `BroadcastTransaction()` for private broadcast requests: * `net_processing: rename RelayTransaction to better describe what it does` * `node: extend node::TxBroadcast with a 3rd option` * `net_processing: store transactions for private broadcast in PeerManager` * Implement the private broadcast connection handling on the `PeerManager` side: * `net_processing: reorder the code that handles the VERSION message` * `net_processing: move the debug log about receiving VERSION earlier` * `net_processing: modernize PushNodeVersion()` * `net_processing: move a debug check in VERACK processing earlier` * `net_processing: handle ConnectionType::PRIVATE_BROADCAST connections` * `net_processing: stop private broadcast of a transaction after round-trip` * `net_processing: retry private broadcast` * Engage the new functionality from `sendrawtransaction`: * `rpc: use private broadcast from sendrawtransaction RPC if -privatebroadcast is ON` * New tests: * `test: add functional test for private broadcast` * `test: add unit test for the private broadcast storage` </details> --- **This PR would resolve the following issues:** bitcoin#3828 Clients leak IPs if they are recipients of a transaction bitcoin#14692 Can't configure bitocoind to only send tx via Tor but receive clearnet transactions bitcoin#19042 Tor-only transaction broadcast onlynet=onion alternative bitcoin#24557 Option for receive events with all networks, but send transactions and/or blocks only with anonymous network[s]? bitcoin#25450 Ability to broadcast wallet transactions only via dedicated oneshot Tor connections bitcoin#32235 Tor: TX circuit isolation **Issues that are related, but (maybe?) not to be resolved by this PR:** bitcoin#21876 Broadcast a transaction to specific nodes bitcoin#28636 new RPC: sendrawtransactiontopeer --- Further extensions: * Have the wallet do the private broadcast as well, bitcoin#11887 would have to be resolved. * Have the `submitpackage` RPC do the private broadcast as well, [draft diff in the comment below](bitcoin#29415 (review)), thanks ismaelsadeeq! * Add some stats via RPC, so that the user can better monitor what is going on during and after the broadcast. Currently this can be done via the debug log, but that is not convenient. * Make the private broadcast storage, currently in peerman, persistent over node restarts. * Add (optional) random delay before starting to broadcast the transaction in order to avoid correlating unrelated transactions based on the time when they were broadcast. Suggested independently of this PR [here](bitcoin#30471). * Consider periodically sending transactions that did not originate from the node as decoy, discussed [here](bitcoin#29415 (comment)). * Consider waiting for peer's FEEFILTER message and if the transaction that was sent to the peer is below that threshold, then assume the peer is going to drop it. Then use this knowledge to retry more aggressively with another peer, instead of the current 10 min. See [comment below](bitcoin#29415 (comment)). * It may make sense to be able to override the default policy -- eg so submitrawtransaction can go straight to the mempool and relay, even if txs are normally privately relayed. See [comment below](bitcoin#29415 (comment)). * As a side effect we have a new metric available - the time it takes for a transaction to reach a random node in the network (from the point of view of the private broadcast recipient the tx originator is a random node somewhere in the network). This can be useful for monitoring, unrelated to privacy characteristics of this feature. --- _A previous incarnation of this can be found at bitcoin#27509. It puts the transaction in the mempool and (tries to) hide it from the outside observers. This turned out to be too error prone or maybe even impossible._ ACKs for top commit: l0rinc: code review diff ACK 8937221 andrewtoth: ACK 8937221 pinheadmz: ACK 8937221 w0xlt: ACK bitcoin@8937221 with nit bitcoin#29415 (comment) mzumsande: re-ACK 8937221 Tree-SHA512: d51dadc865c2eb080c903cbe2f669e69a967e5f9fc64e9a20a68f39a67bf0db6ac2ad682af7fa24ef9f0942a41c89959341a16ba7b616475e1c5ab8e563b9b96
2 parents abc6a3a + 8937221 commit 796f18e

28 files changed

+1534
-81
lines changed

doc/release-notes-29415.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
P2P and network changes
2+
-----------------------
3+
4+
- Normally local transactions are broadcast to all connected peers with
5+
which we do transaction relay. Now, for the `sendrawtransaction` RPC
6+
this behavior can be changed to only do the broadcast via the Tor or
7+
I2P networks. A new boolean option `-privatebroadcast` has been added
8+
to enable this behavior. This improves the privacy of the transaction
9+
originator in two aspects:
10+
1. Their IP address (and thus geolocation) is never known to the
11+
recipients.
12+
2. If the originator sends two otherwise unrelated transactions, they
13+
will not be linkable. This is because a separate connection is used
14+
for broadcasting each transaction. (#29415)

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
244244
policy/rbf.cpp
245245
policy/settings.cpp
246246
policy/truc_policy.cpp
247+
private_broadcast.cpp
247248
rest.cpp
248249
rpc/blockchain.cpp
249250
rpc/external_signer.cpp

src/bitcoin-cli.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
452452
if (conn_type == "block-relay-only") return "block";
453453
if (conn_type == "manual" || conn_type == "feeler") return conn_type;
454454
if (conn_type == "addr-fetch") return "addr";
455+
if (conn_type == "private-broadcast") return "priv";
455456
return "";
456457
}
457458
std::string FormatServices(const UniValue& services)
@@ -703,6 +704,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
703704
" \"manual\" - peer we manually added using RPC addnode or the -addnode/-connect config options\n"
704705
" \"feeler\" - short-lived connection for testing addresses\n"
705706
" \"addr\" - address fetch; short-lived connection for requesting addresses\n"
707+
" \"priv\" - private broadcast; short-lived connection for broadcasting our transactions\n"
706708
" net Network the peer connected through (\"ipv4\", \"ipv6\", \"onion\", \"i2p\", \"cjdns\", or \"npr\" (not publicly routable))\n"
707709
" serv Services offered by the peer\n"
708710
" \"n\" - NETWORK: peer can serve the full block chain\n"

src/init.cpp

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
542542
argsman.AddArg("-forcednsseed", strprintf("Always query for peer addresses via DNS lookup (default: %u)", DEFAULT_FORCEDNSSEED), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
543543
argsman.AddArg("-listen", strprintf("Accept connections from outside (default: %u if no -proxy, -connect or -maxconnections=0)", DEFAULT_LISTEN), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
544544
argsman.AddArg("-listenonion", strprintf("Automatically create Tor onion service (default: %d)", DEFAULT_LISTEN_ONION), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
545-
argsman.AddArg("-maxconnections=<n>", strprintf("Maintain at most <n> automatic connections to peers (default: %u). This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u.", DEFAULT_MAX_PEER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
545+
argsman.AddArg("-maxconnections=<n>", strprintf("Maintain at most <n> automatic connections to peers (default: %u). This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u. It does not apply to short-lived private broadcast connections either, which have a separate limit of %u.", DEFAULT_MAX_PEER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS, MAX_PRIVATE_BROADCAST_CONNECTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
546546
argsman.AddArg("-maxreceivebuffer=<n>", strprintf("Maximum per-connection receive buffer, <n>*1000 bytes (default: %u)", DEFAULT_MAXRECEIVEBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
547547
argsman.AddArg("-maxsendbuffer=<n>", strprintf("Maximum per-connection memory usage for the send buffer, <n>*1000 bytes (default: %u)", DEFAULT_MAXSENDBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
548548
argsman.AddArg("-maxuploadtarget=<n>", strprintf("Tries to keep outbound traffic under the given target per 24h. Limit does not apply to peers with 'download' permission or blocks created within past week. 0 = no limit (default: %s). Optional suffix units [k|K|m|M|g|G|t|T] (default: M). Lowercase is 1000 base while uppercase is 1024 base", DEFAULT_MAX_UPLOAD_TARGET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
@@ -670,6 +670,15 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
670670
OptionsCategory::NODE_RELAY);
671671
argsman.AddArg("-minrelaytxfee=<amt>", strprintf("Fees (in %s/kvB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)",
672672
CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
673+
argsman.AddArg("-privatebroadcast",
674+
strprintf(
675+
"Broadcast transactions submitted via sendrawtransaction RPC using short-lived "
676+
"connections through the Tor or I2P networks, without putting them in the mempool first. "
677+
"Transactions submitted through the wallet are not affected by this option "
678+
"(default: %u)",
679+
DEFAULT_PRIVATE_BROADCAST),
680+
ArgsManager::ALLOW_ANY,
681+
OptionsCategory::NODE_RELAY);
673682
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);
674683
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);
675684

@@ -1009,11 +1018,14 @@ bool AppInitParameterInteraction(const ArgsManager& args)
10091018
if (user_max_connection < 0) {
10101019
return InitError(Untranslated("-maxconnections must be greater or equal than zero"));
10111020
}
1021+
const size_t max_private{args.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)
1022+
? MAX_PRIVATE_BROADCAST_CONNECTIONS
1023+
: 0};
10121024
// Reserve enough FDs to account for the bare minimum, plus any manual connections, plus the bound interfaces
10131025
int min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + nBind;
10141026

10151027
// Try raising the FD limit to what we need (available_fds may be smaller than the requested amount if this fails)
1016-
available_fds = RaiseFileDescriptorLimit(user_max_connection + min_required_fds);
1028+
available_fds = RaiseFileDescriptorLimit(user_max_connection + max_private + min_required_fds);
10171029
// If we are using select instead of poll, our actual limit may be even smaller
10181030
#ifndef USE_POLL
10191031
available_fds = std::min(FD_SETSIZE, available_fds);
@@ -1732,13 +1744,13 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
17321744
}
17331745
}
17341746

1747+
const bool listenonion{args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)};
17351748
if (onion_proxy.IsValid()) {
17361749
SetProxy(NET_ONION, onion_proxy);
17371750
} else {
17381751
// If -listenonion is set, then we will (try to) connect to the Tor control port
17391752
// later from the torcontrol thread and may retrieve the onion proxy from there.
1740-
const bool listenonion_disabled{!args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)};
1741-
if (onlynet_used_with_onion && listenonion_disabled) {
1753+
if (onlynet_used_with_onion && !listenonion) {
17421754
return InitError(
17431755
_("Outbound connections restricted to Tor (-onlynet=onion) but the proxy for "
17441756
"reaching the Tor network is not provided: none of -proxy, -onion or "
@@ -2119,7 +2131,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
21192131
connOptions.onion_binds.push_back(onion_service_target);
21202132
}
21212133

2122-
if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) {
2134+
if (listenonion) {
21232135
if (connOptions.onion_binds.size() > 1) {
21242136
InitWarning(strprintf(_("More than one onion bind address is provided. Using %s "
21252137
"for the automatically created Tor onion service."),
@@ -2192,6 +2204,32 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
21922204
conflict->ToStringAddrPort()));
21932205
}
21942206

2207+
if (args.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)) {
2208+
// If -listenonion is set, then NET_ONION may not be reachable now
2209+
// but may become reachable later, thus only error here if it is not
2210+
// reachable and will not become reachable for sure.
2211+
const bool onion_may_become_reachable{listenonion && (!args.IsArgSet("-onlynet") || onlynet_used_with_onion)};
2212+
if (!g_reachable_nets.Contains(NET_I2P) &&
2213+
!g_reachable_nets.Contains(NET_ONION) &&
2214+
!onion_may_become_reachable) {
2215+
return InitError(_("Private broadcast of own transactions requested (-privatebroadcast), "
2216+
"but none of Tor or I2P networks is reachable"));
2217+
}
2218+
if (!connOptions.m_use_addrman_outgoing) {
2219+
return InitError(_("Private broadcast of own transactions requested (-privatebroadcast), "
2220+
"but -connect is also configured. They are incompatible because the "
2221+
"private broadcast needs to open new connections to randomly "
2222+
"chosen Tor or I2P peers. Consider using -maxconnections=0 -addnode=... "
2223+
"instead"));
2224+
}
2225+
if (!proxyRandomize && (g_reachable_nets.Contains(NET_ONION) || onion_may_become_reachable)) {
2226+
InitWarning(_("Private broadcast of own transactions requested (-privatebroadcast) and "
2227+
"-proxyrandomize is disabled. Tor circuits for private broadcast connections "
2228+
"may be correlated to other connections over Tor. For maximum privacy set "
2229+
"-proxyrandomize=1."));
2230+
}
2231+
}
2232+
21952233
if (!node.connman->Start(scheduler, connOptions)) {
21962234
return false;
21972235
}

src/logging.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ static const std::map<std::string, BCLog::LogFlags, std::less<>> LOG_CATEGORIES_
202202
{"scan", BCLog::SCAN},
203203
{"txpackages", BCLog::TXPACKAGES},
204204
{"kernel", BCLog::KERNEL},
205+
{"privatebroadcast", BCLog::PRIVBROADCAST},
205206
};
206207

207208
static const std::unordered_map<BCLog::LogFlags, std::string> LOG_CATEGORIES_BY_FLAG{

src/logging.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ namespace BCLog {
116116
SCAN = (CategoryMask{1} << 27),
117117
TXPACKAGES = (CategoryMask{1} << 28),
118118
KERNEL = (CategoryMask{1} << 29),
119+
PRIVBROADCAST = (CategoryMask{1} << 30),
119120
ALL = ~NONE,
120121
};
121122
enum class Level {

0 commit comments

Comments
 (0)