Load PSBT error: Unable to decode PSBT #30070

issue foolbear openend this issue on May 9, 2024
  1. foolbear commented at 5:34 am on May 9, 2024: none

    Is there an existing issue for this?

    • I have searched the existing issues

    Current behaviour

    “Load PSBT from …” from menu, error shows: Unable to decode PSBT Unsigned tx does not have empty scriptSigs and scriptWitnesses.: unspecified iostream_category error

    decodepsbt and analyzepsbt show the same error too: TX decode failed Unsigned tx does not have empty scriptSigs and scriptWitnesses.: unspecified iostream_category error (code -22)

    Expected behaviour

    I expect Load PSBT success.

    And same procedure works fine in another wallet of test net. Both wallets are not watch-only

    Steps to reproduce

    I test PSBT in my wallet today, create a transaction in Bitcoin Core, click “send” and “create unsigned”, then “Load PSBT from …” from menu

    I’m using prune mode for block chain. This address I made tx was obtained by importprivkey, and then covert to Descriptor Wallets by migratewallet.

    I found the source code:

                 case PSBT_GLOBAL_UNSIGNED_TX:
                {
                    if (!key_lookup.emplace(key).second) {
                        throw std::ios_base::failure("Duplicate Key, unsigned tx already provided");
                    } else if (key.size() != 1) {
                        throw std::ios_base::failure("Global unsigned tx key is more than one byte type");
                    }
                    CMutableTransaction mtx;
                    // Set the stream to serialize with non-witness since this should always be non-witness
                    UnserializeFromVector(s, TX_NO_WITNESS(mtx));
                    tx = std::move(mtx);
                    // Make sure that all scriptSigs and scriptWitnesses are empty
                    for (const CTxIn& txin : tx->vin) {
                        if (!txin.scriptSig.empty() || !txin.scriptWitness.IsNull()) {
     ---->                  throw std::ios_base::failure("Unsigned tx does not have empty scriptSigs and scriptWitnesses.");
                        }
                    }
                    break;
                }
    

    Relevant log output

    no relevant log.

    How did you obtain Bitcoin Core

    Pre-built binaries

    What version of Bitcoin Core are you using?

    Bitcoin Core version v27.0.0

    Operating system and version

    MacOS 14.4.1

    Machine specifications

    No response

  2. willcl-ark added the label PSBT on May 9, 2024
  3. maflcko commented at 1:06 pm on May 13, 2024: member
    Are you able to reproduce in regtest, with exact steps to reproduce?
  4. foolbear commented at 1:25 pm on May 13, 2024: none

    keywords: not watch-only wallet, prune mode, importprivkey, migratewallet.

    “And same procedure works fine in another wallet of test net”, but I cannot make an address with UTXOs from years ago(maybe have something to do with prune mode?).

  5. maflcko added the label Wallet on May 13, 2024
  6. foolbear commented at 9:37 am on May 28, 2024: none
    In the above issue, PSBT was created from GUI - send - create unsigned. If I create it using ‘walletcreatefundedpsbt,’ there is no problem. in main net.
  7. willcl-ark commented at 1:38 pm on June 26, 2024: member

    @foolbear are you able to provide some concrete steps to reproduce this? I can’t get it to happen in a few quick tests on my side.

    Without reproduction steps it’s likely going to be difficult to fix, and perhaps we should close the issue for now until we get verified repro steps.

  8. foolbear commented at 4:33 pm on June 26, 2024: none
    1. run bitcoin core in prune mode
    2. create a new blank wallet
    3. importprivkey from an old private key
    4. migratewallet
    5. re-sync blockchain
    6. show the balance
    7. create a transaction using GUI, click “send” and “create unsigned”.
    8. then “Load PSBT from clipboard” from menu, error fired.
  9. vasild commented at 2:20 pm on November 14, 2025: contributor

    I can reproduce this. A cursory debug shows that txin.scriptWitness IsNull but txin.scriptSig is not empty:

    https://github.com/bitcoin/bitcoin/blob/e221b2524659d22cc5a2b0d0023254115a7a5622/src/psbt.h#L1253-L1254

    A transaction created via the RPC does not exhibit this problem. Only when created from the GUI.

  10. 151henry151 commented at 10:21 pm on November 14, 2025: contributor

    It looks to me like GUI’s “Create unsigned” path still signs the transaction, so legacy inputs end up with non-empty scriptSig and the PSBT parser rejects them. Perhaps we could call CreateTransaction with signing disabled or explicitly clear each scriptSig/scriptWitness before building the PSBT, or maybe it would be better to reuse the RPC PSBT builder so the GUI and RPC share the same logic.

    I haven’t attempted to reproduce yet, just read through the issue and thinking about possible solutions.

  11. vasild commented at 11:16 am on November 18, 2025: contributor

    Indeed the GUI signs the transaction and the RPC does not. Here is the GUI code path:

    0SendCoinsDialog::sendButtonClicked()
    1  SendCoinsDialog::PrepareSendText()
    2    WalletModel::prepareTransaction() // result saved in SendCoinsDialog::m_current_transaction
    3      WalletImpl::createTransaction(/*sign=*/!wallet().privateKeysDisabled()) // sign=true
    4        CreateTransaction(sign=true)
    

    walletcreatefundedpsbt RPC code path:

    0FundTransaction()
    1  CreateTransaction(sign=false)
    

    Why is this a problem only when spending from P2SH addresses? PartiallySignedTransaction::Serialize() serializes the underlying transaction without a witness: SerializeToVector(s, TX_NO_WITNESS(*tx));, so it drops the scriptWitness from the inputs, but still includes scriptSig.

    Here is a q&d patch that fixes the problem by removing scriptSigs from the transaction when “Create Unsigned” is clicked in the GUI:

     0diff --git i/src/qt/sendcoinsdialog.cpp w/src/qt/sendcoinsdialog.cpp
     1index 3962dd0887..2ce657df7c 100644
     2--- i/src/qt/sendcoinsdialog.cpp
     3+++ w/src/qt/sendcoinsdialog.cpp
     4@@ -501,12 +501,15 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
     5     }
     6 
     7     bool send_failure = false;
     8     if (retval == QMessageBox::Save) {
     9         // "Create Unsigned" clicked
    10         CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
    11+        for (CTxIn& txin : mtx.vin) {
    12+            txin.scriptSig.clear();
    13+        }
    14         PartiallySignedTransaction psbtx(mtx);
    15         bool complete = false;
    16         // Fill without signing
    17         const auto err{model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete)};
    18         assert(!complete);
    19         assert(!err);
    

    Probably a better solution would be to change PartiallySignedTransaction::Serialize() to omit scriptSigs just like it omits scriptWitnesses? @achow101, @ishaanam?

  12. achow101 commented at 8:01 pm on November 18, 2025: member
    I think it would be better to have the prepareTransaction have a sign parameter that is set if “Create Unsigned” is selected. Having PSBT silently drop scriptSigs (and scriptWitnesses too) feels like there is the possibility for misuse/unexpected behavior. PartiallySignedTransaction probably needs some changes to more strongly enforce its invariants.
  13. vasild commented at 7:57 am on November 19, 2025: contributor

    prepareTransaction() is called before “Create Unsigned” is clicked. It seems a bit confusing, there are two “Send” buttons.

    The user selects inputs, fills destination address and amount and clicks “Send”. This is when SendCoinsDialog::sendButtonClicked() is called. Then it calls SendCoinsDialog::PrepareSendText() -> WalletModel::prepareTransaction(). At this point the transaction is created and signed. I think it must be in order to calculate the fee exactly? Then a dialog is presented “Do you want to create this transaction?” which shows the transaction fee and other details and has 3 buttons: “Send” (second send button), “Create Unsigned” and “Cancel”.

    When “Create Unsigned” is called, then we create the PSBT from m_current_transaction (already signed) and do fillPSBT() and presentPSBT().

    Edit: Why is the loading of PSBT upset if there are signatures? In other words, why throw here:

    https://github.com/bitcoin/bitcoin/blob/e221b2524659d22cc5a2b0d0023254115a7a5622/src/psbt.h#L1253-L1254

    it is “partially signed bitcoin transaction” after all, no?

  14. achow101 commented at 6:12 pm on November 19, 2025: member

    We could instead defer signing of the transaction until after “Send” is clicked.

    Edit: Why is the loading of PSBT upset if there are signatures? In other words, why throw here:

    A (v0) PSBT contains the entire unsigned transaction in its own field, with signatures and scripts placed in the PSBT fields. The transaction must be unsigned in order to make further signing and finalizing possible to reason about generically. Trying to make a PSBT from a transaction that already contained signatures and scripts is rather complicated since parsing out the signatures and scripts is fairly difficult, and impossible to do without knowing the UTXOs. We don’t want to drop the scriptSigs or scriptWitnesses since that would be surprising to the user - they already signed but the signatures are lost. So instead we made it a clear error to the user by throwing. All of this was further complicated by the fact that the raw transaction workflow allows for incomplete scriptSigs and scriptWitnesses, and parsing those to figure what was actually signed is a nightmare.

  15. 151henry151 referenced this in commit d95d440e10 on Nov 20, 2025
  16. 151henry151 commented at 0:40 am on November 20, 2025: contributor
    I’ve started some work on this. I modified WalletModel::prepareTransaction in src/qt/walletmodel.h (line 99) and src/qt/walletmodel.cpp (line 150) to accept an optional sign parameter (defaults to false). In lines 206-208 of walletmodel.cpp, I changed the logic so it only signs when explicitly requested via this parameter, rather than always signing when private keys are available. Then in src/qt/sendcoinsdialog.cpp, I updated the call to prepareTransaction on line 293 to pass sign=false, and added a new else block starting at line 545 that signs the transaction when “Send” is clicked (for non-external signer wallets) by creating a PSBT, filling and signing it, then finalizing and extracting the signed transaction before broadcasting. This way, the transaction remains unsigned during preparation (allowing fee calculation and creating truly unsigned PSBTs), and is only signed when the user confirms they want to send.

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: 2025-12-10 21:13 UTC

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