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

pull vasild wants to merge 18 commits into bitcoin:master from vasild:private_broadcast changing 33 files +1497 −208
  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.

    • Introduce a new connection type for private broadcast of transactions with the following properties:

      • started whenever there are local transactions to be sent
      • only opened to Tor or I2P peers
      • 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 Tor or I2P peers. Keep doing this until we receive an INV about 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 somebody, 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 >--- TX ------------> tx-recipient
     9tx-sender >--- PING ----------> tx-recipient
    10tx-sender <--- PONG ----------< tx-recipient
    11tx-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.

    Some explanation of the commits:

    • New logging category and config option to enable private broadcast

      • log: introduce a new category for private broadcast
      • init: introduce a new option to enable/disable private broadcast
    • Implement the private broadcast connection handling on the CConnman side:

      • net: introduce a new connection type for private broadcast
      • net: move peers counting before grant acquisition in ThreadOpenConnections()
      • net: implement opening PRIVATE_BROADCAST connections
    • Prepare BroadcastTransaction() for private broadcast requests:

      • net_processing: rename RelayTransaction to better describe what it does
      • node: change a tx-relay on/off flag to a tri-state
      • net_processing: store transactions for private broadcast in PeerManager
    • Implement the private broadcast connection handling on the PeerManager side:

      • net_processing: reorder the code that handles the VERSION message
      • net_processing: handle ConnectionType::PRIVATE_BROADCAST connections
      • net_processing: stop private broadcast of a transaction after round-trip
      • net_processing: retry private broadcast
    • Engage the new functionality from sendrawtransaction:

      • rpc: use private broadcast from sendrawtransaction RPC if -privatebroadcast is ON
    • Independent test framework improvements (also opened as a standalone PR at #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()
    • 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


    Next step after this PR is done will be to have the wallet do the private broadcast as well, #11887 would have to be resolved.


    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

    For detailed information about the code coverage, see the test coverage report.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK pinheadmz
    Concept ACK andrewtoth, zzzi2p, nothingmuch

    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:

    • #30385 ([WIP] p2p: send not_found msgs for unknown, pruned or unwilling to share blocks by furszy)
    • #30381 ([WIP] net: return result from addnode RPC by willcl-ark)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #30332 (Stratum v2 connman by Sjors)
    • #30315 (Stratum v2 Transport by Sjors)
    • #30277 ([DO NOT MERGE] Erlay: bandwidth-efficient transaction relay protocol (Full implementation) by sr-gi)
    • #30212 (rename TransactionErrors: MISSING_INPUTS and ALREADY_IN_CHAIN by willcl-ark)
    • #30116 (p2p: Fill reconciliation sets (Erlay) attempt 2 by sr-gi)
    • #30111 (locks: introduce mutex for tx download, flush rejection filters on UpdatedBlockTip by glozow)
    • #30065 (init: fixes file descriptor accounting by sr-gi)
    • #29798 (Logging cleanup by vasild)
    • #29680 (wallet: fix unrelated parent conflict doesn’t cause child tx to be marked as conflict by Eunovo)
    • #29641 (scripted-diff: Use LogInfo/LogDebug over LogPrintf/LogPrint by maflcko)
    • #29625 (Several randomness improvements by sipa)
    • #29605 (net: Favor peers from addrman over fetching seednodes by sr-gi)
    • #29431 (test/BIP324: disconnection scenarios during v2 handshake by stratospher)
    • #29346 (Stratum v2 Noise Protocol by Sjors)
    • #29278 (Wallet: Add maxfeerate wallet startup option by ismaelsadeeq)
    • #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)
    • #26697 (logging: use bitset for categories by LarryRuane)
    • #25832 (tracing: network connection tracepoints by 0xB10C)

    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:1635 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:4091 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:451 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:2735 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:131 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:66 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:1012 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:366 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:663 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:2518 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

  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:3962 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:113 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:473 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:483 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:2653 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:1180 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:1629 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.
  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:4270 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:46 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:4716 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:4723 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:31 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:111 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. log: introduce a new category for private broadcast 679f604998
  173. init: introduce a new option to enable/disable private broadcast
    Co-authored-by: brunoerg <brunoely.gc@gmail.com>
    86841b65f9
  174. 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.
    49ae1ce703
  175. 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.
    e277b72b27
  176. 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.
    449d447799
  177. 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.
    ce5f42e92b
  178. 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>
    ea738475fd
  179. 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.
    a1d71b18b7
  180. 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.
    ba63bc6c41
  181. 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
    bab05b17b3
  182. 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.
    15ca8967a9
  183. 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.
    1862cfb6dd
  184. rpc: use private broadcast from sendrawtransaction RPC if -privatebroadcast is ON afceea9c3e
  185. test: improve debug log message from P2PConnection::connection_made()
    This is used in both cases - TCP server (accept) and TCP client (connect).
    The message "Connected & Listening address:port" is confusing.
    
    Print both ends of the TCP connection.
    252a7bb82a
  186. test: extend the SOCKS5 Python proxy to actually connect to a destination
    If requested, make the SOCKS5 Python proxy redirect connections to a set
    of given destinations. Actually act as a real proxy, connecting the
    client to a destination, except that the destination is not what the
    client asked for.
    
    This would enable us to "connect" to Tor addresses from the functional
    tests.
    e7aac1a7aa
  187. test: support WTX INVs from P2PDataStore and fix a comment 75fec22c3e
  188. test: set P2PConnection.p2p_connected_to_node in peer_connect_helper()
    Set `P2PConnection.p2p_connected_to_node` in
    `P2PConnection.peer_connect_helper()` instead of
    `TestNode.add_p2p_connection()` and
    `TestNode.add_outbound_p2p_connection()`.
    
    This way tests can create an instance of `P2PConnection` and use
    `P2PConnection.peer_connect_helper()` directly.
    2d9a9bec49
  189. test: add functional test for private broadcast 3bd618a91f
  190. vasild force-pushed on Jul 2, 2024
  191. vasild commented at 10:43 am on July 2, 2024: contributor
    7a7e7d189d...3bd618a91f: rebase and address suggestions
  192. pinheadmz commented at 3:04 pm on July 2, 2024: member

    utACK 3bd618a91f

    Code changes since last review look good.

  193. in test/functional/p2p_private_broadcast.py:68 in 3bd618a91f
    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 = [
    
  194. in test/functional/p2p_private_broadcast.py:236 in 3bd618a91f
    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/
  195. in test/functional/p2p_private_broadcast.py:303 in 3bd618a91f
    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
  196. DrahtBot commented at 11:22 am on July 4, 2024: contributor

    🐙 This pull request conflicts with the target branch and needs rebase.

  197. DrahtBot added the label Needs rebase on Jul 4, 2024

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-07-05 16:12 UTC

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