Skip to content

Broadcast own transactions only via short-lived Tor or I2P connections

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 (if we take the last commit: fixup!)
tx-sender <--- GETDATA/TX ----< tx-recipient (if we take the last commit: fixup!)
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 5 (NUM_PRIVATE_BROADCAST_PER_TX) recipients right away. If after 10-15 mins 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.

High-level explanation of the commits
  • 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: move peers counting before grant acquisition in ThreadOpenConnections()
    • net: implement opening PRIVATE_BROADCAST connections
  • Prepare BroadcastTransaction() for private broadcast requests:

    • net_processing: rename RelayTransaction to better describe what it does
    • node: change a tx-relay on/off flag to a tri-state
    • 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: 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
  • Independent test framework improvements (also opened as a standalone PR at https://github.com/bitcoin/bitcoin/pull/29420):

    • test: improve debug log message from P2PConnection::connection_made()
    • test: extend the SOCKS5 Python proxy to actually connect to a destination
    • test: support WTX INVs from P2PDataStore and fix a comment
    • test: set P2PConnection::p2p_connected_to_node in peer_connect_helper()
    • test: move create_malleated_version() to messages.py for reuse
  • New functional test that exercies some of the new code:

    • test: add functional test for local tx relay

This addresses: https://github.com/bitcoin/bitcoin/issues/3828 Clients leak IPs if they are recipients of a transaction https://github.com/bitcoin/bitcoin/issues/14692 Can't configure bitocoind to only send tx via Tor but receive clearnet transactions https://github.com/bitcoin/bitcoin/issues/19042 Tor-only transaction broadcast onlynet=onion alternative https://github.com/bitcoin/bitcoin/issues/24557 Option for receive events with all networks, but send transactions and/or blocks only with anonymous network[s]? https://github.com/bitcoin/bitcoin/issues/25450 Ability to broadcast wallet transactions only via dedicated oneshot Tor connections

Related, but different: https://github.com/bitcoin/bitcoin/issues/21876 Broadcast a transaction to specific nodes https://github.com/bitcoin/bitcoin/issues/28636 new RPC: sendrawtransactiontopeer


Further extensions of this planned for subsequent PRs:

  • Have the wallet do the private broadcast as well, https://github.com/bitcoin/bitcoin/issues/11887 would have to be resolved.
  • 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.

A previous incarnation of this can be found at https://github.com/bitcoin/bitcoin/pull/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.

Merge request reports

Loading