wallet: add private broadcast support for wallet transactions #34457

pull w0xlt wants to merge 4 commits into bitcoin:master from w0xlt:wprv_29012 changing 12 files +666 −47
  1. w0xlt commented at 8:06 am on January 30, 2026: contributor

    Summary

    Extends the private broadcast feature (#29415) to wallet transactions. When -privatebroadcast=1 is enabled, wallet RPCs (sendtoaddress, send, sendall, sendmany) now broadcast transactions through short-lived Tor/I2P connections instead of announcing to all connected peers.

    Key Changes

    Centralized availability check (commit 1)

    • Moves the Tor/I2P reachability check from sendrawtransaction RPC to BroadcastTransaction()
    • All broadcast callers now get consistent error handling

    Wallet integration (commit 2)

    • CommitTransaction(): Uses private broadcast when enabled, fails loudly if Tor/I2P unavailable
    • ResubmitWalletTransactions(): Rebroadcasts using the original broadcast method
    • Persists a private_broadcast flag in the wallet transaction’s mapValue

    Functional tests (commits 3-4)

    • Tests wallet send via SOCKS5 proxy
    • Tests flag persistence across restarts
    • Tests error handling when Tor/I2P unavailable

    Behavior Matrix

    Scenario TX Flag Node Setting Expected Behavior Tested
    New tx Private - Via SOCKS5, flag set Test 1 ✓
    Resubmit Private Public Skip Test 2 ✓
    Resubmit Public Private Skip Test 3 ✓
    Resubmit Private Private Rebroadcast Test 3 ✓
    New tx Private - (no Tor) RPC error Test 4 ✓
    Resubmit Private Private (no Tor) Log error Test 4 ✓

    Why skip on mode mismatch? Rebroadcasting via a different method than originally used may allow correlation of transaction to origin, or may indicate origin has -privatebroadcast enabled.

  2. DrahtBot added the label Wallet on Jan 30, 2026
  3. DrahtBot commented at 8:07 am on January 30, 2026: contributor

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK andrewtoth

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34533 (wallet: resubmit transactions with private broadcast if enabled by vasild)
    • #34410 (test: let connections happen in any order in p2p_private_broadcast.py by vasild)
    • #33034 (wallet: Store transactions in a separate sqlite table by achow101)
    • #32763 (wallet: Replace CWalletTx::mapValue and vOrderForm with explicit class members by achow101)
    • #29278 (Wallet: Add maxfeerate wallet startup option by ismaelsadeeq)

    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.

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • assert_raises_rpc_error(-1, “none of the Tor or I2P networks is reachable”, self.sender_wallet.sendtoaddress, dest, 0.1) in test/functional/wallet_private_broadcast.py

    2026-02-07 18:54:56

  4. DrahtBot added the label Needs rebase on Jan 30, 2026
  5. andrewtoth commented at 9:29 pm on January 30, 2026: contributor

    Concept ACK

    I don’t think we need to skip rebroadcast if we initially created the transaction with private broadcast disabled. This is only important the other way - if we initially broadcast via private broadcast, but then restart with private broadcast disabled.

    may indicate origin has -privatebroadcast enabled.

    Ideally every node has this enabled in the future. But also, one cannot conclude this if they receive a tx both from a persistently connected node and then later via a private broadcast. This would actually make the original broadcast more private, since this would indicate that perhaps the originator is not actually the originator.

    Sending decoys will also help here.

  6. w0xlt force-pushed on Jan 31, 2026
  7. w0xlt commented at 3:26 am on January 31, 2026: contributor

    @andrewtoth Thanks for the insight about plausible deniability.

    Updated to only skip private→public (the other direction is now allowed):

    Scenario TX Flag Node Setting Behavior
    Resubmit Private Public Skip
    Resubmit Public Private Rebroadcast privately
    Resubmit Private Private Rebroadcast privately
    • Private→Public: Skip - Protects origin from being correlated
    • Public→Private: Allow - Provides plausible deniability
  8. DrahtBot removed the label Needs rebase on Jan 31, 2026
  9. mzumsande commented at 4:01 pm on February 2, 2026: contributor

    Some conceptual thoughts:

    • The rebroadcasting behavior will probably lead to frequent 3 minute timeouts. If the tx didn’t get mined within ~24 hours due to insufficient fees, but remains in our and our peer’s mempools during this time, the peers wont’s send us a GETDATA to our repeated submission. However, I think that this would not be a huge problem because after 3 attempts, we’d abort during the retry because we check there if the tx is already in the mempool.
    • while this improves the rebroadcast behavior, there is still the issue that transactions relevant for the wallet but submitted via RPC (sendrawtransaction) will be received back from the network, added to mapWallet, and then rebroadcast over clearnet if the user restarted without -privatebroadcast. In master, these txns would be rebroadcast normally over clearnet even without a restart.
    • should something be done about feebumping behavior? I think a user sending via private broadcast, restarting without the flag and then bumping a tx would lead to the tx being sent via clearnet.
  10. w0xlt force-pushed on Feb 7, 2026
  11. w0xlt force-pushed on Feb 7, 2026
  12. DrahtBot added the label CI failed on Feb 7, 2026
  13. DrahtBot commented at 10:00 am on February 7, 2026: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/21778174634/job/62838065557 LLM reason (✨ experimental): Lint failure: trailing whitespace detected in Python code (ruff/trailing_whitespace).

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

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

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

    • An intermittent issue.

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

  14. w0xlt commented at 10:13 am on February 7, 2026: contributor

    I think that this would not be a huge problem because after 3 attempts, we’d abort during the retry

    Agreed.

    there is still the issue that transactions relevant for the wallet but submitted via RPC (sendrawtransaction) will be received back from the network

    As far as I can see there is no simple solution to this scenario. Even if we have a persistent database of privately broadcast transactions (like proposed in #34322) and the wallet could query it when a relevant transaction is received, the user can submit the transaction from a different node than the wallet’s node. One approach would be for the wallet to mark all IsFromMe() transactions not originated through CommitTransaction() as private. Not sure if that is an acceptable solution but it would cover this case unless I am missing something.

    should something be done about feebumping behavior?

    Added a solution and a functional test for this case. Thanks for raising it.

  15. w0xlt commented at 10:15 am on February 7, 2026: contributor
    CI error is unrelated
  16. w0xlt force-pushed on Feb 7, 2026
  17. w0xlt force-pushed on Feb 7, 2026
  18. w0xlt force-pushed on Feb 7, 2026
  19. node: move private broadcast availability check to BroadcastTransaction()
    Move the Tor/I2P network reachability check from the sendrawtransaction RPC
    to BroadcastTransaction(). This centralizes the check so that all callers
    (including wallet RPCs) get consistent behavior when using private broadcast.
    
    Changes:
    - Add TransactionError::PRIVATE_BROADCAST_UNAVAILABLE enum value
    - Add corresponding error message in common/messages.cpp
    - Add RPC error code mapping in rpc/util.cpp
    - Move the check from rpc/mempool.cpp to node/transaction.cpp
    - Set err_string when the error occurs for proper error propagation
    7ad771b363
  20. wallet: integrate private broadcast for wallet transactions
    Enable private broadcast for wallet transaction submission when
    -privatebroadcast is set. This includes both initial sends and
    rebroadcasts, with persistent tracking to prevent privacy leaks.
    
    Changes to CommitTransaction():
    - Use NO_MEMPOOL_PRIVATE_BROADCAST method when -privatebroadcast enabled
    - Store "private_broadcast" flag in wallet transaction mapValue
    - Throw on broadcast failures (e.g., Tor/I2P not reachable) instead of
      silently continuing
    
    Changes to ResubmitWalletTransactions():
    - Use private broadcast for resubmission when -privatebroadcast enabled
    - Skip privately-sent txs when node has public setting (prevents IP leak)
    - Allow publicly-sent txs to rebroadcast privately (provides plausible deniability)
    - Log errors when rebroadcast fails due to Tor/I2P unavailability
    
    Changes to feebumper::CommitTransaction():
    - Refuse to fee-bump a privately-broadcast tx when -privatebroadcast is
      off. The replacement spends the same inputs, so broadcasting it over
      clearnet would link this node's IP to the original private transaction.
    
    The persistence logic is essential for privacy: without it, a node
    restart with -privatebroadcast disabled could cause private transactions
    to be rebroadcast publicly, leaking information about the transaction
    origin.
    b58d0c337b
  21. test: add Socks5ProxyHelper and add_addresses_to_addrman helpers
    Add helper utilities to test_framework/socks5.py to reduce code duplication
    in private broadcast tests:
    
    - Socks5ProxyHelper: Simplifies SOCKS5 proxy setup with ephemeral ports,
      thread-safe destination tracking, and simple redirect factory
    - add_addresses_to_addrman(): Helper to populate a node's addrman with
      test addresses
    - FAKE_ONION_ADDRESSES: Shared list of fake .onion addresses for tests
    
    Update p2p_private_broadcast.py to use ephemeral ports and the new
    add_addresses_to_addrman helper.
    57cbd23286
  22. test: add wallet_private_broadcast.py functional test
    Add a functional test for wallet-specific private broadcast behavior:
    
    - Test 1: sendtoaddress with -privatebroadcast sends via SOCKS5 proxy
    - Test 2: private_broadcast flag persists and prevents public rebroadcast
    - Test 3: Public txs rebroadcast privately (provides plausible deniability)
    - Test 4: Error when Tor/I2P not reachable
    - Test 5: Fee bump refused when -privatebroadcast is off (prevents clearnet leak)
    - Test 6: Fee bump succeeds and broadcasts privately when -privatebroadcast is on
    
    Uses Socks5ProxyHelper for simplified proxy setup and connection tracking.
    3645cccf8a
  23. w0xlt force-pushed on Feb 7, 2026
  24. DrahtBot removed the label CI failed on Feb 7, 2026

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: 2026-02-09 18:13 UTC

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