wallet, rpc, gui: Allow users to manually rebroadcast a wallet transaction #36481

pull achow101 wants to merge 3 commits into bitcoin:master from achow101:manual-rebroadcast changing 9 files +135 −8
  1. achow101 commented at 9:41 PM on October 8, 2026: member

    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.

  2. 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

    <!--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>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #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)
    • #36380 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36380.svg"></sub> (wallet: extend -walletbroadcast to allow initial send only by vasild)
    • #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-->

  3. achow101 force-pushed on Oct 8, 2026
  4. DrahtBot added the label CI failed on Oct 8, 2026
  5. 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 from rebroadcastTransaction() in qt/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>

  6. polespinasa commented at 10:24 PM on October 8, 2026: member

    concept ACK

  7. 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.

  8. murchandamus commented at 7:39 PM on October 21, 2026: member

    Concept ACK

  9. jpk68 commented at 12:27 AM on October 22, 2026: contributor

    Concept ACK

  10. fametrano commented at 11:35 AM on October 25, 2026: contributor

    Concept ACK

    One issue should be fixed before merge: rebroadcastwallettx fails 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 -maxfeerate to 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, sendrawtransaction with maxfeerate=0.0002 returns -25 "Fee rate exceeds maximum configured by user (maxfeerate)".

    The cause is that Chain::broadcastTransaction returns only a bool, and BroadcastTransaction sets no error string for MAX_FEE_EXCEEDED or MAX_FEE_RATE_EXCEEDED. sendrawtransaction falls back to TransactionErrorString in 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 the Chain::broadcastTransaction wrapper would fix it, and the GUI action will need that message once it shows the result. I can share the probe script.

  11. 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.
    

    achow101 commented at 1:16 AM on November 5, 2026:

    Done

  12. 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.
    

    achow101 commented at 1:16 AM on November 5, 2026:

    Done

  13. in src/qt/transactionview.cpp:172 in 648450039a outdated
     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 & in Re&broadcast? It is also in A&bandon.


    achow101 commented at 1:07 AM on November 5, 2026:

    It specifies the previous character as a shortcut key. With this menu open, the user can do ctrl + e to trigger this action rather than needing to move the mouse to it and clicking.

  14. 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 return should be either on the same line as if or there should be { and }.


    achow101 commented at 1:17 AM on November 5, 2026:

    Done

  15. 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 walletModel
    

    achow101 commented at 1:17 AM on November 5, 2026:

    Done

  16. in 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>


    achow101 commented at 1:17 AM on November 5, 2026:

    Added a commit for this

  17. in src/wallet/rpc/transactions.cpp:1115 in 648450039a outdated
    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::VNULL is for "If rebroadcast is successfully initiated".

    -            if (!wallet) return UniValue::VNULL;
    +            if (!wallet) {
    +                throw JSONRPCError(RPC_WALLET_ERROR, "no wallet");
    +            }
    

    achow101 commented at 1:18 AM on November 5, 2026:

    GetWalletForJSONRPCRequest never returns nullptr, this should really be a Assume return a reference. But this is the pattern that we currently use for all of the wallet RPCs.

  18. 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 transactions here.


    achow101 commented at 1:18 AM on November 5, 2026:

    Done

  19. vasild commented at 2:03 PM on October 27, 2026: contributor

    Approach ACK 648450039ab1c93f5e0001e0829f094ca41d2bae

  20. achow101 force-pushed on Nov 5, 2026
  21. achow101 force-pushed on Nov 5, 2026
  22. DrahtBot removed the label CI failed on Nov 5, 2026
  23. in src/interfaces/wallet.h:170 in 96c0e43277 outdated
     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 broadcast.
     167 | +    virtual bool transactionCanBeBroadcast(const Txid& txid) = 0;
     168 | +
     169 | +    //! Rebroadcast a transaction.
     170 | +    virtual util::Expected<void, wallet::WalletError> rebroadcastTransaction(const Txid& txid) = 0;
    


    ryanofsky commented at 1:35 PM on November 5, 2026:

    In commit "gui: Add rebroadcast tx menu action" (96c0e432770d5396294034618d35260510211bff)

    Could this (and RebroadcastSingleTransaction) return util::Result<void> instead of util::Expected<void, wallet::WalletError>? That would make the interface say what it returns: an error message, with no codes callers are expected to handle.

    WalletError can hold any of six codes, but this method only returns two, InvalidParameter and MiscError, and callers can't tell that from the declaration. None of them handles the codes differently either: the GUI shows the message, and the RPC only uses the code to pick an RPC error number. The WalletErrorCode documentation asks for a specific code only "when callers need to handle the condition differently", and GenericError otherwise (https://github.com/bitcoin/bitcoin/blob/0e38ad26af922abdc0fc36c9379e17efd2a72b80/src/wallet/types.h#L63-L65).

    util::Result is meant for this case, functions reporting error strings to end users, and displayAddress() above already returns util::Result<void>. TransactionCanBeBroadcast could return it too, instead of util::Expected<void, bilingual_str>. The RPC could then throw RPC_WALLET_ERROR for failures. It would also stop "Wallet broadcast is disabled" from being reported as an invalid parameter. If the RPC should report an unknown txid the way gettransaction does (RPC_INVALID_ADDRESS_OR_KEY), it could check mapWallet itself before calling, without a wallet-wide code. Since rebroadcastwallettx is new, no client depends on its error codes yet.

    Using util::Expected<void, bilingual_str> instead of util::Result<void> would be fine too. My main concern is the signature saying the function can return error codes that it never returns and no caller handles.


    achow101 commented at 8:36 PM on November 5, 2026:

    My understanding is that for the QML GUI, they generally want error codes as some errors may be handled differently. While the GUI as I have implemented it currently does not use these codes, that does not preclude the future GUI work from using them either, and I'd prefer to avoid needing another interface change at that time as well. cc @pseudoramdom

    Even for the new RPC, I think it makes sense to get the codes so that we can give reasonable RPC error codes. I think it is incorrect to return RPC_WALLET_ERROR for things that are clearly the user providing a transaction that cannot be broadcast, hence using InvalidParameter so that it can return RPC_INVALID_PARAMETER.

    Furthermore, I agree that it is strange that "Wallet broadcast is disabled" is InvalidParameter, but I don't think the solution is to drop the error codes from the wallet, but rather that TransactionCanBeBroadcast should use util::Expected<void, WalletError> as well, which I've done.

  24. achow101 force-pushed on Nov 5, 2026
  25. DrahtBot added the label Needs rebase on Nov 6, 2026
  26. achow101 force-pushed on Nov 6, 2026
  27. DrahtBot added the label CI failed on Nov 6, 2026
  28. DrahtBot removed the label Needs rebase on Nov 6, 2026
  29. in src/wallet/wallet.cpp:1853 in eba7a572f8
    1852 | +bool CWallet::TransactionCanBeBroadcast(const Txid& txid) const
    1853 | +{
    1854 | +    LOCK(cs_wallet);
    1855 | +    const CWalletTx* wtx = GetWalletTx(txid);
    1856 | +    return wtx && TransactionCanBeBroadcast(*wtx);
    1857 | +}
    


    polespinasa commented at 8:36 AM on November 6, 2026:

    in eba7a572f8832fa355f943f005708e4f20a1b5eb wallet, rpc: Add rebroadcastwallettx

    Why do we need two separate functions? This first one only adds one check, and because of the second one being private, they are never called independently outside the class.

    Can't we add the if wtx check inside the private function and make it take a txid? A caller knowing the CWalletTx can always know a txid.

    <details> <summary>suggested diff</summary>

    $ git diff
    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index cf0c45a1b4..c420be2f5f 100644
    --- a/src/wallet/wallet.cpp
    +++ b/src/wallet/wallet.cpp
    @@ -1835,7 +1835,7 @@ util::Expected<void, WalletError> CWallet::RebroadcastSingleTransaction(const Tx
         if (it == mapWallet.end()) {
             return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s not in wallet"), txid.ToString())}};
         }
    -    if (!TransactionCanBeBroadcast(it->second)) {
    +    if (!TransactionCanBeBroadcast(it->first)) {
             return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s is not eligible to be rebroadcast"), txid.ToString())}};
         }
         std::string err;
    @@ -1849,21 +1849,17 @@ bool CWallet::TransactionCanBeBroadcast(const Txid& txid) const
     {
         LOCK(cs_wallet);
         const CWalletTx* wtx = GetWalletTx(txid);
    -    return wtx && TransactionCanBeBroadcast(*wtx);
    -}
    -
    -bool CWallet::TransactionCanBeBroadcast(const CWalletTx& wtx) const
    -{
    -    AssertLockHeld(cs_wallet);
    +    // Transaction is not in the wallet
    +    if (!wtx) return false;
         // Can't relay if wallet is not broadcasting
         if (!GetBroadcastTransactions()) return false;
         // Don't relay abandoned transactions
    -    if (wtx.isAbandoned()) return false;
    +    if (wtx->isAbandoned()) 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()) return false;
         // Don't try to submit conflicted or confirmed transactions.
    -    if (GetTxDepthInMainChain(wtx) != 0) return false;
    +    if (GetTxDepthInMainChain(*wtx) != 0) return false;
         return true;
     }
     
    @@ -1873,7 +1869,7 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx,
     {
         AssertLockHeld(cs_wallet);
     
    -    if (!TransactionCanBeBroadcast(wtx)) return false;
    +    if (!TransactionCanBeBroadcast(wtx.GetHash())) return false;
     
         const char* what{""};
         switch (broadcast_method) {
    diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
    index 0633e928e1..c64e53e0f5 100644
    --- a/src/wallet/wallet.h
    +++ b/src/wallet/wallet.h
    @@ -446,8 +446,6 @@ private:
         //! 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);
    -
     public:
         /**
          * Main wallet lock.
    
    

    </details>


    achow101 commented at 11:22 PM on November 6, 2026:

    To avoid needing to do another mapWallet lookup for callers that already have a CWalletTx.

  30. in src/wallet/wallet.cpp:1866 in eba7a572f8
    1863 | @@ -1843,6 +1864,16 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx,
    1864 |      if (wtx.IsCoinBase()) return false;
    1865 |      // Don't try to submit conflicted or confirmed transactions.
    1866 |      if (GetTxDepthInMainChain(wtx) != 0) return false;
    


    polespinasa commented at 8:43 AM on November 6, 2026:

    in eba7a572f8832fa355f943f005708e4f20a1b5eb wallet, rpc: Add rebroadcastwallettx

    Why not use wtx.isBlockConflicted()?

    Also random and unrelated question. Why IsUnconfirmed() checks if there is any mempool conflict? That existance of a mempool conflict doesn't change the unconfirmed state...


    achow101 commented at 11:29 PM on November 6, 2026:

    Why not use wtx.isBlockConflicted()?

    This is moved code.

    Also random and unrelated question. Why IsUnconfirmed() checks if there is any mempool conflict? That existance of a mempool conflict doesn't change the unconfirmed state...

    Same reason it checks isAbandoned() and isConflicted - unconfirmed in this context means that this transaction is unconfirmed, and we want/expect it to become confirmed. If the transaction is abandoned or has a conflict already in the mempool, we either do not want it to become confirmed, or it cannot become confirmed.

  31. in src/wallet/wallet.cpp:1837 in eba7a572f8 outdated
    1836 | -    AssertLockHeld(cs_wallet);
    1837 | +    LOCK(cs_wallet);
    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 | +    }
    


    polespinasa commented at 9:43 AM on November 6, 2026:

    in eba7a572f8832fa355f943f005708e4f20a1b5eb wallet, rpc: Add rebroadcastwallettx

    small nit, feel free to ignore

    This check is duplicated, it is already done inside TransactionCanBeBroadcast. Not criticla and i guess you need the find function to call SubmitTxMemoryPoolAndRelay.

    I tried to refactor it so TransactionCanBeBroadcast returns a CWalletTx instead of a Void in the next commit, but I think is not worth the work.

    Mentioning in case you come with a better idea :)

  32. in src/wallet/rpc/transactions.cpp:1088 in eba7a572f8 outdated
    1083 | +RPCMethod rebroadcastwallettx()
    1084 | +{
    1085 | +    return RPCMethod{"rebroadcastwallettx",
    1086 | +        "Rebroadcast the specified wallet transaction.\n"
    1087 | +        "Note that rebroadcasting a transaction may reveal that your node is involved as either a sender or receiver in the transaction."
    1088 | +        "Use listrawtransactions to see the wallet's transactions",
    


    polespinasa commented at 10:41 AM on November 6, 2026:

    in eba7a572f8832fa355f943f005708e4f20a1b5eb wallet, rpc: Add rebroadcastwallettx

    nit: missing empty space or line jump after "receiver in the transaction."

    $ ./build/bin/bitcoin-cli -regtest rebroadcastwallettx
    error code: -1
    error message:
    rebroadcastwallettx "txid"
    
    Rebroadcast the specified wallet transaction.
    Note that rebroadcasting a transaction may reveal that your node is involved as either a sender or receiver in the transaction.Use listrawtransactions to see the wallet's transactions
    
    
    

    achow101 commented at 11:40 PM on November 6, 2026:

    DOe

  33. in src/wallet/wallet.cpp:1863 in 592e7465b4 outdated
    1862 | +util::Expected<void, WalletError> CWallet::TransactionCanBeBroadcast(const CWalletTx& wtx) const
    1863 |  {
    1864 |      AssertLockHeld(cs_wallet);
    1865 |      // Can't relay if wallet is not broadcasting
    1866 | -    if (!GetBroadcastTransactions()) return false;
    1867 | +    if (!GetBroadcastTransactions()) return util::Unexpected{WalletError{WalletErrorCode::MiscError, _("Wallet broadcast is disabled")}};;
    


    polespinasa commented at 10:46 AM on November 6, 2026:

    in 592e7465b475fa6a6e5af846e694b84990505878 wallet: Return specific error messages from TransactionCanBeBroadcast

    nit: there are two ; here? I am surprised this does compile with one or two.

    <details> <summary>fun fact</summary>

    Am I the only one who did not know that you can put as many semicolons as you want and still being correct? This does compile:

    $ git diff
    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index 5de55db7fa..c104bbe409 100644
    --- a/src/wallet/wallet.cpp
    +++ b/src/wallet/wallet.cpp
    @@ -1860,7 +1860,7 @@ util::Expected<void, WalletError> CWallet::TransactionCanBeBroadcast(const CWall
     {
         AssertLockHeld(cs_wallet);
         // Can't relay if wallet is not broadcasting
    -    if (!GetBroadcastTransactions()) return util::Unexpected{WalletError{WalletErrorCode::MiscError, _("Wallet broadcast is disabled")}};;
    +    if (!GetBroadcastTransactions()) return util::Unexpected{WalletError{WalletErrorCode::MiscError, _("Wallet broadcast is disabled")}};;;;;;;;;;;;;;;;;;;
         // Don't relay abandoned transactions
         if (wtx.isAbandoned()) return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s is not eligible to be rebroadcast, tx is abandoned"), wtx.GetHash().ToString())}};
         // Don't try to submit coinbase transactions. These would fail anyway but would
    @@ -1869,7 +1869,7 @@ util::Expected<void, WalletError> CWallet::TransactionCanBeBroadcast(const CWall
         // Don't try to submit conflicted or confirmed transactions.
         if (GetTxDepthInMainChain(wtx) > 0) return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s is not eligible to be rebroadcast, tx is confirmed"), wtx.GetHash().ToString())}};
         if (GetTxDepthInMainChain(wtx) < 0) return util::Unexpected{WalletError{WalletErrorCode::InvalidParameter, strprintf(_("%s is not eligible to be rebroadcast, tx is conflicted"), wtx.GetHash().ToString())}};
    -    return {};
    +    return {};;;;;;;;;;;;;;;
     }
    
    

    </details>


    achow101 commented at 11:41 PM on November 6, 2026:

    Fixed

  34. polespinasa commented at 10:52 AM on November 6, 2026: member

    This will need a release note.

    Also might be worth taking a look at ralph comments, specially the GUI crash, the blocks sync and the empty error messages :)

  35. achow101 commented at 11:37 PM on November 6, 2026: member

    Also might be worth taking a look at ralph comments, specially the GUI crash, the blocks sync and the empty error messages :)

    None of these are relevant or things we care about at all.

  36. wallet, rpc: Add rebroadcastwallettx 2545bca85b
  37. achow101 force-pushed on Nov 6, 2026
  38. wallet: Return specific error messages from TransactionCanBeBroadcast 2ff30311cf
  39. gui: Add rebroadcast tx menu action e27218f7ad
  40. achow101 force-pushed on Nov 6, 2026
  41. DrahtBot removed the label CI failed on Nov 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-11-08 19:51 UTC

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