Skip to content

wallet: resubmit transactions with private broadcast if enabled#34533

Draft
vasild wants to merge 2 commits intobitcoin:masterfrom
vasild:wallet_rebroadcast_use_private
Draft

wallet: resubmit transactions with private broadcast if enabled#34533
vasild wants to merge 2 commits intobitcoin:masterfrom
vasild:wallet_rebroadcast_use_private

Conversation

@vasild
Copy link
Contributor

@vasild vasild commented Feb 7, 2026

The wallet keeps track of transactions related to it and periodically rebroadcasts them every 12-36 hours if they are not mined.

If -privatebroadcast=1 and a transaction is submitted locally with the sendrawtransaction RPC and it is related to the wallet, then the rebroadcasts would use the send-to-all method. Change that to use the private broadcast method.

@DrahtBot DrahtBot added the Wallet label Feb 7, 2026
@DrahtBot
Copy link
Contributor

DrahtBot commented Feb 7, 2026

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
ACK w0xlt, andrewtoth
Concept ACK pablomartin4btc

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:

  • #34359 (test: add test for rebroadcast of transaction received via p2p by mzumsande)
  • #34038 (logging: replace -loglevel with -trace, various API improvements by ajtowns)
  • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
  • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)

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.

#include <key.h>
#include <key_io.h>
#include <logging.h>
#include <net.h>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for the constant DEFAULT_PRIVATE_BROADCAST. Is there a better place for it? It is used in the following files:

src/init.cpp
src/net.cpp
src/net_processing.h
src/rpc/mempool.cpp
src/wallet/wallet.cpp (new in this PR)

@vasild vasild mentioned this pull request Feb 7, 2026
13 tasks
@vasild vasild force-pushed the wallet_rebroadcast_use_private branch from f3976df to a374c53 Compare February 7, 2026 05:42
@DrahtBot
Copy link
Contributor

DrahtBot commented Feb 7, 2026

🚧 At least one of the CI tasks failed.
Task lint: https://github.com/bitcoin/bitcoin/actions/runs/21775023286/job/62830040562
LLM reason (✨ experimental): Linting Python code failed (ruff) due to an extraneous f-string without placeholders in wallet_resendwallettransactions.py.

Hints

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.

Copy link
Member

@pablomartin4btc pablomartin4btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK.

This is necessary to support the private transaction broadcast mechanism introduced in #29415 when transactions are rebroadcast or resubmitted, which does not seem to be handled currently.

@w0xlt
Copy link
Contributor

w0xlt commented Feb 8, 2026

Concept ACK — though I am not sure this is the best long-term approach. However, it could work as a pragmatic short-term solution if we need something quickly.

One concern with the approach here is that if a user uses sendrawtransaction to submit a wallet-related transaction while -privatebroadcast is enabled and then restarts the node without -privatebroadcast, the transaction would be rebroadcast via mempool since there is no per-transaction tracking of how it was originally sent.

This is being discussed further here: #34457 (comment). As mentioned there, a possible alternative could be for the wallet to treat all IsFromMe() transactions not originated through CommitTransaction() as private by default (that PR introduces a private flag in CWalletTx::mapValue) and only allow rebroadcasting when -privatebroadcast is enabled.

Copy link
Contributor

@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern with the approach here is that if a user uses sendrawtransaction to submit a wallet-related transaction while -privatebroadcast is enabled and then restarts the node without -privatebroadcast, the transaction would be rebroadcast via mempool since there is no per-transaction tracking of how it was originally sent.

An attacker always has the alternative option to just create their own tx to dust the address - this would circumvent any transaction tracking, and would be more convenient to the attacker anyway because this way the attacker can choose the fee carefully - low enough to make rebroadcast very probable, and high enough to make mempool eviction unprobable.
This PR fixes this problem for both IsFromMe() and IsMine() in a way transaction tracking wouldn't. However, I think the problem is concerning enough that it should also be solved for clearnet-only nodes (#3828)

This is being discussed further here: #34457 (comment). As mentioned there, a possible alternative could be for the wallet to treat all IsFromMe() transactions not originated through CommitTransaction() as private by default (that PR introduces a private flag in CWalletTx::mapValue) and only allow rebroadcasting when -privatebroadcast is enabled.

As argued above, I think this is not complete unless we also do something about IsMine() transactions.

@vasild
Copy link
Contributor Author

vasild commented Feb 13, 2026

I am not sure this is the best long-term approach

Why? I think wallet support for private broadcast includes this change, which only relates to re-broadcasts. I do not see this as a short-term solution. The rest of the wallet support, for transactions initiated from the wallet (I guess in #34457, I did not study it yet, but will) goes together with this.

One concern with the approach here is that if a user uses sendrawtransaction to submit a wallet-related transaction while -privatebroadcast is enabled and then restarts the node without -privatebroadcast, the transaction would be rebroadcast via mempool since there is no per-transaction tracking of how it was originally sent.

Hmm, as a user, this is what I would expect - if restarted with -privatebroadcast=0, then the wallet rebroadcasts should be done using the send-to-all method ("via the mempool"). I would expect that because I have switched off private broadcast.

The log message "Submitting wtx %s" might be misleading because the
printed value is the txid, not wtxid.

Change that to print both: "txid=%s wtxid=%s".

Also, include the outcome of the submit which is useful for the one
caller that does not print the error string.
The wallet keeps track of transactions related to it and periodically
rebroadcasts them every 12-36 hours if they are not mined.

If `-privatebroadcast=1` and a transaction is submitted locally with the
`sendrawtransaction` RPC and it is related to the wallet, then the
rebroadcasts would use the send-to-all method. Change that to use the
private broadcast method.
@vasild vasild force-pushed the wallet_rebroadcast_use_private branch from a374c53 to 16cd713 Compare February 19, 2026 10:08
@vasild
Copy link
Contributor Author

vasild commented Feb 19, 2026

a374c53b8d30ea60e56f07b45f0645f202109eff...16cd713073f45a5ddff1cdce73a7a8459bd8c618: rebase due to conflicts

Copy link
Contributor

@w0xlt w0xlt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect that because I have switched off private broadcast.

In the other PR, I originally proposed tracking privately broadcast transactions in the wallet and only rebroadcasting them when -privatebroadcast is enabled again.

While this works, it adds state complexity (for example, if the user never restarts the node with -privatebroadcast, the transaction would never be rebroadcast).

The main motivation was to prevent unaware users from having their originally privately broadcast transactions later rebroadcast differently if the node is restarted without -privatebroadcast, since rebroadcasting is not a deliberate user action. However, it’s probably better to keep things simple.

ACK 16cd713

@maflcko maflcko closed this Feb 20, 2026
@maflcko maflcko reopened this Feb 20, 2026
Copy link
Contributor

@andrewtoth andrewtoth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 16cd713

@achow101
Copy link
Member

Disclaimer: I have not reviewed the private broadcast code yet, so some of these concerns may not make sense.

I'm not convinced that this is ready for the wallet until there is support for private broadcasting of packages, and for the wallet to submit packages when rebroadcasting.

Specifically, the wallet may contain transactions that have ancestors and descendants, and these may be complex topologies. Now, the rebroadcasting logic is quite naive as it sends all of the unconfirmed transactions into the mempool one by one, which means that if each transaction could not be accepted on its own, it cannot get rebroadcast. The wallet does at least submit them in the correct order so that transactions that depend on other transactions in the wallet won't be rejected for missing-inputs, unless an ancestor was rejected for some other reason.

But, I believe this is made worse if private broadcast is enabled. AFAICT, each private broadcast sends the transaction to a random node. This means that if there is a dependency, a child transaction is unlikely to be sent to the node that the parent was sent to, which means that it is unlikely to be accepted by the node it is sent to due to the parent being unknown. Although looking at the code, such a scenario would result in the child never being attempted to be rebroadcast as it would fail ProcessTransaction(test_accept=true) since the parent is not in the local mempool. At best this delays the rebroadcast of the child for up to 24 hours, at worst, the child can never get rebroadcast.

Here's a specific scenario that I think would be problematic. Consider a typical CPFP - a low feerate parent and a high feerate child. Suppose that these happen to not be in the mempool for whatever reason and the wallet is trying to rebroadcast them.

Without private broadcast, the wallet will send both to the local mempool individually, first the parent, then the child. If the parent cannot be accepted, then the child won't either and nothing gets broadcast. If the parent is accepted, then the child will be accepted as well and both transactions can get relayed as normal, and may even benefit from opportunistic 1p1c.

With private broadcast, first the parent is tested against the local mempool, then the child. If the parent would not be accepted, then neither get broadcast and it's the same situation as with normal relay. But if the parent would be accepted, it gets queued to be sent off to a random peer. Then the child gets tested against the local mempool, and it fails because the parent is not in the local mempool. Once the parent gets relayed and it shows up in the local mempool, if it's still there up to 24 hours later, the child gets broadcast and now it's the normal relay scenario, just 24 hours later.

But, suppose the parent got relayed, but it falls out of the mempool before the next rebroadcast (highly dynamic fee environment, with the parent sitting just around minrelayfeerate). Now we're back to square one and the parent has to get relayed again. Theoretically, this could cycle indefinitely and the transactions never get mined.

@vasild
Copy link
Contributor Author

vasild commented Mar 4, 2026

Here's a specific scenario that I think would be problematic. Consider a typical CPFP ...

Correct. Possible solutions to that:

  • Send the parent and the child via the same private broadcast connection (package private broadcast); or
  • Wait for the parent to round-trip through the network to our mempool (usually a few seconds) and only then private broadcast the child to another random peer (soon, not 24h later).

@andrewtoth
Copy link
Contributor

I believe #34707 fixes this CPFP issue. If both txs are submitted to private broadcast, then when the stale tx check occurs it will mark the child as mempool-rejected but not remove it from the queue. Once the parent makes it back into the mempool, the stale tx check will see that the child is now acceptable and begin broadcasting it again.

@sedited
Copy link
Contributor

sedited commented Mar 11, 2026

Correct. Possible solutions to that:

Should this PR be drafted while those things are being worked on?

@fanquake fanquake marked this pull request as draft March 11, 2026 09:37
@DrahtBot
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants