Implement BIP 370 PSBTv2 #21283

pull achow101 wants to merge 34 commits into bitcoin:master from achow101:psbt2 changing 26 files +1043 −246
  1. achow101 commented at 6:29 pm on February 23, 2021: member
    BIP 370 PSBTv2 introduces several new fields and different invariants for PSBT. This PR implements those new fields and restructures the PSBT implementation to match PSBTv2 but still remain compatible with PSBTv0.
  2. DrahtBot added the label GUI on Feb 23, 2021
  3. DrahtBot added the label RPC/REST/ZMQ on Feb 23, 2021
  4. DrahtBot added the label Wallet on Feb 23, 2021
  5. DrahtBot commented at 0:38 am on February 24, 2021: contributor

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

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/21283.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK rkrux
    Stale ACK scgbckbone

    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:

    • #34520 (refactor: Add [[nodiscard]] to functions returning bool+mutable ref by maflcko)
    • #34124 (refactor: make CCoinsView a pure virtual interface by l0rinc)
    • #33014 (rpc: Fix internal bug in descriptorprocesspsbt when encountering invalid signatures by b-l-u-e)
    • #32876 (refactor: use options struct for signing and PSBT operations by Sjors)
    • #32857 (wallet: allow skipping script paths by Sjors)

    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):

    • SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, nullptr, std::nullopt) in src/node/psbt.cpp
    • SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, input.sighash_type, nullptr, true) in src/psbt.cpp
    • wallet.walletcreatefundedpsbt(…, {target_address: 1}, 0, {‘add_inputs’: False}) in test/functional/rpc_psbt.py
    • wallet.walletcreatefundedpsbt(…, {target_address: 1}, 0, {‘add_inputs’: True, ‘maxconf’: 0, ‘fee_rate’: 10}) in test/functional/rpc_psbt.py
    • wallet.walletcreatefundedpsbt(…, {target_address: 1}, 0, {‘add_inputs’: True, ‘minconf’: 2, ‘fee_rate’: 10}) in test/functional/rpc_psbt.py

    If you want, I can produce suggested named-argument forms for these calls.

    Possible places where comparison-specific test macros should replace generic comparisons:

    • test/functional/rpc_psbt.py: “assert len(psbt1_decoded[‘inputs’][0].keys()) > 3” -> Use assert_greater_than(len(psbt1_decoded[‘inputs’][0].keys()), 3)
    • test/functional/rpc_psbt.py: “assert len(psbt1_decoded[‘inputs’][1].keys()) == 3” -> Use assert_equal(len(psbt1_decoded[‘inputs’][1].keys()), 3)
    • test/functional/rpc_psbt.py: “assert len(psbt2_decoded[‘inputs’][0].keys()) == 3” -> Use assert_equal(len(psbt2_decoded[‘inputs’][0].keys()), 3)
    • test/functional/rpc_psbt.py: “assert len(psbt2_decoded[‘inputs’][1].keys()) > 3” -> Use assert_greater_than(len(psbt2_decoded[‘inputs’][1].keys()), 3)

    No other bare assert a == b comparisons in test/functional/ were added.

    2026-03-05 21:25:06

  6. achow101 force-pushed on Mar 5, 2021
  7. in src/rpc/rawtransaction.cpp:1193 in 6e4c04e3a2 outdated
    1192+        }
    1193+        if (psbtx.fallback_locktime != std::nullopt) {
    1194+            result.pushKV("fallback_locktime", static_cast<uint64_t>(*psbtx.fallback_locktime));
    1195+        }
    1196+        result.pushKV("input_count", psbtx.inputs.size());
    1197+        result.pushKV("output_count", psbtx.inputs.size());
    


    adamjonas commented at 3:50 pm on March 5, 2021:

    Getting a compile error consistent with the CI for these two lines:

     0rpc/rawtransaction.cpp:1192:16: error: call to member function 'pushKV' is ambiguous
     1        result.pushKV("input_count", psbtx.inputs.size());
     2        ~~~~~~~^~~~~~
     3./univalue/include/univalue.h:127:10: note: candidate function
     4    bool pushKV(const std::string& key, int64_t val_) {
     5         ^
     6./univalue/include/univalue.h:131:10: note: candidate function
     7    bool pushKV(const std::string& key, uint64_t val_) {
     8         ^
     9./univalue/include/univalue.h:135:10: note: candidate function
    10    bool pushKV(const std::string& key, bool val_) {
    11         ^
    12./univalue/include/univalue.h:139:10: note: candidate function
    13    bool pushKV(const std::string& key, int val_) {
    14         ^
    15./univalue/include/univalue.h:143:10: note: candidate function
    16    bool pushKV(const std::string& key, double val_) {
    17         ^
    18./univalue/include/univalue.h:118:10: note: candidate function
    19    bool pushKV(const std::string& key, const UniValue& val);
    20         ^
    21./univalue/include/univalue.h:119:10: note: candidate function not viable: no known conversion from 'std::__1::vector<PSBTInput, std::__1::allocator<PSBTInput> >::size_type' (aka 'unsigned long') to 'const std::string' (aka 'const basic_string<char, char_traits<char>, allocator<char> >') for 2nd argument
    22    bool pushKV(const std::string& key, const std::string& val_) {
    23         ^
    24./univalue/include/univalue.h:123:10: note: candidate function not viable: no known conversion from 'std::__1::vector<PSBTInput, std::__1::allocator<PSBTInput> >::size_type' (aka 'unsigned long') to 'const char *' for 2nd argument
    25    bool pushKV(const std::string& key, const char *val_) {
    26         ^
    27rpc/rawtransaction.cpp:1193:16: error: call to member function 'pushKV' is ambiguous
    28        result.pushKV("output_count", psbtx.inputs.size());
    29        ~~~~~~~^~~~~~
    30./univalue/include/univalue.h:127:10: note: candidate function
    31    bool pushKV(const std::string& key, int64_t val_) {
    32         ^
    33./univalue/include/univalue.h:131:10: note: candidate function
    34    bool pushKV(const std::string& key, uint64_t val_) {
    35         ^
    36./univalue/include/univalue.h:135:10: note: candidate function
    37    bool pushKV(const std::string& key, bool val_) {
    38         ^
    39./univalue/include/univalue.h:139:10: note: candidate function
    40    bool pushKV(const std::string& key, int val_) {
    41         ^
    42./univalue/include/univalue.h:143:10: note: candidate function
    43    bool pushKV(const std::string& key, double val_) {
    44         ^
    45./univalue/include/univalue.h:118:10: note: candidate function
    46    bool pushKV(const std::string& key, const UniValue& val);
    47         ^
    48./univalue/include/univalue.h:119:10: note: candidate function not viable: no known conversion from 'std::__1::vector<PSBTInput, std::__1::allocator<PSBTInput> >::size_type' (aka 'unsigned long') to 'const std::string' (aka 'const basic_string<char, char_traits<char>, allocator<char> >') for 2nd argument
    49    bool pushKV(const std::string& key, const std::string& val_) {
    50         ^
    51./univalue/include/univalue.h:123:10: note: candidate function not viable: no known conversion from 'std::__1::vector<PSBTInput, std::__1::allocator<PSBTInput> >::size_type' (aka 'unsigned long') to 'const char *' for 2nd argument
    52    bool pushKV(const std::string& key, const char *val_) {
    53         ^
    542 errors generated.
    

    achow101 commented at 5:51 pm on March 5, 2021:
    I can’t replicate this error at all, but I think I have fixed it.
  8. achow101 force-pushed on Mar 5, 2021
  9. DrahtBot added the label Needs rebase on Mar 17, 2021
  10. achow101 force-pushed on Mar 17, 2021
  11. achow101 force-pushed on Mar 17, 2021
  12. DrahtBot removed the label Needs rebase on Mar 17, 2021
  13. DrahtBot added the label Needs rebase on Mar 29, 2021
  14. achow101 force-pushed on Apr 1, 2021
  15. DrahtBot removed the label Needs rebase on Apr 1, 2021
  16. achow101 force-pushed on Apr 5, 2021
  17. achow101 force-pushed on Apr 5, 2021
  18. achow101 force-pushed on Apr 5, 2021
  19. achow101 force-pushed on Apr 5, 2021
  20. achow101 force-pushed on Apr 5, 2021
  21. achow101 force-pushed on Apr 5, 2021
  22. DrahtBot added the label Needs rebase on Apr 19, 2021
  23. achow101 force-pushed on Apr 19, 2021
  24. DrahtBot removed the label Needs rebase on Apr 19, 2021
  25. in src/psbt.cpp:26 in 776419b72e outdated
    18@@ -19,6 +19,14 @@ PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction
    19     SetupFromTx(tx);
    20 }
    21 
    22+PartiallySignedTransaction::PartiallySignedTransaction(uint32_t version) :
    23+    m_version(version)
    24+{
    25+    if (GetVersion() >= 2) {
    26+        tx_version = CTransaction::CURRENT_VERSION;
    


    adamjonas commented at 2:33 pm on April 21, 2021:
    This is erroring for me: psbt.cpp:(.text+0x4ca): undefined reference to 'CTransaction::CURRENT_VERSION'
  26. in src/psbt.h:808 in dd0ea68a16 outdated
    803@@ -661,6 +804,10 @@ struct PartiallySignedTransaction
    804 
    805         // Read global data
    806         bool found_sep = false;
    807+        uint32_t input_count = 0;
    808+        uint32_t output_count = 0;
    


    sanket1729 commented at 2:49 am on May 11, 2021:
    @achow101, the BIP says that this should CompactSize which I assume means VarInt in bitcoin context? But the implementation serializes this as uint32_t

    achow101 commented at 5:52 pm on May 11, 2021:
    Oops, fixed.
  27. achow101 force-pushed on May 11, 2021
  28. DrahtBot added the label Needs rebase on May 30, 2021
  29. achow101 force-pushed on May 30, 2021
  30. DrahtBot removed the label Needs rebase on May 30, 2021
  31. DrahtBot added the label Needs rebase on Jun 17, 2021
  32. achow101 force-pushed on Jun 18, 2021
  33. DrahtBot removed the label Needs rebase on Jun 19, 2021
  34. DrahtBot added the label Needs rebase on Aug 2, 2021
  35. achow101 force-pushed on Aug 2, 2021
  36. DrahtBot removed the label Needs rebase on Aug 2, 2021
  37. DrahtBot added the label Needs rebase on Sep 23, 2021
  38. achow101 force-pushed on Sep 23, 2021
  39. DrahtBot removed the label Needs rebase on Sep 23, 2021
  40. in src/psbt.h:437 in 015f133e9a outdated
    432@@ -428,6 +433,12 @@ struct PartiallySignedTransaction
    433         OverrideStream<Stream> os(&s, s.GetType(), s.GetVersion() | SERIALIZE_TRANSACTION_NO_WITNESS);
    434         SerializeToVector(os, *tx);
    435 
    436+        // PSBT version
    437+        if (m_version != std::nullopt && m_version > 0) {
    


    kiminuo commented at 11:37 am on October 1, 2021:

    I think it should be:

    if (m_version != std::nullopt && m_version > 0) { -> if (m_version != std::nullopt && *m_version > 0) {

    at least your next commit makes me think so (https://github.com/bitcoin/bitcoin/pull/21283/commits/92ff609c1ef9d3cd848d08b4bbec1cdbf4ae4f0b#diff-2fb519d85c6ae99cd87c94e5b7cba43ff2ddd031b2c523dd2ffb12e991c16494R438)


    achow101 commented at 5:02 pm on October 1, 2021:
    Done
  41. kiminuo commented at 11:52 am on October 1, 2021: contributor
    In c8b879609f02288975e3a6f714fbcfa1a93edcbc (moveonly: Move (Un)Serialize(To/From)Vector, (De)SerializeHDKeypaths to psbt module), should you possibly move/copy #include <streams.h>, #include <span.h> and #include <script/keyorigin.h>?
  42. achow101 force-pushed on Oct 1, 2021
  43. achow101 force-pushed on Oct 1, 2021
  44. kiminuo commented at 7:20 pm on October 2, 2021: contributor

    In commit 0bd656b64e72e29f255a860c71c542f5a9f18c17 (Separate individual HD Keypath serialization into separate functions)

    Should line:

    0if (value_len % 4 || value_len == 0) {
    

    be

    0if (value_len % 4 != 0 || value_len == 0) {
    

    to make it more explicit?

  45. kiminuo commented at 7:41 pm on October 2, 2021: contributor

    Is it possible that in commit 459fd3b0cc646f5ab02dd3cfd239c7a0418e6a32 (*Implement operator< for KeyOriginInfo and CExtPubKey *)

    0else if (a.pubkey > b.pubkey) {
    1    return false;
    2}
    

    is missing?

  46. achow101 force-pushed on Oct 3, 2021
  47. achow101 commented at 5:34 pm on October 3, 2021: member

    @kiminuo

    In commit 0bd656b (Separate individual HD Keypath serialization into separate functions)

    Should line:

    0if (value_len % 4 || value_len == 0) {
    

    be

    0if (value_len % 4 != 0 || value_len == 0) {
    

    to make it more explicit?

    This is moved code, so I will not be changing it.

    Is it possible that in commit 459fd3b (*Implement operator< for KeyOriginInfo and CExtPubKey *)

    0else if (a.pubkey > b.pubkey) {
    1    return false;
    2}
    

    is missing?

    Done.


    In the future, please leave such comments on the code as inline comments rather than comments like this. It makes it much easier to respond directly to specific questions such as these.

  48. in src/pubkey.h:314 in 4130efb9cd outdated
    306@@ -305,8 +307,20 @@ struct CExtPubKey {
    307         return !(a == b);
    308     }
    309 
    310+    friend bool operator<(const CExtPubKey &a, const CExtPubKey &b)
    311+    {
    312+        if (a.pubkey < b.pubkey) {
    313+            return true;
    314+        } else if (a.pubkey > b.pubkey) {
    


    kiminuo commented at 7:13 am on October 5, 2021:

    I believe that operator> implementation is missing for CPubKey class:

    0    friend bool operator>(const CPubKey& a, const CPubKey& b)
    1    {
    2        return a.vch[0] > b.vch[0] ||
    3               (a.vch[0] == b.vch[0] && memcmp(a.vch, b.vch, a.size()) > 0);
    4    }
    

    for this line to work and that’s why tests fail at the moment.


    achow101 commented at 6:13 pm on October 5, 2021:
    Done. Keep in mind this commit (and the first 15 of this PR) are part of #17034 so comments on them are better suited for that PR.
  49. in src/psbt.h:187 in 4130efb9cd outdated
    183@@ -56,68 +184,109 @@ struct PSBTInput
    184     CScriptWitness final_script_witness;
    185     std::map<CPubKey, KeyOriginInfo> hd_keypaths;
    186     std::map<CKeyID, SigPair> partial_sigs;
    187+    uint256 prev_txid;
    


    kiminuo commented at 7:15 am on October 5, 2021:
    Should #include <uint256.h> be added?

    achow101 commented at 6:13 pm on October 5, 2021:
    Done
  50. in src/psbt.h:202 in 4130efb9cd outdated
    198     bool IsNull() const;
    199     void FillSignatureData(SignatureData& sigdata) const;
    200     void FromSignatureData(const SignatureData& sigdata);
    201     void Merge(const PSBTInput& input);
    202-    PSBTInput() {}
    203+    bool GetUTXO(CTxOut& utxo) const;
    


    kiminuo commented at 7:19 am on October 5, 2021:
    Should the original comment be preserved in some way?

    achow101 commented at 6:13 pm on October 5, 2021:
    Done
  51. achow101 force-pushed on Oct 5, 2021
  52. DrahtBot added the label Needs rebase on Nov 16, 2021
  53. achow101 force-pushed on Nov 16, 2021
  54. DrahtBot removed the label Needs rebase on Nov 16, 2021
  55. achow101 force-pushed on Nov 23, 2021
  56. achow101 force-pushed on Nov 24, 2021
  57. achow101 force-pushed on Nov 24, 2021
  58. achow101 force-pushed on Nov 24, 2021
  59. DrahtBot added the label Needs rebase on Nov 29, 2021
  60. achow101 force-pushed on Dec 5, 2021
  61. DrahtBot removed the label Needs rebase on Dec 5, 2021
  62. DrahtBot added the label Needs rebase on Dec 8, 2021
  63. achow101 force-pushed on Dec 8, 2021
  64. DrahtBot removed the label Needs rebase on Dec 8, 2021
  65. DrahtBot added the label Needs rebase on Dec 9, 2021
  66. achow101 force-pushed on Dec 9, 2021
  67. DrahtBot removed the label Needs rebase on Dec 9, 2021
  68. achow101 force-pushed on Dec 9, 2021
  69. achow101 marked this as a draft on Dec 9, 2021
  70. achow101 force-pushed on Dec 10, 2021
  71. achow101 marked this as ready for review on Dec 10, 2021
  72. DrahtBot added the label Needs rebase on Dec 13, 2021
  73. achow101 force-pushed on Dec 13, 2021
  74. DrahtBot removed the label Needs rebase on Dec 13, 2021
  75. achow101 force-pushed on Feb 2, 2022
  76. DrahtBot added the label Needs rebase on Mar 30, 2022
  77. achow101 force-pushed on Mar 30, 2022
  78. DrahtBot removed the label Needs rebase on Mar 30, 2022
  79. DrahtBot added the label Needs rebase on Mar 31, 2022
  80. achow101 force-pushed on Mar 31, 2022
  81. DrahtBot removed the label Needs rebase on Mar 31, 2022
  82. DrahtBot added the label Needs rebase on Apr 15, 2022
  83. achow101 force-pushed on Apr 15, 2022
  84. DrahtBot removed the label Needs rebase on Apr 15, 2022
  85. DrahtBot added the label Needs rebase on Jun 28, 2022
  86. achow101 force-pushed on Jun 29, 2022
  87. DrahtBot removed the label Needs rebase on Jun 29, 2022
  88. achow101 force-pushed on Jul 5, 2022
  89. achow101 force-pushed on Jul 6, 2022
  90. DrahtBot added the label Needs rebase on Jul 13, 2022
  91. achow101 force-pushed on Jul 13, 2022
  92. DrahtBot removed the label Needs rebase on Jul 13, 2022
  93. achow101 force-pushed on Jul 18, 2022
  94. achow101 force-pushed on Jul 19, 2022
  95. DrahtBot added the label Needs rebase on Jul 27, 2022
  96. achow101 force-pushed on Jul 28, 2022
  97. DrahtBot removed the label Needs rebase on Jul 28, 2022
  98. DrahtBot added the label Needs rebase on Aug 1, 2022
  99. achow101 force-pushed on Aug 1, 2022
  100. DrahtBot removed the label Needs rebase on Aug 1, 2022
  101. achow101 force-pushed on Sep 20, 2022
  102. DrahtBot added the label Needs rebase on Oct 20, 2022
  103. achow101 force-pushed on Oct 21, 2022
  104. DrahtBot removed the label Needs rebase on Oct 21, 2022
  105. achow101 force-pushed on Nov 30, 2022
  106. DrahtBot added the label Needs rebase on Dec 9, 2022
  107. achow101 force-pushed on Dec 9, 2022
  108. DrahtBot removed the label Needs rebase on Dec 9, 2022
  109. DrahtBot added the label Needs rebase on Jan 17, 2023
  110. achow101 force-pushed on Jan 18, 2023
  111. DrahtBot removed the label Needs rebase on Jan 18, 2023
  112. in src/psbt.cpp:23 in be9e558f3e outdated
    19@@ -20,6 +20,14 @@ PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction
    20     SetupFromTx(tx);
    21 }
    22 
    23+PartiallySignedTransaction::PartiallySignedTransaction(uint32_t version) :
    


    instagibbs commented at 2:52 pm on January 27, 2023:
    does this constructor ever get used? I don’t see it being used

    achow101 commented at 9:00 pm on January 27, 2023:
    Dropped that commit.
  113. in src/wallet/rpc/spend.cpp:1698 in d4f0cdc8e9 outdated
    1694+    }
    1695+    if (psbt_version != 2 && psbt_version != 0) {
    1696+        throw JSONRPCError(RPC_INVALID_PARAMETER, "The PSBT version can only be 2 or 0");
    1697+    }
    1698+
    1699+    PartiallySignedTransaction psbtx(rawTx, /* version=*/ 2);
    


    instagibbs commented at 2:59 pm on January 27, 2023:
    should this be using psbt_version?

    achow101 commented at 9:00 pm on January 27, 2023:
    Yes, done
  114. in src/psbt.h:1172 in d4f0cdc8e9 outdated
    1178-     * @param[in] input_index Index of the input to retrieve the UTXO of
    1179-     * @return Whether the UTXO for the specified input was found
    1180-     */
    1181-    bool GetInputUTXO(CTxOut& utxo, int input_index) const;
    1182+    PartiallySignedTransaction(uint32_t version);
    1183+    explicit PartiallySignedTransaction(const CMutableTransaction& tx, uint32_t version = 0);
    


    instagibbs commented at 3:01 pm on January 27, 2023:
    If we’re internally defaulting to v2, probably better just to have constructor default for that value as well?

    achow101 commented at 9:00 pm on January 27, 2023:
    Added a commit changing it. It’s 0 when introduced in order to not break everything.
  115. in src/rpc/rawtransaction.cpp:1784 in d4f0cdc8e9 outdated
    1784 
    1785     // Create a blank psbt where everything will be added
    1786     PartiallySignedTransaction merged_psbt;
    1787+    merged_psbt.tx_version = best_version;
    1788+    merged_psbt.fallback_locktime = best_locktime;
    1789+    // TODO: Remove for PSBTv2
    


    instagibbs commented at 3:05 pm on January 27, 2023:
    this function is only for v0, why would we remove something for v2?

    achow101 commented at 8:40 pm on January 27, 2023:
    The intent is to enable this rpc for v2 in the future.

    instagibbs commented at 9:41 pm on January 27, 2023:
    ah, wasn’t clear to me
  116. in src/rpc/rawtransaction.cpp:1766 in d4f0cdc8e9 outdated
    1762         PartiallySignedTransaction psbtx;
    1763         std::string error;
    1764         if (!DecodeBase64PSBT(psbtx, txs[i].get_str(), error)) {
    1765             throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
    1766         }
    1767+        if (psbtx.GetVersion() != 0) {
    


    instagibbs commented at 3:07 pm on January 27, 2023:
    Please add a test for this case

    achow101 commented at 8:42 pm on January 27, 2023:
    One was added in 318d818df7de30ac00551b655640512598ac1595
  117. achow101 force-pushed on Jan 27, 2023
  118. instagibbs commented at 2:42 pm on January 30, 2023: member

    Looking good so far.

    Ran this against CLN test suite and got no unexpected behavior.

  119. DrahtBot added the label Needs rebase on Feb 16, 2023
  120. achow101 force-pushed on Feb 17, 2023
  121. DrahtBot removed the label Needs rebase on Feb 17, 2023
  122. DrahtBot added the label Needs rebase on Mar 16, 2023
  123. achow101 force-pushed on Mar 17, 2023
  124. DrahtBot removed the label Needs rebase on Mar 17, 2023
  125. in src/rpc/rawtransaction.cpp:1775 in 6433fe1d16 outdated
    1769@@ -1770,6 +1770,9 @@ static RPCHelpMan joinpsbts()
    1770         if (!DecodeBase64PSBT(psbtx, txs[i].get_str(), error)) {
    1771             throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
    1772         }
    1773+        if (psbtx.GetVersion() != 0) {
    1774+            throw JSONRPCError(RPC_INVALID_PARAMETER, "joinpsbts only operates on version 0 PSBTs");
    1775+        }
    


    unknown commented at 7:43 pm on April 12, 2023:
    Is there a reason for this restriction?

    achow101 commented at 7:51 pm on April 12, 2023:
    IIRC updating this RPC would require pretty significant changes to it that felt out of scope for this PR. I’ve left it as something to do in a followup in order to keep the scope of the PR limited to just the bare minimum to work with PSBTv2.
  126. DrahtBot added the label Needs rebase on Apr 21, 2023
  127. achow101 force-pushed on May 1, 2023
  128. achow101 force-pushed on May 1, 2023
  129. DrahtBot removed the label Needs rebase on May 1, 2023
  130. DrahtBot added the label CI failed on May 23, 2023
  131. achow101 force-pushed on May 27, 2023
  132. DrahtBot removed the label CI failed on May 27, 2023
  133. DrahtBot added the label Needs rebase on Jun 1, 2023
  134. achow101 force-pushed on Jun 1, 2023
  135. DrahtBot removed the label Needs rebase on Jun 1, 2023
  136. DrahtBot added the label CI failed on Aug 6, 2023
  137. maflcko commented at 9:28 am on August 17, 2023: member
    Needs rebase, if still relevant.
  138. DrahtBot added the label Needs rebase on Aug 17, 2023
  139. achow101 force-pushed on Aug 17, 2023
  140. DrahtBot removed the label Needs rebase on Aug 17, 2023
  141. DrahtBot removed the label CI failed on Aug 18, 2023
  142. DrahtBot added the label Needs rebase on Sep 12, 2023
  143. achow101 force-pushed on Sep 12, 2023
  144. DrahtBot removed the label Needs rebase on Sep 12, 2023
  145. DrahtBot added the label Needs rebase on Oct 2, 2023
  146. achow101 force-pushed on Oct 3, 2023
  147. DrahtBot removed the label Needs rebase on Oct 3, 2023
  148. DrahtBot added the label Needs rebase on Oct 16, 2023
  149. achow101 force-pushed on Oct 16, 2023
  150. DrahtBot removed the label Needs rebase on Oct 16, 2023
  151. DrahtBot added the label Needs rebase on Oct 25, 2023
  152. achow101 force-pushed on Oct 25, 2023
  153. DrahtBot removed the label Needs rebase on Oct 25, 2023
  154. DrahtBot added the label Needs rebase on Nov 15, 2023
  155. achow101 force-pushed on Nov 15, 2023
  156. DrahtBot removed the label Needs rebase on Nov 15, 2023
  157. DrahtBot added the label CI failed on Nov 27, 2023
  158. maflcko commented at 1:29 pm on November 27, 2023: member
    0psbt.cpp: In member function CMutableTransaction PartiallySignedTransaction::GetUnsignedTx() const:
    1psbt.cpp:107:35: error: transaction_identifier<has_witness>::transaction_identifier(const uint256&) [with bool has_witness = false] is private within this context
    2  107 |         txin.prevout.hash = input.prev_txid;
    3      |                                   ^~~~~~~~~
    
  159. achow101 force-pushed on Nov 27, 2023
  160. DrahtBot removed the label CI failed on Nov 28, 2023
  161. DrahtBot added the label Needs rebase on Dec 11, 2023
  162. achow101 force-pushed on Dec 11, 2023
  163. DrahtBot removed the label Needs rebase on Dec 11, 2023
  164. DrahtBot added the label CI failed on Jan 24, 2024
  165. achow101 commented at 3:00 pm on April 9, 2024: member
    Will split this up
  166. DrahtBot added the label Needs rebase on Apr 22, 2024
  167. achow101 force-pushed on Apr 25, 2024
  168. DrahtBot removed the label Needs rebase on Apr 25, 2024
  169. DrahtBot removed the label CI failed on Apr 26, 2024
  170. DrahtBot added the label Needs rebase on May 23, 2024
  171. achow101 force-pushed on May 29, 2024
  172. DrahtBot removed the label Needs rebase on May 29, 2024
  173. DrahtBot added the label Needs rebase on Jun 12, 2024
  174. achow101 force-pushed on Jun 13, 2024
  175. achow101 force-pushed on Jun 13, 2024
  176. DrahtBot removed the label Needs rebase on Jun 13, 2024
  177. DrahtBot added the label Needs rebase on Jul 8, 2024
  178. achow101 force-pushed on Jul 10, 2024
  179. DrahtBot removed the label Needs rebase on Jul 10, 2024
  180. DrahtBot added the label Needs rebase on Jul 11, 2024
  181. achow101 force-pushed on Jul 22, 2024
  182. DrahtBot removed the label Needs rebase on Jul 22, 2024
  183. hebasto added the label Needs CMake port on Aug 16, 2024
  184. DrahtBot added the label Needs rebase on Sep 2, 2024
  185. maflcko removed the label GUI on Sep 3, 2024
  186. maflcko removed the label Needs CMake port on Sep 3, 2024
  187. achow101 force-pushed on Sep 3, 2024
  188. DrahtBot removed the label Needs rebase on Sep 3, 2024
  189. in src/psbt.cpp:130 in ce9d05fa4f outdated
    127+
    128+    // Get the unsigned transaction
    129+    CMutableTransaction mtx = GetUnsignedTx();
    130+    // Compute the locktime
    131+    bool locktime_success = ComputeTimeLock(mtx.nLockTime);
    132+    assert(locktime_success);
    


    tcharding commented at 11:21 pm on September 18, 2024:

    Is there a reason to compute the locktime again? It is done in the call to GetUnsignedTx just above.

    (In 0cf1a9febb0205dadc03cd87a7af074e7b4cfd8c)


    achow101 commented at 0:27 am on September 19, 2024:
    Good point, removed.
  190. in src/rpc/rawtransaction.cpp:1801 in ce9d05fa4f outdated
    1797@@ -1740,40 +1798,46 @@ static RPCHelpMan joinpsbts()
    1798         throw JSONRPCError(RPC_INVALID_PARAMETER, "At least two PSBTs are required to join PSBTs.");
    1799     }
    1800 
    1801-    uint32_t best_version = 1;
    1802+    int32_t best_version = 1;
    


    tcharding commented at 11:30 pm on September 18, 2024:
    Why change to a signed int? IIUC this is checked against the version field in transaction which is unsigned.

    achow101 commented at 0:27 am on September 19, 2024:

    It was unsigned when this PR was written 3 years ago lol.

    Updated to be unsigned.


    tcharding commented at 0:45 am on September 19, 2024:
    Nice!
  191. in src/wallet/rpc/spend.cpp:101 in ce9d05fa4f outdated
     97@@ -98,7 +98,7 @@ std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& sffo_in
     98 static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, const CMutableTransaction& rawTx)
     99 {
    100     // Make a blank psbt
    101-    PartiallySignedTransaction psbtx(rawTx);
    102+    PartiallySignedTransaction psbtx(rawTx, /*version=*/2);
    


    tcharding commented at 11:40 pm on September 18, 2024:
    Whitespace is different here compared to below (mtx, /* version=*/ 2). I don’t know if you guys worry about that sort of thing.

    achow101 commented at 0:28 am on September 19, 2024:
    Fixed the whitespace to be consistent with our style guidelines
  192. in src/psbt.h:1175 in ce9d05fa4f outdated
    1181-     * @param[out] utxo The UTXO of the input if found
    1182-     * @param[in] input_index Index of the input to retrieve the UTXO of
    1183-     * @return Whether the UTXO for the specified input was found
    1184-     */
    1185-    bool GetInputUTXO(CTxOut& utxo, int input_index) const;
    1186+    explicit PartiallySignedTransaction(const CMutableTransaction& tx, uint32_t version = 2);
    


    tcharding commented at 11:46 pm on September 18, 2024:
    In 7d46c2488679ccca70cdd1dc71100724347a47e8 this is a pretty big change behavour, right? I would have expected more in the patch that just a one line change (eg docs somewhere or perhaps a test somewhere that checks default behavour).

    achow101 commented at 0:29 am on September 19, 2024:

    I’ve added a basic test for this and a release note fragment. The bulk of the test changes in previous commits is to setup for this though.

    Changing to PSBTv2 is should largely be transparent to the user, so there shouldn’t be many changes in the functional tests. Most of the testing for it is in the unit tests.


    tcharding commented at 0:45 am on September 19, 2024:
    Cheers
  193. tcharding commented at 11:47 pm on September 18, 2024: contributor

    Caveat: I’m not a C++ dev and don’t know much about Core development.

    I did however check the changes here against my Rust implementation of the same, and found some bugs in mine, so cheers!

  194. achow101 force-pushed on Sep 19, 2024
  195. DrahtBot added the label CI failed on Nov 1, 2024
  196. DrahtBot removed the label CI failed on Nov 1, 2024
  197. Sjors commented at 10:22 am on January 16, 2025: member
    Aside from the existential question, it would be useful to have some projects to test this PR against: https://bitcoin.stackexchange.com/questions/125384/who-uses-or-wants-to-use-psbtv2-bip370
  198. tcharding commented at 10:18 pm on January 16, 2025: contributor
  199. whitslack commented at 0:19 am on January 17, 2025: contributor
    @Sjors: You want to know who wants this? I’ve been wanting to use Bitcoin Core to decode and manipulate the PSBTs that Core Lightning stores in its database, but it only works for the older, first-gen PSBTs. Core Lightning moved on to PSBTv2 some time ago, so now all the newer PSBTs fail to parse in Bitcoin Core. I’ve been having to hack together various half-baked tools I’ve found around the Internet, but it would be so good if Bitcoin Core would be upgraded to the current spec.
  200. instagibbs commented at 0:36 am on January 17, 2025: member
    @whitslack yes I authored the migration, and v2<->v0 roundtripping is supported in libwally as well. Would be nice to move the industry a bit closer to making v2 the defacto default.
  201. Sjors commented at 8:50 am on January 17, 2025: member
    @whitslack @instagibbs thanks for mentioning CLN. Is it possible to turn off its round tripping so I can test this PR against it? And are there any good nodes on signet or testnet4?
  202. Sjors commented at 10:14 am on January 17, 2025: member
    @bigspider thanks for your reply on the mailinglist. So I guess another way to test this PR is to patch HWI to not do the conversion between PSBT 0 and 2, and then make (any?) transaction with a Ledger that runs at least 2.0.0.
  203. bigspider commented at 1:08 pm on January 17, 2025: contributor

    @bigspider thanks for your reply on the mailinglist. So I guess another way to test this PR is to patch HWI to not do the conversion between PSBT 0 and 2, and then make (any?) transaction with a Ledger that runs at least 2.0.0.

    I suppose so, although it feels somewhat circular as bitcoin-core was always the reference for the tests of the Ledger Bitcoin app!

  204. scgbckbone commented at 2:34 pm on January 17, 2025: contributor

    Coldcard Mk4/Q also support PSBT v2 if you need to test ( from October 2023 https://github.com/Coldcard/firmware/blob/master/releases/History-Mk4.md#520---2023-10-10)

    but as @bigspider, I also used core PR to implement this

  205. Sjors commented at 7:12 pm on January 17, 2025: member

    IIUC the recently merged Silent Payments PSBT BIP only works with PSBTv2? https://github.com/bitcoin/bips/blob/master/bip-0375.mediawiki

    Having a (rough) draft PR implementing those fields on top of this PR could also aid in review. cc @josibake @andrewtoth

  206. jonatack commented at 7:33 pm on January 17, 2025: member

    IIUC the recently merged Silent Payments PSBT BIP only works with PSBTv2?

    Suggested to consider renaming BIP375 to be more clear about this, unless there’s a reason I’m missing: https://github.com/bitcoin/bips/pull/1687#discussion_r1913779155

  207. Sjors commented at 12:36 pm on January 20, 2025: member

    I briefly tested a Ledger NanoS with Bitcoin Testnet app 2.2.5 by disabling its v0 <-> v2 conversion: https://github.com/Sjors/HWI/commit/28bfb53e136918e5c6279b6bc6ceb33e1421b934

    On master this triggers a “failed to sign” error. With this PR (rebased) it works.

    it feels somewhat circular

    Agreed, an independent implementation would be nice to test against.

    (I also read BIP 370 again, so should be in a better position now to review this PR)

  208. instagibbs commented at 6:14 pm on January 23, 2025: member

    Is it possible to turn off its round tripping so I can test this PR against it?

    Internally it only uses v2, but it will output v0 if you ask for it.

  209. Sjors commented at 8:20 am on January 24, 2025: member

    The fundchannel_start -> fundchannel_complete id psbt workflow is the easiest to test. If I give it a v2 PSBT, it’s not going to be smart and try to fix encoding mistakes?

    Tested that flow, and also the sendpsbt RPC.

    So this PR makes the GUI produce v2 PSBTs by default. We might want to change that, or at least have a downgrade checkbox. Similarly the send RPC and friends should have a way to force a lower version.


    Here’s a branch that:

    • makes v0 the GUI default: it’s too much work to add a checkbox everywhere. Later on for Silent Payments we can just make v2 the default if an sp address is involved.
    • adds a psbt_version option to send, sendall, converttopsbt, bumpfee and psbtbumpfee

    https://github.com/Sjors/bitcoin/tree/2025/01/psbtv2

  210. instagibbs commented at 2:17 pm on January 24, 2025: member

    If I give it a v2 PSBT, it’s not going to be smart and try to fix encoding mistakes?

    That would be a libwally question. I don’t recall.

  211. in test/functional/data/rpc_psbt.json:160 in 09c8656aaa outdated
    155@@ -156,10 +156,10 @@
    156         },
    157         {
    158             "combine" : [
    159-                "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAKDwECAwQFBgcICQ8BAgMEBQYHCAkKCwwNDg8ACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAoPAQIDBAUGBwgJDwECAwQFBgcICQoLDA0ODwA=",
    160-                "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAKDwECAwQFBgcIEA8BAgMEBQYHCAkKCwwNDg8ACg8BAgMEBQYHCBAPAQIDBAUGBwgJCgsMDQ4PAAoPAQIDBAUGBwgQDwECAwQFBgcICQoLDA0ODwA="
    161+                "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAK8AECAwQFBgcICQ8BAgMEBQYHCAkKCwwNDg8ACvABAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAArwAQIDBAUGBwgJDwECAwQFBgcICQoLDA0ODwA=",
    162+                "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAK8AECAwQFBgcIEA8BAgMEBQYHCAkKCwwNDg8ACvABAgMEBQYHCBAPAQIDBAUGBwgJCgsMDQ4PAArwAQIDBAUGBwgQDwECAwQFBgcICQoLDA0ODwA="
    


    Sjors commented at 10:19 am on January 28, 2025:
    09c8656aaa27d18e2d14a52cdd67ee46f6a5cae6: note to other reviewers: you can run both through decodepsbt and notice the “unknown” key f0010203040506070809 in the first vector, f0010203040506070810 in the second and both appear in the combined “result” below.
  212. in src/psbt.h:1224 in 6a73e374fb outdated
    1218@@ -1063,6 +1219,10 @@ struct PartiallySignedTransaction
    1219 
    1220         // Read global data
    1221         bool found_sep = false;
    1222+        uint64_t input_count = 0;
    1223+        uint64_t output_count = 0;
    1224+        bool found_input_count = false;
    


    Sjors commented at 10:26 am on January 28, 2025:

    6a73e374fb0835dd06433e079c7c9300d6412a11 nit: variable 'found_input_count' set but not used [-Werror,-Wunused-but-set-variable]

    Same for found_output_count.

    The “test-each-commit” job doesn’t catch this, because it’s limited to the last 6 commits.

    Although PSBT_GLOBAL_INPUT_COUNT case does set it, it’s not enough for the compiler to be happy. It wants you to introduce these variables in c6743bf7137615eb20145c6eb3e2da5c82903b1e. Doing so also increases clarity imo.


    Fwiw all later commits compile fine.

  213. in src/psbt.h:622 in 6a73e374fb outdated
    613@@ -584,6 +614,70 @@ struct PSBTInput
    614                     hash256_preimages.emplace(hash, std::move(preimage));
    615                     break;
    616                 }
    617+                case PSBT_IN_PREVIOUS_TXID:
    618+                {
    619+                    if (!key_lookup.emplace(key).second) {
    620+                        throw std::ios_base::failure("Duplicate Key, previous txid is already provided");
    621+                    } else if (key.size() != 1) {
    622+                        throw std::ios_base::failure("Previous txid key is more than one byte type");
    


    Sjors commented at 1:53 pm on January 28, 2025:
    6a73e374fb0835dd06433e079c7c9300d6412a11: as I noted in #31247 (review) it would be really nice if we can abstract this duplicate and error prone size checking stuff into e.g. a DSL that defines it for all fields.

    achow101 commented at 11:15 pm on January 31, 2025:
    This PR is big enough already, I’d prefer to do that separately.

    Sjors commented at 9:42 am on February 3, 2025:
    Agreed.

    davidgumberg commented at 5:06 pm on March 6, 2026:

    I agree that this should be fixed, most of the code in serialize and doing in a follow-up is fair enough, but if you’re interested I made the following commit which a more minimal version could be made to make this PR smaller rather than lager, and brings us closer to what @Sjors proposes.

    https://github.com/davidgumberg/bitcoin/commit/30dc9a1d4a1db47031085c7497c2201e4578f59b

  214. in src/psbt.h:624 in 6a73e374fb outdated
    619+                    if (!key_lookup.emplace(key).second) {
    620+                        throw std::ios_base::failure("Duplicate Key, previous txid is already provided");
    621+                    } else if (key.size() != 1) {
    622+                        throw std::ios_base::failure("Previous txid key is more than one byte type");
    623+                    }
    624+                    UnserializeFromVector(s, prev_txid);
    


    Sjors commented at 2:09 pm on January 28, 2025:

    6a73e374fb0835dd06433e079c7c9300d6412a11: IIUC UnserializeFromVector checks that the first byte of s represents a size and that it matches the number of bytes left in in s.

    So that protects against passing in too many bytes. But does it protect against too few bytes?

    The UnserializeMany method uses std::make_shared which calls the constructor for Txid (in this case), which in return, I think, picks the uint256(Span<const unsigned char> vch) constructor. Which doesn’t check the length.

    Maybe UnserializeMany could do a sizeof on its args?


    achow101 commented at 11:27 pm on January 31, 2025:

    No… that’s not at all how deserialization works. There’s no constructors called, prev_txid is an already initialized uint256. Unserializing into it means that the data is overwritten. There’s no std::make_shared called anywhere, nothing is constructed or initialized.

    The call chain is UnserializeFromVector -> UnserializeMany -> uint256::Unserialize() -> Unserialize(Stream& s, Span<B> span) -> <template Stream> read(), and every read(Span) function checks that the stream has at least span.size() bytes, and reads exactly span.size() bytes into the span.

    Unserializing uint256 always reads exactly 32 bytes, that’s why no hashes ever need to be length prefixed. The same applies here.


    Sjors commented at 9:44 am on February 3, 2025:
    Thanks for clarifying. So in that case both too many and too few bytes are prevented.
  215. Sjors commented at 2:12 pm on January 28, 2025: member
    Some feedback on the first three commits.
  216. bitcoin deleted a comment on Jan 30, 2025
  217. rkrux commented at 10:45 am on February 6, 2025: contributor

    Definite Concept ACK

    This one is a biggie, I should get around to reviewing this PR sometime later this month and will leave a review then.

    Edit: Could not get to it before, it will take me some time for me to start reviewing it.

  218. in src/psbt.h:844 in 2dc96bd4dc outdated
    840     bool IsNull() const;
    841     void FillSignatureData(SignatureData& sigdata) const;
    842     void FromSignatureData(const SignatureData& sigdata);
    843     void Merge(const PSBTOutput& output);
    844-    PSBTOutput() = default;
    845+    PSBTOutput(uint32_t version) : m_psbt_version(version) {}
    


    Sjors commented at 10:15 am on February 7, 2025:
    In 2dc96bd4dc5169f692b94ae98eeb6bcd6b0f1ee9 “Have PSBTInput and PSBTOutput know the PSBT’s version”, nit: calling this param psbt_version makes it clear that inputs and outputs don’t have their own versioning. And consistent with the eventual variable name.

    achow101 commented at 7:59 pm on February 10, 2025:
    Done
  219. in src/psbt.h:1411 in 2dc96bd4dc outdated
    1407@@ -1404,10 +1408,12 @@ struct PartiallySignedTransaction
    1408             throw std::ios_base::failure("No unsigned transaction was provided");
    1409         }
    1410 
    1411+        uint32_t psbt_ver = GetVersion();
    


    Sjors commented at 10:18 am on February 7, 2025:
    In https://github.com/bitcoin/bitcoin/commit/2dc96bd4dc5169f692b94ae98eeb6bcd6b0f1ee9 “Have PSBTInput and PSBTOutput know the PSBT’s version”, μnit const.

    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  220. in src/rpc/rawtransaction.cpp:1588 in 2dc96bd4dc outdated
    1584@@ -1585,10 +1585,10 @@ static RPCHelpMan createpsbt()
    1585     PartiallySignedTransaction psbtx;
    1586     psbtx.tx = rawTx;
    1587     for (unsigned int i = 0; i < rawTx.vin.size(); ++i) {
    1588-        psbtx.inputs.emplace_back();
    1589+        psbtx.inputs.emplace_back(0);
    


    Sjors commented at 10:23 am on February 7, 2025:
    In 2dc96bd4dc5169f692b94ae98eeb6bcd6b0f1ee9 “Have PSBTInput and PSBTOutput know the PSBT’s version”, nit: /*psbt_version=*/ would be useful here, but this goes away in a later commit (and before the default is changed to 2).
  221. in src/test/fuzz/deserialize.cpp:200 in 2dc96bd4dc outdated
    196@@ -197,11 +197,11 @@ FUZZ_TARGET_DESERIALIZE(prefilled_transaction_deserialize, {
    197     DeserializeFromFuzzingInput(buffer, prefilled_transaction);
    198 })
    199 FUZZ_TARGET_DESERIALIZE(psbt_input_deserialize, {
    200-    PSBTInput psbt_input;
    201+    PSBTInput psbt_input(0);
    


    Sjors commented at 10:27 am on February 7, 2025:

    In 2dc96bd4dc5169f692b94ae98eeb6bcd6b0f1ee9 “Have PSBTInput and PSBTOutput know the PSBT’s version”: somewhere later in the PR, we should add a target for version 2?

    Or better: any version, which would detect that the PSBTInput constructor doesn’t prevent version 1 (not currently a problem, but could creep in if we build functionality to add inputs to a PSBT).

  222. in src/psbt.h:1503 in c6743bf713 outdated
    1498@@ -1419,10 +1499,14 @@ struct PartiallySignedTransaction
    1499 
    1500             // Make sure the non-witness utxo matches the outpoint
    1501             if (input.non_witness_utxo) {
    1502-                if (input.non_witness_utxo->GetHash() != tx->vin[i].prevout.hash) {
    1503+                if ((tx != std::nullopt && input.non_witness_utxo->GetHash() != tx->vin[i].prevout.hash)
    1504+                    || (!input.prev_txid.IsNull() && input.non_witness_utxo->GetHash() != input.prev_txid)
    


    Sjors commented at 11:19 am on February 7, 2025:
    In c6743bf7137615eb20145c6eb3e2da5c82903b1e “Enforce PSBT version constraints”: this is hard to read. Would prefer two separate checks based on the PSBT version.

    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  223. in src/psbt.cpp:17 in cef9de00f4 outdated
    13@@ -14,6 +14,7 @@ PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction
    14 {
    15     inputs.resize(tx.vin.size(), PSBTInput(GetVersion()));
    16     outputs.resize(tx.vout.size(), PSBTOutput(GetVersion()));
    17+    CacheUnsignedTxPieces();
    


    Sjors commented at 11:28 am on February 7, 2025:
    In cef9de00f4c2d4c9c033e7dce2b7ffac2e694a26 “Call CacheUnsignedTxPieces in PSBT constructor”: might as well do this in the previous commit. Ditto for the next commit.

    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  224. in src/psbt.h:1162 in 0c459e43ba outdated
    1157@@ -1158,6 +1158,8 @@ struct PartiallySignedTransaction
    1158     [[nodiscard]] bool Merge(const PartiallySignedTransaction& psbt);
    1159     bool AddInput(const CTxIn& txin, PSBTInput& psbtin);
    1160     bool AddOutput(const CTxOut& txout, const PSBTOutput& psbtout);
    1161+    void SetupFromTx(const CMutableTransaction& tx);
    1162+    void CacheUnsignedTxPieces();
    


    Sjors commented at 11:33 am on February 7, 2025:

    In 0c459e43ba1ce74d2e88dde352e28a63743f4746 “Add PSBT::CacheUnsignedTxPieces”: this seems fine, but it would be good to have a method that verifies that for v0 PSBTs these two representations are in sync. You could then enable that method in debug builds and call it in a bunch of places.

    The could e.g call GetUnsignedTx() (introduced later), skip its if (tx != std::nullopt) early return, and compare.

  225. in src/psbt.cpp:115 in 162d091677 outdated
    109@@ -110,6 +110,21 @@ CMutableTransaction PartiallySignedTransaction::GetUnsignedTx() const
    110     return mtx;
    111 }
    112 
    113+uint256 PartiallySignedTransaction::GetUniqueID() const
    114+{
    115+    if (tx != std::nullopt) {
    


    Sjors commented at 11:40 am on February 7, 2025:

    In 162d0916774c719f1e3749b0d4d204e296ee4ac8 “Add PSBT::GetUniqueID”: I think this should check the version number, and then Assume(version == 0 || tx == std::nullopt)

    The way it’s written now suggests tx is just a cache that might be stale.


    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  226. in src/psbt.cpp:139 in aeb9e44bc5 outdated
    142-    psbtin.final_script_witness.SetNull();
    143-    inputs.push_back(psbtin);
    144-    return true;
    145+
    146+    if (tx != std::nullopt) {
    147+        // This is a v0 psbt, so do the v0 AddInput
    


    Sjors commented at 11:51 am on February 7, 2025:
    In aeb9e44bc5a221553d2010752c76f8f16205551c “Change PSBT::AddInput to take just PSBTInput”: I think it’s more readable to just check the PSBT version directly.

    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  227. in src/psbt.cpp:152 in aeb9e44bc5 outdated
    155+        psbtin.final_script_witness.SetNull();
    156+        inputs.push_back(psbtin);
    157+        return true;
    158+    }
    159+
    160+    // TODO: Do PSBTv2
    


    Sjors commented at 11:53 am on February 7, 2025:
    In https://github.com/bitcoin/bitcoin/commit/aeb9e44bc5a221553d2010752c76f8f16205551c “Change PSBT::AddInput to take just PSBTInput”: assert(false) ?

    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  228. in src/psbt.cpp:141 in aeb9e44bc5 outdated
    144-    return true;
    145+
    146+    if (tx != std::nullopt) {
    147+        // This is a v0 psbt, so do the v0 AddInput
    148+        CTxIn txin(COutPoint(psbtin.prev_txid, *psbtin.prev_out));
    149+        if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) {
    


    Sjors commented at 11:56 am on February 7, 2025:
    In https://github.com/bitcoin/bitcoin/commit/aeb9e44bc5a221553d2010752c76f8f16205551c “Change PSBT::AddInput to take just PSBTInput”: could add // Prevent duplicate input

    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  229. in src/psbt.cpp:131 in aeb9e44bc5 outdated
    124@@ -125,17 +125,32 @@ uint256 PartiallySignedTransaction::GetUniqueID() const
    125     return mtx.GetHash();
    126 }
    127 
    128-bool PartiallySignedTransaction::AddInput(const CTxIn& txin, PSBTInput& psbtin)
    129+bool PartiallySignedTransaction::AddInput(PSBTInput& psbtin)
    130 {
    131-    if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) {
    132+    if (std::find_if(inputs.begin(), inputs.end(),
    133+        [psbtin](const PSBTInput& psbt) {
    


    Sjors commented at 12:01 pm on February 7, 2025:

    In https://github.com/bitcoin/bitcoin/commit/aeb9e44bc5a221553d2010752c76f8f16205551c “Change PSBT::AddInput to take just PSBTInput”:

    0// Avoid duplicate input
    1[psbtin](const PSBTInput& psbt_input) {
    

    (the comment about duplicate inputs is introduced in 3fe17931ae2d0e1b83cbe9c376f5d255ac9de68d “Implement PSBTv2 AddInput and AddOutput”)

    But this only works for v2, since prev_txid is nullptr otherwise. That’s why the duplicate input check for v0 is maintained below.

    It’s more clear if you explicitly check the PSBT version in both places.


    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  230. in src/psbt.cpp:33 in a79e519323 outdated
    29@@ -30,6 +30,7 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt)
    30         return false;
    31     }
    32 
    33+    assert(*tx_version == psbt.tx_version);
    


    Sjors commented at 12:06 pm on February 7, 2025:
    In a79e519323ee35544e854efd6cbf7ce8577fd4e3 “Implement PSBTv2 field merging”: Assume(...) || return false;?

    achow101 commented at 8:00 pm on February 10, 2025:
    Done
  231. Sjors commented at 12:19 pm on February 7, 2025: member

    Studied the other commits as well.

    Perhaps there’s an opportunity to split this PR by first introducing the internal refactoring that anticipates PSBTv2. But other that I don’t think it can be made much smaller.

  232. achow101 force-pushed on Feb 10, 2025
  233. DrahtBot added the label Needs rebase on Apr 16, 2025
  234. achow101 force-pushed on Apr 16, 2025
  235. DrahtBot removed the label Needs rebase on Apr 16, 2025
  236. DrahtBot added the label Needs rebase on Apr 18, 2025
  237. achow101 force-pushed on Apr 18, 2025
  238. DrahtBot removed the label Needs rebase on Apr 19, 2025
  239. achow101 force-pushed on Apr 23, 2025
  240. achow101 force-pushed on Apr 25, 2025
  241. DrahtBot added the label Needs rebase on May 7, 2025
  242. achow101 force-pushed on May 7, 2025
  243. DrahtBot removed the label Needs rebase on May 7, 2025
  244. DrahtBot added the label Needs rebase on May 16, 2025
  245. achow101 force-pushed on May 16, 2025
  246. DrahtBot removed the label Needs rebase on May 16, 2025
  247. DrahtBot added the label CI failed on May 19, 2025
  248. DrahtBot removed the label CI failed on May 19, 2025
  249. DrahtBot added the label Needs rebase on May 21, 2025
  250. achow101 force-pushed on May 21, 2025
  251. DrahtBot removed the label Needs rebase on May 21, 2025
  252. DrahtBot added the label Needs rebase on Aug 19, 2025
  253. achow101 force-pushed on Aug 19, 2025
  254. DrahtBot removed the label Needs rebase on Aug 19, 2025
  255. DrahtBot added the label CI failed on Aug 19, 2025
  256. achow101 force-pushed on Sep 25, 2025
  257. maflcko removed the label CI failed on Sep 26, 2025
  258. bitcoin deleted a comment on Sep 29, 2025
  259. DrahtBot added the label Needs rebase on Oct 14, 2025
  260. achow101 force-pushed on Oct 21, 2025
  261. DrahtBot removed the label Needs rebase on Oct 21, 2025
  262. DrahtBot added the label Needs rebase on Nov 18, 2025
  263. achow101 force-pushed on Nov 19, 2025
  264. DrahtBot removed the label Needs rebase on Nov 19, 2025
  265. DrahtBot added the label Needs rebase on Jan 12, 2026
  266. achow101 force-pushed on Jan 15, 2026
  267. DrahtBot removed the label Needs rebase on Jan 15, 2026
  268. darosior commented at 9:35 pm on February 3, 2026: member

    In 20 days it will have been 5 years since this PR was open. Let’s try to re-gain some momentum around reviewing this?

    If a few other regular contributors volunteer to review this, i will commit to review it too. Some (re?)testing from users and outside contributors could be useful too.

  269. in contrib/signet/miner:70 in 35347ea461
    65@@ -66,8 +66,8 @@ def signet_txs(block, challenge):
    66 def decode_challenge_psbt(b64psbt):
    67     psbt = PSBT.from_base64(b64psbt)
    68 
    69-    assert len(psbt.tx.vin) == 1
    70-    assert len(psbt.tx.vout) == 1
    71+    assert len(psbt.i) == 1
    72+    assert len(psbt.i) == 1
    


    kannapoix commented at 2:00 am on February 4, 2026:
    Should this be ‎psbt.o instead of ‎psbt.i here?

    achow101 commented at 10:47 pm on March 4, 2026:
    Done
  270. in src/psbt.h:1314 in 35347ea461 outdated
    1309+        // Make sure required PSBTv2 fields are present
    1310+        if (m_psbt_version >= 2) {
    1311+            if (amount == std::nullopt) {
    1312+                throw std::ios_base::failure("Output amount is required in PSBTv2");
    1313+            }
    1314+            if (!script.has_value()) {
    


    kannapoix commented at 4:10 am on February 10, 2026:
    PSBT_OUT_SCRIPT is not required in BIP375. Should we address that here, or is it out of scope for this PR?

    achow101 commented at 10:05 pm on March 3, 2026:
    No, that is out of scope for this PR.
  271. in src/psbt.cpp:587 in 35347ea461 outdated
    586-    std::vector<CTxOut> utxos(tx.vin.size());
    587-    for (size_t idx = 0; idx < tx.vin.size(); ++idx) {
    588-        if (!psbt.GetInputUTXO(utxos[idx], idx)) have_all_spent_outputs = false;
    589+    std::vector<CTxOut> utxos;
    590+    for (const PSBTInput& input : psbt.inputs) {
    591+        if (!input.GetUTXO(utxos.emplace_back())) have_all_spent_outputs = false;
    


    brunoerg commented at 4:51 pm on March 2, 2026:

    Unkilled mutant:

     0diff --git a/src/psbt.cpp b/src/psbt.cpp
     1index cb7b832965..d4b3478fab 100644
     2--- a/src/psbt.cpp
     3+++ b/src/psbt.cpp
     4@@ -584,7 +584,7 @@ PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction&
     5     bool have_all_spent_outputs = true;
     6     std::vector<CTxOut> utxos;
     7     for (const PSBTInput& input : psbt.inputs) {
     8-        if (!input.GetUTXO(utxos.emplace_back())) have_all_spent_outputs = false;
     9+        if (!input.GetUTXO(utxos.emplace_back())) have_all_spent_outputs = true;
    10     }
    11     PrecomputedTransactionData txdata;
    12     if (have_all_spent_outputs) {
    

    The following test case might be useful to address it:

     0BOOST_AUTO_TEST_CASE(psbt_precompute_data_missing_utxo)
     1{
     2    // Build a simple transaction with one input and one output
     3    CMutableTransaction mtx;
     4    mtx.version = 2;
     5    mtx.vin.resize(1);
     6    mtx.vin[0].prevout.hash = Txid::FromUint256(uint256::ONE);
     7    mtx.vin[0].prevout.n = 0;
     8    mtx.vout.resize(1);
     9    mtx.vout[0].nValue = COIN;
    10    mtx.vout[0].scriptPubKey = CScript() << OP_TRUE;
    11
    12    // A PSBTv2 constructed from this transaction has inputs without UTXO data
    13    // (no non_witness_utxo or witness_utxo set), so GetUTXO returns false.
    14    // PrecomputePSBTData must detect that and initialize txdata without spent outputs.
    15    PartiallySignedTransaction psbt_no_utxo(mtx);
    16    PrecomputedTransactionData txdata_no_utxo = PrecomputePSBTData(psbt_no_utxo);
    17    BOOST_CHECK(!txdata_no_utxo.m_spent_outputs_ready);
    18
    19    // When witness_utxo is set for every input, GetUTXO succeeds and
    20    // PrecomputePSBTData must mark spent outputs as ready.
    21    PartiallySignedTransaction psbt_with_utxo(mtx);
    22    psbt_with_utxo.inputs[0].witness_utxo = CTxOut(COIN, CScript() << OP_TRUE);
    23    PrecomputedTransactionData txdata_with_utxo = PrecomputePSBTData(psbt_with_utxo);
    24    BOOST_CHECK(txdata_with_utxo.m_spent_outputs_ready);
    25}
    

    achow101 commented at 10:17 pm on March 4, 2026:
    This mutant should exist in master currently, and I think fixing it is out of scope for this PR.
  272. in src/psbt.cpp:164 in 35347ea461
    169+            return false;
    170+        }
    171+        // Prevent duplicate inputs
    172+        if (std::find_if(inputs.begin(), inputs.end(),
    173+            [psbtin](const PSBTInput& psbt) {
    174+                return psbt.prev_txid == psbtin.prev_txid && psbt.prev_out == psbtin.prev_out;
    


    brunoerg commented at 5:03 pm on March 2, 2026:

    Unkilled mutant:

     0diff --git a/src/psbt.cpp b/src/psbt.cpp
     1index cb7b832965..ba54b63d7d 100644
     2--- a/src/psbt.cpp
     3+++ b/src/psbt.cpp
     4@@ -161,7 +161,7 @@ bool PartiallySignedTransaction::AddInput(PSBTInput& psbtin)
     5         // Prevent duplicate inputs
     6         if (std::find_if(inputs.begin(), inputs.end(),
     7             [psbtin](const PSBTInput& psbt) {
     8-                return psbt.prev_txid == psbtin.prev_txid && psbt.prev_out == psbtin.prev_out;
     9+                return psbt.prev_txid == psbtin.prev_txid || psbt.prev_out == psbtin.prev_out;
    10             }
    11         ) != inputs.end()) {
    12             return false;
    

    The following test case might be useful to address it:

     0BOOST_AUTO_TEST_CASE(psbt2_addinput_duplicate_test)
     1{
     2    // A PSBTv2 with inputs modifiable (bit 0 of m_tx_modifiable set)
     3    PartiallySignedTransaction psbt;
     4    psbt.m_version = 2;
     5    psbt.m_tx_modifiable = std::bitset<8>(0b00000001);
     6
     7    const Txid txidA = Txid::FromUint256(uint256::ONE);
     8    const Txid txidB = Txid::FromUint256(uint256(2));
     9
    10    // Add first input: (txidA, out=0)
    11    PSBTInput input1(2);
    12    input1.prev_txid = txidA;
    13    input1.prev_out = 0;
    14    BOOST_CHECK(psbt.AddInput(input1));
    15
    16    // Same txid, different output index: not a duplicate, must succeed.
    17    // A mutant replacing && with || in the duplicate check would incorrectly
    18    // reject this input because the txids match.
    19    PSBTInput input2(2);
    20    input2.prev_txid = txidA;
    21    input2.prev_out = 1;
    22    BOOST_CHECK(psbt.AddInput(input2));
    23
    24    // Different txid, same output index as input1: not a duplicate, must succeed.
    25    // A mutant replacing && with || would incorrectly reject this because
    26    // the output indices match.
    27    PSBTInput input3(2);
    28    input3.prev_txid = txidB;
    29    input3.prev_out = 0;
    30    BOOST_CHECK(psbt.AddInput(input3));
    31
    32    // Exact duplicate of input1 (same txid AND same output index): must be rejected.
    33    PSBTInput dup(2);
    34    dup.prev_txid = txidA;
    35    dup.prev_out = 0;
    36    BOOST_CHECK(!psbt.AddInput(dup));
    37}
    
  273. in src/psbt.cpp:189 in 35347ea461
    194+                return false;
    195+            }
    196+
    197+            std::optional<uint32_t> time_lock = psbtin.time_locktime;
    198+            std::optional<uint32_t> height_lock = psbtin.height_locktime;
    199+            bool has_sigs = false;
    


    brunoerg commented at 5:17 pm on March 2, 2026:

    Unkilled mutant:

     0diff --git a/src/psbt.cpp b/src/psbt.cpp
     1index cb7b832965..f6a1abee4a 100644
     2--- a/src/psbt.cpp
     3+++ b/src/psbt.cpp
     4@@ -186,7 +186,7 @@ bool PartiallySignedTransaction::AddInput(PSBTInput& psbtin)
     5
     6             std::optional<uint32_t> time_lock = psbtin.time_locktime;
     7             std::optional<uint32_t> height_lock = psbtin.height_locktime;
     8-            bool has_sigs = false;
     9+            bool has_sigs = true;
    10             for (const PSBTInput& input : inputs) {
    11                 if (input.time_locktime != std::nullopt && input.height_locktime == std::nullopt) {
    12                     height_lock.reset(); // Transaction can no longer have a height locktime
    

    The following test case might be useful to address it:

     0BOOST_AUTO_TEST_CASE(psbt2_add_input_no_sigs_allows_locktime_change)
     1{
     2    // When no existing input has partial signatures, AddInput must succeed even if the
     3    // new input would raise the computed locktime.  The has_sigs flag must begin as
     4    // false and only become true when an existing input carries partial_sigs.
     5    //
     6    // Mutant under test: initialising has_sigs to true would incorrectly block this
     7    // call because the timelock check (has_sigs && old_timelock != new_timelock) would
     8    // fire even though no signatures are present.
     9
    10    PartiallySignedTransaction psbt;
    11    psbt.m_version = 2;
    12    psbt.m_tx_modifiable.emplace();
    13    psbt.m_tx_modifiable->set(0); // mark inputs as modifiable
    14
    15    // Existing input: height_locktime = 1000, no partial_sigs
    16    PSBTInput existing_input(2);
    17    existing_input.prev_txid = Txid::FromUint256(uint256::ONE);
    18    existing_input.prev_out = 0;
    19    existing_input.height_locktime = 1000;
    20    psbt.inputs.push_back(existing_input);
    21
    22    uint32_t locktime;
    23    BOOST_CHECK(psbt.ComputeTimeLock(locktime));
    24    BOOST_CHECK_EQUAL(locktime, 1000U);
    25
    26    // New input with a larger height_locktime; would raise the computed locktime to 2000
    27    PSBTInput new_input(2);
    28    new_input.prev_txid = Txid::FromUint256(uint256(2));
    29    new_input.prev_out = 0;
    30    new_input.height_locktime = 2000;
    31
    32    // No existing input has partial_sigs, so changing the locktime is allowed.
    33    BOOST_CHECK(psbt.AddInput(new_input));
    34    BOOST_CHECK_EQUAL(psbt.inputs.size(), 2U);
    35
    36    BOOST_CHECK(psbt.ComputeTimeLock(locktime));
    37    BOOST_CHECK_EQUAL(locktime, 2000U);
    38}
    
  274. in src/psbt.cpp:242 in 35347ea461 outdated
    247+    if (tx != std::nullopt) {
    248+        // This is a v0 psbt, do the v0 AddOutput
    249+        CTxOut txout(*psbtout.amount, *psbtout.script);
    250+        tx->vout.push_back(txout);
    251+        outputs.push_back(psbtout);
    252+        return true;
    


    brunoerg commented at 7:51 pm on March 2, 2026:

    Unkilled mutant:

     0diff --git a/src/psbt.cpp b/src/psbt.cpp
     1index cb7b832965..989f0c3ffd 100644
     2--- a/src/psbt.cpp
     3+++ b/src/psbt.cpp
     4@@ -239,7 +239,7 @@ bool PartiallySignedTransaction::AddOutput(const PSBTOutput& psbtout)
     5         CTxOut txout(*psbtout.amount, *psbtout.script);
     6         tx->vout.push_back(txout);
     7         outputs.push_back(psbtout);
     8-        return true;
     9+        return false;
    10     }
    

    The following test case might be useful to address it:

     0BOOST_AUTO_TEST_CASE(psbt_addoutput_v0_returns_true)
     1{
     2    // Build a PSBTv0 from a CMutableTransaction (sets tx field, indicating v0)
     3    CMutableTransaction mtx;
     4    mtx.version = 2;
     5    PartiallySignedTransaction psbt{mtx, /*version=*/0};
     6    BOOST_REQUIRE(psbt.tx.has_value());
     7
     8    // PSBTOutput with amount and script set
     9    PSBTOutput psbt_out(/*psbt_version=*/0);
    10    psbt_out.amount = CAmount{1000};
    11    psbt_out.script = CScript() << OP_TRUE;
    12
    13    // AddOutput on a PSBTv0 must return true and actually append the output
    14    BOOST_CHECK(psbt.AddOutput(psbt_out));
    15    BOOST_CHECK_EQUAL(psbt.outputs.size(), 1U);
    16    BOOST_CHECK_EQUAL(psbt.tx->vout.size(), 1U);
    17}
    

    achow101 commented at 10:22 pm on March 4, 2026:
    This mutant should exist in master currently, and I think fixing it is out of scope for this PR.
  275. darosior commented at 4:36 pm on March 3, 2026: member

    I’ve been (fixing and) playing with the psbt fuzz harness, and it uncovered a PSBT that has a different unique ID before and after roundtripping to serialization.

    The PSBT in question is:

    0cHNidP8BAFwCAfsEAv8AAAAAcHdiFRUVFRUVFRUXcHNidCcBBQECAfsECgoBAAAAAAAAAAAAAAgAAAAAAGJ0JwoKAQwKV4gBBgABQElMAAoOCgAAANIAALcAAAAAAAIAAAAAAAH7BAAAAAAAAAA=
    

    Here is a repro. This is a patch that applies on top of this PR:

     0diff --git a/src/test/psbt_tests.cpp b/src/test/psbt_tests.cpp
     1index 1e37b59121b..09c706e8c06 100644
     2--- a/src/test/psbt_tests.cpp
     3+++ b/src/test/psbt_tests.cpp
     4@@ -26,6 +26,20 @@ void CheckTimeLock(const std::string& base64_psbt, std::optional<uint32_t> timel
     5     }
     6 }
     7 
     8+void CheckRoundTrips(const std::string& base64_psbt)
     9+{
    10+    PartiallySignedTransaction psbt, psbt_roundtrip;
    11+    std::string error;
    12+    bool decoded = DecodeBase64PSBT(psbt, base64_psbt, error);
    13+    BOOST_CHECK_MESSAGE(decoded, error);
    14+
    15+    std::vector<uint8_t> roundtrip_bytes;
    16+    VectorWriter{roundtrip_bytes, 0, psbt};
    17+    SpanReader{roundtrip_bytes} >> psbt_roundtrip;
    18+    BOOST_CHECK_EQUAL(psbt.GetUniqueID(), psbt_roundtrip.GetUniqueID());
    19+    std::cout << psbt.GetUniqueID().ToString() << std::endl;
    20+}
    21+
    22 BOOST_AUTO_TEST_CASE(psbt2_timelock_test)
    23 {
    24     CheckTimeLock("cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", 0);
    25@@ -38,6 +52,7 @@ BOOST_AUTO_TEST_CASE(psbt2_timelock_test)
    26     CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEQSLjcRiARIEECcAAAABDiA6Gzs8g31kiep6Mdjmx91QPAAb7z4GlY51dICNaMp4pQEPBAAAAAABEQSMjcRiAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA=", 1657048460);
    27     CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAAAAQ4gOhs7PIN9ZInqejHY5sfdUDwAG+8+BpWOdXSAjWjKeKUBDwQAAAAAAREEjI3EYgABAwhPkzV3AAAAAAEEFgAUCxNSys0Dz2qht/PI1jiGcbNKXhEA", 1657048460);
    28     CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAERBIyNxGIAAQMIT5M1dwAAAAABBBYAFAsTUsrNA89qobfzyNY4hnGzSl4RAA==", std::nullopt);
    29+    CheckRoundTrips("cHNidP8BAFwCAfsEAv8AAAAAcHdiFRUVFRUVFRUXcHNidCcBBQECAfsECgoBAAAAAAAAAAAAAAgAAAAAAGJ0JwoKAQwKV4gBBgABQElMAAoOCgAAANIAALcAAAAAAAIAAAAAAAH7BAAAAAAAAAA=");
    30 }
    31 
    32 BOOST_AUTO_TEST_SUITE_END()
    

    This was a bug in the implementation of GetUniqueID(), see #21283 (review).

  276. in src/psbt.cpp:181 in 35347ea461
    186+        // For now, we only do this if the new input has a required time lock.
    187+        // The BIP states that we should also do this if m_tx_modifiable's bit 2 is set
    188+        // (Has SIGHASH_SINGLE flag) but since we are only adding inputs at the end of the vector,
    189+        // we don't care about that.
    190+        bool iterate_inputs = psbtin.time_locktime != std::nullopt || psbtin.height_locktime != std::nullopt;
    191+        if (iterate_inputs) {
    


    brunoerg commented at 5:53 pm on March 3, 2026:

    Unkilled mutant:

     0diff --git a/src/psbt.cpp b/src/psbt.cpp
     1index cb7b832965..ea0aa7bac0 100644
     2--- a/src/psbt.cpp
     3+++ b/src/psbt.cpp
     4@@ -178,7 +178,7 @@ bool PartiallySignedTransaction::AddInput(PSBTInput& psbtin)
     5         // (Has SIGHASH_SINGLE flag) but since we are only adding inputs at the end of the vector,
     6         // we don't care about that.
     7         bool iterate_inputs = psbtin.time_locktime != std::nullopt || psbtin.height_locktime != std::nullopt;
     8-        if (iterate_inputs) {
     9+        if (1==1) {
    10             uint32_t old_timelock;
    11             if (!ComputeTimeLock(old_timelock)) {
    12                 return false;
    

    The following test case might be useful to address it:

     0BOOST_AUTO_TEST_CASE(psbt2_add_input_no_locktime_with_timelocked_existing)
     1{
     2    // When an existing input has only time_locktime set (no height_locktime),
     3    // adding a new input with no required timelocks must succeed. The
     4    // iterate_inputs flag should be false (new input has no timelocks), so the
     5    // locktime-compatibility loop is skipped. If the loop ran unconditionally,
     6    // the check `if (time_lock == std::nullopt)` would incorrectly trigger and
     7    // reject the new input.
     8    PartiallySignedTransaction psbt;
     9    psbt.m_version = 2;
    10    psbt.tx_version = 2;
    11    psbt.m_tx_modifiable = std::bitset<8>(0x01); // bit 0: inputs modifiable
    12
    13    // Existing input with only time_locktime set (no height_locktime)
    14    PSBTInput existing_input(2);
    15    existing_input.prev_txid = Txid::FromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").value();
    16    existing_input.prev_out = 0;
    17    existing_input.time_locktime = 1000000;
    18    psbt.inputs.push_back(existing_input);
    19
    20    // New input with no timelocks at all — must be accepted
    21    PSBTInput new_input(2);
    22    new_input.prev_txid = Txid::FromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").value();
    23    new_input.prev_out = 0;
    24    BOOST_CHECK(psbt.AddInput(new_input));
    25    BOOST_CHECK_EQUAL(psbt.inputs.size(), 2U);
    26}
    
  277. brunoerg commented at 5:54 pm on March 3, 2026: contributor
    I’ve ran a mutation testing analysis for this PR - mostly for src/psbt.cpp. The results were not great, less than 50% of mutation score. Left some comments if some unkilled mutants (changes that are not detected by any test). Let me know if they’re useful or not useful at all.
  278. in src/psbt.cpp:127 in 35347ea461
    131+    return mtx;
    132+}
    133+
    134+Txid PartiallySignedTransaction::GetUniqueID() const
    135+{
    136+    if (m_version == 0) {
    


    darosior commented at 6:12 pm on March 3, 2026:

    This is incorrect because for version 0, m_version may also be nullopt. This explains the different unique IDs i reported earlier.

    It seems you had it right in other conditionals, such as in AddInput() where you have:

    0if (m_version == std::nullopt || m_version == 0) {
    

    In other places you simply check whether the tx member is nullopt as a proxy of checking for V2.

    So i would recommend having a method that is simply IsV0() (or alternatively IsV2()) and use it everywhere.


    achow101 commented at 10:48 pm on March 4, 2026:
    I’ve changed the checks I could find to use GetVersion() < 2 which should be an identical check.
  279. in src/psbt.h:475 in 6f95465a13
    470@@ -463,6 +471,28 @@ struct PSBTInput
    471             SerializeToVector(s, final_script_witness.stack);
    472         }
    473 
    474+        // Write prev txid, vout, sequence, and lock times
    475+        if (!prev_txid.IsNull()) {
    


    darosior commented at 6:52 pm on March 4, 2026:
    We would happily parse an all-0 txid, but not serialize it? I think we should be consistent. Using an optional as for other fields would allow to distinguish absence of prev_txid key and all-0 prev_txid.
  280. in src/psbt.h:785 in 6f95465a13 outdated
    780+                        throw std::ios_base::failure("Required height based locktime is more than one byte type");
    781+                    }
    782+                    uint32_t v;
    783+                    UnserializeFromVector(s, v);
    784+                    if (v >= LOCKTIME_THRESHOLD) {
    785+                        throw std::ios_base::failure("Required time based locktime is invalid (greater than or equal to 500000000)");
    


    darosior commented at 6:59 pm on March 4, 2026:
    copypasta typo: s/time/height/
  281. in src/psbt.h:1157 in 6f95465a13 outdated
    1152+                    } else if (key.size() != 1) {
    1153+                        throw std::ios_base::failure("Output script key is more than one byte type");
    1154+                    }
    1155+                    CScript v;
    1156+                    s >> v;
    1157+                    script = v;
    


    darosior commented at 7:11 pm on March 4, 2026:

    nit: std::move it? Or maybe just don’t use a temporary:

     0diff --git a/src/psbt.h b/src/psbt.h
     1index 891fc85540c..82d39b4d131 100644
     2--- a/src/psbt.h
     3+++ b/src/psbt.h
     4@@ -1200,9 +1200,8 @@ struct PSBTOutput
     5                     } else if (m_psbt_version == 0) {
     6                         throw std::ios_base::failure("Output script is not allowed in PSBTv0");
     7                     }
     8-                    CScript v;
     9-                    s >> v;
    10-                    script = v;
    11+                    script.emplace();
    12+                    s >> *script;
    13                     break;
    14                 }
    15                 case PSBT_OUT_TAP_INTERNAL_KEY:
    
  282. in src/psbt.h:314 in ad7a98f983 outdated
    310     bool IsNull() const;
    311     void FillSignatureData(SignatureData& sigdata) const;
    312     void FromSignatureData(const SignatureData& sigdata);
    313     void Merge(const PSBTInput& input);
    314-    PSBTInput() = default;
    315+    PSBTInput(uint32_t psbt_version) : m_psbt_version(psbt_version) {}
    


    darosior commented at 7:21 pm on March 4, 2026:
    nit: should those be explicit?
  283. in src/psbt.h:737 in 9dd85826bd outdated
    733@@ -731,6 +734,8 @@ struct PSBTInput
    734                         throw std::ios_base::failure("Duplicate Key, previous txid is already provided");
    735                     } else if (key.size() != 1) {
    736                         throw std::ios_base::failure("Previous txid key is more than one byte type");
    737+                    } else if (m_psbt_version == 0) {
    


    darosior commented at 7:24 pm on March 4, 2026:
    Here and below are other instances of the incorrect version check #21283 (review)
  284. in src/psbt.h:1528 in 9dd85826bd outdated
    1524@@ -1482,6 +1525,7 @@ struct PartiallySignedTransaction
    1525                     }
    1526                     CompactSizeReader reader(input_count);
    1527                     UnserializeFromVector(s, reader);
    1528+                    found_input_count = true;
    


    darosior commented at 7:33 pm on March 4, 2026:
    nit: meh at caching the presence in a boolean to do the check after parsing, rather than doing the checks right at parsing times like for pre-existing fields.
  285. in src/psbt.cpp:59 in 983082fe6a outdated
    54+{
    55+    std::optional<uint32_t> time_lock{0};
    56+    std::optional<uint32_t> height_lock{0};
    57+    for (const PSBTInput& input : inputs) {
    58+        if (input.time_locktime != std::nullopt && input.height_locktime == std::nullopt) {
    59+            height_lock.reset(); // Transaction can no longer have a height locktime
    


    darosior commented at 8:02 pm on March 4, 2026:

    This makes sense to require that fields are mutually exclusive, but this is not specified by the BIP. In fact it seems to hint that it is explicitly supported:

    If a PSBT has both types of locktimes possible because one or more inputs specify both PSBT_IN_REQUIRED_TIME_LOCKTIME and PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, then locktime determined by looking at the PSBT_IN_REQUIRED_HEIGHT_LOCKTIME fields of the inputs must be chosen

  286. in src/psbt.cpp:98 in ca942672b5 outdated
    93+    }
    94+
    95+    CMutableTransaction mtx;
    96+    mtx.version = *tx_version;
    97+    bool locktime_success = ComputeTimeLock(mtx.nLockTime);
    98+    assert(locktime_success);
    


    darosior commented at 8:05 pm on March 4, 2026:
    Hmm this assert can be triggered since we will happily parse a PSBT with an input containing both a time-based locktime and a height-based locktime?
  287. in src/psbt.cpp:140 in 90b6766c1a outdated
    138+        CTxIn txin(COutPoint(psbtin.prev_txid, *psbtin.prev_out));
    139+        if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) {
    140+            // Prevent duplicate inputs
    141+            return false;
    142+        }
    143+        tx->vin.push_back(txin);
    


    darosior commented at 8:53 pm on March 4, 2026:
    nit: move it?
  288. in src/psbt.cpp:135 in 90b6766c1a outdated
    133 {
    134-    if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) {
    135-        return false;
    136+    if (m_version == std::nullopt || m_version == 0) {
    137+        // This is a v0 psbt, so do the v0 AddInput
    138+        CTxIn txin(COutPoint(psbtin.prev_txid, *psbtin.prev_out));
    


    darosior commented at 8:57 pm on March 4, 2026:

    UB’ing if the passed PSBTInput has no prevout, which even its default constructor doesn’t set, is quite the footgun. Especially as this precondition comes entirely undocumented..

    Since AddInput already returns bool, could you just return false when it’s not set?

  289. in src/psbt.cpp:167 in d0b4e19330 outdated
    166-    tx->vout.push_back(txout);
    167-    outputs.push_back(psbtout);
    168-    return true;
    169+    if (tx != std::nullopt) {
    170+        // This is a v0 psbt, do the v0 AddOutput
    171+        CTxOut txout(*psbtout.amount, *psbtout.script);
    


    darosior commented at 9:01 pm on March 4, 2026:
    Can you return false instead of UB’ing when expected fields aren’t set?
  290. in src/psbt.cpp:54 in a1b4081df8 outdated
    48@@ -46,6 +49,9 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt)
    49             m_xpubs[xpub_pair.first].insert(xpub_pair.second.begin(), xpub_pair.second.end());
    50         }
    51     }
    52+    if (fallback_locktime == std::nullopt && psbt.fallback_locktime != std::nullopt) fallback_locktime = psbt.fallback_locktime;
    53+    if (m_tx_modifiable != std::nullopt && psbt.m_tx_modifiable != std::nullopt) *m_tx_modifiable |= *psbt.m_tx_modifiable;
    54+    if (m_tx_modifiable == std::nullopt && psbt.m_tx_modifiable != std::nullopt) m_tx_modifiable = psbt.m_tx_modifiable;
    


    darosior commented at 9:13 pm on March 4, 2026:

    Hmm are we sure we want the union of sets of modifiable bits, rather than the intersection? For instance if you merge a PSBT with an ACP signature and only its inputs-modifiable bit set, with a PSBT with a NONE signature and only its outputs-modifiable bit set, then i think you want as a result a PSBT with none of those bits set.

    This is annoying because you want the opposite semantics for the SIGHASH_SINGLE bit it seems: if any of the PSBT has a SIGHASH_SINGLE sig, the result should set the has-single bit.

  291. in src/psbt.cpp:327 in a1b4081df8 outdated
    322@@ -317,6 +323,9 @@ void PSBTInput::FromSignatureData(const SignatureData& sigdata)
    323 
    324 void PSBTInput::Merge(const PSBTInput& input)
    325 {
    326+    assert(prev_txid == input.prev_txid);
    327+    assert(*prev_out == *input.prev_out);
    


    darosior commented at 9:18 pm on March 4, 2026:
    Should Merge on inputs and outputs return a boolean like the method on the top-level PSBT? Crashing with no documented pre-condition is not great, and triggering UB on a missing expected field is worse.
  292. in src/psbt.cpp:528 in 47c3f751ee outdated
    524@@ -525,7 +525,7 @@ PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction&
    525 PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, std::optional<int> sighash,  SignatureData* out_sigdata, bool finalize)
    526 {
    527     PSBTInput& input = psbt.inputs.at(index);
    528-    const CMutableTransaction& tx = *psbt.tx;
    529+    const CMutableTransaction& tx = psbt.GetUnsignedTx();
    


    darosior commented at 9:43 pm on March 4, 2026:
    nit: Those call sites could have been updated in the same commit they were introduced. In this PR you often have one commit introducing a method, and then one or more commits updating various call sites. I think it’s unnecessary but not a big deal, but i’m curious why you took this approach?
  293. darosior commented at 9:58 pm on March 4, 2026: member

    Leaving a review to surface some comments on the first ~15 commits. I think the most important things are:

    • I don’t think the logic in merging the modifiable bitsets is correct;
    • I don’t think we should UB / crash in various methods if a PSBT input/output is missing some fields. (I understand those are always set when the input/output comes from a top-level PSBT, but still.)

    Here is also a fix / improvement for the psbt fuzz target. I’ve been running this (along with #34725) for a few days now with no crash:

     0diff --git a/src/test/fuzz/psbt.cpp b/src/test/fuzz/psbt.cpp
     1index 4ea75eb4d61..c10716f9c11 100644
     2--- a/src/test/fuzz/psbt.cpp
     3+++ b/src/test/fuzz/psbt.cpp
     4@@ -33,6 +33,29 @@ FUZZ_TARGET(psbt)
     5     }
     6     const PartiallySignedTransaction psbt = psbt_mut;
     7 
     8+    // We are on purpose not forward compatible, and version 1 is disabled.
     9+    const auto psbt_version{psbt.GetVersion()};
    10+    Assert(psbt_version == 0 || psbt_version == 2);
    11+
    12+    // The transaction version is always set regardless of the PSBT version.
    13+    Assert(psbt.tx_version.has_value());
    14+
    15+    // PSBT v0 must always have a transaction. PSBT v2 must never have one.
    16+    if (psbt_version == 0) {
    17+        Assert(psbt.tx.has_value());
    18+        // When present the transaction must be consistent with the PSBT metadata.
    19+        Assert(psbt.tx->vin.size() == psbt.inputs.size());
    20+        Assert(psbt.tx->vout.size() == psbt.outputs.size());
    21+        // And it is always possible to create a PSBT from the inner transaction.
    22+        (void)PartiallySignedTransaction{*psbt.tx, /*version=*/0};
    23+        (void)PartiallySignedTransaction{*psbt.tx, /*version=*/2};
    24+    } else {
    25+        Assert(!psbt.tx.has_value());
    26+        Assert(psbt.m_version.has_value());
    27+        // FIXME: this crashes but the specs mandate that tx version be >=2
    28+        // Assert(*psbt.tx_version >= 2);
    29+    }
    30+
    31     // A PSBT must roundtrip.
    32     PartiallySignedTransaction psbt_roundtrip;
    33     std::vector<uint8_t> psbt_ser;
    34@@ -52,28 +75,36 @@ FUZZ_TARGET(psbt)
    35 
    36     (void)psbt.IsNull();
    37 
    38-    std::optional<CMutableTransaction> tx = psbt.tx;
    39-    if (tx) {
    40-        const CMutableTransaction& mtx = *tx;
    41-        const PartiallySignedTransaction psbt_from_tx{mtx};
    42-    }
    43-
    44     for (const PSBTInput& input : psbt.inputs) {
    45         (void)PSBTInputSigned(input);
    46         (void)input.IsNull();
    47+        CTxOut tx_out;
    48+        if (input.GetUTXO(tx_out)) {
    49+            (void)tx_out.IsNull();
    50+            (void)tx_out.ToString();
    51+        }
    52+        // A PSBT input must roundtrip to signature data.
    53+        PSBTInput input_fill{psbt_version};
    54+        SignatureData sig_data;
    55+        input.FillSignatureData(sig_data);
    56+        input_fill.FromSignatureData(sig_data);
    57+        // FIXME: actually it crashes. Do we want this invariant?
    58+        // Assert(input == input_fill);
    59     }
    60     (void)CountPSBTUnsignedInputs(psbt);
    61 
    62     for (const PSBTOutput& output : psbt.outputs) {
    63         (void)output.IsNull();
    64-    }
    65-
    66-    for (size_t i = 0; i < psbt.tx->vin.size(); ++i) {
    67-        CTxOut tx_out;
    68-        if (psbt.inputs.at(i).GetUTXO(tx_out)) {
    69-            (void)tx_out.IsNull();
    70-            (void)tx_out.ToString();
    71-        }
    72+        // PSBT v2 outputs must have amount and script set. PSBT v0 outputs must not have them
    73+        // set in the serialization, but we cache them from the inner tx after parsing.
    74+        Assert(output.amount.has_value() && output.script.has_value());
    75+        // A PSBT output must roundtrip to signature data.
    76+        PSBTOutput output_fill{psbt_version};
    77+        SignatureData sig_data;
    78+        output.FillSignatureData(sig_data);
    79+        output_fill.FromSignatureData(sig_data);
    80+        // FIXME: actually it crashes. Do we want this invariant?
    81+        //Assert(output == output_fill);
    82     }
    83 
    84     psbt_mut = psbt;
    85@@ -99,7 +130,9 @@ FUZZ_TARGET(psbt)
    86         (void)psbt_mut.AddInput(psbt_in);
    87     }
    88     for (const auto& psbt_out : psbt_merge.outputs) {
    89-        Assert(psbt_mut.AddOutput(psbt_out));
    90+        (void)psbt_mut.AddOutput(psbt_out);
    91     }
    92     psbt_mut.unknown.insert(psbt_merge.unknown.begin(), psbt_merge.unknown.end());
    93+
    94+    RemoveUnnecessaryTransactions(psbt_mut);
    95 }
    
  294. darosior commented at 10:03 pm on March 4, 2026: member
    Ping @rkrux @bigspider @nervana21 @erickcestari @macgyver13 @codeabysss since you reacted to my comment above. If you want to help get this through the finish line, momentum is now. (And not all review need to inspect the code in details, for instance reporting successful compatibility testing with other projects is useful as well!)
  295. nervana21 commented at 10:35 pm on March 4, 2026: contributor
    Oh that’s a neat idea. I’ll see if some compatibility testing can surface any issues.
  296. Define psbtv2 field numbers 0efb85840b
  297. Change PSBT unknown fields test to use higher numbers
    Previously these tests were using 0x0f as the field number. Changed to
    use 0xf0 instead as it is unlikely we will hit that anytime soon.
    26716d30b6
  298. Implement PSBTv2 fields de/ser 3951fd9e94
  299. Have PSBTInput and PSBTOutput know the PSBT's version b3a09ce106
  300. Enforce PSBT version constraints
    With PSBTv2, some fields are not allowed in PSBTv2, and some are
    required. Enforce those.
    2b669b3ee5
  301. achow101 force-pushed on Mar 4, 2026
  302. achow101 commented at 10:50 pm on March 4, 2026: member

    Rebased for #34725

    Will be addressing comments soon(tm), likely after 31.0 branching for major things.

    For the unkilled mutants, all of those would benefit from having tests for AddInput and AddOutput, both of which are pretty much untested for v2 as there are no unit tests for them and the RPCs currently restrict to v0. I’ll add unit tests for those eventually.

  303. DrahtBot added the label CI failed on Mar 5, 2026
  304. DrahtBot commented at 0:47 am on March 5, 2026: contributor

    🚧 At least one of the CI tasks failed. Task Windows native, VS: https://github.com/bitcoin/bitcoin/actions/runs/22693047593/job/65793056491 LLM reason (✨ experimental): test_bitcoin-qt test failed, causing CTest to exit non-zero and fail the CI run.

    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.

  305. Add PSBT::CacheUnsignedTxPieces
    Fetches the PSBTv2 fields from PSBTv0's global unsigned tx. This allows
    us to pretend everything internally is a PSBTv2 and makes things easier
    to work with.
    7215ea7fc2
  306. Replace PSBT::GetInputUTXO with PSBTInput::GetUTXO
    Now that PSBTInput's track their own prevouts, there's no need for a
    PSBT global function to fetch input specific data.
    40f8856f2a
  307. Add PSBT::ComputeLockTime()
    Function to compute the lock time for the transaction
    d3b47657b2
  308. Add PSBT::GetUnsignedTx
    A helper function for getting the unsigned transaction regardless of
    psbt version.
    01747828a7
  309. Add PSBT::GetUniqueID
    The unique ID for PSBTv2 is different from v0. Use this function to get
    the ID without requiring the caller to know the version number.
    5b9de8c817
  310. Change PSBT::AddInput to take just PSBTInput bef7bcab67
  311. Change PSBT::AddOutput to take just PSBTOutput 6b1202d263
  312. Implement PSBTv2 field merging 98b49a32be
  313. Add PSBTInput::GetOutPoint
    Helper for getting the PSBTInput COutPoint
    07642013e0
  314. Update SignPSBTInput for PSBTv2 1a55f25ecb
  315. Change PrecomputePSBTData to use GetUnsignedTx 1d22354996
  316. Update FinalizeAndExtract for v2 7bc5c184fc
  317. Update AnalyzePSBT for PSBTv2 0c3ca0817b
  318. Update PSBT Operations Dialog for v2 b3ac25ab29
  319. Update RPCs for PSBTv2 7c3239bea9
  320. Update wallet for PSBTv2 5bdbfd3896
  321. Implement PSBTv2 AddInput and AddOutput 16d8459af8
  322. Update PSBTInputSignedAndVerified for PSBTv2 11f0d67980
  323. Update test_framework/psbt.py for PSBTv2 9b199a9150
  324. Implement PSBTv2 in decodepsbt b1e37b16f2
  325. Allow specifying PSBT version in constructor 21bd49ef74
  326. Update PSBT::UpdatePSBTOutput to use GetUnsignedTx 43c70bbc8f
  327. Restrict joinpsbts to PSBTv0 only 355dc9a1a8
  328. Allow createpsbt and walletcreatefundedpsbt to take psbt version
    Use v2 for other RPCs. For some tests to work, PSBTv0 is set explicitly.
    1cf57803ea
  329. Use GetUnsignedTx when serializing in PSBTv0
    If we are asked to make a PSBTv0, we may not necessarily have made an
    unsigned transaction. So instead use GetUnsignedTx which will either
    fetch one that already exists, or construct a new one from the stored
    data. Internally we may be storing a PSBTv0 like a PSBTv2, but still
    want to serialize those as v0.
    e21083191e
  330. psbt: Change default psbt version to 2 67497c99b3
  331. tests: Add test vectors from BIP 370 bc75444768
  332. tests: Add PSBT unit test for ComputeTimeLock c8adf0803e
  333. doc: Release notes for psbtv2 7a10a429ab
  334. in test/functional/rpc_psbt.py:797 in 26716d30b6 outdated
    793@@ -794,7 +794,7 @@ def run_test(self):
    794         # BIP 174 Test Vectors
    795 
    796         # Check that unknown values are just passed through
    797-        unknown_psbt = "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA="
    798+        unknown_psbt = "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACvABAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA="
    


  335. achow101 force-pushed on Mar 5, 2026
  336. in src/rpc/rawtransaction.cpp:1496 in 7c3239bea9
    1492@@ -1497,8 +1493,8 @@ static RPCHelpMan decodepsbt()
    1493         outputs.push_back(std::move(out));
    1494 
    1495         // Fee calculation
    1496-        if (MoneyRange(psbtx.tx->vout[i].nValue) && MoneyRange(output_value + psbtx.tx->vout[i].nValue)) {
    1497-            output_value += psbtx.tx->vout[i].nValue;
    1498+        if (MoneyRange(*output.amount) && MoneyRange(output_value + *output.amount)) {
    


    darosior commented at 9:28 pm on March 5, 2026:
    API nit: if callers are always going to assume these fields are present, should they really be optionals in the first place?
  337. in src/rpc/rawtransaction.cpp:1826 in 7c3239bea9
    1825     }
    1826 
    1827     // Create a blank psbt where everything will be added
    1828     PartiallySignedTransaction merged_psbt;
    1829+    merged_psbt.tx_version = best_version;
    1830+    merged_psbt.fallback_locktime = best_locktime;
    


    darosior commented at 9:36 pm on March 5, 2026:
    What of the PSBT version? If the transactions to be joined are PSBT v2, this one should be too.

    darosior commented at 9:37 pm on March 5, 2026:
    Ah, this is addressed later in “Restrict joinpsbts to v0 only”. Another case of having numerous small commits being confusing.
  338. DrahtBot removed the label CI failed on Mar 5, 2026
  339. in test/functional/test_framework/psbt.py:115 in 9b199a9150
    110@@ -107,37 +111,78 @@ def __init__(self, *, g=None, i=None, o=None):
    111         self.g = g if g is not None else PSBTMap()
    112         self.i = i if i is not None else []
    113         self.o = o if o is not None else []
    114-        self.tx = None
    115+        self.in_count = len(i) if i is not None else None
    116+        self.out_count = len(o) if o is not None else None
    


    darosior commented at 4:38 pm on March 6, 2026:
    Why cache the length? This seems unnecessary and error-prone
  340. in doc/release-notes-21283.md:6 in 7a10a429ab
    0@@ -0,0 +1,6 @@
    1+Updated RPCs
    2+------------
    3+
    4+- `createpsbt` and `walletcreatepsbt` will now default to creating
    5+  version 2 PSBTs. An optional `psbt_version` argument is added to
    6+  both which allows specifying the version of PSBT to create.
    


    darosior commented at 6:58 pm on March 6, 2026:
    Also mention the RPCs that will now always return v2 PSBTs? Some downstream project may be relying on being able to parse the output as a v0 PSBT.
  341. in src/psbt.h:1376 in e21083191e
    1372@@ -1373,7 +1373,7 @@ struct PartiallySignedTransaction
    1373             SerializeToVector(s, CompactSizeWriter(PSBT_GLOBAL_UNSIGNED_TX));
    1374 
    1375             // Write serialized tx to a stream
    1376-            SerializeToVector(s, TX_NO_WITNESS(*tx));
    1377+            SerializeToVector(s, TX_NO_WITNESS(GetUnsignedTx()));
    


    darosior commented at 8:54 pm on March 6, 2026:
    That the tx member is present when PSBT version is 0 is an invariant that you already rely on throughout this PR. What is different in this instance?
  342. darosior commented at 9:00 pm on March 6, 2026: member
    Some more comments as i finished going through the commits one by one. I also further improved the fuzz harness, will edit the patch in my previous comment. Been running it for a few days now on a beefy machine with no issue.

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-03-08 06:12 UTC

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