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

pull vasild wants to merge 16 commits into bitcoin:master from vasild:private_broadcast changing 32 files +1616 −227
  1. vasild commented at 1:39 pm on February 9, 2024: contributor

    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:

     0tx-sender >--- connect -------> tx-recipient
     1tx-sender >--- VERSION -------> tx-recipient (dummy VERSION with no revealing data)
     2tx-sender <--- VERSION -------< tx-recipient
     3tx-sender <--- WTXIDRELAY ----< tx-recipient (maybe)
     4tx-sender <--- SENDADDRV2 ----< tx-recipient (maybe)
     5tx-sender <--- SENDTXRCNCL ---< tx-recipient (maybe)
     6tx-sender <--- VERACK --------< tx-recipient
     7tx-sender >--- VERACK --------> tx-recipient
     8tx-sender >--- INV/TX --------> tx-recipient (if we take the last commit: fixup!)
     9tx-sender <--- GETDATA/TX ----< tx-recipient (if we take the last commit: fixup!)
    10tx-sender >--- TX ------------> tx-recipient
    11tx-sender >--- PING ----------> tx-recipient
    12tx-sender <--- PONG ----------< tx-recipient
    13tx-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.

    • 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 #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: #3828 Clients leak IPs if they are recipients of a transaction #14692 Can’t configure bitocoind to only send tx via Tor but receive clearnet transactions #19042 Tor-only transaction broadcast onlynet=onion alternative #24557 Option for receive events with all networks, but send transactions and/or blocks only with anonymous network[s]? #25450 Ability to broadcast wallet transactions only via dedicated oneshot Tor connections

    Related, but different: #21876 Broadcast a transaction to specific nodes #28636 new RPC: sendrawtransactiontopeer


    Further extensions of this planned for subsequent PRs:

    • Have the wallet do the private broadcast as well, #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 #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.

  2. DrahtBot commented at 1:39 pm on February 9, 2024: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/29415.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK andrewtoth, zzzi2p, nothingmuch
    Stale ACK pinheadmz

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #31282 (refactor: Make node_id a const& in RemoveBlockRequest by maflcko)
    • #31001 (refactor: ensure type safety for txid and wtxid in RelayTransaction by marcofleon)
    • #30988 (Split CConnman by vasild)
    • #30595 (kernel: Introduce initial C header API by TheCharlatan)
    • #30529 (Fix -norpcwhitelist, -norpcallowip, and similar corner case behavior by ryanofsky)
    • #30381 ([WIP] net: return result from addnode RPC by willcl-ark)
    • #30116 (p2p: Fill reconciliation sets (Erlay) attempt 2 by sr-gi)
    • #29278 (Wallet: Add maxfeerate wallet startup option by ismaelsadeeq)
    • #28584 (Fuzz: extend CConnman tests by vasild)
    • #28521 (net, net_processing: additional and consistent disconnect logging by Sjors)
    • #28488 (p2p: Evict outbound peers with high minFeeRate by naumenkogs)
    • #28463 (p2p: Increase inbound capacity for block-relay only connections by mzumsande)
    • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)
    • #27826 (validation: log which peer sent us a header by Sjors)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. 1440000bytes commented at 2:12 pm on February 9, 2024: none

    I am not sure about this approach to improve privacy. Is it necessary to open new short lived tor/i2p connections for broadcasting the transaction? What are the trade-offs in this implementation vs a simple implementation to relay tx to one or more peers that our node is already connected to?

    Related issues:

    #21876 https://github.com/bitcoin/bitcoin/issues/28636

  4. DrahtBot added the label CI failed on Feb 9, 2024
  5. vasild force-pushed on Feb 10, 2024
  6. vasild commented at 12:16 pm on February 10, 2024: contributor

    @1440000bytes, thanks for asking! There is some discussion at #27509 (the previous attempt on this).

    Is it necessary to open new short lived tor/i2p connections for broadcasting the transaction?

    Yes, it is. See below.

    What are the trade-offs in this implementation vs a simple implementation to relay tx to one or more peers that our node is already connected to?

    Sending the transaction over clearnet reveals the IP address/geolocation of the sender. A spy with many connections to the network could try to guess who was the originator. So, why not send it to our Tor peers only? Because it is relatively easy for a spy to fingerprint and link clearnet and Tor connections to the same peer. That is, a long running connection over Tor could be linked to a long running clearnet connection. This is why the proposed changes open a short-lived connection that does not reveal any of the identity of the sender.

    Would this benefit nodes that don’t have clearnet connections, e.g. Tor/I2P-only nodes? Yes! In the case where the sender sends two otherwise unrelated transactions over the same long-running Tor connection, the recipient will know that they have the same origin, even though they are not related on-chain. Using single shot connections fixes that too.

    Related issues:

    Linked in the OP, thanks!

  7. DrahtBot removed the label CI failed on Feb 10, 2024
  8. in src/logging.cpp:174 in 2b05af2760 outdated
    210-    {BCLog::TXRECONCILIATION, "txreconciliation"},
    211-    {BCLog::SCAN, "scan"},
    212-    {BCLog::TXPACKAGES, "txpackages"},
    213-    {BCLog::ALL, "1"},
    214-    {BCLog::ALL, "all"},
    215+    {"util", BCLog::UTIL},
    


    davidgumberg commented at 9:48 pm on February 10, 2024:
    While touching this, maybe BCLog::UTIL which is not used since #27896 should be dropped?

    vasild commented at 2:28 pm on February 11, 2024:
    Done in #29419, thanks for the suggestion!
  9. epiccurious commented at 9:56 pm on February 10, 2024: contributor

    v2 Transport will be enabled by default in the next release (https://github.com/bitcoin/bitcoin/pull/29347).

    If there were eventually a change to force clearnet transactions over v2 transport (so the details of the communications were encrypted), would that solve the same problem that this PR is aiming to solve?

  10. vasild commented at 2:13 pm on February 11, 2024: contributor
    @epiccurious, p2p encryption “solves” the spying from intermediate routers on clearnet (aka man-in-the-middle). Tor, I2P and CJDNS solve that too. While this PR uses only Tor and I2P it would solve that problem. But there is more - it will as well solve issues with spying bitcoin nodes.
  11. vasild referenced this in commit b0344c219a on Feb 11, 2024
  12. DrahtBot added the label Needs rebase on Feb 27, 2024
  13. vasild force-pushed on Feb 29, 2024
  14. vasild commented at 1:18 pm on February 29, 2024: contributor
    74ba7c7fb5...6fad02cf03: rebase due to conflicts
  15. DrahtBot removed the label Needs rebase on Feb 29, 2024
  16. pinheadmz commented at 4:32 pm on March 8, 2024: member

    concept ACK 6fad02cf03

    (code review in progress)

    I am also testing this feature in Warnet, which deploys a regtest network and even has an internal Tor DA so I can simulate onion routing locally. Currently using a 20-node graph and a scenario which connects the graph, adds onion addresses to the test node, and then sends a raw transaction from the node running this branch.

    The private broadcast succeeds frequently but not always. In Warnet anyway I had better luck when the test node had -listenonion=0, I tried that after suspecting that inbound onion connections were removing potential peers from the private broadcast list, but I’m not sure.

    I think I noticed this in the original PR as well, if multiple transactions are sent, the count keeps going up without a limit:

    0 [privatebroadcast] Requested to open 60 connection(s), trying to open one
    

    Screenshot below, I managed to capture a private broadcast connection! I’ll mention when i get to that commit in review as well, but the connection type "privbcast" is breaking the very nice -netinfo table :-)

    So far I have a few questions about the strategy:

    1. How do we pick the onion peers to relay to? If we avoid reusing peers then (especially in my miniature network) we can run out quickly, and nothing ever gets broadcast.

    2. Are we using fresh Tor identities for these connections? I think Wasabi does something like this:

    Wasabi connects to each peer through a different Tor stream.

    1. Do you think we need any RPC to get the status of private tx sends or provide the option to abandon (or revert to clearnet?)

    2. I assume you are restricting the feature to sendraw so the wallet doesn’t get involved and try something silly like rebroadcasting over clearnet or inserting in to our own mempool? Do you think future work will integrate the wallet?

    privbroadcast

  17. in src/logging.h:73 in e672e55ddb outdated
    69@@ -70,6 +70,7 @@ namespace BCLog {
    70         TXRECONCILIATION = (1 << 27),
    71         SCAN        = (1 << 28),
    72         TXPACKAGES  = (1 << 29),
    73+        PRIVATE_BROADCAST = (1 << 30),
    


    pinheadmz commented at 4:42 pm on March 8, 2024:
    e672e55ddb49378bd5c8e4f03fadae5774310c70 does this mean there is only one bit left in LogFlags : uint32_t ?!

    vasild commented at 2:37 pm on March 11, 2024:
    Yes. LogFlags is not stored on disk (I hope!) or participate in some permanent on-disk format. It is memory only and can be flipped to uint64_t when needed.
  18. in src/init.cpp:1961 in 72f0dbc99e outdated
    1910@@ -1900,6 +1911,23 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
    1911 
    1912     connOptions.m_i2p_accept_incoming = args.GetBoolArg("-i2pacceptincoming", DEFAULT_I2P_ACCEPT_INCOMING);
    1913 
    1914+    if (args.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)) {
    1915+        // If -listenonion is set, then NET_ONION may not be reachable now
    1916+        // but may become reachable later, thus only error here if it is not
    1917+        // reachable and will not become reachable for sure.
    1918+        if (!g_reachable_nets.Contains(NET_I2P) && !g_reachable_nets.Contains(NET_ONION) &&
    1919+            (!listenonion || args.IsArgSet("-onlynet"))) {
    


    pinheadmz commented at 4:50 pm on March 8, 2024:
    72f0dbc99e6eed26bef21965dabc5f0bebf01b33 if user sets -onlynet=onion do we require (by the time we get to this line) that NET_ONION is reachable? Or couldn’t it come up later, similar to the -listenonion configuration?

    vasild commented at 5:15 pm on March 11, 2024:

    if user sets -onlynet=onion do we require (by the time we get to this line) that NET_ONION is reachable?

    No. -listen=1 -listenonion=1 -torcontrol=... -torpassword=... -onlynet=onion would get us here with NET_ONION being unreachable.

    Or couldn’t it come up later, similar to the -listenonion configuration?

    Yes, if -listenonion=1 -torcontrol=... -torpassword=... is given then we will later, after startup, connect to the Tor daemon and possibly make NET_ONION reachable.


    pinheadmz commented at 5:23 pm on March 11, 2024:
    ok so then isn’t it possible for a user to start with -privatebroadcast but i2p and onion aren’t reachable yet? They would get an init error but the Tor daemon could become reachable later, by the time we need it.

    vasild commented at 1:12 pm on March 12, 2024:

    It would be possible. -privatebroadcast=1 -listenonion=1 -torcontrol=... -torpassword=... would do exactly that and allow the startup to proceed. However, args.IsArgSet("-onlynet") was misplaced. It was intended to give error on things like -privatebroadcast=1 -listenonion=1 -torcontrol=... -torpassword=... -onlynet=ipv4 but it was too strong and would mistakenly forbid -privatebroadcast=1 -listenonion=1 -torcontrol=... -torpassword=... -onlynet=onion as well.

    Changed.

  19. in src/bitcoin-cli.cpp:455 in 7a0f492104 outdated
    451@@ -452,6 +452,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
    452         if (conn_type == "block-relay-only") return "block";
    453         if (conn_type == "manual" || conn_type == "feeler") return conn_type;
    454         if (conn_type == "addr-fetch") return "addr";
    455+        if (conn_type == "private-broadcast") return "privbcast";
    


    pinheadmz commented at 5:51 pm on March 8, 2024:

    7a0f492104c1621b7089a64fa330c72b06d30574 "privbcast" needs to be <= 6 characters to fit nicely in the table layout, or you could extend the column width:

    https://github.com/bitcoin/bitcoin/blob/1cd2e29870e4ad3b7c57b1d567df0e6df56572b0/src/bitcoin-cli.cpp#L549-L552


    vasild commented at 5:54 pm on March 11, 2024:
    Changed to privat. Feel free to suggest another name.
  20. in src/node/connection_types.h:84 in 7a0f492104 outdated
    79+    /**
    80+     * Private broadcast connections are short-lived and only opened to
    81+     * privacy networks (Tor, I2P) for relaying privacy-sensitive data (like
    82+     * our own transactions) and closed afterwards.
    83+     */
    84+    PRIVATE_BROADCAST,
    


    pinheadmz commented at 6:06 pm on March 8, 2024:

    7a0f492104c1621b7089a64fa330c72b06d30574 I noticed this wasn’t added to the fuzzer, dunno if it needs to be:

    https://github.com/bitcoin/bitcoin/blob/1cd2e29870e4ad3b7c57b1d567df0e6df56572b0/src/test/util/net.h#L112-L119


    vasild commented at 5:55 pm on March 11, 2024:
    Good catch, added!
  21. vasild commented at 6:21 pm on March 8, 2024: contributor

    @pinheadmz, excellent! Thank you for looking into this! Replies inline:

    The private broadcast succeeds frequently but not always

    Lets debug this in a sub-thread in order not to overwhelm the PR main thread: here.

    suspecting that inbound onion connections were removing potential peers from the private broadcast list

    No, this cannot be the case. Inbound onion connections appear as IPv4 connections coming from the Tor daemon. They do not have any source address associated with them, other than the one of the Tor daemon itself (usually 127.0.0.1).

    I think I noticed this in the original PR as well, if multiple transactions are sent, the count keeps going up without a limit

    That is now capped at size_t MAX_PRIVATE_BROADCAST_CONNECTIONS{64}.

    the connection type "privbcast" is breaking the very nice -netinfo table

    This better be addressed in the visualization engine, e.g. by using %5.5s

    How do we pick the onion peers to relay to? If we avoid reusing peers then…

    We pick a random Tor (or I2P) peer. In the same way we choose the address when we want to have an outbound connection to a peer from a particular network. No attempt to avoid reuse. But we don’t connect to already connected peers. This is the logic as in master.

    Are we using fresh Tor identities for these connections?

    Yes, if -proxyrandomize=1 (the default). This is the same in master - separate Tor circuit per connection. This PR does the same as Wasabi: https://docs.wasabiwallet.app/why-wasabi/NetworkLevelPrivacy.html#wasabi-wallet-light-node

    Do you think we need any RPC to get the status of private tx sends or provide the option to abandon (or revert to clearnet?)

    Yes, both would be useful, but as a followup PR. I am afraid putting those here would bloat this PR too much. I could however implement that and publish it under Draft PR that depends on this one, so people can use it during testing.

    I assume you are restricting the feature to sendraw so the wallet doesn’t get involved and try something silly like rebroadcasting over clearnet or inserting in to our own mempool?

    The reason to omit the wallet is that it currently does not count non-mempool change. For example, if we have 1 BTC and send 0.6 to somebody and 0.4 as change, the wallet will show a balance of 0 (or 1, unchanged? somebody correct me if I am wrong) until the transaction enters the mempool (or gets mined in a block). This would be quite rough user experience. Note that this is already the case in master if -walletbroadcast=0.

    Do you think future work will integrate the wallet?

    Yes, but I left that for another PR. Omitting it here reduces the size of this PR and allows to deal with the wallet in isolation, after all the distracting infrastructure is in (e.g. log category, new type of connection, the “net” code, etc).

  22. in src/net.cpp:494 in 4e19f9cc98 outdated
    455@@ -456,7 +456,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
    456         if (addrConnect.IsI2P() && use_proxy) {
    457             i2p::Connection conn;
    458 
    459-            if (m_i2p_sam_session) {
    460+            if (m_i2p_sam_session && conn_type != ConnectionType::PRIVATE_BROADCAST) {
    


    pinheadmz commented at 6:38 pm on March 8, 2024:
    4e19f9cc9834c0b76a2d03fea0921879de46fea9 Just to clarify: if a i2p sam session already exists, normally we would re-use it. But in the case of private broadcast we force a new transient session? Comment might be nice here.

    vasild commented at 5:55 pm on March 11, 2024:
    Yes. Added a comment.
  23. in src/net.cpp:2465 in 4e19f9cc98 outdated
    2465+    }
    2466+
    2467+    LogPrintLevel(BCLog::PRIVATE_BROADCAST,
    2468+                  BCLog::Level::Debug,
    2469+                  "Requested to open %d connection(s), trying to open one\n",
    2470+                  num_needed);
    


    pinheadmz commented at 7:01 pm on March 8, 2024:
    4e19f9cc9834c0b76a2d03fea0921879de46fea9 Feels like this log message is not in the right place since this function does not try to open a connection.

    vasild commented at 1:16 pm on March 12, 2024:
    I think it is ok. This function is a helper of the caller which opens the connection. The doc of the function reads: Decide whether to open a private broadcast connection and if yes, to which network.
  24. in src/net.cpp:3054 in 4e19f9cc98 outdated
    2963@@ -2886,6 +2964,16 @@ void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFai
    2964         return;
    2965     pnode->grantOutbound = std::move(grant_outbound);
    2966 
    2967+    if (conn_type == ConnectionType::PRIVATE_BROADCAST) {
    2968+        const size_t before_sub{m_private_broadcast_connections_to_open.fetch_sub(1)};
    


    pinheadmz commented at 7:04 pm on March 8, 2024:
    4e19f9cc9834c0b76a2d03fea0921879de46fea9 does this need underflow protection? Just like an Assume() even?

    vasild commented at 2:29 pm on March 12, 2024:
    Added Assume()
  25. in src/wallet/wallet.cpp:2069 in 532e63279a outdated
    2064@@ -2051,7 +2065,7 @@ NodeClock::time_point CWallet::GetDefaultNextResend() { return FastRandomContext
    2065 // that depends on the `relay` option. Periodic rebroadcast uses the pattern
    2066 // relay=true force=false, while loading into the mempool
    2067 // (on start, or after import) uses relay=false force=true.
    2068-void CWallet::ResubmitWalletTransactions(bool relay, bool force)
    2069+void CWallet::ResubmitWalletTransactions(TxBroadcastMethod broadcast_method, bool force)
    


    pinheadmz commented at 7:22 pm on March 8, 2024:
    532e63279a3c04fef8da2c1bb95e5280c94abc4e consider rephrasing the comment on the previous line since relay=true isn’t a thing any more?

    vasild commented at 2:36 pm on March 12, 2024:
    Right, changed.
  26. in src/net_processing.cpp:1236 in eb9d7386ca outdated
    1126+            CTransactionRef tx;
    1127+            Priority priority;
    1128+        };
    1129+
    1130+        struct TxidHasher {
    1131+            size_t operator()(const Txid& txid) const { return SipHashUint256(0, 0, txid.ToUint256()); }
    


    pinheadmz commented at 7:47 pm on March 8, 2024:
    eb9d7386ca82319ab682fc20ab35c13340d108a4 Hashing a hash? Does this just make the std::unordered_map lookups faster because the keys are smaller (looks like 64 bits?)

    vasild commented at 4:49 pm on March 12, 2024:
  27. in src/net_processing.cpp:1150 in 07dee91112 outdated
    1126+                return std::nullopt;
    1127+            }
    1128+            const Txid& txid = m_by_priority.begin()->second;
    1129+            auto it = m_by_txid.find(txid);
    1130+            if (it == m_by_txid.end()) {
    1131+                return std::nullopt;
    


    pinheadmz commented at 8:23 pm on March 8, 2024:
    07dee91112189be02c6694385de261570725b56b this would be unexpected if we had the txid in the priority list but not in the actual txid list, right? Maybe we should at least remove the txid from the priority list if this ever happens, or log something, or like, call the police or something?

    vasild commented at 5:20 pm on March 12, 2024:
    Added Assume() that the entry is found.
  28. in src/net_processing.cpp:1525 in 07dee91112 outdated
    1691+    if (pnode.IsPrivateBroadcastConn()) {
    1692+        my_services = NODE_NONE;
    1693+        my_time = 0;
    1694+        your_services = NODE_NONE;
    1695+        your_addr = CService{};
    1696+        my_user_agent = "/pynode:0.0.1/";
    


    pinheadmz commented at 8:32 pm on March 8, 2024:
    07dee91112189be02c6694385de261570725b56b could this just be blank?

    vasild commented at 5:27 pm on March 12, 2024:
    See this: #27509 (review)

    sr-gi commented at 4:03 pm on May 10, 2024:
    I think /pynode:0.0.1/ is as good as any other of the options commented on the linked thread, but having a comment there specifying why this was picked is worth. Any future traveller will be so confuse to see that user agent without context
  29. in src/net_processing.cpp:3813 in 07dee91112 outdated
    3884@@ -3706,7 +3885,7 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    3885                       (mapped_as ? strprintf(", mapped_as=%d", mapped_as) : ""));
    3886         }
    3887 
    3888-        if (pfrom.GetCommonVersion() >= SHORT_IDS_BLOCKS_VERSION) {
    3889+        if (pfrom.GetCommonVersion() >= SHORT_IDS_BLOCKS_VERSION && !pfrom.IsPrivateBroadcastConn()) {
    


    pinheadmz commented at 8:52 pm on March 8, 2024:
    07dee91112189be02c6694385de261570725b56b do you need this here? I thought you had filtered out all the unnecessary messages already in CConnman::PushMessage()

    vasild commented at 5:28 pm on March 12, 2024:
    Somehow I find this more clear. Some redundancy doesn’t hurt.
  30. in src/net_processing.cpp:5357 in 07dee91112 outdated
    5132@@ -4929,6 +5133,16 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    5133                     if (ping_time.count() >= 0) {
    5134                         // Let connman know about this successful ping-pong
    5135                         pfrom.PongReceived(ping_time);
    5136+                        if (pfrom.IsPrivateBroadcastConn()) {
    5137+                            m_tx_for_private_broadcast.BroadcastEnd(pfrom.GetId(), /*confirmed_by_node=*/true);
    


    pinheadmz commented at 8:55 pm on March 8, 2024:
    07dee91112189be02c6694385de261570725b56b BroadcastEnd() returns a bool, seems like you could include that in the log message where you have the word “probably”

    vasild commented at 5:37 pm on March 12, 2024:

    By “probably successful private broadcast” I meant that probably the transaction will successfully reach the network, not that we successfully gave it to one peer. Reworded.

    ~Also the return value of BroadcastEnd() was not used by the callers. Changed it to void.~ ignore this nonsense

  31. pinheadmz commented at 8:59 pm on March 8, 2024: member

    Reviewed code through first 11 commits (up to 07dee91112189be02c6694385de261570725b56b) some nits but mostly questions below. I will continue review next week.

    Really great clean up in first commit in logging.cpp, very satisfying!

    One other out-of-line question, about commit eb9d7386ca82319ab682fc20ab35c13340d108a4, the commit message mentions “poking CConnman to start broadcast” but I don’t think that is in the code in that commit.

  32. DrahtBot added the label Needs rebase on Mar 9, 2024
  33. in src/bitcoin-cli.cpp:457 in 6fad02cf03 outdated
    451@@ -452,6 +452,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
    452         if (conn_type == "block-relay-only") return "block";
    


    vasild commented at 9:35 am on March 11, 2024:

    From the PR main thread #29415 (comment):

    The private broadcast succeeds frequently but not always

    In the originator’s log you can check to whom is the transaction being sent. Then in the recipient’s logs you can check if it was accepted and further broadcast to everybody. Enable -debug=net -debug=privatebroadcast.

  34. in src/net_processing.cpp:4663 in bc05ddc7b9 outdated
    4514@@ -4499,6 +4515,16 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    4515         const uint256& hash = peer->m_wtxid_relay ? wtxid : txid;
    4516         AddKnownTx(*peer, hash);
    4517 
    4518+        if (m_tx_for_private_broadcast.Remove(ptx)) {
    


    pinheadmz commented at 5:19 pm on March 11, 2024:
    bc05ddc7b9369cf263bf88d0a773ca853d3c9af5 maybe I’m missing something, but should we not also adjust m_private_broadcast_connections_to_open when we get our privately broadcast tx back over regular relay? In my first local test, all 5 onion connections were completed before I got the tx back. But if the very first private connection relays the tx around and we get it back, do we still need to bother with the next 4?

    vasild commented at 2:33 pm on March 18, 2024:
    Yes, good idea. Implemented.
  35. in src/net.cpp:2761 in 4e19f9cc98 outdated
    2732@@ -2657,7 +2733,9 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
    2733             CAddress addr;
    2734             NodeSeconds addr_last_try{0s};
    2735 
    2736-            if (fFeeler) {
    2737+            if (open_private_broadcast_to.has_value()) {
    2738+                std::tie(addr, addr_last_try) = addrman.Select(/*new_only=*/false, open_private_broadcast_to.value());
    


    pinheadmz commented at 5:31 pm on March 11, 2024:
    4e19f9cc9834c0b76a2d03fea0921879de46fea9 what if MaybePickPrivateBroadcastNetwork() chooses i2p but we don’t have any addrs for that network? I think the while loop below may try forever until addrman finally has something for i2p? But we could call MaybePickPrivateBroadcastNetwork() again or just switch to Tor if we know we have addrs there.

    vasild commented at 5:50 pm on March 12, 2024:
    No, in that case Select() will return an invalid address and we will break; from the inside loop. Then the outside loop will call MaybePickPrivateBroadcastNetwork() again.
  36. in src/net_processing.cpp:1901 in c09d6409c9 outdated
    1867+                m_tx_for_private_broadcast.Remove(stale_tx);
    1868+            }
    1869+        }
    1870+    }
    1871+
    1872+    if (num_stale > num_active + num_to_open) {
    


    pinheadmz commented at 5:40 pm on March 11, 2024:
    c09d6409c957af7c449a60778689d6715d9a6628 num_stale is a count of transactions but the other two numbers here count p2p connections, are we still going to use NUM_PRIVATE_BROADCAST_PER_TX here?

    vasild commented at 5:53 pm on March 12, 2024:
    NUM_PRIVATE_BROADCAST_PER_TX is just for the initial broadcast. Then if a transaction gets stale, we retry one shot at a time. There is no special reason for this. Could be either way.

    vasild commented at 2:34 pm on March 18, 2024:
    I also renamed the variables to make it clear that we are comparing number of transactions with number of connections.
  37. vasild force-pushed on Mar 11, 2024
  38. vasild commented at 5:56 pm on March 11, 2024: contributor
    6fad02cf03...d10a0649b0: rebase due to conflicts and address some suggestions
  39. in src/rpc/mempool.cpp:95 in e36a1b4848 outdated
    90@@ -89,11 +91,14 @@ static RPCHelpMan sendrawtransaction()
    91             std::string err_string;
    92             AssertLockNotHeld(cs_main);
    93             NodeContext& node = EnsureAnyNodeContext(request.context);
    94+            const auto method = gArgs.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST) ?
    95+                                    NO_MEMPOOL_PRIVATE_BROADCAST :
    


    pinheadmz commented at 5:59 pm on March 11, 2024:
    e36a1b4848f8f762b550a9469993211d41bd6cb9 heh, makes the placement of this function in mempool.cpp a bit arbitrary now!

    vasild commented at 12:27 pm on March 13, 2024:
    Yeah, it was before as well. Somehow the code and the comments around this say that “the transaction is sent to the mempool for broadcast” which is not quite accurate (in master). I don’t think that the mempool is broadcasting the transaction. Will leave it as it is for now.
  40. in test/functional/test_framework/socks5.py:174 in 2b2e747455 outdated
    127@@ -117,6 +128,33 @@ def handle(self):
    128             cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
    129             self.serv.queue.put(cmdin)
    130             logger.debug('Proxy: %s', cmdin)
    131+
    


    pinheadmz commented at 6:13 pm on March 11, 2024:

    2b2e747455699236200fa3cddddfff7005f6a44a Could be overkill, but you could actually adjust the “dummy response” a few lines up to actually respond accurately, in particular the REPLY field could be used to report a connection failure back to bitcoind.

    From https://www.rfc-editor.org/rfc/rfc1928

     0diff --git a/test/functional/test_framework/socks5.py b/test/functional/test_framework/socks5.py
     1index 48ad8bbeea..568e9fa64c 100644
     2--- a/test/functional/test_framework/socks5.py
     3+++ b/test/functional/test_framework/socks5.py
     4@@ -123,7 +123,14 @@ class Socks5Connection():
     5             port = (port_hi << 8) | port_lo
     6 
     7             # Send dummy response
     8-            self.conn.sendall(bytearray([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
     9+            self.conn.sendall(bytearray([
    10+                0x05, # Protocol version
    11+                0x00, # Reply (0x00 = success)
    12+                0x00, # Reserved (must be 0x00)
    13+                0x01, # Server bound address type (0x01 = ipv4)
    14+                0x00, 0x00, 0x00, 0x00, # Server bound ipv4 address
    15+                0x00, 0x00 # Server bound port
    16+            ]))
    17 
    18             cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
    19             self.serv.queue.put(cmdin)
    

    vasild commented at 2:16 pm on March 18, 2024:
    I will leave it as it is, looks too complicated after looking at doing this for some time.
  41. in test/functional/p2p_private_broadcast.py:51 in d10a0649b0 outdated
    61+MAX_BLOCK_RELAY_ONLY_CONNECTIONS = 2
    62+NUM_INITIAL_CONNECTIONS = MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS
    63+NUM_PRIVATE_BROADCAST_PER_TX = 5
    64+
    65+# Fill addrman with these addresses. Must have enough Tor addresses, so that even
    66+# if all 10 default connections are opened to a Tor address (!?) there must be more
    


    pinheadmz commented at 6:21 pm on March 11, 2024:

    d10a0649b018638574fa7dc316b3e783bf6cee69 just a guess, would this work?

    1. start the node with -addnode=<ipv4> x10
    2. wait for 10 outbounds to connect
    3. addpeeraddress with all the onion and i2p addrs
    4. send raw, etc…

    vasild commented at 2:17 pm on March 18, 2024:
    No, because -addnode is counted separately from the 10 automatic outbound connections.
  42. in test/functional/p2p_private_broadcast.py:182 in d10a0649b0 outdated
    177+            if i == NUM_INITIAL_CONNECTIONS:
    178+                # Instruct the SOCKS5 server to redirect the first private
    179+                # broadcast connection from nodes[0] to nodes[1]
    180+                self.socks5_server.conf.destinations.append({
    181+                    "listen_addr": "127.0.0.1", # nodes[1] listen address
    182+                    "listen_port": p2p_port(1), # nodes[1] listen port
    


    pinheadmz commented at 6:26 pm on March 11, 2024:
    d10a0649b018638574fa7dc316b3e783bf6cee69 nit: slightly confusing to prefix these with listen_ I would expect, like: to_addr to_port requested_to_addr

    vasild commented at 2:29 pm on March 18, 2024:
    Renamed.
  43. in test/functional/p2p_private_broadcast.py:296 in d10a0649b0 outdated
    291+
    292+        wtxid_int = int(tx["wtxid"], 16)
    293+        inv = CInv(MSG_WTX, wtxid_int)
    294+
    295+        self.log.info("Checking that the transaction is not in the originator node's mempool")
    296+        assert len(tx_originator.getrawmempool()) == 0
    


    pinheadmz commented at 6:40 pm on March 11, 2024:
    d10a0649b018638574fa7dc316b3e783bf6cee69 sanity check: you can assert this because none of the private-broadcast peers are connected back to the tx_originator?

    vasild commented at 12:40 pm on March 13, 2024:
    Yes, exactly.
  44. pinheadmz commented at 6:41 pm on March 11, 2024: member

    code review ACK d10a0649b0

    Although I think you rebased while was in there, so a few of my commit hashes might be off.

    One additional out of line comment, in commit message for 6b8c645ea623875de9e3e3d3f77054dd0a784c20 you are using C++ syntax to describe python :-)

  45. DrahtBot removed the label Needs rebase on Mar 11, 2024
  46. vasild force-pushed on Mar 12, 2024
  47. vasild commented at 6:03 pm on March 12, 2024: contributor
    d10a0649b0...5ca891f4e0: address some suggestions
  48. DrahtBot added the label Needs rebase on Mar 12, 2024
  49. vasild force-pushed on Mar 12, 2024
  50. vasild commented at 6:06 pm on March 12, 2024: contributor
    5ca891f4e0...b6dce67064: rebase due to conflicts
  51. DrahtBot removed the label Needs rebase on Mar 12, 2024
  52. vasild force-pushed on Mar 13, 2024
  53. vasild commented at 12:44 pm on March 13, 2024: contributor
    b6dce67064...cc867ebd62: rebase due to conflicts
  54. iotamega commented at 5:56 pm on March 15, 2024: none

    Is this going to be on by default or just an option?

    Haven’t had a chance to look at the code submissions yet but I do have concerns if this is on by default that many corporate networks will start blocking installation or that transactions will not be submitted as many corporate networks block Tor or I2P connections by default at the corporate firewall level.

    I assume it has failovers as well if it is by default?

  55. pinheadmz commented at 7:09 pm on March 15, 2024: member

    Is this going to be on by default or just an option?

    This PR only affects transactions sent with RPC sendrawtransaction and only if -privatebroadcast=1 is configured, which is not default

  56. vasild force-pushed on Mar 18, 2024
  57. vasild commented at 2:33 pm on March 18, 2024: contributor
    cc867ebd62...4828d46209: rebase and address suggestions
  58. andrewtoth commented at 8:10 pm on March 19, 2024: contributor

    Concept ACK

    I made similar solutions for EPS and CoreLightning, which I’m happy to see being obviated by adding this functionality to the source!

    Curious why this is just pushing an unsolicited TX message instead of using INV/GETDATA/TX? This conversation seems to convince me the latter is better? Also, if the submitted tx is spending unconfirmed outputs, should we support sending the parents to the peer if requested?

  59. vasild commented at 5:23 pm on March 21, 2024: contributor

    Curious why this is just pushing an unsolicited TX message instead of using INV/GETDATA/TX? #27509 (review) seems to convince me the latter is better?

    I followed up in that conversion. For now I am keeping it as it is, but if you are happy with all the rest and would otherwise ACK, then say so! ;-)

    Also, if the submitted tx is spending unconfirmed outputs, should we support sending the parents to the peer if requested?

    Right now in master sendrawtransaction RPC operates on a single transaction. Its parents must already be in the mempool. This PR keeps it that way. Parent+child is more for the submitpackage RPC. That + the wallet I will deal with in a followup PRs.

  60. andrewtoth commented at 2:43 pm on March 22, 2024: contributor

    Curious why this is just pushing an unsolicited TX message instead of using INV/GETDATA/TX? #27509 (comment) seems to convince me the latter is better?

    I followed up in that conversion. For now I am keeping it as it is, but if you are happy with all the rest and would otherwise ACK, then say so! ;-)

    Thanks. I read through the proposal to not accept unsolicited TX messages, and the reasoning to reject it makes sense to me. I suppose if you know you’re the originator of a tx, the only reason not to push the tx unsolicited to all your peers is for privacy reasons. However, in this case the privacy reasons are moot because of the ephemeral private connections.

    Also, if the submitted tx is spending unconfirmed outputs, should we support sending the parents to the peer if requested?

    Right now in master sendrawtransaction RPC operates on a single transaction. Its parents must already be in the mempool. This PR keeps it that way. Parent+child is more for the submitpackage RPC. That + the wallet I will deal with in a followup PRs.

    I mean the case where we have a parent unconfirmed tx in our mempool, and the peer we connected to does not. If we push a tx spending that unconfirmed parent, the peer will respond with a GETDATA for the parent tx. So we can either:

    1. Ignore the GETDATA . This will result in our tx not relaying through this connection if the peer doesn’t have all unconfirmed parents.
    2. Wait for the GETDATA and respond with a TX message. We won’t know if the peer will eventually ask for the parent, so this will be tricky and we will have to wait for some time. However, in this time if we receive an INV for it from another peer we can close because we know we were successful.
    3. Push all unconfirmed parents along with the tx we are sending. This will result in potentially wasted bandwidth.
  61. in src/qt/rpcconsole.cpp:519 in 9eee0e9d71 outdated
    513@@ -514,7 +514,10 @@ RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformSty
    514         tr("Outbound Feeler: short-lived, for testing addresses"),
    515         /*: Explanatory text for a short-lived outbound peer connection that is used
    516             to request addresses from a peer. */
    517-        tr("Outbound Address Fetch: short-lived, for soliciting addresses")};
    518+        tr("Outbound Address Fetch: short-lived, for soliciting addresses"),
    519+        /*: Explanatory text for a short-lived outbound peer connection that is used
    520+            to broadcast privacy-sensitve data (like our transactions). */
    


    mzumsande commented at 5:58 pm on March 26, 2024:
    typo: sensitive (this and next line)

    vasild commented at 4:08 pm on March 28, 2024:
    Fixed, will be in next push.
  62. in src/init.cpp:1024 in 64742d7881 outdated
    1011@@ -1007,10 +1012,22 @@ bool AppInitParameterInteraction(const ArgsManager& args)
    1012 #endif
    1013     // Trim requested connection counts, to fit into system limitations
    1014     // <int> in std::min<int>(...) to work around FreeBSD compilation issue described in #2695
    1015-    nMaxConnections = std::max(std::min<int>(nMaxConnections, fd_max - nBind - MIN_CORE_FILEDESCRIPTORS - MAX_ADDNODE_CONNECTIONS - NUM_FDS_MESSAGE_CAPTURE), 0);
    1016+    nMaxConnections = std::max(std::min<int>(nMaxConnections,
    


    mzumsande commented at 6:13 pm on March 26, 2024:
    It seems excessive that MAX_PRIVATE_BROADCAST_CONNECTIONS=64 has the power to reduce nMaxConnections to a lower value even in the case where private broadcast isn’t enabled. Might be better to make that conditional on privatebroadcast, similar to how it’s done with nBind.

    vasild commented at 4:10 pm on March 28, 2024:

    Done, will be in next push. Good observation!

    This raised the unrelated-to-this-PR question https://www.erisian.com.au/bitcoin-core-dev/log-2024-03-28.html#l-59

  63. in src/primitives/transaction.h:446 in 52e6a40d2a outdated
    438@@ -439,4 +439,18 @@ class GenTxid
    439     friend bool operator<(const GenTxid& a, const GenTxid& b) { return std::tie(a.m_is_wtxid, a.m_hash) < std::tie(b.m_is_wtxid, b.m_hash); }
    440 };
    441 
    442+/**
    443+ * Methods to broadcast a local transaction.
    444+ * Used to influence BroadcastTransaction() and its callers.
    445+ */
    446+enum TxBroadcastMethod : uint8_t {
    


    mzumsande commented at 7:53 pm on March 26, 2024:
    Wouldn’t this fit better into node/transaction.h , together with BroadcastTransaction ?

    vasild commented at 1:22 pm on March 28, 2024:

    I think so, but this drags some further changes, are you ok with those:

      0diff --git i/src/interfaces/chain.h w/src/interfaces/chain.h
      1index 315211ae49..eeb5fef42b 100644
      2--- i/src/interfaces/chain.h
      3+++ w/src/interfaces/chain.h
      4@@ -4,12 +4,13 @@
      5 
      6 #ifndef BITCOIN_INTERFACES_CHAIN_H
      7 #define BITCOIN_INTERFACES_CHAIN_H
      8 
      9 #include <blockfilter.h>
     10 #include <common/settings.h>
     11+#include <node/transaction.h>
     12 #include <primitives/transaction.h> // For CTransactionRef
     13 #include <util/result.h>
     14 
     15 #include <functional>
     16 #include <memory>
     17 #include <optional>
     18@@ -215,13 +216,13 @@ public:
     19     //! [@param](/bitcoin-bitcoin/contributor/param/)[in] broadcast_method Whether to add the transaction to the
     20     //! mempool and how/whether to broadcast it.
     21     //! [@param](/bitcoin-bitcoin/contributor/param/)[out] err_string Set if an error occurs.
     22     //! [@return](/bitcoin-bitcoin/contributor/return/) False if the transaction could not be added due to the fee or for another reason.
     23     virtual bool broadcastTransaction(const CTransactionRef& tx,
     24                                       const CAmount& max_tx_fee,
     25-                                      TxBroadcastMethod broadcast_method,
     26+                                      node::TxBroadcastMethod broadcast_method,
     27                                       std::string& err_string) = 0;
     28 
     29     //! Calculate mempool ancestor and descendant counts for the given transaction.
     30     virtual void getTransactionAncestry(const uint256& txid, size_t& ancestors, size_t& descendants, size_t* ancestorsize = nullptr, CAmount* ancestorfees = nullptr) = 0;
     31 
     32     //! For each outpoint, calculate the fee-bumping cost to spend this outpoint at the specified
     33diff --git i/src/node/transaction.h w/src/node/transaction.h
     34index 75c50bab28..01cdfaf1ff 100644
     35--- i/src/node/transaction.h
     36+++ w/src/node/transaction.h
     37@@ -29,12 +29,26 @@ static const CFeeRate DEFAULT_MAX_RAW_TX_FEE_RATE{COIN / 10};
     38 /** Maximum burn value for sendrawtransaction, submitpackage, and testmempoolaccept RPC calls.
     39  * By default, a transaction with a burn value higher than this will be rejected
     40  * by these RPCs and the GUI. This can be overridden with the maxburnamount argument.
     41  */
     42 static const CAmount DEFAULT_MAX_BURN_AMOUNT{0};
     43 
     44+/**
     45+ * Methods to broadcast a local transaction.
     46+ * Used to influence BroadcastTransaction() and its callers.
     47+ */
     48+enum TxBroadcastMethod : uint8_t {
     49+    /// Add the transaction to the mempool and broadcast to all currently connected peers.
     50+    ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL,
     51+    /// Add the transaction to the mempool, but don't broadcast to anybody.
     52+    ADD_TO_MEMPOOL_NO_BROADCAST,
     53+    /// Omit the mempool and directly send the transaction via a few dedicated connections to
     54+    /// peers on privacy networks.
     55+    NO_MEMPOOL_PRIVATE_BROADCAST,
     56+};
     57+
     58 /**
     59  * Submit a transaction to the mempool and (optionally) relay it to all P2P peers.
     60  *
     61  * Mempool submission can be synchronous (will await mempool entry notification
     62  * over the CValidationInterface) or asynchronous (will submit and not wait for
     63  * notification), depending on the value of wait_callback. wait_callback MUST
     64diff --git i/src/primitives/transaction.h w/src/primitives/transaction.h
     65index 2051ebae5a..ccbeb3ec49 100644
     66--- i/src/primitives/transaction.h
     67+++ w/src/primitives/transaction.h
     68@@ -436,21 +436,7 @@ public:
     69     bool IsWtxid() const { return m_is_wtxid; }
     70     const uint256& GetHash() const LIFETIMEBOUND { return m_hash; }
     71     friend bool operator==(const GenTxid& a, const GenTxid& b) { return a.m_is_wtxid == b.m_is_wtxid && a.m_hash == b.m_hash; }
     72     friend bool operator<(const GenTxid& a, const GenTxid& b) { return std::tie(a.m_is_wtxid, a.m_hash) < std::tie(b.m_is_wtxid, b.m_hash); }
     73 };
     74 
     75-/**
     76- * Methods to broadcast a local transaction.
     77- * Used to influence BroadcastTransaction() and its callers.
     78- */
     79-enum TxBroadcastMethod : uint8_t {
     80-    /// Add the transaction to the mempool and broadcast to all currently connected peers.
     81-    ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL,
     82-    /// Add the transaction to the mempool, but don't broadcast to anybody.
     83-    ADD_TO_MEMPOOL_NO_BROADCAST,
     84-    /// Omit the mempool and directly send the transaction via a few dedicated connections to
     85-    /// peers on privacy networks.
     86-    NO_MEMPOOL_PRIVATE_BROADCAST,
     87-};
     88-
     89 #endif // BITCOIN_PRIMITIVES_TRANSACTION_H
     90diff --git i/src/rpc/mempool.cpp w/src/rpc/mempool.cpp
     91index 14d303a0b1..ba69446f36 100644
     92--- i/src/rpc/mempool.cpp
     93+++ w/src/rpc/mempool.cpp
     94@@ -8,12 +8,13 @@
     95 #include <kernel/mempool_persist.h>
     96 
     97 #include <chainparams.h>
     98 #include <core_io.h>
     99 #include <kernel/mempool_entry.h>
    100 #include <node/mempool_persist_args.h>
    101+#include <node/transaction.h>
    102 #include <policy/rbf.h>
    103 #include <policy/settings.h>
    104 #include <primitives/transaction.h>
    105 #include <rpc/server.h>
    106 #include <rpc/server_util.h>
    107 #include <rpc/util.h>
    108@@ -91,13 +92,13 @@ static RPCHelpMan sendrawtransaction()
    109             AssertLockNotHeld(cs_main);
    110             NodeContext& node = EnsureAnyNodeContext(request.context);
    111             const TransactionError err = BroadcastTransaction(node,
    112                                                               tx,
    113                                                               err_string,
    114                                                               max_raw_tx_fee,
    115-                                                              ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL,
    116+                                                              node::ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL,
    117                                                               /*wait_callback=*/true);
    118             if (TransactionError::OK != err) {
    119                 throw JSONRPCTransactionError(err, err_string);
    120             }
    121 
    122             return tx->GetHash().GetHex();
    123@@ -953,13 +954,13 @@ static RPCHelpMan submitpackage()
    124                 // We do not expect an error here; we are only broadcasting things already/still in mempool
    125                 std::string err_string;
    126                 const auto err = BroadcastTransaction(node,
    127                                                       tx,
    128                                                       err_string,
    129                                                       /*max_tx_fee=*/0,
    130-                                                      ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL,
    131+                                                      node::ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL,
    132                                                       /*wait_callback=*/true);
    133                 if (err != TransactionError::OK) {
    134                     throw JSONRPCTransactionError(err,
    135                         strprintf("transaction broadcast failed: %s (%d transactions were broadcast successfully)",
    136                             err_string, num_broadcast));
    137                 }
    138diff --git i/src/wallet/rpc/backup.cpp w/src/wallet/rpc/backup.cpp
    139index fdbd44ffe5..2e61a0e945 100644
    140--- i/src/wallet/rpc/backup.cpp
    141+++ w/src/wallet/rpc/backup.cpp
    142@@ -10,12 +10,13 @@
    143 #include <clientversion.h>
    144 #include <core_io.h>
    145 #include <hash.h>
    146 #include <interfaces/chain.h>
    147 #include <key_io.h>
    148 #include <merkleblock.h>
    149+#include <node/transaction.h>
    150 #include <rpc/util.h>
    151 #include <script/descriptor.h>
    152 #include <script/script.h>
    153 #include <script/solver.h>
    154 #include <sync.h>
    155 #include <uint256.h>
    156@@ -308,13 +309,13 @@ RPCHelpMan importaddress()
    157             throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script");
    158         }
    159     }
    160     if (fRescan)
    161     {
    162         RescanWallet(*pwallet, reserver);
    163-        pwallet->ResubmitWalletTransactions(ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    164+        pwallet->ResubmitWalletTransactions(node::ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    165     }
    166 
    167     return UniValue::VNULL;
    168 },
    169     };
    170 }
    171@@ -479,13 +480,13 @@ RPCHelpMan importpubkey()
    172 
    173         pwallet->ImportPubKeys({pubKey.GetID()}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*internal=*/false, /*timestamp=*/1);
    174     }
    175     if (fRescan)
    176     {
    177         RescanWallet(*pwallet, reserver);
    178-        pwallet->ResubmitWalletTransactions(ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    179+        pwallet->ResubmitWalletTransactions(node::ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    180     }
    181 
    182     return UniValue::VNULL;
    183 },
    184     };
    185 }
    186@@ -1400,13 +1401,13 @@ RPCHelpMan importmulti()
    187                 nLowestTimestamp = timestamp;
    188             }
    189         }
    190     }
    191     if (fRescan && fRunScan && requests.size()) {
    192         int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, /*update=*/true);
    193-        pwallet->ResubmitWalletTransactions(ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    194+        pwallet->ResubmitWalletTransactions(node::ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    195 
    196         if (pwallet->IsAbortingRescan()) {
    197             throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
    198         }
    199         if (scannedTime > nLowestTimestamp) {
    200             std::vector<UniValue> results = response.getValues();
    201@@ -1694,13 +1695,13 @@ RPCHelpMan importdescriptors()
    202         pwallet->ConnectScriptPubKeyManNotifiers();
    203     }
    204 
    205     // Rescan the blockchain using the lowest timestamp
    206     if (rescan) {
    207         int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, /*update=*/true);
    208-        pwallet->ResubmitWalletTransactions(ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    209+        pwallet->ResubmitWalletTransactions(node::ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    210 
    211         if (pwallet->IsAbortingRescan()) {
    212             throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
    213         }
    214 
    215         if (scanned_time > lowest_timestamp) {
    216diff --git i/src/wallet/test/wallet_tests.cpp w/src/wallet/test/wallet_tests.cpp
    217index 003d1aab29..2377c71e21 100644
    218--- i/src/wallet/test/wallet_tests.cpp
    219+++ w/src/wallet/test/wallet_tests.cpp
    220@@ -10,12 +10,13 @@
    221 #include <vector>
    222 
    223 #include <addresstype.h>
    224 #include <interfaces/chain.h>
    225 #include <key_io.h>
    226 #include <node/blockstorage.h>
    227+#include <node/transaction.h>
    228 #include <policy/policy.h>
    229 #include <rpc/server.h>
    230 #include <script/solver.h>
    231 #include <test/util/logging.h>
    232 #include <test/util/random.h>
    233 #include <test/util/setup_common.h>
    234@@ -819,13 +820,13 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
    235     });
    236     std::string error;
    237     m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
    238     auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
    239     m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
    240     auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
    241-    BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, ADD_TO_MEMPOOL_NO_BROADCAST, error));
    242+    BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, node::ADD_TO_MEMPOOL_NO_BROADCAST, error));
    243 
    244 
    245     // Reload wallet and make sure new transactions are detected despite events
    246     // being blocked
    247     // Loading will also ask for current mempool transactions
    248     wallet = TestLoadWallet(context);
    249@@ -861,13 +862,13 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
    250     auto handler = HandleLoadWallet(context, [&](std::unique_ptr<interfaces::Wallet> wallet) {
    251             BOOST_CHECK(rescan_completed);
    252             m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
    253             block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
    254             m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
    255             mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
    256-            BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, ADD_TO_MEMPOOL_NO_BROADCAST, error));
    257+            BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, node::ADD_TO_MEMPOOL_NO_BROADCAST, error));
    258             m_node.validation_signals->SyncWithValidationInterfaceQueue();
    259         });
    260     wallet = TestLoadWallet(context);
    261     // Since mempool transactions are requested at the end of loading, there will
    262     // be 2 additional AddToWallet calls, one from the previous test, and a duplicate for mempool_tx
    263     BOOST_CHECK_EQUAL(addtx_count, 2 + 2);
    264diff --git i/src/wallet/wallet.cpp w/src/wallet/wallet.cpp
    265index 0904399926..2566b2d543 100644
    266--- i/src/wallet/wallet.cpp
    267+++ w/src/wallet/wallet.cpp
    268@@ -24,12 +24,13 @@
    269 #include <interfaces/wallet.h>
    270 #include <kernel/chain.h>
    271 #include <kernel/mempool_removal_reason.h>
    272 #include <key.h>
    273 #include <key_io.h>
    274 #include <logging.h>
    275+#include <node/transaction.h>
    276 #include <outputtype.h>
    277 #include <policy/feerate.h>
    278 #include <primitives/block.h>
    279 #include <primitives/transaction.h>
    280 #include <psbt.h>
    281 #include <pubkey.h>
    282@@ -1999,13 +2000,13 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
    283     }
    284     return result;
    285 }
    286 
    287 bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx,
    288                                          std::string& err_string,
    289-                                         TxBroadcastMethod broadcast_method) const
    290+                                         node::TxBroadcastMethod broadcast_method) const
    291 {
    292     AssertLockHeld(cs_wallet);
    293 
    294     // Can't relay if wallet is not broadcasting
    295     if (!GetBroadcastTransactions()) return false;
    296     // Don't relay abandoned transactions
    297@@ -2016,19 +2017,19 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx,
    298     // Don't try to submit conflicted or confirmed transactions.
    299     if (GetTxDepthInMainChain(wtx) != 0) return false;
    300 
    301     // Submit transaction to mempool for relay
    302     const char* what{""};
    303     switch (broadcast_method) {
    304-    case ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL:
    305+    case node::ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL:
    306         what = "to mempool and for broadcast to all peers";
    307         break;
    308-    case ADD_TO_MEMPOOL_NO_BROADCAST:
    309+    case node::ADD_TO_MEMPOOL_NO_BROADCAST:
    310         what = "to mempool without broadcast";
    311         break;
    312-    case NO_MEMPOOL_PRIVATE_BROADCAST:
    313+    case node::NO_MEMPOOL_PRIVATE_BROADCAST:
    314         what = "for private broadcast without adding to the mempool";
    315         break;
    316     }
    317     WalletLogPrintf("Submitting wtx %s %s\n", wtx.GetHash().ToString(), what);
    318     // We must set TxStateInMempool here. Even though it will also be set later by the
    319     // entered-mempool callback, if we did not there would be a race where a
    320@@ -2095,13 +2096,13 @@ NodeClock::time_point CWallet::GetDefaultNextResend() { return FastRandomContext
    321 // The `force` option results in all unconfirmed transactions being submitted to
    322 // the mempool. This does not necessarily result in those transactions being relayed,
    323 // that depends on the `broadcast_method` option. Periodic rebroadcast uses the pattern
    324 // broadcast_method=ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL force=false, while loading into
    325 // the mempool (on start, or after import) uses
    326 // broadcast_method=ADD_TO_MEMPOOL_NO_BROADCAST force=true.
    327-void CWallet::ResubmitWalletTransactions(TxBroadcastMethod broadcast_method, bool force)
    328+void CWallet::ResubmitWalletTransactions(node::TxBroadcastMethod broadcast_method, bool force)
    329 {
    330     // Don't attempt to resubmit if the wallet is configured to not broadcast,
    331     // even if forcing.
    332     if (!fBroadcastTransactions) return;
    333 
    334     int submitted_tx_count = 0;
    335@@ -2136,13 +2137,13 @@ void CWallet::ResubmitWalletTransactions(TxBroadcastMethod broadcast_method, boo
    336 /** @} */ // end of mapWallet
    337 
    338 void MaybeResendWalletTxs(WalletContext& context)
    339 {
    340     for (const std::shared_ptr<CWallet>& pwallet : GetWallets(context)) {
    341         if (!pwallet->ShouldResend()) continue;
    342-        pwallet->ResubmitWalletTransactions(ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL, /*force=*/false);
    343+        pwallet->ResubmitWalletTransactions(node::ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL, /*force=*/false);
    344         pwallet->SetNextResend();
    345     }
    346 }
    347 
    348 
    349 /** [@defgroup](/bitcoin-bitcoin/contributor/defgroup/) Actions
    350@@ -2344,13 +2345,13 @@ void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve
    351     if (!fBroadcastTransactions) {
    352         // Don't submit tx to the mempool
    353         return;
    354     }
    355 
    356     std::string err_string;
    357-    if (!SubmitTxMemoryPoolAndRelay(*wtx, err_string, ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL)) {
    358+    if (!SubmitTxMemoryPoolAndRelay(*wtx, err_string, node::ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL)) {
    359         WalletLogPrintf("CommitTransaction(): Transaction cannot be broadcast immediately, %s\n", err_string);
    360         // TODO: if we expect the failure to be long term or permanent, instead delete wtx from the wallet and return failure.
    361     }
    362 }
    363 
    364 DBErrors CWallet::LoadWallet()
    365@@ -3389,13 +3390,13 @@ bool CWallet::UpgradeWallet(int version, bilingual_str& error)
    366 }
    367 
    368 void CWallet::postInitProcess()
    369 {
    370     // Add wallet transactions that aren't already in a block to mempool
    371     // Do this here as mempool requires genesis block to be loaded
    372-    ResubmitWalletTransactions(ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    373+    ResubmitWalletTransactions(node::ADD_TO_MEMPOOL_NO_BROADCAST, /*force=*/true);
    374 
    375     // Update wallet transactions with current mempool transactions.
    376     WITH_LOCK(cs_wallet, chain().requestMempoolTransactions(*this));
    377 }
    378 
    379 bool CWallet::BackupWallet(const std::string& strDest) const
    380diff --git i/src/wallet/wallet.h w/src/wallet/wallet.h
    381index 8f9881c304..ceadabf165 100644
    382--- i/src/wallet/wallet.h
    383+++ w/src/wallet/wallet.h
    384@@ -9,12 +9,13 @@
    385 #include <addresstype.h>
    386 #include <consensus/amount.h>
    387 #include <interfaces/chain.h>
    388 #include <interfaces/handler.h>
    389 #include <kernel/cs_main.h>
    390 #include <logging.h>
    391+#include <node/transaction.h>
    392 #include <outputtype.h>
    393 #include <policy/feerate.h>
    394 #include <primitives/transaction.h>
    395 #include <script/interpreter.h>
    396 #include <script/script.h>
    397 #include <support/allocators/secure.h>
    398@@ -631,13 +632,13 @@ public:
    399     ScanResult ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional<int> max_height, const WalletRescanReserver& reserver, bool fUpdate, const bool save_progress);
    400     void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) override;
    401     /** Set the next time this wallet should resend transactions to 12-36 hours from now, ~1 day on average. */
    402     void SetNextResend() { m_next_resend = GetDefaultNextResend(); }
    403     /** Return true if all conditions for periodically resending transactions are met. */
    404     bool ShouldResend() const;
    405-    void ResubmitWalletTransactions(TxBroadcastMethod broadcast_method, bool force);
    406+    void ResubmitWalletTransactions(node::TxBroadcastMethod broadcast_method, bool force);
    407 
    408     OutputType TransactionChangeType(const std::optional<OutputType>& change_type, const std::vector<CRecipient>& vecSend) const;
    409 
    410     /** Fetch the inputs and sign with SIGHASH_ALL. */
    411     bool SignTransaction(CMutableTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    412     /** Sign the tx given the input coins and sighash. */
    413@@ -676,13 +677,13 @@ public:
    414      * [@param](/bitcoin-bitcoin/contributor/param/)[in] mapValue key-values to be set on the transaction.
    415      * [@param](/bitcoin-bitcoin/contributor/param/)[in] orderForm BIP 70 / BIP 21 order form details to be set on the transaction.
    416      */
    417     void CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::vector<std::pair<std::string, std::string>> orderForm);
    418 
    419     /** Pass this transaction to node for optional mempool insertion and relay to peers. */
    420-    bool SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string, TxBroadcastMethod broadcast_method) const
    421+    bool SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string, node::TxBroadcastMethod broadcast_method) const
    422         EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    423 
    424     bool ImportScripts(const std::set<CScript> scripts, int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    425     bool ImportPrivKeys(const std::map<CKeyID, CKey>& privkey_map, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    426     bool ImportPubKeys(const std::vector<CKeyID>& ordered_pubkeys, const std::map<CKeyID, CPubKey>& pubkey_map, const std::map<CKeyID, std::pair<CPubKey, KeyOriginInfo>>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    427     bool ImportScriptPubKeys(const std::string& label, const std::set<CScript>& script_pub_keys, const bool have_solving_data, const bool apply_label, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    

    mzumsande commented at 3:27 pm on March 28, 2024:
    Hmm, after looking at that diff I’m not so sure . It’s probably a layer violation include node/ files from the wallet. On the other hand, having this enum in primitive/transaction.h still doesn’t feel right, given that this isn’t really a primitive property of a transaction like COutPoint. Maybe put it into another (new) file in kernel/, and handle it similar to enum class ChainstateRole and enum class MemPoolRemovalReason could be the best way?

    vasild commented at 4:24 pm on March 28, 2024:

    Maybe it is ok to include node/ from the wallet:

     0$ git grep node/ origin/master -- "*wallet*"
     1origin/master:src/bench/wallet_balance.cpp:7:#include <node/chainstate.h>
     2origin/master:src/bench/wallet_balance.cpp:8:#include <node/context.h>
     3origin/master:src/bench/wallet_create.cpp:10:#include <node/context.h>
     4origin/master:src/bench/wallet_create_tx.cpp:10:#include <node/context.h>
     5origin/master:src/bench/wallet_ismine.cpp:12:#include <node/context.h>
     6origin/master:src/bench/wallet_loading.cpp:11:#include <node/context.h>
     7origin/master:src/qt/walletframe.cpp:7:#include <node/interface_ui.h>
     8origin/master:src/qt/walletmodel.cpp:21:#include <node/interface_ui.h>
     9origin/master:src/qt/walletview.cpp:22:#include <node/interface_ui.h>
    10origin/master:src/wallet/init.cpp:16:#include <node/context.h>
    11origin/master:src/wallet/init.cpp:17:#include <node/interface_ui.h>
    12origin/master:src/wallet/test/coinselector_tests.cpp:6:#include <node/context.h>
    13origin/master:src/wallet/test/init_test_fixture.h:10:#include <node/context.h>
    14origin/master:src/wallet/test/ismine_tests.cpp:7:#include <node/context.h>
    15origin/master:src/wallet/test/wallet_test_fixture.h:12:#include <node/context.h>
    16origin/master:src/wallet/test/wallet_tests.cpp:15:#include <node/blockstorage.h>
    17origin/master:test/functional/test_framework/wallet.py:141:            # Sort tx by ancestor count. See BlockAssembler::SortForBlock in src/node/miner.cpp
    

    mzumsande commented at 4:53 pm on March 28, 2024:
    @TheCharlatan you probably dealt a lot with these kind of questions for kernel, do you have an opinion where this enum should be?

    ryanofsky commented at 5:39 pm on March 28, 2024:

    re: #29415 (review)

    I’d suggest moving TxBroadcastMethod enum to node/types.h, which doesn’t exist yet but is introduced in #29015, and could be added here as well.

    It’s probably a layer violation include node/ files from the wallet.

    We don’t want wallet code to call node functions or use internal node types, but node and wallet code do define a handful of “public” types (CNodeStats, CNodeStateStats, and SynchronizationState, AddressPurpose, isminetype, CRecipient, and CCoinControl) I mentioned recently in #29015 (review) that are useful even without depending on node or wallet libraries, and I think it makes sense to put the lightweight types in node/types.h, wallet/types.h, common/types.h files so they can be explained and used without dragging in other things. Other options I mentioned in that comment:

    We do have a choice where about where to put public types which are not really part of any library. We can put them in the interfaces directory, or in common, or in the node and wallet directories. Personally, I think it makes sense to put them in the wallet directory when they are related to wallet functionality and used by wallet code, put them in the node directory when they are related to node functionality and used by node code, put them in the common directory when they are related to common functionality (like PSBT) and used by common code. The comments at the top of {node,wallet,common}/types.h files are meant to explain the idea that these are public types.


    vasild commented at 10:27 am on March 29, 2024:

    Moved to node/types.h (new file). Could put it elsewhere if there are better suggestions. Thanks for the suggestion!

    Edit: btw, the doxygen comment should have \file on its own line. See https://www.doxygen.nl/manual/commands.html#cmdfile and run doxygen doc/Doxyfile and then inspect doc/doxygen/html/node_2types_8h.html.

  64. in src/net.cpp:371 in 5ce95d9960 outdated
    365@@ -366,7 +366,16 @@ bool CConnman::CheckIncomingNonce(uint64_t nonce)
    366 {
    367     LOCK(m_nodes_mutex);
    368     for (const CNode* pnode : m_nodes) {
    369-        if (!pnode->fSuccessfullyConnected && !pnode->IsInboundConn() && pnode->GetLocalNonce() == nonce)
    370+        // Omit private broadcast connections from this check to prevent this privacy attack:
    


    mzumsande commented at 9:35 pm on March 26, 2024:

    The peer starts connecting to (clearnet) nodes and sends them a VERSION message which contains our nonce. If the peer manages to connect to us we would disconnect.

    I’m confused by this attack. First, if the peer connects to us over clearnet it would be an inbound connection for us - we don’t disconnect inbounds, see !pnode->IsInboundConn() a few lines below. Second, even if we did disconnect, I still don’t see the how the attack could work - it would be a new connection with a new ID and a new nonce (the nonce is created using the peer id here, so it should be different for each peer).


    vasild commented at 2:01 pm on March 28, 2024:

    I reread your statement and the code a few times and I think that the code is legit (or the attack the code is guarding against could happen). Let me elaborate:

    1. We connect for a private broadcast to boopeer.onion and send them nonce=1234 in our VERSION message. For this an entry is added to CConnman::m_nodes that has IsInboundConn() == false.
    2. The boo guy delays their VERACK on the private connection and connects to random nodes on clearnet. Assuming we also listen on clearnet address, they are lucky enough and manage to connect to us. They send us VERSION with nonce=1234. For this connection IsInboundConn() == true on our side and this code is executed:

    https://github.com/bitcoin/bitcoin/blob/c8e3978114716bb8fb10695b9d187652f3ab4926/src/net_processing.cpp#L3532-L3537

    passing nonce=1234 to CheckIncomingNonce() which finds the entry in m_nodes from 1. and returns false. Then the clearnet connection from 2. is closed by us.

    Makes sense?


    mzumsande commented at 3:30 pm on March 28, 2024:
    Yes, now it makes sense, thanks! I’m not sure where I got confused, I probably misread the existing code.
  65. mzumsande commented at 9:47 pm on March 26, 2024: contributor
    Haven’t finished my review yet, but submitting a few comments.
  66. jb55 commented at 3:51 pm on March 27, 2024: contributor
    Oh wow just saw this! Thank you. Will review.
  67. zzzi2p commented at 7:08 pm on March 28, 2024: none

    Concept ACK

    This is consistent with i2p guidelines on resource usage. Good work!

  68. vasild force-pushed on Mar 29, 2024
  69. vasild commented at 10:48 am on March 29, 2024: contributor
    4828d46209...09ad469cc1: rebase and address suggestions
  70. vasild commented at 11:02 am on March 29, 2024: contributor

    I mean the case where we have a parent unconfirmed tx in our mempool, and the peer we connected to does not. If we push a tx spending that unconfirmed parent, the peer will respond with a GETDATA for the parent tx…

    I will keep it simple stupid for now, meaning that in this case that particular peer will not relay the transaction further. Others should succeed. If “nobody” has the parent in their mempool, then somehow we are trying to broadcast an orphaned transaction with parent(s) not seen by the network. Can say that this is not supported at this point and will best be supported by a future submitpackage which does private broadcast.

  71. vasild force-pushed on Mar 29, 2024
  72. DrahtBot commented at 11:41 am on March 29, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    Leave a comment here, if you need help tracking down a confusing failure.

    Debug: https://github.com/bitcoin/bitcoin/runs/23235305634

  73. DrahtBot added the label CI failed on Mar 29, 2024
  74. in test/functional/p2p_private_broadcast.py:296 in c7e4d5cb2b outdated
    291+
    292+        wtxid_int = int(tx["wtxid"], 16)
    293+        inv = CInv(MSG_WTX, wtxid_int)
    294+
    295+        self.log.info("Checking that the transaction is not in the originator node's mempool")
    296+        assert len(tx_originator.getrawmempool()) == 0
    


    brunoerg commented at 6:35 pm on March 29, 2024:

    nit:

    0        assert_equal(len(tx_originator.getrawmempool()), 0)
    

    vasild commented at 4:21 pm on April 1, 2024:
    Done.
  75. in src/rpc/mempool.cpp:96 in cf76f4f0fe outdated
    92@@ -91,11 +93,14 @@ static RPCHelpMan sendrawtransaction()
    93             std::string err_string;
    94             AssertLockNotHeld(cs_main);
    95             NodeContext& node = EnsureAnyNodeContext(request.context);
    96+            const auto method = gArgs.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)
    


    brunoerg commented at 8:55 pm on March 29, 2024:
    In cf76f4f0fe1a3ef0730f70a035c10e01712b6d6b: Shouldn’t we check here whether any private network is reachable? Because we can start a node with -privatebroadcast with NET_ONION becoming reachable later, no?

    vasild commented at 4:23 pm on April 1, 2024:
    Done.
  76. in src/bitcoin-cli.cpp:696 in c7e4d5cb2b outdated
    664+        "           \"full\"      - full relay, the default\n"
    665+        "           \"block\"     - block relay; like full relay but does not relay transactions or addresses\n"
    666+        "           \"manual\"    - peer we manually added using RPC addnode or the -addnode/-connect config options\n"
    667+        "           \"feeler\"    - short-lived connection for testing addresses\n"
    668+        "           \"addr\"      - address fetch; short-lived connection for requesting addresses\n"
    669+        "           \"privat\"    - private broadcast; short-lived connection for broadcasting our transactions\n"
    


    brunoerg commented at 2:20 pm on March 30, 2024:
    0        "           \"private\"    - private broadcast; short-lived connection for broadcasting our transactions\n"
    

    vasild commented at 4:28 pm on April 1, 2024:
    It was suggested that this be 6 chars or less: #29415 (review). Feel free to suggest another name.

    sr-gi commented at 8:21 pm on May 6, 2024:
    I’d go with priv, but just my opinion

    vasild commented at 1:08 pm on May 21, 2024:
    Changed to "priv".
  77. in src/net.cpp:2580 in b1f65ba837 outdated
    2560@@ -2493,7 +2561,17 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
    2561             }
    2562         }
    2563 
    2564-        CSemaphoreGrant grant(*semOutbound);
    2565+        MaybePickPrivateBroadcastNetwork(open_private_broadcast_to,
    2566+                                         m_private_broadcast_connections_to_open.load(),
    2567+                                         num_private_broadcast_opened);
    2568+
    2569+        // Don't wait for outbound connection slot to be available if we are going
    


    brunoerg commented at 9:47 am on April 1, 2024:
    Private connections are opened regardless of max connections limits, I think we could cover it in the functional test.

    vasild commented at 4:25 pm on April 1, 2024:
    The current functional test already opens the usual 10 connections and then fires a private broadcast one. Is this what you mean?

    brunoerg commented at 4:27 pm on April 1, 2024:
    Yes, I missed that. Nevermind.
  78. in src/net.cpp:2411 in b1f65ba837 outdated
    2406+ * value. When the function ends if this has value then it will contain the network
    2407+ * to which to open the connection.
    2408+ * @param[in] num_needed Number of private broadcast connections that need to be opened.
    2409+ * @param[in] num_opened Number of private broadcast connections that are currently opened.
    2410+ */
    2411+static void MaybePickPrivateBroadcastNetwork(std::optional<Network>& net, size_t num_needed, size_t num_opened)
    


    brunoerg commented at 12:12 pm on April 1, 2024:
    In b1f65ba837a805dc8f855c02337c1593c77add6d: non-blocker: I think it would be more elegant if MaybePickPrivateBroadcastNetwork returns std::optional<Network> net.

    vasild commented at 4:25 pm on April 1, 2024:
    Done.
  79. vasild force-pushed on Apr 1, 2024
  80. vasild commented at 4:25 pm on April 1, 2024: contributor
    c7e4d5cb2b...413574e81e: rebase and address suggestions
  81. in test/functional/p2p_private_broadcast.py:232 in 413574e81e outdated
    230+        for addr in addresses:
    231+            res = tx_originator.addpeeraddress(address=addr, port=8333, tried=False)
    232+            if res["success"]:
    233+                self.log.debug(f"Added {addr} to tx_originator's addrman")
    234+            else:
    235+                self.log.debug(f"Could not add {addr} to tx_originator's addrman (collision?)")
    


    brunoerg commented at 7:29 pm on April 1, 2024:
    In 413574e81ef4d608b9b0a2079f81a4e90ee2a341: You can use -test=addrman to make addrman deterministic. Running the test several times I realized that sometimes a collision happens, sometimes not.

    vasild commented at 4:39 pm on April 2, 2024:
    Done.
  82. ryanofsky referenced this in commit 3b987d03a4 on Apr 2, 2024
  83. DrahtBot added the label Needs rebase on Apr 2, 2024
  84. vasild force-pushed on Apr 2, 2024
  85. vasild commented at 4:40 pm on April 2, 2024: contributor
    413574e81e...390e628a07: rebase due to conflicts and address suggestion
  86. DrahtBot removed the label Needs rebase on Apr 2, 2024
  87. in src/net_processing.cpp:1110 in 08c017b640 outdated
    1105+    /**
    1106+     * Store a list of transactions to be broadcast privately. Supports the following operations:
    1107+     * - Add a new transaction
    1108+     * - Remove a transaction, after it has been seen by the network
    1109+     * - Mark a broadcast of a transaction (remember when and how many times)
    1110+     * - Get a transaction for broadcast, the one that has been broadcast less times and least recently
    


    andrewtoth commented at 7:59 pm on April 2, 2024:
    0     * - Get a transaction for broadcast, the one that has been broadcast fewer times and least recently
    

    vasild commented at 12:38 pm on April 4, 2024:
    Done
  88. in src/net_processing.cpp:1134 in e03f1b1eb0 outdated
    1129@@ -1122,6 +1130,72 @@ class PeerManagerImpl final : public PeerManager
    1130             }
    1131         }
    1132 
    1133+        /**
    1134+         * Get the transaction that has been broadcast less times and least recently.
    


    andrewtoth commented at 8:34 pm on April 2, 2024:
    0         * Get the transaction that has been broadcast fewest times and least recently.
    

    vasild commented at 12:38 pm on April 4, 2024:
    Done
  89. in src/net_processing.cpp:1264 in e03f1b1eb0 outdated
    1225         };
    1226 
    1227+        /**
    1228+         * Get iterators in `m_by_txid` and `m_by_priority` for a given transaction.
    1229+         */
    1230+        std::optional<Iterators> Find(const Txid& txid) EXCLUSIVE_LOCKS_REQUIRED(m_mutex)
    


    andrewtoth commented at 8:43 pm on April 2, 2024:
    nit: This could be const method?

    vasild commented at 12:30 pm on April 4, 2024:
    Indeed the Find() method does not modify any of the members. I tried to make it const, but it has to return non-const iterators to the members… :(
  90. in src/net_processing.cpp:1742 in e03f1b1eb0 outdated
    1755     if (fLogIPs) {
    1756-        LogPrint(BCLog::NET, "send version message: version %d, blocks=%d, them=%s, txrelay=%d, peer=%d\n", PROTOCOL_VERSION, nNodeStartingHeight, addr_you.ToStringAddrPort(), tx_relay, nodeid);
    1757+        LogPrint(BCLog::NET, "send version message: version %d, blocks=%d, them=%s, txrelay=%d, peer=%d\n", PROTOCOL_VERSION, my_height, your_addr.ToStringAddrPort(), my_tx_relay, nodeid);
    1758     } else {
    1759-        LogPrint(BCLog::NET, "send version message: version %d, blocks=%d, txrelay=%d, peer=%d\n", PROTOCOL_VERSION, nNodeStartingHeight, tx_relay, nodeid);
    1760+        LogPrint(BCLog::NET, "send version message: version %d, blocks=%d, txrelay=%d, peer=%d\n", PROTOCOL_VERSION, my_height, my_tx_relay, nodeid);
    


    andrewtoth commented at 8:55 pm on April 2, 2024:
    Since we’re touching these lines, maybe update to LogPrintLevel?

    vasild commented at 12:38 pm on April 4, 2024:
    Done
  91. andrewtoth commented at 9:56 pm on April 2, 2024: contributor
    Do we need test coverage for retrying stale txs and having multiple concurrent txs queued for private broadcast?
  92. vasild force-pushed on Apr 4, 2024
  93. vasild commented at 12:39 pm on April 4, 2024: contributor

    390e628a07...9297437af2: rebase and address suggestions

    Do we need test coverage for retrying stale txs and having multiple concurrent txs queued for private broadcast?

    Yes.

  94. DrahtBot removed the label CI failed on Apr 4, 2024
  95. in src/net.cpp:2527 in fe57027b3e outdated
    2499@@ -2440,6 +2500,11 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
    2500         LogPrintf("Fixed seeds are disabled\n");
    2501     }
    2502 
    2503+    // Private broadcast connections are opened with priority over others, but only half
    2504+    // of the time to avoid depriving other connection types if private broadcast is
    2505+    // needed but opening such connections is unsuccessful for some reason.
    


    nothingmuch commented at 10:34 am on April 7, 2024:

    With regards to this comment and:

    We pick a random Tor (or I2P) peer.

    To increase success rate and reduce latency, clearnet peers could be also be selected when using Tor, without harming privacy of broadcast…

    Clearnet connections only need one Tor circuit, whereas hidden service connections which require two circuits for the connecting peer and one from the accepting peer.

    Furthermore, if SOCKS proxy credentials are reused for the broadcast of a single transaction (but not all transactions in the private broadcast queue), multiple connections could share the same circuit/exit node. This specific optimization does introduce a censorship risk (even if BIP 324 is used [edit: due] to the MITM consideration) and there precedent for such malicious exit nodes.

    From a privacy standpoint both optimizations would be more or less equivalent to broadcasting through isolated connections to peers with hidden service addresses, since the origin is still protected and being able to link these connections does not aid in deanonymization since these connections all pertain to the same data.

    The main rationale for these optimizations is that Tor daemon builds circuits sequentially, and does not keep many built circuits in reserve (IIRC 2 unless their construction is explicitly requested through the control socket), so when SOCKS connections are opened with distinct credentials the connection establishment and time to first byte will typically scale linearly in the order that the SOCKS connections are accepted by tor daemon itself, and since building a new circuit can take on the order of seconds.


    vasild commented at 5:23 pm on April 23, 2024:
    I added broadcast to IPv4 and IPv6 peers through the Tor proxy. Thanks!
  96. in test/functional/p2p_private_broadcast.py:68 in 9297437af2 outdated
    63+NUM_PRIVATE_BROADCAST_PER_TX = 5
    64+
    65+# Fill addrman with these addresses. Must have enough Tor addresses, so that even
    66+# if all 10 default connections are opened to a Tor address (!?) there must be more
    67+# for private broadcast.
    68+addresses = [
    


    brunoerg commented at 9:48 am on April 8, 2024:

    In 9297437af2c2f45a7b9a12be78fa0e597c54ac79: Since the addrman is deterministic now, you can remove addresses that are conflicting.

    0--- a/test/functional/p2p_private_broadcast.py
    1+++ b/test/functional/p2p_private_broadcast.py
    2@@ -118,7 +118,6 @@ addresses = [
    3     "bsqbtctulf2g4jtjsdfgl2ed7qs6zz5wqx27qnyiik7laockryvszqqd.onion",
    4     "cwi3ekrwhig47dhhzfenr5hbvckj7fzaojygvazi2lucsenwbzwoyiqd.onion",
    5     "devinbtcmwkuitvxl3tfi5of4zau46ymeannkjv6fpnylkgf3q5fa3id.onion",
    6-    "devinbtctu7uctl7hly2juu3thbgeivfnvw3ckj3phy6nyvpnx66yeyd.onion",
    7     "devinbtcyk643iruzfpaxw3on2jket7rbjmwygm42dmdyub3ietrbmid.onion",
    8     "dtql5vci4iaml4anmueftqr7bfgzqlauzfy4rc2tfgulldd3ekyijjyd.onion",
    9     "emzybtc25oddoa2prol2znpz2axnrg6k77xwgirmhv7igoiucddsxiad.onion",
    

    brunoerg commented at 12:37 pm on April 8, 2024:

    Also, maybe we do not need this verification anymore?

    0if res["success"]:
    1    self.log.info(f"Added {addr} to tx_originator's addrman")
    2else:
    3    self.log.info(f"Could not add {addr} to tx_originator's addrman (collision?)")
    

    vasild commented at 5:22 pm on April 23, 2024:
    Right, removed the conflicting address and reduced the logging to log only if adding to the addrman fails.
  97. nothingmuch commented at 10:07 am on April 8, 2024: contributor

    cACK, with a suggestion and some observations on the design space WRT reliability/success.

    Yes, if -proxyrandomize=1 (the default). This is the same in master - separate Tor circuit per connection.

    If this is not the case and a peer to which a long lived connection already exists is selected again, then tor will multiplex both connections on the same established circuit, which means that the two connections are unambiguously correlated.

    Avoiding already connected peers would work around this, but perhaps it’s sufficient just to warn on startup if privatebroadcast=1 and proxyrandomize=0, since that is unlikely to be configured (requires opt out & opt in), and unlikely to be an issue even if it is configured since a malicious peer would need to be selected twice.

    This would be less of a concern if clearnet hosts are also allowed, since the likelyhood of selecting the same peer diminishes quadratically in the size of the set of potential peers.

    I assume you are restricting the feature to sendraw so the wallet doesn’t get involved and try something silly like rebroadcasting over clearnet or inserting in to our own mempool?

    The reason to omit the wallet is that it currently does not count non-mempool change.

    If I understand correctly, the concern with rebroadcasting still applies, but in the case that private broadcast was successful?

  98. vasild force-pushed on Apr 23, 2024
  99. vasild commented at 5:21 pm on April 23, 2024: contributor

    9297437af2...cc760207b8: rebase and address suggestions:

    • Give a startup warning if -privatebroadcast=1, -proxyrandomize=0 and the Tor network is reachable (i.e. we will use Tor for private broadcast).
    • Enforce -walletbroadcast=0 if -privatebroadcast=1 because it would be confusing to have the wallet do the traditional broadcast while the sendrawtransaction RPC does a private broadcast. Furthermore if a wallet transaction is sent via sendrawtransaction and ends up in the mempool from outside and is not mined for some time, then the wallet will try to broadcast it using the traditional mechanism.
    • Private broadcast also to IPv4 and IPv6 peers (!) through the Tor proxy.
    • Remove conflicting Tor address from the functional test and don’t log every address added to addrman.
  100. vasild commented at 5:27 pm on April 23, 2024: contributor

    Avoiding already connected peers would work around this, but perhaps it’s sufficient just to warn on startup if privatebroadcast=1 and proxyrandomize=0

    In master we already avoid connecting to an already connected address, regardless of the connection type:

    https://github.com/bitcoin/bitcoin/blob/256e1703197fdddd78bc6d659431cd0fc3b63cde/src/net.cpp#L2862

    I added a startup warning anyway.

    I assume you are restricting the feature to sendraw so the wallet doesn’t get involved and try something silly like rebroadcasting over clearnet or inserting in to our own mempool?

    The reason to omit the wallet is that it currently does not count non-mempool change.

    If I understand correctly, the concern with rebroadcasting still applies, but in the case that private broadcast was successful?

    Yes, I enforced walletbroadcast=0 to avoid the wallet rebroadcasting.

  101. DrahtBot added the label CI failed on Apr 23, 2024
  102. vasild force-pushed on Apr 24, 2024
  103. vasild commented at 7:30 am on April 24, 2024: contributor
    cc760207b8...ea1ca3715e: adjust feature_config_args.py after forbidding -walletbroadcast when -privatebroadcast is enabled.
  104. vasild force-pushed on Apr 24, 2024
  105. DrahtBot removed the label CI failed on Apr 24, 2024
  106. in src/wallet/init.cpp:122 in 1407b3fa26 outdated
    119@@ -120,6 +120,12 @@ bool WalletInit::ParameterInteraction() const
    120         LogPrintf("%s: parameter interaction: -blocksonly=1 -> setting -walletbroadcast=0\n", __func__);
    121     }
    122 
    123+    if (gArgs.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST) &&
    


    mzumsande commented at 9:25 pm on April 24, 2024:
    have you considered using SoftSetBoolArg to set -walletbroadcast to 0 if -privatebroadcast was chosen? It’s not the most user-friendly thing to introduce a new command arg that would only work if another arg was also changed from its default.

    vasild commented at 1:46 pm on May 1, 2024:

    Yes, but I did not like the behavior of the wallet “unexpectedly” stopping to broadcast its own transactions because -privatebroadcast is enabled.

    It’s not the most user-friendly thing to introduce a new command arg that would only work if another arg was also changed from its default.

    Yes, I agree. I do not like that either but somewhat prefer it over automatically switching off wallet broadcast. I can imagine GUI users who enable -privatebroadcast and still try to send transactions from their wallets and have no clue why they are not being mined.

    Leaving this open to discussion and proposals.


    sr-gi commented at 8:18 pm on May 6, 2024:

    I was pretty confused about this, maybe because I misunderstood the proposal. By just reading what the help for -privatebroadcast I wouldn’t assume that transactions made using the wallet won’t be broadcast. My understanding was that anything done via the wallet would work as usual, but that transactions sent via sendrawtransaction would use the private broadcast mechanism if set.

    I think this should be mentioned in the help of -privatebroadcast.


    pinheadmz commented at 5:19 pm on June 26, 2024:

    I wonder if the option should be renamed -privaterpcbroadcast for now, and we can add -privatewalletbroadcast later.

    We also won’t be able to prevent users from using their wallet with sendrawtransaction anyway. So if the goal here is to prevent accidental privacy leaks from confused users, we might want to consider “hiding” the option or restricting it to test networks until wallet integration is complete.


    vasild commented at 12:28 pm on July 1, 2024:

    I thought about this further and removed the requirement to set -walletbroadcast=0 when -privatebroadcast=1. The description of -privatebroadcast begins with

    Broadcast transactions submitted via sendrawtransaction RPC using short lived connections…

    so it should be clear that this applies to the sendrawtransaction RPC. In addition I extended the text with

    Transactions submitted through the wallet are not affected by this option


    vasild commented at 9:31 am on November 8, 2024:

    Marking this as resolved because I removed the requirement to set -walletbroadcast=0 and extended the -privatebroadcast doc.

    My understanding was that anything done via the wallet would work as usual, but that transactions sent via sendrawtransaction would use the private broadcast mechanism if set

    Yes, this is exactly how it works now.

  107. in src/net.cpp:406 in 6b4693ee85 outdated
    397+    }
    398+    Proxy tor_proxy;
    399+    const std::string onion_arg{gArgs.GetArg("-onion", "")};
    400+    if (onion_arg != "" && onion_arg == "0" && GetProxy(NET_ONION, tor_proxy)) {
    401+        // Use the Tor proxy (if provided) for private broadcast connections to clearnet peers.
    402+        // Note: we check whether -onion is explicitly used because GetProxy(NET_ONION) may
    


    mzumsande commented at 5:12 pm on April 25, 2024:
    I think this requires an update to tor.md, which currently says: -onion=ip:port Set the proxy server to use for Tor onion services. You do not need to set this if it's the same as -proxy. After this PR, specifiying it explicitly is necessary if you want to do private broadcast to clearnet peers.

    vasild commented at 1:47 pm on May 1, 2024:
    Updated.

    sr-gi commented at 5:15 pm on May 9, 2024:
    Is there no better way of checking whether -proxy is an actual Tor proxy or just a regular SOCKS5 proxy? It feels like the amount of custom settings that this will require to run will make it hard to use

    vasild commented at 7:35 am on May 21, 2024:
    Hmm, maybe we can keep track of whether we have successfully managed to connect to at least one .onion address via the -proxy, then we can assume that is a Tor proxy? How does that sound?

    vasild commented at 4:26 pm on May 22, 2024:

    Is there no better way of checking whether -proxy is an actual Tor proxy or just a regular SOCKS5 proxy?

    maybe we can keep track of whether we have successfully managed to connect to at least one .onion address

    Done in e85cc59bad...057c79365c, see #29415 (comment), also dropped the change in tor.md because that is not needed anymore.

    Looks better now, thank you!

  108. in src/net.cpp:400 in 6b4693ee85 outdated
    395+    if (clearnet != NET_IPV4 && clearnet != NET_IPV6) {
    396+        return std::nullopt;
    397+    }
    398+    Proxy tor_proxy;
    399+    const std::string onion_arg{gArgs.GetArg("-onion", "")};
    400+    if (onion_arg != "" && onion_arg == "0" && GetProxy(NET_ONION, tor_proxy)) {
    


    mzumsande commented at 5:13 pm on April 25, 2024:
    You probably meant onion_arg != "0"

    vasild commented at 1:47 pm on May 1, 2024:
    Yes, fixed!
  109. in src/net.cpp:3985 in ae6651d267 outdated
    3913+        msg.m_type != NetMsgType::VERACK &&
    3914+        msg.m_type != NetMsgType::TX &&
    3915+        msg.m_type != NetMsgType::PING) {
    3916+        // Ensure private broadcast connections only send the above message types.
    3917+        // Others are not needed and may degrade privacy.
    3918+        LogPrintLevel(BCLog::PRIVATE_BROADCAST,
    


    mzumsande commented at 8:55 pm on April 25, 2024:
    It looks like we make sure in net_processing to not send any of theses messages, and here we check again. I haven’t thought about this long, but can this log be triggered, or could this be an Assume(false) instead?

    vasild commented at 1:48 pm on May 1, 2024:
    Added Assume(false) and a comment.

    vasild commented at 3:59 pm on May 1, 2024:
    This was triggered in the fuzz tests. I could complicate the fuzz tests to avoid sending unwanted messages if the random node happens to be a private broadcast, but CConnman::PushMessage() is called from a lot of places including from the RPC and that may change in the future as well. It is somewhat hard to judge that nobody will call PushMessage() with an unwanted message. Thus I think it is better to assume that this is the real check here in CConnman::PushMessage() and that the one in PeerManagerImpl::SendMessages() is an optimization. Removed the Assume(false).
  110. DrahtBot added the label Needs rebase on Apr 25, 2024
  111. in src/net_processing.cpp:1155 in 4a5433c3c8 outdated
    1150+        };
    1151+
    1152+        mutable Mutex m_mutex;
    1153+        ByTxid m_by_txid GUARDED_BY(m_mutex);
    1154+        ByPriority m_by_priority GUARDED_BY(m_mutex);
    1155+    } m_tx_for_private_broadcast;
    


    mzumsande commented at 9:20 pm on April 25, 2024:
    This class is getting pretty large in the end, maybe put it into its own module (similar to TxReconciliationTracker or TxRequestTracker)

    vasild commented at 1:48 pm on May 1, 2024:
    Yes, I was thinking the same. Moved away.
  112. in test/functional/p2p_private_broadcast.py:208 in fd603b8f5c outdated
    203+
    204+                self.socks5_server.conf.destinations[i]["node"] = listener
    205+
    206+        self.extra_args = [
    207+            [
    208+                "-peerbloomfilters", # needed to test replies to MEMPOOL
    


    mzumsande commented at 10:23 pm on April 25, 2024:
    I don’t think is needed, MEMPOOL is never tested.

    vasild commented at 1:48 pm on May 1, 2024:
    Removed.
  113. in src/rpc/mempool.cpp:115 in 180e51a723 outdated
     93@@ -91,11 +94,23 @@ static RPCHelpMan sendrawtransaction()
     94             std::string err_string;
     95             AssertLockNotHeld(cs_main);
     96             NodeContext& node = EnsureAnyNodeContext(request.context);
     97+            const bool private_broadcast_enabled{gArgs.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)};
     98+            if (private_broadcast_enabled &&
     99+                !g_reachable_nets.Contains(NET_ONION) &&
    100+                !g_reachable_nets.Contains(NET_I2P)) {
    101+                throw JSONRPCError(RPC_MISC_ERROR,
    102+                                   "-privatebroadcast is enabled, but none of the Tor or I2P networks is "
    


    mzumsande commented at 10:30 pm on April 25, 2024:
    sendrawtransaction doc should be updated (it already mentions privacy issues).

    vasild commented at 1:48 pm on May 1, 2024:
    Updated.
  114. mzumsande commented at 10:34 pm on April 25, 2024: contributor

    Did another round of review.

    nit: local tx relay -> private broadcast in last commit message

  115. in src/net.cpp:3102 in 6b4693ee85 outdated
    3029+{
    3030+    size_t before{m_private_broadcast_connections_to_open.load()};
    3031+    size_t desired;
    3032+    do {
    3033+        desired = before > n ? before - n : 0;
    3034+    } while (!m_private_broadcast_connections_to_open.compare_exchange_weak(before, desired));
    


    brunoerg commented at 12:22 pm on April 30, 2024:
    In 6b4693ee859d6bddf559a0b32749b1dafe015ef4 “net: implement opening PRIVATE_BROADCAST connections”: Just a question: why using compare_exchange_weak instead of fetch_sub?

    vasild commented at 1:49 pm on May 1, 2024:
    To avoid negative values. I added a comment in the source code to explain.
  116. vasild force-pushed on May 1, 2024
  117. vasild commented at 1:40 pm on May 1, 2024: contributor
    fd603b8f5c...42cb080600: rebase and address suggestions
  118. vasild force-pushed on May 1, 2024
  119. DrahtBot removed the label Needs rebase on May 1, 2024
  120. vasild force-pushed on May 1, 2024
  121. DrahtBot added the label CI failed on May 1, 2024
  122. DrahtBot removed the label CI failed on May 1, 2024
  123. in src/net.h:75 in 0ba30dec08 outdated
    70@@ -71,6 +71,8 @@ static const int MAX_ADDNODE_CONNECTIONS = 8;
    71 static const int MAX_BLOCK_RELAY_ONLY_CONNECTIONS = 2;
    72 /** Maximum number of feeler connections */
    73 static const int MAX_FEELER_CONNECTIONS = 1;
    74+/** Maximum number of private broadcast connections */
    75+static constexpr size_t MAX_PRIVATE_BROADCAST_CONNECTIONS{64};
    


    sr-gi commented at 2:48 pm on May 9, 2024:

    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd

    What is the rationale for 64? That seems like a lot of connections.


    vasild commented at 5:06 pm on May 20, 2024:
    It could be lower. I picked 64 because I imagine somebody submitting 100s or even 1000s of transactions at the same time. Processing those serially would take a lot of time. Would you suggest another number?
  124. in src/net.cpp:390 in 0ba30dec08 outdated
    385@@ -386,6 +386,27 @@ static CAddress GetBindAddress(const Sock& sock)
    386     return addr_bind;
    387 }
    388 
    389+/**
    390+ * Check if private broadcast can be done to clearnet peers and if yes via which proxy.
    


    sr-gi commented at 5:17 pm on May 9, 2024:

    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd:

    nit: if yes -> if so


    vasild commented at 1:09 pm on May 21, 2024:
    Done
  125. in src/net.cpp:393 in 0ba30dec08 outdated
    385@@ -386,6 +386,27 @@ static CAddress GetBindAddress(const Sock& sock)
    386     return addr_bind;
    387 }
    388 
    389+/**
    390+ * Check if private broadcast can be done to clearnet peers and if yes via which proxy.
    391+ * If private broadcast connections should not be opened to `net`, then this will return an empty optional.
    392+ */
    393+static std::optional<Proxy> ProxyForClearnetPrivateBroadcast(Network clearnet)
    


    sr-gi commented at 5:19 pm on May 9, 2024:

    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd:

    nit: sorry for bikeshedding clearnet doesn’t seem the best name for this. Maybe net or network?


    sr-gi commented at 5:20 pm on May 9, 2024:
    Also, this is already referenced as net in the docs

    vasild commented at 1:10 pm on May 21, 2024:
    I removed the argument to this function and renamed it to ProxyForIPv4or6PrivateBroadcast(). It is a bit more simpler now.
  126. in src/net.cpp:478 in 0ba30dec08 outdated
    483+                const auto proxy_opt{ProxyForClearnetPrivateBroadcast(addrConnect.GetNetwork())};
    484+                if (proxy_opt.has_value()) {
    485+                    use_proxy = true;
    486+                    proxy = proxy_opt.value();
    487+                }
    488+            }
    


    sr-gi commented at 5:37 pm on May 9, 2024:

    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd

    The way you are defining use_proxy could lead to proxy_opt having no value, yet use_proxy being true

    0            bool use_proxy;
    1            if (conn_type == ConnectionType::PRIVATE_BROADCAST) {
    2                const auto proxy_opt{ProxyForClearnetPrivateBroadcast(addrConnect.GetNetwork())};
    3                if (proxy_opt.has_value()) {
    4                    use_proxy = true;
    5                    proxy = proxy_opt.value();
    6                }
    7            } else {
    8                use_proxy = GetProxy(addrConnect.GetNetwork(), proxy);
    9            }
    

    sr-gi commented at 5:39 pm on May 9, 2024:
    use_proxy can even be defined right after proxy (a few lined above) for simplicity

    vasild commented at 10:36 am on May 21, 2024:

    proxy_opt exists only inside the if. What matters after this if are the values of use_proxy and proxy. I think your suggestion is equivalent to the current code. I.e.

    0set use_proxy and proxy (normal)
    1if (special case)
    2    replace use_proxy and proxy with special values
    

    vs

    0if (special case)
    1    set use_proxy and proxy to special values
    2else
    3    set use_proxy and proxy (normal)
    

    Right?

  127. in src/net.cpp:488 in 0ba30dec08 outdated
    494 
    495-                if (m_i2p_sam_session) {
    496+                // If an I2P SAM session already exists, normally we would re-use it. But in the case of
    497+                // private broadcast we force a new transient session. A Connect() using m_i2p_sam_session
    498+                // would use our permanent I2P address as a source address.
    499+                if (m_i2p_sam_session && conn_type != ConnectionType::PRIVATE_BROADCAST) {
    


    sr-gi commented at 5:57 pm on May 9, 2024:

    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd

    I’m confused here. We choose not to use our permanent m_i2p_sam_session to avoid linking this to our permanent I2P ID, however, we use a pool of transient I2P sessions (m_unused_i2p_sessions) instead. I understand these transient sessions can be reused (or at least that’s what I get from the docs of m_unused_i2p_sessions). So if the same (peer, session_id) pair is picked, the node will know that we created both transactions, wouldn’t it?


    vasild commented at 10:47 am on May 21, 2024:

    The transient sessions are not reused. m_unused_i2p_sessions description reads:

    If connecting to a host fails, then the created session is put to this pool for reuse.

    The code is this:

    https://github.com/bitcoin/bitcoin/blob/a786fd2041913d82ca90b561de309421bd24e41b/src/net.cpp#L472-L480

    I.e. pop/extract a session from m_unused_i2p_sessions and if we cannot connect, then we did not use the session and thus put it back to m_unused_i2p_sessions.

    Do you think that the description of m_unused_i2p_sessions can be improved? Would you suggest an exact wording?

  128. in src/net.cpp:2686 in 0ba30dec08 outdated
    2742+        // * BLOCK_RELAY if it's time to try an extra block-relay-only peer (to confirm our tip is current)
    2743+        // * FEELER if it's time to try a feeler
    2744+        // * else retry the loop (sleep a bit and start from the top of this list)
    2745+
    2746+        if (open_private_broadcast_to.has_value()) {
    2747+            conn_type = ConnectionType::PRIVATE_BROADCAST;
    


    sr-gi commented at 6:21 pm on May 9, 2024:

    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd

    I don’t think we should be giving PRIVATE_BROADCAST priority over every other type of connection. At the very least, they should go after BLOCK_RELAY.

    Having connections that keep us up to date should be more important than sending out our own stuff


    vasild commented at 11:02 am on May 21, 2024:

    A few considerations:

    • Private broadcast connections are not blocked on the semOutbound grant. They come to this code right away.

    • Even if there is a high demand for private broadcast connections, we would try to open such ones only half of the time in CConnman::ThreadOpenConnections() to avoid such starvation of only opening private broadcast and not others for a long time (see how MaybePickPrivateBroadcastNetwork() yields to other connection types if we previously opened a private broadcast one).

    • Non-BLOCK_RELAY connections keep us up to date as well (with new blocks). I.e blocks could come via inbound, full-outbound, etc.

    Given the above, I think the current code does not need to be changed.

  129. in src/net.cpp:2497 in 0ba30dec08 outdated
    2482@@ -2452,6 +2483,72 @@ bool CConnman::MaybePickPreferredNetwork(std::optional<Network>& network)
    2483     return false;
    2484 }
    2485 
    2486+/**
    2487+ * Decide whether to open a private broadcast connection and if yes, to which network.
    2488+ * @param[in] prev_was_private_broadcast Whether the previous attempt to open a connection
    2489+ * was an attempt to open a private broadcast connection (successful or not).
    2490+ * @param[in] num_needed Number of private broadcast connections that need to be opened.
    


    sr-gi commented at 7:20 pm on May 9, 2024:

    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd

    This took me a while to grasp. num_needed is set to m_private_broadcast_connections_to_open.load(), which is only set to anything above 0 if there are transactions pending to be broadcast.

    I think it’d be good to clarify this here, to make it clear that we won’t try to open a connection of this type if we have no transactions pending


    vasild commented at 11:38 am on May 21, 2024:
    Added this sentence: “We try to open a connection of this type only if there are transactions pending, but up to a limit.”
  130. in src/net_processing.cpp:182 in 434a20371e outdated
    179@@ -179,6 +180,9 @@ static constexpr size_t MAX_ADDR_PROCESSING_TOKEN_BUCKET{MAX_ADDR_TO_SEND};
    180 /** The compactblocks version we support. See BIP 152. */
    181 static constexpr uint64_t CMPCTBLOCKS_VERSION{2};
    182 
    


    sr-gi commented at 2:27 pm on May 10, 2024:

    In 434a20371e441660147da6d0c6c1832cb0d0073b

    nit: remove line break


    vasild commented at 11:40 am on May 21, 2024:
    Removed.
  131. in src/net_processing.cpp:183 in 434a20371e outdated
    179@@ -179,6 +180,9 @@ static constexpr size_t MAX_ADDR_PROCESSING_TOKEN_BUCKET{MAX_ADDR_TO_SEND};
    180 /** The compactblocks version we support. See BIP 152. */
    181 static constexpr uint64_t CMPCTBLOCKS_VERSION{2};
    182 
    183+/** For private broadcast, send a transaction to this many peers per one broadcast attempt. */
    


    sr-gi commented at 2:28 pm on May 10, 2024:

    In 434a20371e441660147da6d0c6c1832cb0d0073b

    nit: per one -> for each


    vasild commented at 11:42 am on May 21, 2024:
    Done
  132. in src/net_processing.cpp:1058 in 434a20371e outdated
    1179@@ -1175,6 +1180,8 @@ class PeerManagerImpl final : public PeerManager
    1180 
    1181     void AddAddressKnown(Peer& peer, const CAddress& addr) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex);
    1182     void PushAddress(Peer& peer, const CAddress& addr) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex);
    1183+
    


    sr-gi commented at 2:29 pm on May 10, 2024:

    In 434a20371e441660147da6d0c6c1832cb0d0073b

    This is missing docs


    vasild commented at 11:44 am on May 21, 2024:
    Added “A list of transactions to be broadcast privately.”
  133. in src/net_processing.cpp:2459 in 434a20371e outdated
    2352+{
    2353+    m_tx_for_private_broadcast.Add(tx);
    2354+
    2355+    LogPrintLevel(BCLog::PRIVATE_BROADCAST,
    2356+                  BCLog::Level::Debug,
    2357+                  "Requesting %d new connections due to txid=%s, wtxid=%s\n",
    


    sr-gi commented at 2:31 pm on May 10, 2024:

    In 434a20371e441660147da6d0c6c1832cb0d0073b

    I don’t follow this log. What is the “due to …” trying to convey here?


    vasild commented at 11:47 am on May 21, 2024:
    “due to” = “because of” = “because there is a new transaction for broadcast - txid=…”. I am open to suggestions for better wording.
  134. in src/private_broadcast.h:26 in 434a20371e outdated
    17+/**
    18+ * Store a list of transactions to be broadcast privately. Supports the following operations:
    19+ * - Add a new transaction
    20+ * - Remove a transaction, after it has been seen by the network
    21+ * - Mark a broadcast of a transaction (remember when and how many times)
    22+ * - Get a transaction for broadcast, the one that has been broadcast fewer times and least recently
    


    sr-gi commented at 2:37 pm on May 10, 2024:

    In 434a20371e441660147da6d0c6c1832cb0d0073b

    nit: I don’t think these belong to this commit yet


    vasild commented at 11:54 am on May 21, 2024:
    Right! Moved to the corresponding commits.
  135. in src/private_broadcast.cpp:1 in 434a20371e outdated
    0@@ -0,0 +1,23 @@
    1+// Copyright (c) 2023-present The Bitcoin Core developers
    


    sr-gi commented at 2:41 pm on May 10, 2024:

    In 434a20371e441660147da6d0c6c1832cb0d0073b

    nit: 2024


    vasild commented at 11:55 am on May 21, 2024:
    Some of the code was written in 2023
  136. in src/private_broadcast.h:1 in 434a20371e outdated
    0@@ -0,0 +1,55 @@
    1+// Copyright (c) 2023-present The Bitcoin Core developers
    


    sr-gi commented at 2:41 pm on May 10, 2024:

    In 434a20371e441660147da6d0c6c1832cb0d0073b

    nit: 2024


    vasild commented at 11:55 am on May 21, 2024:
    Same, some of the code was written in 2023
  137. in src/net_processing.cpp:1519 in fecab592de outdated
    1650+    int64_t my_time;
    1651+    uint64_t your_services;
    1652+    CService your_addr;
    1653+    std::string my_user_agent;
    1654+    int my_height;
    1655+    bool my_tx_relay;
    


    sr-gi commented at 4:12 pm on May 10, 2024:

    In fecab592de8d0994e6d61e454e118d652ffd7b93

    micro-nit: There is no your_ counterparty for this, so there’s no need to call them my_. This is kind of assumed


    vasild commented at 12:00 pm on May 21, 2024:
    I prefixed everything with my_ or your_ because otherwise it is unclear whether it is mine or yours. Deducting that without my/yours prefix requires an extra effort.
  138. in src/net_processing.cpp:5290 in fecab592de outdated
    5281@@ -5155,6 +5282,17 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    5282                     if (ping_time.count() >= 0) {
    5283                         // Let connman know about this successful ping-pong
    5284                         pfrom.PongReceived(ping_time);
    5285+                        if (pfrom.IsPrivateBroadcastConn()) {
    5286+                            m_tx_for_private_broadcast.BroadcastEnd(pfrom.GetId(), /*confirmed_by_node=*/true);
    5287+                            LogPrintLevel(
    5288+                                BCLog::PRIVATE_BROADCAST,
    5289+                                BCLog::Level::Info,
    5290+                                "Got a PONG (probably the transaction will reach the network), "
    


    sr-gi commented at 4:22 pm on May 10, 2024:

    In fecab592de8d0994e6d61e454e118d652ffd7b93

    nit:

    0                                "Got a PONG (the transaction will probably reach the network), "
    

    vasild commented at 12:09 pm on May 21, 2024:
    Done
  139. in src/net_processing.cpp:1847 in fecab592de outdated
    1842+    if (node.IsPrivateBroadcastConn() &&
    1843+        m_tx_for_private_broadcast.BroadcastEnd(nodeid, /*confirmed_by_node=*/false)) {
    1844+
    1845+        LogPrintLevel(BCLog::PRIVATE_BROADCAST,
    1846+                      BCLog::Level::Info,
    1847+                      "Never got a PONG (probably the transaction will not "
    


    sr-gi commented at 4:23 pm on May 10, 2024:

    In fecab592de8d0994e6d61e454e118d652ffd7b93

    0                      "Never got a PONG (the transaction will probably not "
    

    vasild commented at 12:09 pm on May 21, 2024:
    Done
  140. in src/private_broadcast.h:50 in fecab592de outdated
    45+    /**
    46+     * Mark the end of a broadcast of a transaction. Either successful by receiving a PONG,
    47+     * or unsuccessful by closing the connection to the node without getting PONG.
    48+     * @return true if the reference by the given node id was removed
    49+     */
    50+    bool BroadcastEnd(const NodeId& nodeid, bool confirmed_by_node) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
    


    sr-gi commented at 4:49 pm on May 10, 2024:

    In fecab592de8d0994e6d61e454e118d652ffd7b93

    FinishBroadcast may sound better


    vasild commented at 12:10 pm on May 21, 2024:
    Renamed to FinishBroadcast().
  141. in src/private_broadcast.cpp:65 in fecab592de outdated
    60+
    61+    ++priority.num_broadcasted;
    62+    priority.last_broadcasted = GetTime<std::chrono::microseconds>();
    63+
    64+    m_by_priority.erase(iters->by_priority);
    65+    m_by_priority.emplace(priority, iters->by_txid->first);
    


    sr-gi commented at 5:08 pm on May 10, 2024:

    In fecab592de8d0994e6d61e454e118d652ffd7b93

    Isn’t iters->by_txid->first the same as txid? It would read way simpler by using the latter


    sr-gi commented at 5:11 pm on May 10, 2024:
    Also, excuse my lack of cpp proficiency but, is it necessary to delete and re-add the item? Why is not updating priority enough (as done in the above two lines)?

    vasild commented at 12:38 pm on May 21, 2024:

    Yes, iters->by_txid->first is the same as txid, changed it to use txid which is shorter.

    It is necessary to remove and re-add because we have changed the key and C++ maps do not support modifying of the keys. That is, changing the key means that the entry will have to be in another place in the map, so it has to be removed from the old place and added to the new one.

  142. in src/net_processing.cpp:4768 in bb697ba5f5 outdated
    4667+                          pfrom.GetId(),
    4668+                          fLogIPs ? strprintf(", peeraddr=%s", pfrom.addr.ToStringAddrPort()) : "");
    4669+            if (NUM_PRIVATE_BROADCAST_PER_TX > num_broadcasted.value()) {
    4670+                // Not all of the initial NUM_PRIVATE_BROADCAST_PER_TX connections were needed.
    4671+                // Tell CConnman it does not need to start the remaining ones.
    4672+                m_connman.PrivateBroadcastSub(NUM_PRIVATE_BROADCAST_PER_TX - num_broadcasted.value());
    


    sr-gi commented at 7:28 pm on May 10, 2024:

    In bb697ba5f51f5e31ec472f32682fbb6eb692fa37

    AFAICT this is the only use of CConnman::PrivateBroadcastSub, meaning we should be able to simplify the method by unconditionally setting m_private_broadcast_connections_to_open to 0


    vasild commented at 12:58 pm on May 21, 2024:

    Yes, if there is only one transaction submitted for broadcast. But if there is more than one, then m_private_broadcast_connections_to_open should not be set to 0.

    For example:

    • 3 transactions are submitted at approximately the same time: tx1, tx2, tx3.
    • NUM_PRIVATE_BROADCAST_PER_TX=5 for each one, so m_private_broadcast_connections_to_open is set to 15.
    • tx1 is broadcast 2 times, tx2 once and tx3 once, m_private_broadcast_connections_to_open is now 15-2-1-1=11.
    • tx1 is received back from the network. We come to this code and see that tx1 has been broadcast two times and deduce that the other 5-2=3 we created are not needed and call PrivateBroadcastSub(3). m_private_broadcast_connections_to_open should be set to 11-3=8 (4 for tx2 and 4 for tx3), not 0.
  143. sr-gi commented at 2:57 pm on May 13, 2024: member

    Code review up to 28adad8eefd18a62232b5c77cbc9ca739a53308d (no tests so far).

    The main thing I’m wondering currently is why are we tacking m_private_broadcast_connections_to_open loosely? It feels harder to reason about, but I don’t see what the benefit of it is.

  144. in src/net.h:1517 in 0ba30dec08 outdated
    1513@@ -1498,6 +1514,7 @@ class CConnman
    1514     int m_max_outbound_block_relay;
    1515 
    1516     int m_max_addnode{MAX_ADDNODE_CONNECTIONS};
    1517+    int m_max_private_broadcast{MAX_PRIVATE_BROADCAST_CONNECTIONS};
    


    brunoerg commented at 3:12 pm on May 13, 2024:
    In 0ba30dec085f860c89f4fd8c8a398aa8a13b5ebd “net: implement opening PRIVATE_BROADCAST connections”: m_max_private_broadcast is not used.

    vasild commented at 1:04 pm on May 21, 2024:
    Right! Removed.
  145. vasild force-pushed on May 21, 2024
  146. vasild commented at 1:18 pm on May 21, 2024: contributor

    65ba8d0203...e85cc59bad: rebase, address suggestions and remove the argument of ProxyForClearnetPrivateBroadcast() because it is always IPv4 or IPv6 and we treat both in the same way.

    The main thing I’m wondering currently is why are we tacking m_private_broadcast_connections_to_open loosely? It feels harder to reason about, but I don’t see what the benefit of it is.

    Continued at #29415 (review) to avoid overwhelming the main discussion thread of the PR and to make replies close to each other (rather than scattered in the main thread).

  147. in src/net.h:1584 in e85cc59bad outdated
    1576@@ -1547,6 +1577,12 @@ class CConnman
    1577      */
    1578     std::atomic_bool m_start_extra_block_relay_peers{false};
    1579 
    1580+    /**
    1581+     * Number of `ConnectionType::PRIVATE_BROADCAST` connections to open.
    1582+     * Whenever such a connection is opened this is decremented with 1.
    1583+     */
    1584+    std::atomic_size_t m_private_broadcast_connections_to_open{0};
    


    vasild commented at 1:21 pm on May 21, 2024:

    from the main thread at #29415#pullrequestreview-2041604763

    The main thing I’m wondering currently is why are we tacking m_private_broadcast_connections_to_open loosely? It feels harder to reason about, but I don’t see what the benefit of it is.

    The reason for this is that it is simpler to implement that way (I think, somebody has a simpler proposal?). This is because the connman thread is creating the connections, they are consumed/used by the peerman thread and the requests for new connections can come from yet another (3rd) thread. For example, while connman is creating a new connection which is needed to broadcast a given transaction, that transaction may be successfully broadcast by peerman and received back from the network, rendering the just created new connection by connman unnecessary. This is harmless and I accept it because avoiding it would make things too complicated or slow.

  148. vasild force-pushed on May 22, 2024
  149. vasild commented at 4:22 pm on May 22, 2024: contributor

    e85cc59bad...057c79365c: in order to decide whether to private broadcast to IPv4/IPv6 peers, instead of (before this force push) requiring that -onion= is explicitly set, do this (after this force push): keep track if we ever managed to connect to an .onion address via the NET_ONION proxy, if yes assume that this proxy is a Tor proxy and connecting to IPv4/IPv6 addresses through it will be done via the Tor network and via a Tor exit node.

    This is better because it is stronger guarantee (before the user could still misconfigure an orginary non-Tor proxy as -onion=) and works automatically, without requiring config options to be set.

  150. vasild referenced this in commit a63c7c7ea2 on May 23, 2024
  151. alfred-hodler referenced this in commit cb75581df2 on May 23, 2024
  152. DrahtBot added the label Needs rebase on Jun 12, 2024
  153. vasild force-pushed on Jun 20, 2024
  154. vasild commented at 1:44 pm on June 20, 2024: contributor
    057c79365c...7a7e7d189d: rebase due to conflicts
  155. DrahtBot removed the label Needs rebase on Jun 20, 2024
  156. in src/net.h:1172 in ba75010289 outdated
    1168+         * Decrement by `n` the number of new connections of type `ConnectionType::PRIVATE_BROADCAST`
    1169+         * to be opened by `CConnman::ThreadOpenConnections()`. Will not go negative, for example a
    1170+         * value of 4 is ok to be decremented by 5 and will result in 0.
    1171+         * @return the value preceding this operation
    1172+         */
    1173+        size_t NumToOpen() const;
    


    pinheadmz commented at 6:31 pm on June 26, 2024:

    ba75010289f1d9bd3d3a6c635ad18b07cf914041

    This is copy/paste the comment for NumToOpenSub() but doesn’t apply to this get-only method.

    Also, the comments for the two following functions are missing @param[in] n ... but I don’t know how strict you need to be about doxygen comments for everything.


    vasild commented at 12:40 pm on July 1, 2024:

    Fixed and added @param[in] comments.

    What is enforced by clang (not by doxygen) via -Wdocumentation is a mismatch in the parameter names - e.g. if there is @param[in] throughput ... and the actual parameter is int thrughput, then it will give a warning (or error if -Werror is used).

  157. in src/net.cpp:3002 in ba75010289 outdated
    2988+CConnman::PrivateBroadcast::ShouldOpen(bool prev_was_private_broadcast, size_t num_opened) const
    2989+{
    2990+    if (prev_was_private_broadcast) {
    2991+        // Yield to other connection types.
    2992+        return std::nullopt;
    2993+    }
    


    pinheadmz commented at 6:37 pm on June 26, 2024:

    ba75010289f1d9bd3d3a6c635ad18b07cf914041

    Why pass this in here instead of just checking it before calling?


    vasild commented at 12:44 pm on July 1, 2024:
    Could be either way. If there is more than one caller of this function, then having it here would avoid duplicating the check in every caller, but there is one caller so far.

    vasild commented at 7:47 am on August 1, 2024:
    In the latest push I moved this logic to the caller.
  158. in src/private_broadcast.h:22 in cade8e8331 outdated
    13+#include <chrono>
    14+#include <map>
    15+#include <unordered_map>
    16+
    17+/**
    18+ * Store a list of transactions to be broadcast privately. Supports the following operations:
    


    pinheadmz commented at 7:15 pm on June 26, 2024:

    cade8e833197a202dbb03c50359396ecd437ac8c

    ~optional, could use a comment explaining the priority metric. Looks like transactions with fewer broadcasts or older broadcasts are prioritized. A unit test would help make that clear as well.~

    nm, added later

  159. in src/net_processing.cpp:3999 in b81c3cc19e outdated
    4257+                          pfrom.GetId(),
    4258+                          fLogIPs ? strprintf(", peeraddr=%s", pfrom.addr.ToStringAddrPort()) : "");
    4259+            return;
    4260+        }
    4261+    }
    4262+
    


    pinheadmz commented at 7:36 pm on June 26, 2024:

    b81c3cc19ec8055d04b38a72fdfabbfcb2ed2358

    Why isn’t this at the top of the super long function (and include VERACK also?)


    vasild commented at 12:58 pm on July 1, 2024:
    Because we want to ignore incoming messages after a successful handshake. Ignoring the VERACK would disturb the handshake.
  160. in src/private_broadcast.cpp:47 in b81c3cc19e outdated
    24+    const Txid& txid = m_by_priority.begin()->second;
    25+    auto it = m_by_txid.find(txid);
    26+    if (Assume(it != m_by_txid.end())) {
    27+        return it->second.tx;
    28+    }
    29+    m_by_priority.erase(m_by_priority.begin());
    


    pinheadmz commented at 7:41 pm on June 26, 2024:

    b81c3cc19ec8055d04b38a72fdfabbfcb2ed2358

    Is it ok to erase from this map now? What if none of our connections are successful ?


    vasild commented at 1:09 pm on July 1, 2024:
    Note that this would only be reached if m_by_priority contains txid that does not have an entry in m_by_txid which is a gross bug in the PrivateBroadast class. IIRC this was an assert before but somebody suggested to use Assume() instead.
  161. in src/private_broadcast.cpp:62 in b81c3cc19e outdated
    57+        return true;
    58+    }
    59+    Priority& priority = iters->by_txid->second.priority;
    60+
    61+    ++priority.num_broadcasted;
    62+    priority.last_broadcasted = GetTime<std::chrono::microseconds>();
    


    pinheadmz commented at 7:47 pm on June 26, 2024:

    b81c3cc19ec8055d04b38a72fdfabbfcb2ed2358

    Isn’t this GetTime() deprecated?

    https://github.com/bitcoin/bitcoin/blob/7a7e7d189dfe5efa6ab3c644f55f410412163503/src/util/time.h#L95-L100


    vasild commented at 1:34 pm on July 1, 2024:
    Replaced with NodeClock::now().
  162. pinheadmz commented at 7:49 pm on June 26, 2024: member
    Re-reviewed first half of commits, will continue tomorrow. A few comments and questions below.
  163. in src/net_processing.cpp:4479 in 3f5a4187a9 outdated
    4700@@ -4701,6 +4701,21 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    4701         const uint256& hash = peer->m_wtxid_relay ? wtxid : txid;
    4702         AddKnownTx(*peer, hash);
    4703 
    4704+        if (auto num_broadcasted = m_tx_for_private_broadcast.Remove(ptx)) {
    4705+            LogPrintLevel(BCLog::PRIVATE_BROADCAST,
    4706+                          BCLog::Level::Info,
    4707+                          "Received our privately broadcast transaction (txid=%s) from the "
    4708+                          "network from peer=%d%s; stopping private broadcast attempts\n",
    


    pinheadmz commented at 7:55 pm on June 27, 2024:

    3f5a4187a9e5d2bbb65c06c1799155fd7dd0d132

    This made me wonder, if the tx gets confirmed in a block before we get it via relay, will it ever get removed?

    update: looks like this will happen in ReattemptPrivateBroadcast() when we call m_chainman.ProcessTransaction() ?


    vasild commented at 5:01 pm on July 1, 2024:
    Yes, it will return something other than MempoolAcceptResult::ResultType::VALID. Would happen as well if a conflicting transaction is mined in the meantime.
  164. in src/net_processing.cpp:4486 in 3f5a4187a9 outdated
    4710+                          pfrom.GetId(),
    4711+                          fLogIPs ? strprintf(", peeraddr=%s", pfrom.addr.ToStringAddrPort()) : "");
    4712+            if (NUM_PRIVATE_BROADCAST_PER_TX > num_broadcasted.value()) {
    4713+                // Not all of the initial NUM_PRIVATE_BROADCAST_PER_TX connections were needed.
    4714+                // Tell CConnman it does not need to start the remaining ones.
    4715+                m_connman.m_private_broadcast.NumToOpenSub(NUM_PRIVATE_BROADCAST_PER_TX - num_broadcasted.value());
    


    pinheadmz commented at 8:10 pm on June 27, 2024:

    3f5a4187a9e5d2bbb65c06c1799155fd7dd0d132

    Feels like this should be part of Remove(), which I think highlights some possible confusion between the two class PrivateBroadcast you have. One in ConnMan and one in its own unit, used in PeerManager.

    I dunno if there’s a cleaner approach available, to ensure that the two objects don’t get out of sync? Like instead of m_num_to_open in ConnMan, could you refer to the size of the ByNodeId map?


    vasild commented at 5:16 pm on July 1, 2024:

    There is some layer separation between net/CConnman and net_processing/PeerManager which I am trying not to blur too much. The lower net level only knows about connections and things like “I need to open N connections of type X”. It does not know anything about transactions, or P2P messages which are handled by the net_processing layer.

    The constant NUM_PRIVATE_BROADCAST_PER_TX (send a transaction to this many peers) belongs to the net_processing layer. Calling this NumToOpenSub() is only done if Remove() succeeds and NUM_PRIVATE_BROADCAST_PER_TX > num_broadcasted.value() holds. So, no way to do it in CConnman because that would mean accessing NUM_PRIVATE_BROADCAST_PER_TX from CConnman.

    could you refer to the size of the ByNodeId map?

    That is a “transaction id mapped by node id”. Couldn’t deal with transactions from within CConnman.

  165. in src/private_broadcast.cpp:32 in 3f5a4187a9 outdated
    22+    if (!iters) {
    23+        return std::nullopt;
    24+    }
    25+    const size_t num_broadcasted{iters->by_priority->first.num_broadcasted};
    26+    m_by_priority.erase(iters->by_priority);
    27+    m_by_txid.erase(iters->by_txid);
    


    pinheadmz commented at 8:11 pm on June 27, 2024:
    what about m_by_nodeid?

    vasild commented at 9:04 am on July 2, 2024:
    m_by_nodeid is managed by PrivateBroadcast::PushedToNode() and PrivateBroadcast::FinishBroadcast().
  166. in src/private_broadcast.cpp:87 in a86785dc29 outdated
    80@@ -81,6 +81,19 @@ bool PrivateBroadcast::FinishBroadcast(const NodeId& nodeid, bool confirmed_by_n
    81     return true;
    82 }
    83 
    84+std::vector<CTransactionRef> PrivateBroadcast::GetStale() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
    85+{
    86+    LOCK(m_mutex);
    87+    const auto stale_time = GetTime<std::chrono::microseconds>() - 1min;
    


    pinheadmz commented at 3:38 pm on June 28, 2024:

    a86785dc29faa2dcff301641e2f823b73089dae2

    should 1min be defined as a constant in the header file? This is what we consider stale?


    vasild commented at 9:53 am on July 2, 2024:
    Yes, moved to a constant and added a comment to it. Placed the constant in the .cpp file since it is only used in that .cpp file, to limit its scope. It is not part of the “public” private_broadcast.h/cpp interface
  167. in src/rpc/mempool.cpp:113 in 5f49e61ffa outdated
    104@@ -93,11 +105,23 @@ static RPCHelpMan sendrawtransaction()
    105             std::string err_string;
    106             AssertLockNotHeld(cs_main);
    107             NodeContext& node = EnsureAnyNodeContext(request.context);
    108+            const bool private_broadcast_enabled{gArgs.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)};
    109+            if (private_broadcast_enabled &&
    110+                !g_reachable_nets.Contains(NET_ONION) &&
    111+                !g_reachable_nets.Contains(NET_I2P)) {
    


    pinheadmz commented at 3:46 pm on June 28, 2024:

    5f49e61ffa576aa6c27de30a7db51d17c4f5c516

    How could this be condition be different now than when it is checked in init.cpp?


    vasild commented at 10:05 am on July 2, 2024:
    The Tor connectivity could be “off” during startup and become “on” later at runtime, when we connect to the Tor daemon and it tells us the Tor proxy address. This is why the startup condition is a bit different, it has the onion_may_become_reachable variable.
  168. in test/functional/test_framework/p2p.py:226 in 0a6797ffb3 outdated
    221+        info = transport.get_extra_info("socket")
    222+        us = info.getsockname()
    223+        them = info.getpeername()
    224+        logger.debug(f"Connected: us={us[0]}:{us[1]}, them={them[0]}:{them[1]}")
    225+        self.dstaddr = them[0]
    226+        self.dstport = them[1]
    


    pinheadmz commented at 5:28 pm on June 28, 2024:

    0a6797ffb3eb2d45bce1dc68e830efd89ac2a442

    Teleporting the comment from #29420 (review)

    Why didn’t we need to do this on master?

    The old log message was Connected & Listening: 127.0.0.1:15689 not Connected & Listening: 0:0


    vasild commented at 10:23 am on July 2, 2024:
    I replied to that thread to keep the discussion in one place.
  169. pinheadmz commented at 5:51 pm on June 28, 2024: member

    code review ACK 7a7e7d189dfe5efa6ab3c644f55f410412163503

    some questions and comments above and below (sorry spread out review over a few days)

    I want to run this in warnet again with a lot more TXs and see how we do as well

  170. DrahtBot requested review from nothingmuch on Jun 28, 2024
  171. DrahtBot requested review from andrewtoth on Jun 28, 2024
  172. vasild force-pushed on Jul 2, 2024
  173. vasild commented at 10:43 am on July 2, 2024: contributor
    7a7e7d189d...3bd618a91f: rebase and address suggestions
  174. pinheadmz commented at 3:04 pm on July 2, 2024: member

    utACK 3bd618a91f

    Code changes since last review look good.

  175. in test/functional/p2p_private_broadcast.py:68 in 3bd618a91f outdated
    63+NUM_PRIVATE_BROADCAST_PER_TX = 5
    64+
    65+# Fill addrman with these addresses. Must have enough Tor addresses, so that even
    66+# if all 10 default connections are opened to a Tor address (!?) there must be more
    67+# for private broadcast.
    68+addresses = [
    


    instagibbs commented at 4:42 pm on July 2, 2024:
    0ADDRMAN_ADDRESSES = [
    

    vasild commented at 1:26 pm on July 31, 2024:
    Done.
  176. in test/functional/p2p_private_broadcast.py:236 in 3bd618a91f outdated
    231+            if not res["success"]:
    232+                self.log.debug(f"Could not add {addr} to tx_originator's addrman (collision?)")
    233+
    234+        self.wait_until(lambda: self.socks5_server.conf.destinations_used == NUM_INITIAL_CONNECTIONS)
    235+
    236+        node1 = self.nodes[1]
    


    instagibbs commented at 6:38 pm on July 2, 2024:
    s/node1/tx_receiver/

    vasild commented at 1:26 pm on July 31, 2024:
    Done, even though there is more than one tx receiver.
  177. in test/functional/p2p_private_broadcast.py:303 in 3bd618a91f outdated
    298+        observer_inbound.wait_until(lambda: "getdata" in observer_inbound.last_message)
    299+        self.wait_until(lambda: len(tx_originator.getrawmempool()) > 0)
    300+
    301+        self.log.info("Waiting for normal broadcast to another observer")
    302+        observer_outbound = self.socks5_server.conf.destinations[0]["node"]
    303+        observer_outbound.wait_for_inv([inv])
    


    instagibbs commented at 7:16 pm on July 2, 2024:

    I think the test needs coverage for:

    1. retrying broadcasts
    2. sending the same tx again via sendrawtransaction
    3. sending a different wtxid via sendrawtransaction
    4. sending a tx with in-mempool dependencies (both that succeed and fail due to existing/missing parent)
    5. rpc failing if it detects !g_reachable_nets
    6. what happens if there’s not enough private connections when first broadcast attempt is tried, but more found later
    7. what happens to pending broadcasts on restart of node

    vasild commented at 1:31 pm on July 31, 2024:

    I think the test needs coverage for:

    I was stuck at #30543, thank you @mzumsande for resolving it!

    1. retrying broadcasts
    

    Done.

    2. sending the same tx again via sendrawtransaction
    

    Done.

    3. sending a different wtxid via sendrawtransaction
    

    Done, but the new tx has garbage witness. Would be nice to add one more test with a valid but different witness.

    4. sending a tx with in-mempool dependencies (both that succeed and fail due to existing/missing parent)
    

    Done, both.

    5. rpc failing if it detects `!g_reachable_nets`
    

    Done.

    6. what happens if there's not enough private connections when first broadcast attempt is tried, but more found later
    

    Private broadcast connections are created on demand. When first broadcast attempt is tried they are 0 (I did not add new test).

    7. what happens to pending broadcasts on restart of node
    

    They are lost. Persistence over restart is not in this PR, but will be in a subsequent one (I did not add new test).


    instagibbs commented at 2:54 pm on July 31, 2024:
    thanks for attending to this laundry list :+1:
  178. DrahtBot added the label Needs rebase on Jul 4, 2024
  179. vasild force-pushed on Jul 31, 2024
  180. vasild commented at 1:25 pm on July 31, 2024: contributor

    3bd618a91f...8baf19fb2a:

    • Rebase due to conflicts.

    • Tune the logic of the decision to open a private broadcast connection - we yield to other connection types every other attempt, but it might happen that when we yield the loop blocks on the semOutbound grant. This means that we are not ready to open other connection types. In this case and if private broadcast is needed, proceed with private broadcast even if the previous connection was private broadcast as well. This simplifies ShouldOpen() as the logic “previous was private broadcast” is moved to the caller.

    • Rename tor_proxy to proxy_for_ipv4or6 and actually set it, it’s used only for logging.

    • Run transactions to be broadcast through mempool acceptance test, they were not before.

    • If the same transaction is repeatedly given to sendrawtransaction RPC, then don’t open 5 new private broadcast connections every time. Change subsequent requests to do nothing but log a message that it was not necessary to sendrawtransaction the transaction again.

    • Change the extension of the SOCKS5 proxy - instead of being given a set of predefined destinations to redirect the connections to, it is now given a callback function which returns the destination where to redirect, or it can decide to return None asking the SOCKS5 proxy to close the connection (default behavior). This is more flexible.

    • Move create_malleated_version() from p2p_orphan_handling.py to test_framework/messages.py where it can be reused.

    • Extend the functional tests as per #29415 (review), thanks @instagibbs for the suggestions!

  181. DrahtBot removed the label Needs rebase on Jul 31, 2024
  182. DrahtBot commented at 4:50 pm on July 31, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/28160559851

    Make sure to run all tests locally, according to the documentation.

    The failure may happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  183. DrahtBot added the label CI failed on Jul 31, 2024
  184. vasild force-pushed on Aug 1, 2024
  185. vasild commented at 8:20 am on August 1, 2024: contributor
    8baf19fb2a...97ae670dc4: fix the scope of two local variables in socks5.py
  186. vasild force-pushed on Aug 1, 2024
  187. vasild commented at 9:36 am on August 1, 2024: contributor
    97ae670dc4...30ec819596: pet the python linter
  188. DrahtBot removed the label CI failed on Aug 1, 2024
  189. in src/init.cpp:649 in 30ec819596 outdated
    642@@ -643,6 +643,18 @@ void SetupServerArgs(ArgsManager& argsman)
    643                    OptionsCategory::NODE_RELAY);
    644     argsman.AddArg("-minrelaytxfee=<amt>", strprintf("Fees (in %s/kvB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)",
    645         CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
    646+    argsman.AddArg("-privatebroadcast",
    647+                   strprintf(
    648+                       "Broadcast transactions submitted via sendrawtransaction RPC using short lived "
    649+                       "connections to Tor or I2P peers without putting them in the mempool first. "
    


    MarnixCroes commented at 12:18 pm on August 1, 2024:
    to Tor or I2P peers, this should be updated? or maybe rewrite to simplify, something like: broadcast via short-lived Tor or I2P connection As it can also broadcast to ipv4 and ipv6 peers now (https://github.com/bitcoin/bitcoin/pull/29415/files#r1576619537)

    vasild commented at 5:37 pm on August 1, 2024:

    Yes, it needs to be updated, thanks for spotting this! How about this:

    0 +                       "Broadcast transactions submitted via sendrawtransaction RPC using short lived "
    1-+                       "connections to Tor or I2P peers without putting them in the mempool first. "
    2++                       "connections through the Tor or I2P networks without putting them in the mempool first. "
    

    vasild commented at 9:14 am on August 2, 2024:
    Done
  190. in src/net.cpp:2528 in 30ec819596 outdated
    2516@@ -2496,6 +2517,11 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
    2517         LogPrintf("Fixed seeds are disabled\n");
    2518     }
    2519 
    2520+    // Private broadcast connections are opened with priority over others, but only half
    2521+    // of the time to avoid depriving other connection types if private broadcast is
    2522+    // needed but opening such connections is unsuccessful for some reason.
    2523+    std::optional<Network> open_private_broadcast_to;
    


    MarnixCroes commented at 12:26 pm on August 1, 2024:

    on debian12, during compiling it shows this warning

    0net.cpp: In member function ‘void CConnman::ThreadOpenConnections(std::vector<std::__cxx11::basic_string<char> >)’:
    1net.cpp:2523:28: note: ‘*(unsigned int*)((char*)&open_private_broadcast_to + offsetof(std::optional<Network>,std::optional<Network>::<unnamed>.std::_Optional_base<Network, true, true>::<unnamed>))’ was declared here
    2 2523 |     std::optional<Network> open_private_broadcast_to;
    3      |                            ^~~~~~~~~~~~~~~~~~~~~~~~~
    

    (there are also other unrelated warnings, #29359)


    vasild commented at 5:39 pm on August 1, 2024:
    This does not look like the whole warning? I only see in member function ... : note: '...' was declared here.

    MarnixCroes commented at 8:33 am on August 2, 2024:

    oh sry. this is the full warning:

     0In file included from ./bip324.h:10,
     1                 from ./net.h:9,
     2                 from net.cpp:8:
     3In constructor constexpr std::_Optional_payload_base<_Tp>::_Storage<_Up, <anonymous> >::_Storage(std::in_place_t, _Args&& ...) [with _Args = {Network&}; _Up = Network; bool <anonymous> = true; _Tp = Network],
     4    inlined from constexpr std::_Optional_payload_base<_Tp>::_Optional_payload_base(std::in_place_t, _Args&& ...) [with _Args = {Network&}; _Tp = Network] at /usr/include/c++/12/optional:126:4,
     5    inlined from constexpr std::_Optional_payload<Network, true, true, true>::_Optional_payload(std::in_place_t, _Args&& ...) [with _Args = {Network&}][inherited from std::_Optional_payload_base<Network>] at /usr/include/c++/12/optional:339:42,
     6    inlined from constexpr std::_Optional_base<_Tp, true, true>::_Optional_base(std::in_place_t, _Args&& ...) [with _Args = {Network&}; typename std::enable_if<is_constructible_v<_Tp, _Args ...>, bool>::type <anonymous> = false; _Tp = Network] at /usr/include/c++/12/optional:650:4,
     7    inlined from constexpr std::optional<_Tp>::optional(_Up&&) [with _Up = Network&; typename std::enable_if<__and_v<std::__not_<std::is_same<std::optional<_Tp>, typename std::remove_cv<typename std::remove_reference<_Iter>::type>::type> >, std::__not_<std::is_same<std::in_place_t, typename std::remove_cv<typename std::remove_reference<_Iter>::type>::type> >, std::is_constructible<_Tp, _Up>, std::is_convertible<_Iter, _Iterator> >, bool>::type <anonymous> = true; _Tp = Network] at /usr/include/c++/12/optional:749:47,
     8    inlined from void CConnman::ThreadOpenConnections(std::vector<std::__cxx11::basic_string<char> >) at net.cpp:2741:63:
     9/usr/include/c++/12/optional:214:15: warning: *(unsigned int*)((char*)&open_private_broadcast_to + offsetof(std::optional<Network>,std::optional<Network>::<unnamed>.std::_Optional_base<Network, true, true>::<unnamed>)) may be used uninitialized [-Wmaybe-uninitialized]
    10  214 |             : _M_value(std::forward<_Args>(__args)...)
    11      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    12net.cpp: In member function void CConnman::ThreadOpenConnections(std::vector<std::__cxx11::basic_string<char> >):
    13net.cpp:2523:28: note: *(unsigned int*)((char*)&open_private_broadcast_to + offsetof(std::optional<Network>,std::optional<Network>::<unnamed>.std::_Optional_base<Network, true, true>::<unnamed>)) was declared here
    14 2523 |     std::optional<Network> open_private_broadcast_to;
    15      |                            ^~~~~~~~~~~~~~~~~~~~~~~~~
    

    MarnixCroes commented at 8:33 am on August 2, 2024:
    maybe it is ok as it is, I’m just mentioning it

    vasild commented at 9:08 am on August 2, 2024:
    Alright, it is upset that std::optional<Network> open_private_broadcast_to may be used uninitialized. But std::optional has a well defined default constructor - https://en.cppreference.com/w/cpp/utility/optional/optional, so this warning seems bogus. I will leave it as is. Which compiler it is? c++ --version

    maflcko commented at 9:12 am on August 2, 2024:
    Looks like a duplicate of #29359 (known gcc bug)
  191. in src/init.cpp:1998 in 30ec819596 outdated
    1990+        // reachable and will not become reachable for sure.
    1991+        const bool onion_may_become_reachable{listenonion && (!args.IsArgSet("-onlynet") || onlynet_used_with_onion)};
    1992+        if (!g_reachable_nets.Contains(NET_I2P) &&
    1993+            !g_reachable_nets.Contains(NET_ONION) &&
    1994+            !onion_may_become_reachable) {
    1995+            return InitError(_("Private broadcast of own transactions requested (-privatebroadcast), "
    


    MarnixCroes commented at 12:51 pm on August 1, 2024:
    question: when running with -connect and privatebroadcast=1, bitcoind will immediately shutdown. shouldn’t the node just run, but fail to broadcast when using the rpc? additionally, bitcoin-qt fails to run too which doesn’t use the private broadcast (when using GUI).

    vasild commented at 5:44 pm on August 1, 2024:
    Emitting errors from the RPC is also an option, but I prefer to keep the current way because it clearly shows something is misconfigured and starting up knowing that the RPC will never work and always give errors seems like a bad user experience. I would prefer that the user stops and is forced to fix the misconfiguration of the node. bitcoin-qt also has a RPC console.

    MarnixCroes commented at 8:42 am on August 2, 2024:
    ok thanks for the answer. I also thought about that it might make sense to have an optional argument for sendrawtransaction rpc, whether to use privatebroadcast or not (for example in the case when it cannot be used, but user still wants to broadcast without restarting bitcoind) Same answer also applies to that I guess

    vasild commented at 9:12 am on August 2, 2024:
    Yeah. Also, having an extra argument to the RPC is an alternative of the -privatebroadcast config option that would be maybe viable if the plan was to stop with the RPC, but having in mind that the wallet will eventually also use private broadcast, I would like to avoid having to configure each module separately for private broadcast, so a single global config option seems to fit best.

    MarnixCroes commented at 9:49 am on August 2, 2024:
    makes sense, thanks

    andrewtoth commented at 7:30 pm on November 10, 2024:
    This is unfortunate that privatebroadcast is incompatible with connect. The use case would be to connect to a single trusted peer for listening, but broadcast through ephemeral connections to not link the trusted peer to our wallet.

    vasild commented at 8:39 am on November 13, 2024:

    By “for listening” you mean “for learning new transactions and blocks”, right? So, in that setup the node is not listening (not accepting inbound connections), has only one connection to a known honest node and broadcasts its own transactions using private broadcast? That seems like an useful setup that would be nice to have support for. But the current semantic of -connect is “only connect to the specified node(s)” which is at odds with “connect to random nodes for transaction broadcast”. I don’t like changing the semantic of -connect like that.

    I wonder if this can be supported and to keep the current meaning of -connect… don’t use -connect but somehow restrict our node to be able to only connect to 1 address :thinking:


    andrewtoth commented at 2:22 pm on November 13, 2024:
    But we are adding a new option here privatebroadcast. Could we not change the semantics of connect if the new option privatebroadcast is used?

    vasild commented at 2:49 pm on November 13, 2024:
    Yeah, you are right that -connect=0 -privatebroadcast=1 did not exist before because -privatebroadcast= did not exist. Then the semantic of -connect=0 would be “do this if privatebroadcast=0 and do something else if privatebroadcast=1”. I do not like that, seems convoluted and a possible source of confusion to make the behavior of one option dependent on the value of another option. I would like to have less of that stuff, not more.

    andrewtoth commented at 2:58 pm on November 13, 2024:
    I suppose I can addnode my trusted node for now, but I will be leaking other outbound connections. I’m not sure how to handle my case unless we don’t consider the ephemeral broadcast connections as true “connections” in the sense of connect option.

    vasild commented at 4:00 pm on November 13, 2024:
    Just an idea - if the trusted node is accepting inbound Tor connections, then run the private node with -onlynet=onion -addnode=trusted.onion:8333 -privatebroadcast=1. Benefit of connecting to the trusted node over Tor - you know that you actually connected to the trusted node (or somebody who owns the private key for the trusted.onion service). If you connect over clearnet to an IP address, then you have to use v2 (bip324) and verify that the session ids match (session_id in getpeerinfo RPC output on both sides of the connection).

    mzumsande commented at 4:05 pm on November 13, 2024:
    This is a hack and I haven’t tested it, but instead of using -connect, -maxconnections=0 -privatebrodcast and then addnode your trusted peer might work for your use case?
  192. MarnixCroes commented at 1:52 pm on August 1, 2024: contributor
    30ec819596f4000b46c9b7bf8d3ee1518af5d533 tested several things/scenarios and succesfully privately broadcasted several transactions✅
  193. vasild force-pushed on Aug 2, 2024
  194. vasild commented at 9:14 am on August 2, 2024: contributor
    30ec819596...2b478bf332: update description of -privatebroadcast as per #29415 (review)
  195. gmaxwell commented at 6:08 am on August 4, 2024: contributor
    This shouldn’t send transactions without sending an INV. Instead, it should INV and wait for the GETDATA. (thanks to mzumsande for making a note of the behavior).
  196. DrahtBot added the label Needs rebase on Aug 5, 2024
  197. mzumsande commented at 6:17 pm on August 6, 2024: contributor

    This shouldn’t send transactions without sending an INV. Instead, it should INV and wait for the GETDATA. (thanks to mzumsande for making a note of the behavior).

    For context, an old discussion on this: #27509 (review) (I want to think a bit more on the potential benefits of #30572 before voicing an opinion).

  198. instagibbs commented at 1:58 pm on August 7, 2024: member

    (I want to think a bit more on the potential benefits of #30572 before voicing an opinion).

    The cost here is that if this PR stays as-is, we can’t change our minds later abour 30572, fwiw. It’s something I’ve noted a few times elsewhere.

  199. vasild force-pushed on Aug 13, 2024
  200. vasild commented at 9:17 am on August 13, 2024: contributor
    2b478bf332...11364e2e58: rebase due to conflicts
  201. DrahtBot removed the label Needs rebase on Aug 13, 2024
  202. vasild commented at 12:11 pm on August 13, 2024: contributor

    11364e2e58..2db437fbd1: do the transaction send with INV+GETDATA+TX instead of just TX, so that this PR is now not related or tied to #30572.

    I think this complicates the code without providing a benefit, thus my preference is to avoid it, so I kept the change in separate commits (the two on the top of the branch). It can be reviewed separately and squashed accordingly if people like it, or easily dropped.

  203. DrahtBot added the label CI failed on Aug 13, 2024
  204. DrahtBot commented at 1:18 pm on August 13, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/28704849578

    Make sure to run all tests locally, according to the documentation.

    The failure may happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  205. vasild force-pushed on Aug 13, 2024
  206. vasild commented at 2:06 pm on August 13, 2024: contributor
    2db437fbd1...df3a22f87a: pet clang-tidy and combine the last two commits (that implement the INV+GETDATA) because otherwise the test-each-commit CI task fails.
  207. in src/private_broadcast.cpp:75 in 4a955955ec outdated
    35+{
    36+    LOCK(m_mutex);
    37+    m_by_nodeid.emplace(nodeid, txid);
    38+}
    39+
    40+bool PrivateBroadcast::FinishBroadcast(const NodeId& nodeid, bool confirmed_by_node) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
    


    brunoerg commented at 5:42 pm on August 13, 2024:
    In 4a955955ec587120fb121e5068e3bc1f80406ac5 “net_processing: handle ConnectionType::PRIVATE_BROADCAST connections”: It seems FinishBroadcast returns a bool, but this value is never used.

    vasild commented at 8:26 am on August 14, 2024:

    The return value is used here:

    0    // This is done when we get a PONG from the peer. Repeat here too in case we never receive a PONG.
    1    if (node.IsPrivateBroadcastConn() &&
    2        m_tx_for_private_broadcast.FinishBroadcast(nodeid, /*confirmed_by_node=*/false)) {
    3...
    4        log a message that we never got a PONG response
    

    The point is to avoid printing this log message in the happy path where we do get a PONG response.


    brunoerg commented at 9:21 am on August 14, 2024:
    Oh sorry, missed that. It’s fine.
  208. vasild force-pushed on Aug 14, 2024
  209. DrahtBot removed the label CI failed on Aug 14, 2024
  210. hebasto added the label Needs CMake port on Aug 16, 2024
  211. gmaxwell commented at 11:51 pm on August 18, 2024: contributor

    Private broadcast can also reduce privacy. I think the weakness is a tossup vs the obvious alternative (run ordinary connections exclusively over Tor) so it’s not a reason to not implement the feature, but it’s worth discussing in order to understand the limits and see if mitigations are possible.

    The issue on my mind is that Tor has little to no resistance to traffic analysis. The small amount of resistance it has is by quantizing messages into fixed size cells. Otherwise, it essentially doesn’t have much resistance because analysis resistance is incompatible with achieving low latency. Too bad for us because for txn broadcast latency is fairly irrelevant.

    We assume the attacker is able to passively monitor the internet connection of our victim. Lets also assume their non-blocksonly Bitcoin P2P connection is encrypted as is increasingly so (and it’s a ~6 line change to add an option to make peer v2-only, which I assume someone will propose once they feel v2 listeners are widespread enough).

    The attacker is attempting to confirm that the monitored peer is the originator of specific transactions on the network, or alternatively is looking at all the transactions and try to determine which are likely authored by the monitored peer.

    Against this threat model, I think this PR creates a substantial weakness compared to not having it: any time the user transacts they will create a predictable amount of traffic to tor which is a somewhat a function of the size of the transaction. There may be enough noise that a single transaction isn’t enough alone, but someone who transacted a couple times a day would likely be identifiable.

    Of course, this isn’t the threat model this PR really hopes to address. This PR is strong against a threat model where the attacker cannot traffic analyze anyone but instead runs many bitcoin “nodes” and then is able to correlate incoming (decrypted) traffic to originating IP address. Of the two threat models I think this latter one is a greater concern.

    But, perhaps if there were padding traffic and a tiny amount of broadcast of third-party txn as if they were private broadcast then the weakness under the traffic analysis case would be improved to be no worse than you’d have from just running ordinary P2P across tor?

  212. instagibbs commented at 1:10 pm on August 19, 2024: member

    @gmaxwell

    But, perhaps if there were padding traffic and a tiny amount of broadcast of third-party txn as if they were private broadcast then the weakness under the traffic analysis case would be improved to be no worse than you’d have from just running ordinary P2P across tor?

    Assuming a switch to INV-based private relay, you might need to take some care to make sure the decoy tx gets requested at high rates, since the real private relay will tend to result in the getadata for the tx?

  213. gmaxwell commented at 0:07 am on August 22, 2024: contributor
    It might be possible to conceal from a traffic analysis perspective what if anything was requested using padding.
  214. DrahtBot added the label CI failed on Sep 1, 2024
  215. DrahtBot commented at 6:08 am on September 1, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/28761292823

    Make sure to run all tests locally, according to the documentation.

    The failure may happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  216. maflcko removed the label Needs CMake port on Sep 2, 2024
  217. vasild force-pushed on Sep 2, 2024
  218. vasild commented at 4:41 pm on September 2, 2024: contributor
    31d841ddb8...08c9a8254a: rebase to pick CMake
  219. DrahtBot removed the label CI failed on Sep 2, 2024
  220. DrahtBot added the label Needs rebase on Sep 3, 2024
  221. vasild force-pushed on Sep 4, 2024
  222. vasild commented at 8:00 am on September 4, 2024: contributor
    08c9a8254a...ed7040a9da: rebase due to conflicts
  223. DrahtBot removed the label Needs rebase on Sep 4, 2024
  224. DrahtBot added the label CI failed on Sep 4, 2024
  225. DrahtBot removed the label CI failed on Sep 4, 2024
  226. DrahtBot added the label Needs rebase on Sep 4, 2024
  227. vasild force-pushed on Sep 11, 2024
  228. vasild commented at 1:47 pm on September 11, 2024: contributor
    ed7040a9da...abec00bb80: rebase due to conflicts
  229. DrahtBot removed the label Needs rebase on Sep 11, 2024
  230. DrahtBot added the label CI failed on Sep 11, 2024
  231. DrahtBot removed the label CI failed on Sep 12, 2024
  232. DrahtBot added the label CI failed on Sep 19, 2024
  233. DrahtBot commented at 12:19 pm on September 19, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/29995994415

    Make sure to run all tests locally, according to the documentation.

    The failure may happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  234. vasild force-pushed on Sep 30, 2024
  235. vasild commented at 10:52 am on September 30, 2024: contributor
    abec00bb80...4101c7ac8d: rebase due to conflicts
  236. vasild force-pushed on Sep 30, 2024
  237. in src/init.cpp:648 in bc1b5670ec outdated
    639@@ -640,6 +640,18 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
    640                    OptionsCategory::NODE_RELAY);
    641     argsman.AddArg("-minrelaytxfee=<amt>", strprintf("Fees (in %s/kvB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)",
    642         CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
    643+    argsman.AddArg("-privatebroadcast",
    644+                   strprintf(
    645+                       "Broadcast transactions submitted via sendrawtransaction RPC using short lived "
    646+                       "connections through the Tor or I2P networks without putting them in the mempool first. "
    647+                       "There are two aspects of this - delinking transaction-origin/Bitcoin-owner from "
    648+                       "IP-address/geolocation and delinking onchain-unrelated transactions from their "
    


    mzumsande commented at 3:51 pm on September 30, 2024:
    I found this help text hard to parse with all the “/” (had to read it multiple times to understand it). I’m not sure if the rpc help is the best place to give motivation for why features exist (instead of just describing them), but maybe this could be a bit more succinct?

    vasild commented at 9:05 am on October 3, 2024:

    I’m not sure if the rpc help is the best place to give motivation for why features exist

    Right. I dropped some of the text. I will apply further rewording if you suggest some.

  238. in src/net.cpp:2608 in e24c2706f7 outdated
    2594+        CSemaphoreGrant grant{*semOutbound, /*fTry=*/true};
    2595+        if (open_private_broadcast_to.has_value() && grant) {
    2596+            // Previous connection was private broadcast, thus yield to other connection types
    2597+            // if we are ready to do so (the outbound semaphore gave green light).
    2598+            open_private_broadcast_to.reset();
    2599+        } else if (!(open_private_broadcast_to = m_private_broadcast.ShouldOpen(num_private_broadcast_opened))) {
    


    mzumsande commented at 8:07 pm on September 30, 2024:
    I would find the ThreadOpenConnection logic (which is already convoluted in master) easier to understand if the private broadcast related logic would be separated more clearly, e.g. this whole thing could be behind some bool private_broadcast_enabled so m_private_broadcast.ShouldOpen wouldn’t be called in normal mode and one wouldn’t need to scroll down and verify that this function does nothing then. Of course there is no functional difference, so it may be a matter of taste.

    vasild commented at 4:56 am on October 3, 2024:

    This code in CConnman::ThreadOpenConnections() is not checking whether private broadcast is enabled because it is just a servant, its logic is: “if there is a request to open private broadcast connections, then I will open them”. The higher level code is making the decision (currently in RPC only, but eventually in the wallet as well) - “if private broadcast is enabled, then open such connections because I am now broadcasting a transaction (and don’t put it in the mempool)”. This is why there is no check in net.cpp whether -privatebroadcast is enabled.

    I also find this snippet non-trivial to understand:

     0CSemaphoreGrant grant{*semOutbound, /*fTry=*/true};
     1if (open_private_broadcast_to.has_value() && grant) {
     2    // Previous connection was private broadcast, thus yield to other connection types
     3    // if we are ready to do so (the outbound semaphore gave green light).
     4    open_private_broadcast_to.reset();
     5} else if (!(open_private_broadcast_to = m_private_broadcast.ShouldOpen(num_private_broadcast_opened))) {
     6    // No need for private broadcast connections, go with other ones. Acquire the
     7    // grant if not already acquired. 
     8    grant.Acquire();
     9}
    10// else private broadcast is needed to network *open_private_broadcast_to, try to open one.
    

    Let me summarize why the logic is like that:

    • since private broadcast connections are needed now and are short lived, they override all others
    • however if there is a lot of demand for private broadcast connections we want to avoid starving other connection types, so we don’t open two consecutive private broadcast connections and instead yield to others
    • but this yield to others may end up blocking on the grant for a long time because we are not ready to open other connection types whereas private broadcast connections are needed now.

    I am open to suggestions on how to implement that in a simpler way or how to simplify the logic itself (without sacrificing the functionality).


    mzumsande commented at 6:34 pm on October 7, 2024:
    I didn’t mean to suggest a change to the logic, just to make it more conditional on -privatebroadcast, so that in principle we wouldn’t even need to initiate a m_private_broadcast object unless the user is running in -privatebroadcast mode. But I don’t have a concrete suggestion right now (I might suggest on at a later point but need to look deeper into the grant logic for that) so it’s fine to resolve for now.
  239. in src/node/types.h:54 in f945fd5eb2 outdated
    49+/**
    50+ * Methods to broadcast a local transaction.
    51+ * Used to influence `BroadcastTransaction()` and its callers.
    52+ */
    53+enum TxBroadcastMethod : uint8_t {
    54+    /// Add the transaction to the mempool and broadcast to all currently connected peers.
    


    mzumsande commented at 8:32 pm on September 30, 2024:
    nit: all peers for which tx relay is enabled (so not blocks-only, feelers etc.).

    vasild commented at 9:06 am on October 3, 2024:
    Changed, thanks!
  240. in src/wallet/wallet.cpp:2039 in f945fd5eb2 outdated
    2032@@ -2031,7 +2033,19 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string
    2033     if (GetTxDepthInMainChain(wtx) != 0) return false;
    2034 
    2035     // Submit transaction to mempool for relay
    2036-    WalletLogPrintf("Submitting wtx %s to mempool for relay\n", wtx.GetHash().ToString());
    2037+    const char* what{""};
    2038+    switch (broadcast_method) {
    2039+    case node::ADD_TO_MEMPOOL_AND_BROADCAST_TO_ALL:
    2040+        what = "to mempool and for broadcast to all peers";
    


    mzumsande commented at 8:34 pm on September 30, 2024:
    nit: same here, I’d remove “all”

    vasild commented at 9:06 am on October 3, 2024:
    Removed.
  241. in src/net_processing.cpp:3679 in b628d96f21 outdated
    3867+            // AddrMan::Connected() on block-relay-only peers; see
    3868+            // FinalizeNode().
    3869+            //
    3870+            // This moves an address from New to Tried table in Addrman,
    3871+            // resolves tried-table collisions, etc.
    3872+            m_addrman.Good(pfrom.addr);
    


    mzumsande commented at 9:50 pm on September 30, 2024:

    I’ve been thinking about leaking information about private broadcast via addrman: 1.) Here, we call Good(), possibly moving the peer from new to tried. Since tried table is smaller and sparsely filled especially for newer nodes, and addrs are picked with 50% probability from new/tried, this usually means that we are more likely to connect to them in the future - maybe via clearnet, maybe via future private connections. 2.) On disconnection, we call Connected(), changing the nTime of an address (that could be included in getaddr answers).

    Not super easy to use either of these methods as an attack, but I still think it would be better not to do change addrman based on what happens in private connections.

    Even if we disabled that, an existing addrman that has only a few Tor peers in tried could make it likely that subsequent private txns would be made to the same node (which could help that peer connect these two transactions). That is a separate problem I don’t see a straightforward solution for, but it would be mitigated by 1.) gathering more tried peers over time 2.) having many peers in the network make use of private broadcast.


    vasild commented at 9:11 am on October 3, 2024:

    it would be better not to do change addrman based on what happens in private connections

    I agree! Changed to avoid the Good() and Connected() calls for private broadcast.

    On the second problem - I agree that more tried peers (which happens naturally over time) and more people using private broadcast would help because then the recipient just sees multiple broadcasts and has no way to relate them.

  242. in src/private_broadcast.h:89 in 80246442b8 outdated
    40+    struct TxWithPriority {
    41+        CTransactionRef tx;
    42+        Priority priority;
    43+    };
    44+
    45+    using ByTxid = std::unordered_map<Txid, TxWithPriority, SaltedTxidHasher>;
    


    mzumsande commented at 10:54 pm on September 30, 2024:
    Why store by TxId instead of WtxId? Would this add some attack surface, e.g. an attacker with many connections to peers receiving a private tx, and then sending each of their peers a modified version of the same tx with invalid witness data (but same TxId), in the hope of connecting to the originating node and clearing any further attempts to broadcast the transaction (note that m_tx_for_private_broadcast.Remove() also happens for invalid txs becaust it’s done before validation)

    vasild commented at 8:34 am on October 3, 2024:

    Because if stored by wtxid then the same could happen, but the attacker malleates the transaction to a valid witness data, so it has same txid, different wtxid, and is valid. When we receive that transaction we would not recognize it because we look at the wtxid, so we would keep broadcasting the original one, until the code in PeerManagerImpl::ReattemptPrivateBroadcast() gives up on it because it is not acceptable in the mempool.

    Do you think we should check both txid and wtxid when we receive a transaction from the network, or try to validate the transaction before removing it via m_tx_for_private_broadcast.Remove(ptx)?


    mzumsande commented at 3:27 pm on October 3, 2024:
    I’d say it might be a good idea to keep using TxIDs, but move the removal until after validation - that should address both scenarios. We are going to validate it anyway, so I don’t really see any downsides to that.

    vasild commented at 12:28 pm on October 4, 2024:

    Here is the change:

     0--- i/src/net_processing.cpp
     1+++ w/src/net_processing.cpp
     2@@ -4803,27 +4803,12 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
     3         const uint256& txid = ptx->GetHash();
     4         const uint256& wtxid = ptx->GetWitnessHash();
     5 
     6         const uint256& hash = peer->m_wtxid_relay ? wtxid : txid;
     7         AddKnownTx(*peer, hash);
     8 
     9-        if (auto num_broadcasted = m_tx_for_private_broadcast.Remove(ptx)) {
    10-            LogPrintLevel(BCLog::PRIVATE_BROADCAST,
    11-                          BCLog::Level::Info,
    12-                          "Received our privately broadcast transaction (txid=%s) from the "
    13-                          "network from peer=%d%s; stopping private broadcast attempts\n",
    14-                          txid.ToString(),
    15-                          pfrom.GetId(),
    16-                          fLogIPs ? strprintf(", peeraddr=%s", pfrom.addr.ToStringAddrPort()) : "");
    17-            if (NUM_PRIVATE_BROADCAST_PER_TX > num_broadcasted.value()) {
    18-                // Not all of the initial NUM_PRIVATE_BROADCAST_PER_TX connections were needed.
    19-                // Tell CConnman it does not need to start the remaining ones.
    20-                m_connman.m_private_broadcast.NumToOpenSub(NUM_PRIVATE_BROADCAST_PER_TX - num_broadcasted.value());
    21-            }
    22-        }
    23-
    24         LOCK2(cs_main, m_tx_download_mutex);
    25 
    26         m_txrequest.ReceivedResponse(pfrom.GetId(), txid);
    27         if (tx.HasWitness()) m_txrequest.ReceivedResponse(pfrom.GetId(), wtxid);
    28 
    29         // We do the AlreadyHaveTx() check using wtxid, rather than txid - in the
    30@@ -4878,21 +4863,37 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    31             //
    32             // Note that m_lazy_recent_rejects doesn't just record DoSy or invalid
    33             // transactions, but any tx not accepted by the mempool, which may be
    34             // due to node policy (vs. consensus). So we can't blanket penalize a
    35             // peer simply for relaying a tx that our m_lazy_recent_rejects has caught,
    36             // regardless of false positives.
    37+            // XXX what about Remove() also here?
    38             return;
    39         }
    40 
    41         const MempoolAcceptResult result = m_chainman.ProcessTransaction(ptx);
    42         const TxValidationState& state = result.m_state;
    43 
    44         if (result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
    45             ProcessValidTx(pfrom.GetId(), ptx, result.m_replaced_transactions);
    46             pfrom.m_last_tx_time = GetTime<std::chrono::seconds>();
    47+
    48+            if (auto num_broadcasted = m_tx_for_private_broadcast.Remove(ptx)) {
    49+                LogPrintLevel(BCLog::PRIVATE_BROADCAST,
    50+                              BCLog::Level::Info,
    51+                              "Received our privately broadcast transaction (txid=%s) from the "
    52+                              "network from peer=%d%s; stopping private broadcast attempts\n",
    53+                              txid.ToString(),
    54+                              pfrom.GetId(),
    55+                              fLogIPs ? strprintf(", peeraddr=%s", pfrom.addr.ToStringAddrPort()) : "");
    56+                if (NUM_PRIVATE_BROADCAST_PER_TX > num_broadcasted.value()) {
    57+                    // Not all of the initial NUM_PRIVATE_BROADCAST_PER_TX connections were needed.
    58+                    // Tell CConnman it does not need to start the remaining ones.
    59+                    m_connman.m_private_broadcast.NumToOpenSub(NUM_PRIVATE_BROADCAST_PER_TX - num_broadcasted.value());
    60+                }
    61+            }
    62         }
    63         else if (state.GetResult() == TxValidationResult::TX_MISSING_INPUTS)
    64         {
    65             bool fRejectedParents = false; // It may be the case that the orphans parents have all been rejected
    66 
    67             // Deduplicate parent txids, so that we don't have to loop over
    

    Note that if the tx is not accepted in our mempool because it has too low fee for our mempool, but is accepted in everybody else’s mempool, then we would do the remaining broadcasts up to NUM_PRIVATE_BROADCAST_PER_TX (5), then it would get stale and will be removed by PeerManagerImpl::ReattemptPrivateBroadcast(). Whereas currently, without this change it will be removed right away and we will not do the remaining broadcasts (up to 5).

    I got another idea - remove the transaction only if the received tx has the same txid and the same wtxid as the one that we broadcast. Currently we only compare the txid.

     0--- i/src/private_broadcast.cpp
     1+++ w/src/private_broadcast.cpp
     2@@ -21,13 +21,13 @@ bool PrivateBroadcast::Add(const CTransactionRef& tx) EXCLUSIVE_LOCKS_REQUIRED(!
     3 }
     4 
     5 std::optional<size_t> PrivateBroadcast::Remove(const CTransactionRef& tx) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
     6 {                 
     7     LOCK(m_mutex);
     8     auto iters = Find(tx->GetHash());
     9-    if (!iters) {
    10+    if (!iters || iters->by_txid->second.tx->GetWitnessHash() != tx->GetWitnessHash()) {
    11         return std::nullopt;
    12     }
    13     const size_t num_broadcasted{iters->by_priority->first.num_broadcasted};
    14     m_by_priority.erase(iters->by_priority);
    15     m_by_txid.erase(iters->by_txid);
    16     return num_broadcasted;
    

    With this, if a malleated and valid transaction makes it to our mempool, then we would keep broadcasting the original one, up to 5 times, and then it will be removed by PeerManagerImpl::ReattemptPrivateBroadcast().

    Which one?


    mzumsande commented at 6:35 pm on October 7, 2024:

    Which one?

    No strong opinion (I think that both should work), but the second one (also check wtxid) seems simpler.


    vasild commented at 11:24 am on October 8, 2024:
    I went for the second one being simpler and also easier to reason about - if we received something other than what we broadcasted, then keep broadcasting (but not infinitely).
  243. vasild force-pushed on Oct 3, 2024
  244. vasild commented at 10:36 am on October 3, 2024: contributor
    b98320ceff...54f4f3b05e: address suggestions and blindly try to fix a bogus warning about uninitialized optional (didn’t work)
  245. DrahtBot added the label Needs rebase on Oct 5, 2024
  246. vasild force-pushed on Oct 7, 2024
  247. vasild commented at 1:42 pm on October 7, 2024: contributor
    54f4f3b05e...f9b2eaf96c: rebase due to conflicts
  248. DrahtBot removed the label Needs rebase on Oct 7, 2024
  249. in src/net_processing.cpp:4203 in f9b2eaf96c outdated
    4534@@ -4537,6 +4535,41 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
    4535             LogDebug(BCLog::NET, "received getdata for: %s peer=%d\n", vInv[0].ToString(), pfrom.GetId());
    4536         }
    4537 
    4538+        if (pfrom.IsPrivateBroadcastConn()) {
    


    mzumsande commented at 5:54 pm on October 7, 2024:
    The final fixup commit leads to the question what should happen if the peer doesn’t send a GETDATA to our INV. I think this would happen in practice if they are one of the later broadcast attempts and already received the tx from someone else (as a result of our first attempts). I think that we would currently never disconnect on our end, but they might disconnect us after TIMEOUT_INTERVAL=20min because we don’t answer their ping. But if they don’t have this logic the connection might stay open indefinitely, whicih seems unfortunate, so we’d probably need some kind of timer here (see also other related comment)?

    vasild commented at 11:22 am on October 8, 2024:
    Right. To resolve this and the issue at #29415 (review) I introduced a maximum lifetime of a private broadcast connection, after which time, we disconnect the peer regardless of the state the connection is in.

    vasild commented at 9:27 am on November 8, 2024:
    Marking this as resolved, feel free to comment if you have concerns with the “maximum lifetime” solution.
  250. in src/net_processing.cpp:4143 in d8325e33e2 outdated
    4138+            // relay logic is designed to work even in cases when the peer drops
    4139+            // the transaction (due to it being too cheap, or for other reasons).
    4140+            PushPrivateBroadcastTx(pfrom);
    4141+
    4142+            peer->m_ping_queued = true; // Ensure a ping will be sent: mimick a request via RPC.
    4143+            MaybeSendPing(pfrom, *peer, GetTime<std::chrono::microseconds>());
    


    mzumsande commented at 6:07 pm on October 7, 2024:
    What if the peer doesn’t answer our ping (because they are broken / malicious)? I think we would never disconnect them because the disconnection logic is inside of MaybeSendPing(), which will never be called again for private peers.

    vasild commented at 11:19 am on October 8, 2024:
    Continuing at #29415 (review)
  251. vasild force-pushed on Oct 8, 2024
  252. vasild commented at 11:26 am on October 8, 2024: contributor
    f9b2eaf96c...a51c2cdda5: address suggestions and further try to silence the bogus std::optional warning.
  253. DrahtBot added the label Needs rebase on Oct 9, 2024
  254. vasild force-pushed on Oct 9, 2024
  255. vasild commented at 9:33 am on October 9, 2024: contributor
    a51c2cdda5...09a7394759: silence the bogus GCC warning about uninitialized std::optional
  256. vasild force-pushed on Oct 9, 2024
  257. vasild commented at 9:36 am on October 9, 2024: contributor
    09a7394759...6b10008441: rebase due to conflicts
  258. DrahtBot removed the label Needs rebase on Oct 9, 2024
  259. DrahtBot removed the label CI failed on Oct 9, 2024
  260. vasild force-pushed on Oct 22, 2024
  261. vasild commented at 11:18 am on October 22, 2024: contributor
    6b10008441...ec7693a9aa: take suggestions from #29420 - in the SOCKS5 proxy, put the sockets forwarding snippet from socks5.py to its own function and retry send()s.
  262. achow101 referenced this in commit 97b790e844 on Oct 29, 2024
  263. log: introduce a new category for private broadcast 4e74cfe334
  264. init: introduce a new option to enable/disable private broadcast
    Co-authored-by: brunoerg <brunoely.gc@gmail.com>
    7ccd2286ff
  265. net: introduce a new connection type for private broadcast
    We will open a short-lived connection to a random Tor or I2P peer,
    send our transaction to that peer and close the connection.
    80e805cc85
  266. net: move peers counting before grant acquisition in ThreadOpenConnections()
    In a subsequent commit we will need to conditionally acquire the
    semaphore grant based on the collected stats (number and types of opened
    connections). Thus move the snippet that does the counting earlier. It
    jumps accross the fixed seeds addition, but both are unrelated and can
    be done in any order.
    
    This is a non-functional change, a mechanical move that is easy to
    verify. The code should behave identically before and after it.
    4203fd0f15
  267. net: implement opening PRIVATE_BROADCAST connections
    Implement opening `ConnectionType::PRIVATE_BROADCAST` connections with the
    following properties:
    * They have higher priority over other connection types
    * Don't wait for outbound connection slot to be available
    * Only to Tor or I2P (or IPv4/IPv6 through the Tor proxy, if provided)
    * Open such connections only when requested and don't maintain N opened
      connections of this type.
    90379e2d95
  268. net_processing: rename RelayTransaction to better describe what it does
    Rename `PeerManager::RelayTransaction()` to
    `PeerManager::ScheduleTxForBroadcastToAll()`. The transaction is not relayed
    when the method returns. It is only scheduled for broadcasting at a later
    time. Also, there will be another method which only schedules for broadcast
    to Tor or I2P peers.
    3caecd73bf
  269. node: change a tx-relay on/off flag to a tri-state
    Previously the `bool relay` argument to `BroadcastTransaction()`
    designated:
    
    ```
    relay=true: add to the mempool and broadcast to all peers
    relay=false: add to the mempool
    ```
    
    Extend this with a third option to not add the transaction to the
    mempool and broadcast privately.
    
    This is a non-functional change - the new third option is not handled
    inside `BroadcastTransaction()` and is not used by any of the callers.
    
    The idea for the new `node/types.h` and the comments in it by Ryan.
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    45324486fb
  270. net_processing: store transactions for private broadcast in PeerManager
    Extend `PeerManager` with a transaction storage and a new method
    `ScheduleTxForPrivateBroadcast()` which:
    * adds a transaction to that storage and
    * calls `CConnman::PrivateBroadcastAdd()` to open dedicated privacy
      connections that will pick an entry from the transaction storage and
      broadcast it.
    5aaf3d9b0a
  271. net_processing: reorder the code that handles the VERSION message
    Change the order in which code snippets are executed as a result of
    receiving the `VERSION` message. Move the snippets that do
    `MakeAndPushMessage()` near the end. This will help with handling of
    private broadcast connections - they do not require any of that.
    
    This is a non-functional change.
    4be5481141
  272. net_processing: handle ConnectionType::PRIVATE_BROADCAST connections
    For connections of type `ConnectionType::PRIVATE_BROADCAST`:
    * After receiving VERACK, relay a transaction from the list of
      transactions for private broadcast and disconnect
    * Don't process any messages after VERACK
    * Don't send any messages other than the minimum required for the
      transaction relay
    db58323dbd
  273. net_processing: stop private broadcast of a transaction after round-trip
    Remove the transaction from the list of transactions to broadcast after
    we receive it from the network.
    
    Only remove the transaction if it is the same as the one we sent: both
    txid and wtxid match. Don't remove transactions that have the same txid
    and different wxtid. Such transactions show that some of the private
    broadcast recipients malleated the witness and the transaction made it
    back to us. The witness could be either:
    * invalid, in which case the transaction will not be accepted in
      anybody's pool; or
    * valid, in which case either the original or the malleated transaction
      will make it to nodes' mempools and eventually be mined. Our response
      is to keep broadcasting the original. If the malleated transaction
      wins then we will eventually stop broadcasting the original when it
      gets stale and gets removed from the "to broadcast" storage cause it
      is not acceptable in our mempool.
    876872f2af
  274. net_processing: retry private broadcast
    Periodically check for stale transactions in peerman and if found,
    reschedule new connections to be opened by connman for broadcasting
    them.
    cbcdde4d66
  275. rpc: use private broadcast from sendrawtransaction RPC if -privatebroadcast is ON 129373bf2d
  276. test: move create_malleated_version() to messages.py for reuse b19e372d4e
  277. test: add functional test for private broadcast 6f46c185c8
  278. fixup! net_processing: handle ConnectionType::PRIVATE_BROADCAST connections
    This change adds INV+GETDATA to the transaction sending sequence.
    f5fc9451aa
  279. vasild force-pushed on Nov 8, 2024
  280. vasild commented at 9:22 am on November 8, 2024: contributor
    ec7693a9aa...f5fc9451aa: rebase and drop the commits that were already merged via https://github.com/bitcoin/bitcoin/pull/29420
  281. 1440000bytes commented at 7:37 pm on November 10, 2024: none

    @1440000bytes, thanks for asking! There is some discussion at #27509 (the previous attempt on this).

    Is it necessary to open new short lived tor/i2p connections for broadcasting the transaction?

    Yes, it is. See below.

    What are the trade-offs in this implementation vs a simple implementation to relay tx to one or more peers that our node is already connected to?

    Sending the transaction over clearnet reveals the IP address/geolocation of the sender. A spy with many connections to the network could try to guess who was the originator. So, why not send it to our Tor peers only? Because it is relatively easy for a spy to fingerprint and link clearnet and Tor connections to the same peer. That is, a long running connection over Tor could be linked to a long running clearnet connection. This is why the proposed changes open a short-lived connection that does not reveal any of the identity of the sender.

    Would this benefit nodes that don’t have clearnet connections, e.g. Tor/I2P-only nodes? Yes! In the case where the sender sends two otherwise unrelated transactions over the same long-running Tor connection, the recipient will know that they have the same origin, even though they are not related on-chain. Using single shot connections fixes that too.

    Related issues:

    Linked in the OP, thanks!

    I am still not satisfied with this answer as a general approach could have allowed dandelion++ without changing bitcoin core. However, it improves privacy compared to status quo so I wont be a roadblock.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2024-11-21 12:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me