From the discussion in #36403, it is clear that there needs to be a way for users to rebroadcast a transaction. Currently, the way to do so requires getting the raw transaction, getting the hex, and using sendrawtransaction. This PR adds rebroadcastwallettx RPC that takes a txid to attempt to rebroadcast that single transaction. An action is also added to the GUI menu for each transaction to do the same.
wallet, rpc, gui: Allow users to manually rebroadcast a wallet transaction #36481
pull achow101 wants to merge 2 commits into bitcoin:master from achow101:manual-rebroadcast changing 9 files +123 −4-
achow101 commented at 9:41 PM on October 8, 2026: member
-
DrahtBot commented at 9:41 PM on October 8, 2026: contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
External sites
- Code coverage & benchmarks: https://corecheck.dev/bitcoin/bitcoin/pulls/36481
- Ralph review <img src=https://review.fish.foo/assets/bitcoin/bitcoin/36481.png alt="Ralph review status"> https://git.fish.foo/bitcoin/bitcoin/pulls/36481#author-ralph
<!--021abf342d371248e50ceaed478a90ca-->
Reviews
See the guideline and AI policy for information on the review process.
Type Reviewers Concept ACK polespinasa, mzumsande, murchandamus, jpk68, fametrano Approach ACK vasild If your review is incorrectly listed, please copy-paste <code><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--174a7506f384e20aa4161008e828411d-->
Conflicts
Reviewers, this pull request conflicts with the following ones:
- #36597 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36597.svg"></sub> (wallet: return broadcast errors from CommitTransaction by pseudoramdom)
- #36482 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36482.svg"></sub> (wallet: Only broadcast resubmitted transactions that were added to the mempool by achow101)
- #36330 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36330.svg"></sub> (wallet: don't resubmit transactions under -privatebroadcast by instagibbs)
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.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
-
wallet, rpc: Add rebroadcastwallettx 0a9df9bf23
-
gui: Add rebroadcast tx menu action 648450039a
- achow101 force-pushed on Oct 8, 2026
- DrahtBot added the label CI failed on Oct 8, 2026
-
DrahtBot commented at 10:06 PM on October 8, 2026: contributor
<!--85328a0da195eb286784d51f73fa0af9-->
🚧 At least one of the CI tasks failed. <sub>Task
tidy: https://github.com/bitcoin/bitcoin/actions/runs/37848508934/job/113555303677</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy flagged an ignored return value fromrebroadcastTransaction()inqt/transactionview.cpp:408(warnings treated as errors).</sub><details><summary>Hints</summary>
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.
</details>
-
polespinasa commented at 10:24 PM on October 8, 2026: member
concept ACK
-
mzumsande commented at 3:36 PM on October 9, 2026: contributor
Concept ACK
Whatever the solution of the existing issues with rebroadcast will be, giving the users the option to rebroadcast transactions manually is useful in all of these scenarios.
-
murchandamus commented at 7:39 PM on October 21, 2026: member
Concept ACK
-
jpk68 commented at 12:27 AM on October 22, 2026: contributor
Concept ACK
-
fametrano commented at 11:35 AM on October 25, 2026: none
Concept ACK
One issue should be fixed before merge:
rebroadcastwallettxfails with code -1 and an empty message when the transaction is out of the mempool and its feerate is above the wallet's-maxfeerate.I ran the RPC on regtest at 648450039a. Other mempool rejections keep their reason, such as "bad-txns-inputs-missingorspent" for a child whose parent expired. The empty message shows up when a user lowers
-maxfeerateto 20 sat/vB and restarts without mempool.dat. Our own 50 sat/vB child of an incoming payment then stays out of the mempool, and the RPC returns{"code": -1, "message": ""}. For the same transaction,sendrawtransactionwithmaxfeerate=0.0002returns -25 "Fee rate exceeds maximum configured by user (maxfeerate)".The cause is that
Chain::broadcastTransactionreturns only a bool, andBroadcastTransactionsets no error string forMAX_FEE_EXCEEDEDorMAX_FEE_RATE_EXCEEDED.sendrawtransactionfalls back toTransactionErrorStringin that case. The wallet's other callers only log the string or drop it, so this RPC is the first to show it to a user. The same fallback inside theChain::broadcastTransactionwrapper would fix it, and the GUI action will need that message once it shows the result. I can share the probe script. -
in src/interfaces/wallet.h:166 in 648450039a
162 | @@ -163,6 +163,12 @@ class Wallet 163 | //! Abandon transaction. 164 | virtual bool abandonTransaction(const Txid& txid) = 0; 165 | 166 | + //! Return whether transaction can be abandoned.
vasild commented at 10:41 AM on October 27, 2026://! Return whether transaction can be broadcast.in src/interfaces/wallet.h:169 in 648450039a
162 | @@ -163,6 +163,12 @@ class Wallet 163 | //! Abandon transaction. 164 | virtual bool abandonTransaction(const Txid& txid) = 0; 165 | 166 | + //! Return whether transaction can be abandoned. 167 | + virtual bool transactionCanBeBroadcast(const Txid& txid) = 0; 168 | + 169 | + //! Abandon transaction.
vasild commented at 10:41 AM on October 27, 2026://! Rebroadcast a transaction.in src/qt/transactionview.cpp:172 in 648450039a
168 | @@ -169,6 +169,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa 169 | GUIUtil::ExceptionSafeConnect(bumpFeeAction, &QAction::triggered, this, &TransactionView::bumpFee); 170 | bumpFeeAction->setObjectName("bumpFeeAction"); 171 | abandonAction = contextMenu->addAction(tr("A&bandon transaction"), this, &TransactionView::abandonTx); 172 | + m_rebroadcast_action = contextMenu->addAction(tr("Re&broadcast transaction"), this, &TransactionView::rebroadcastTx);
vasild commented at 10:45 AM on October 27, 2026:What is the
&inRe&broadcast? It is also inA&bandon.in src/qt/transactionview.cpp:400 in 648450039a
393 | @@ -392,6 +394,20 @@ void TransactionView::abandonTx() 394 | model->wallet().abandonTransaction(hash); 395 | } 396 | 397 | +void TransactionView::rebroadcastTx() 398 | +{ 399 | + if(!transactionView || !transactionView->selectionModel()) 400 | + return;
vasild commented at 10:48 AM on October 27, 2026:if (!transactionView || !transactionView->selectionModel()) return;also, the
returnshould be either on the same line asifor there should be{and}.in src/qt/transactionview.cpp:407 in 648450039a
402 | + 403 | + // get the hash from the TxHashRole (QVariant / QString) 404 | + QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString(); 405 | + Txid hash = Txid::FromHex(hashQStr.toStdString()).value(); 406 | + 407 | + // Abandon the wallet transaction over the walletModel
vasild commented at 10:50 AM on October 27, 2026:// Rebroadcast the wallet transaction over the walletModelin src/wallet/wallet.cpp:1839 in 648450039a
1838 | + auto it = mapWallet.find(txid); 1839 | + if (it == mapWallet.end()) { 1840 | + return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s not in wallet"), txid.ToString())}}; 1841 | + } 1842 | + if (!TransactionCanBeBroadcast(it->second)) { 1843 | + return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s is not eligible to be rebroadcast"), txid.ToString())}};
vasild commented at 1:38 PM on October 27, 2026:If the wallet broadcast is disabled (
-walletbroadcast=0), then the "is not eligible to be rebroadcast" will be misleading because it implies that there is something specific about this transaction that makes it not eligible.Consider extending
TransactionCanBeBroadcast()to return the reason why the transaction can not be broadcast:<details> <summary>[patch] TransactionCanBeBroadcast() with a reason</summary>
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 29460e3ba2..dcd571b684 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1832,51 +1832,64 @@ util::Expected<void, WalletError> CWallet::RebroadcastSingleTransaction(const Tx { LOCK(cs_wallet); auto it = mapWallet.find(txid); if (it == mapWallet.end()) { return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s not in wallet"), txid.ToString())}}; } - if (!TransactionCanBeBroadcast(it->second)) { - return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s is not eligible to be rebroadcast"), txid.ToString())}}; - } std::string err; + if (!TransactionCanBeBroadcast(it->second, err)) { + return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("can not rebroadcast %s: %s"), txid.ToString(), err)}}; + } if (!SubmitTxMemoryPoolAndRelay(it->second, err, node::TxBroadcast::MEMPOOL_AND_BROADCAST_TO_ALL)) { return util::Unexpected{WalletError{WalletErrorCode::MiscError, Untranslated(err)}}; } return {}; } -bool CWallet::TransactionCanBeBroadcast(const Txid& txid) const +bool CWallet::TransactionCanBeBroadcast(const Txid& txid, std::string& reason) const { LOCK(cs_wallet); const CWalletTx* wtx = GetWalletTx(txid); - return wtx && TransactionCanBeBroadcast(*wtx); + return wtx && TransactionCanBeBroadcast(*wtx, reason); } -bool CWallet::TransactionCanBeBroadcast(const CWalletTx& wtx) const +bool CWallet::TransactionCanBeBroadcast(const CWalletTx& wtx, std::string& reason) const { AssertLockHeld(cs_wallet); // Can't relay if wallet is not broadcasting - if (!GetBroadcastTransactions()) return false; + if (!GetBroadcastTransactions()) { + reason = "the wallet is configured to not broadcast transactions"; + return false; + } // Don't relay abandoned transactions - if (wtx.isAbandoned()) return false; + if (wtx.isAbandoned()) { + reason = "the transaction is abandoned"; + return false; + } // Don't try to submit coinbase transactions. These would fail anyway but would // cause log spam. - if (wtx.IsCoinBase()) return false; + if (wtx.IsCoinBase()) { + reason = "the transaction is a coinbase transaction"; + return false; + } // Don't try to submit conflicted or confirmed transactions. - if (GetTxDepthInMainChain(wtx) != 0) return false; + if (GetTxDepthInMainChain(wtx) != 0) { + reason = "the transaction is already confirmed or is conflicted"; + return false; + } return true; } bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string, node::TxBroadcast broadcast_method) const { AssertLockHeld(cs_wallet); - if (!TransactionCanBeBroadcast(wtx)) return false; + std::string reason; + if (!TransactionCanBeBroadcast(wtx, reason)) return false; const char* what{""}; switch (broadcast_method) { case node::TxBroadcast::MEMPOOL_AND_BROADCAST_TO_ALL: what = "to mempool and for broadcast to peers"; break; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 8d7c72f4d7..0b64d62571 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -443,13 +443,13 @@ private: // Update last block processed in memory only void SetLastBlockProcessedInMem(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); //! Update mempool conflicts for TRUC sibling transactions void UpdateTrucSiblingConflicts(const CWalletTx& parent_wtx, const Txid& child_txid, bool add_conflict) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - bool TransactionCanBeBroadcast(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool TransactionCanBeBroadcast(const CWalletTx& wtx, std::string& reason) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); public: /** * Main wallet lock. * This lock protects all the fields added by CWallet. */ @@ -635,13 +635,13 @@ public: void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) override; /** Set the next time this wallet should resend transactions to 12-36 hours from now, ~1 day on average. */ void SetNextResend() { m_next_resend = GetDefaultNextResend(); } /** Return true if all conditions for periodically resending transactions are met. */ bool ShouldResend() const; void ResubmitWalletTransactions(node::TxBroadcast broadcast_method, bool force); - bool TransactionCanBeBroadcast(const Txid& txid) const; + bool TransactionCanBeBroadcast(const Txid& txid, std::string& reason) const; util::Expected<void, WalletError> RebroadcastSingleTransaction(const Txid& txid); OutputType TransactionChangeType(const std::optional<OutputType>& change_type, const std::vector<CRecipient>& vecSend) const; /** Fetch the inputs and sign with SIGHASH_ALL. */ bool SignTransaction(CMutableTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 78139f635d..1759680b02 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -283,13 +283,17 @@ public: bool transactionCanBeAbandoned(const Txid& txid) override { return m_wallet->TransactionCanBeAbandoned(txid); } bool abandonTransaction(const Txid& txid) override { LOCK(m_wallet->cs_wallet); return m_wallet->AbandonTransaction(txid); } - bool transactionCanBeBroadcast(const Txid& txid) override { return m_wallet->TransactionCanBeBroadcast(txid); } + bool transactionCanBeBroadcast(const Txid& txid) override + { + std::string reason; + return m_wallet->TransactionCanBeBroadcast(txid, reason); + } util::Expected<void, WalletError> rebroadcastTransaction(const Txid& txid) override { LOCK(m_wallet->cs_wallet); return m_wallet->RebroadcastSingleTransaction(txid); } bool transactionCanBeBumped(const Txid& txid) override</details>
in src/wallet/rpc/transactions.cpp:1115 in 648450039a
1110 | + HelpExampleRpc("rebroadcastwallettx", "\"txid\"") 1111 | + }, 1112 | + [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue 1113 | + { 1114 | + std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request); 1115 | + if (!wallet) return UniValue::VNULL;
vasild commented at 1:43 PM on October 27, 2026:Should we not throw here?
UniValue::VNULLis for "If rebroadcast is successfully initiated".- if (!wallet) return UniValue::VNULL; + if (!wallet) { + throw JSONRPCError(RPC_WALLET_ERROR, "no wallet"); + }in src/wallet/rpc/transactions.cpp:1103 in 648450039a
1094 | @@ -1095,4 +1095,32 @@ RPCMethod abortrescan() 1095 | }, 1096 | }; 1097 | } 1098 | + 1099 | +RPCMethod rebroadcastwallettx() 1100 | +{ 1101 | + return RPCMethod{"rebroadcastwallettx", 1102 | + "Rebroadcast the specified wallet transaction.\n" 1103 | + "Note that rebroadcasting a transaction may reveal that your node is involved as either a sender or receiver in the transaction.",
vasild commented at 1:45 PM on October 27, 2026:I think it would be useful to add the sentence
Use the listtransactions RPC to see the wallet transactionshere.vasild commented at 2:03 PM on October 27, 2026: contributorApproach ACK 648450039ab1c93f5e0001e0829f094ca41d2bae
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-10-30 09:40 UTC