rpc: Fix internal bug in descriptorprocesspsbt when encountering invalid signatures #33014

pull b-l-u-e wants to merge 1 commits into bitcoin:master from b-l-u-e:fix-32849-descriptorprocesspsbt-internal-bug changing 3 files +54 −3
  1. b-l-u-e commented at 5:15 am on July 19, 2025: contributor
    Fixes #32849 The descriptorprocesspsbt RPC was crashing with an internal bug assertion when processing PSBTs with invalid signatures (like invalid Schnorr signatures with SIGHASH_SINGLE | ANYONECANPAY flags). Instead of returning a helpful error message, it would just crash. This change replaces the CHECK_NONFATAL call with proper error handling that catches the failure and returns a JSONRPCError with a clear message to the user. Tested with Bitcoin Core v29.0.0 using the exact PSBT from the issue report. The fix now returns proper error messages instead of crashing.
  2. DrahtBot added the label RPC/REST/ZMQ on Jul 19, 2025
  3. DrahtBot commented at 5:15 am on July 19, 2025: 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/33014.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK rkrux
    Stale ACK achow101

    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:

    • #34193 (wallet: make migration more robust against failures by furszy)
    • #34176 (wallet: crash fix, handle non-writable db directories by furszy)
    • #21283 (Implement BIP 370 PSBTv2 by achow101)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  4. achow101 commented at 10:47 pm on July 22, 2025: member

    The CHECK_NONFATAL is correct. complete == true means that the PSBT is complete and can be finalized. The invariant here is that complete == true means FinalizeAndExtract must succeed.

    BIP 174 states that a finalizer must only construct valid scriptSigs and witnesses. We encounter this error because we assume that if a PSBT is finalized, the scriptSigs and witnesses are valid so we are not performing the extra step of verifying the script. However, in this situation, the finalizer has misbehaved and produce invalid scriptSigs or witnesses which causes the assumption to fail.

    This should be resolved by using PSBTInputSignedAndVerified instead. It may even be worth it to combine PSBTInputSigned with PSBTInputSignedAndVerified and replacing PSBTInputSigned wherever it is used. Although that may not be possible to do.

  5. b-l-u-e force-pushed on Jul 24, 2025
  6. b-l-u-e commented at 1:00 pm on July 24, 2025: contributor

    @achow101… my initial approach was incorrect. CHECK_NONFATAL is correctly protecting the invariant that a PSBT marked as complete must be finalizable.

    The actual bug, as you pointed out, was that the completeness check was too lenient, only checking for the presence of a signature rather than its cryptographic validity.

    After ensuring that the logic first verifies the existing signatures before declaring the PSBT complete (the approach you suggested with PSBTInputSignedAndVerified), the system now behaves as expected. The same invalid PSBT now correctly results in { “complete”: false } instead of triggering the internal bug.

  7. b-l-u-e force-pushed on Jul 24, 2025
  8. achow101 requested review from achow101 on Oct 22, 2025
  9. in src/rpc/rawtransaction.cpp:2093 in 507501ddfa
    2087@@ -2088,8 +2088,9 @@ RPCHelpMan descriptorprocesspsbt()
    2088 
    2089     // Check whether or not all of the inputs are now signed
    2090     bool complete = true;
    2091-    for (const auto& input : psbtx.inputs) {
    2092-        complete &= PSBTInputSigned(input);
    2093+    const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx);
    2094+    for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) {
    2095+        complete &= PSBTInputSignedAndVerified(psbtx, i, &txdata);
    


    rkrux commented at 1:35 pm on October 24, 2025:

    It’s discouraged to use bitwise operators on booleans: https://stackoverflow.com/questions/24542/using-bitwise-operators-for-booleans-in-c

    0- complete &= PSBTInputSignedAndVerified(psbtx, i, &txdata);
    1+ complete = complete && PSBTInputSignedAndVerified(psbtx, i, &txdata); 
    

    l0rinc commented at 2:23 pm on October 24, 2025:
    this is likely a change in behavior since && short circuits
  10. in src/rpc/rawtransaction.cpp:2089 in 507501ddfa
    2087@@ -2088,8 +2088,9 @@ RPCHelpMan descriptorprocesspsbt()
    2088 
    2089     // Check whether or not all of the inputs are now signed
    


    rkrux commented at 1:36 pm on October 24, 2025:
    0- // Check whether or not all of the inputs are now signed
    1+ // Check whether or not all of the inputs are now correctly signed 
    
  11. rkrux commented at 1:39 pm on October 24, 2025: contributor

    I don’t think the corresponding bug is still present after v30, see: #32849 (comment)

    Also, a test case in rpc_psbt.py would be good to verify the fix.

  12. rkrux commented at 11:50 am on October 25, 2025: contributor

    Concept ACK 507501ddfa3f2c538a0817a5645b20ddde554362

    I don’t think the corresponding bug is still present after v30, see: #32849 (comment)

    I was able to reproduce this bug in the functional tests.

    Also, a test case in rpc_psbt.py would be good to verify the fix.

    Consider adding the following test case in the second commit in this PR. The following test fails without this fix and passes with it.

    Edit: Added bitflipper function in util (taken from feature_taproot.py) and used it to make the signature invalid. The previous approach I used here changed the last byte to 0x42 always that could have been proven to be flaky.

     0diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
     1index 933e4cd1ad..55c5df4a95 100755
     2--- a/test/functional/rpc_psbt.py
     3+++ b/test/functional/rpc_psbt.py
     4@@ -37,6 +37,7 @@ from test_framework.psbt import (
     5     PSBT_IN_WITNESS_UTXO,
     6     PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS,
     7     PSBT_OUT_TAP_TREE,
     8+    PSBT_IN_FINAL_SCRIPTWITNESS,
     9 )
    10 from test_framework.script import CScript, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY
    11 from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE
    12@@ -50,6 +51,7 @@ from test_framework.util import (
    13     assert_raises_rpc_error,
    14     find_vout_for_address,
    15     wallet_importprivkey,
    16+    bitflipper,
    17 )
    18 from test_framework.wallet_util import (
    19     calculate_input_weight,
    20@@ -369,6 +371,35 @@ class PSBTTest(BitcoinTestFramework):
    21         changepos = psbtx["changepos"]
    22         assert_equal(decoded_psbt["tx"]["vout"][changepos]["scriptPubKey"]["type"], expected_type)
    23 
    24+    def test_psbt_with_invalid_signature(self):
    25+        self.log.info("Test descriptorprocesspsbt with invalid signature in signed PSBT")
    26+
    27+        def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    28+        outputs = [{def_wallet.getnewaddress(address_type="bech32m"): 1}]
    29+        def_wallet.send(outputs)
    30+        self.generate(self.nodes[0], 6)
    31+
    32+        utxos = [utxo for utxo in def_wallet.listunspent() if utxo["desc"].startswith("tr")]
    33+        psbt = def_wallet.walletcreatefundedpsbt(utxos, [{def_wallet.getnewaddress(): 0.5}])["psbt"]
    34+        descs = [desc for desc in def_wallet.listdescriptors(True)["descriptors"] if desc["desc"].startswith("tr")]
    35+
    36+        # unload wallet so as to not use the wallet RPC in further process PSBT calls
    37+        def_wallet.unloadwallet()
    38+
    39+        desc_psbt = self.nodes[0].descriptorprocesspsbt(psbt=psbt, descriptors=descs, finalize=True)["psbt"]
    40+        flawed_psbt = PSBT.from_base64(desc_psbt)
    41+        valid_sig = flawed_psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS)
    42+        invalid_sig = bitflipper(valid_sig)
    43+        flawed_psbt.i[0].map[PSBT_IN_FINAL_SCRIPTWITNESS] = invalid_sig
    44+        flawed_psbt = flawed_psbt.to_base64()
    45+
    46+        result = self.nodes[0].descriptorprocesspsbt(psbt=flawed_psbt, descriptors=descs)
    47+        assert_equal(result["complete"], False)
    48+        assert_equal("hex" in result, False)
    49+
    50+        # load the default wallet back
    51+        self.nodes[0].loadwallet(self.default_wallet_name)
    52+
    53     def run_test(self):
    54         # Create and fund a raw tx for sending 10 BTC
    55         psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']
    56@@ -1211,6 +1242,7 @@ class PSBTTest(BitcoinTestFramework):
    57         if not self.options.usecli:
    58             self.test_sighash_mismatch()
    59         self.test_sighash_adding()
    60+        self.test_psbt_with_invalid_signature()
    61 
    62 if __name__ == '__main__':
    63     PSBTTest(__file__).main()
    64diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py
    65index bbad6ea771..b4e597ce1d 100644
    66--- a/test/functional/test_framework/util.py
    67+++ b/test/functional/test_framework/util.py
    68@@ -715,3 +715,6 @@ def wallet_importprivkey(wallet_rpc, privkey, timestamp, *, label=""):
    69     }]
    70     import_res = wallet_rpc.importdescriptors(req)
    71     assert_equal(import_res[0]["success"], True)
    72+
    73+def bitflipper(input):
    74+    assert isinstance(input, bytes)
    75+    return (int.from_bytes(input, 'little') ^ (1 << random.randrange(len(input) * 8))).to_bytes(len(input), 'little')
    76\ No newline at end of file
    
  13. achow101 commented at 10:17 pm on November 17, 2025: member

    A test is definitely necessary here to avoid future regressions. @rkrux’s proposed test would be fine.

    The commit message has a duplicate line that needs to be removed.

  14. b-l-u-e force-pushed on Dec 7, 2025
  15. b-l-u-e requested review from l0rinc on Dec 7, 2025
  16. b-l-u-e requested review from rkrux on Dec 7, 2025
  17. in test/functional/rpc_psbt.py:433 in ac99af4f80
    428+        valid_sig = flawed_psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS)
    429+        invalid_sig = bitflipper(valid_sig)
    430+        flawed_psbt.i[0].map[PSBT_IN_FINAL_SCRIPTWITNESS] = invalid_sig
    431+        flawed_psbt = flawed_psbt.to_base64()
    432+
    433+        result = self.nodes[0].descriptorprocesspsbt(psbt=flawed_psbt, descriptors=descs)
    


    rkrux commented at 10:24 am on December 10, 2025:

    Nit containing verbose naming and arguments.

     0diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
     1index cec937658f..ae1619d625 100755
     2--- a/test/functional/rpc_psbt.py
     3+++ b/test/functional/rpc_psbt.py
     4@@ -417,20 +417,24 @@ class PSBTTest(BitcoinTestFramework):
     5         self.generate(self.nodes[0], 6)
     6 
     7         utxos = [utxo for utxo in def_wallet.listunspent() if utxo["desc"].startswith("tr")]
     8-        psbt = def_wallet.walletcreatefundedpsbt(utxos, [{def_wallet.getnewaddress(): 0.5}])["psbt"]
     9+        unsigned_psbt = def_wallet.walletcreatefundedpsbt(utxos, [{def_wallet.getnewaddress(): 0.5}])["psbt"]
    10         descs = [desc for desc in def_wallet.listdescriptors(True)["descriptors"] if desc["desc"].startswith("tr")]
    11 
    12         # unload wallet so as to not use the wallet RPC in further process PSBT calls
    13         def_wallet.unloadwallet()
    14 
    15-        desc_psbt = self.nodes[0].descriptorprocesspsbt(psbt=psbt, descriptors=descs, finalize=True)["psbt"]
    16-        flawed_psbt = PSBT.from_base64(desc_psbt)
    17+        result = self.nodes[0].descriptorprocesspsbt(psbt=unsigned_psbt, descriptors=descs, finalize=True)
    18+        assert_equal(result["complete"], True)
    19+        assert_equal("hex" in result, True)
    20+
    21+        valid_sig_psbt = result["psbt"]
    22+        flawed_psbt = PSBT.from_base64(valid_sig_psbt)
    23         valid_sig = flawed_psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS)
    24         invalid_sig = bitflipper(valid_sig)
    25         flawed_psbt.i[0].map[PSBT_IN_FINAL_SCRIPTWITNESS] = invalid_sig
    26-        flawed_psbt = flawed_psbt.to_base64()
    27+        invalid_sig_psbt = flawed_psbt.to_base64()
    28 
    29-        result = self.nodes[0].descriptorprocesspsbt(psbt=flawed_psbt, descriptors=descs)
    30+        result = self.nodes[0].descriptorprocesspsbt(psbt=invalid_sig_psbt, descriptors=descs, finalize=True)
    31         assert_equal(result["complete"], False)
    32         assert_equal("hex" in result, False)
    33 
    
  18. rkrux approved
  19. rkrux commented at 10:24 am on December 10, 2025: contributor

    lgtm ACK ac99af4f80191caa24badcfbb5cca6811fc92cf1

    Thanks for accepting the suggestion adding the functional test.

  20. DrahtBot added the label CI failed on Dec 15, 2025
  21. DrahtBot commented at 6:04 pm on December 15, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/20010940750/job/58081580703 LLM reason (✨ experimental): Lint check failed due to trailing whitespace in the codebase.

    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.

  22. DrahtBot commented at 9:34 am on December 22, 2025: contributor
    Could turn into draft while CI is red? @b-l-u-e Are you still working on this?
  23. b-l-u-e commented at 12:36 pm on December 22, 2025: contributor

    Could turn into draft while CI is red?

    @b-l-u-e Are you still working on this?

    yes still workin on it

  24. b-l-u-e force-pushed on Dec 22, 2025
  25. b-l-u-e requested review from rkrux on Dec 22, 2025
  26. in test/functional/rpc_psbt.py:141 in 7e468a6fb0 outdated
    137@@ -136,7 +138,7 @@ def test_utxo_conversion(self):
    138 
    139         # Have the offline node sign the PSBT (which will remove the non-witness UTXO)
    140         signed_psbt = offline_node.walletprocesspsbt(psbt_new.to_base64())
    141-        assert not "non_witness_utxo" in mining_node.decodepsbt(signed_psbt["psbt"])["inputs"][0]
    142+        assert "non_witness_utxo" not in mining_node.decodepsbt(signed_psbt["psbt"])["inputs"][0]
    


    rkrux commented at 1:10 pm on December 22, 2025:
    These changes seem unrelated to this fix, it’s usually preferred to avoid them but okay because it’s more readable this way and the test passes.

    b-l-u-e commented at 4:19 pm on December 24, 2025:
    These were auto-fixed by the linter “ruff check –fix” which flagged them as style issues (E713). I kept them since they improve readability, but I can revert to keep the PR focused only on the bug fix.

    maflcko commented at 7:32 am on December 26, 2025:
    i guess it could make sense to enable this globally, so i’ve done that in #34154. Mixing unrelated refactoring cleanup with a bugfix in the same commit often makes review harder.
  27. rkrux approved
  28. rkrux commented at 1:17 pm on December 22, 2025: contributor

    code review re-ACK 7e468a6

    0git range-diff ac99af4...7e468a6
    

    Thanks for accepting the nit suggestion.

  29. in test/functional/rpc_psbt.py:425 in 7e468a6fb0
    420+        unsigned_psbt = def_wallet.walletcreatefundedpsbt(utxos, [{def_wallet.getnewaddress(): 0.5}])["psbt"]
    421+        descs = [desc for desc in def_wallet.listdescriptors(True)["descriptors"] if desc["desc"].startswith("tr")]
    422+
    423+        # unload wallet so as to not use the wallet RPC in further process PSBT calls
    424+        def_wallet.unloadwallet()
    425+        
    


    achow101 commented at 0:23 am on December 24, 2025:

    In 7e468a6fb01b36edb6955ccb2b869ec1063a891c “rpc: Fix descriptorprocesspsbt internal bug on invalid signatures”

    Extra whitespace on this line

  30. in test/functional/rpc_psbt.py:429 in 7e468a6fb0
    424+        def_wallet.unloadwallet()
    425+        
    426+        result = self.nodes[0].descriptorprocesspsbt(psbt=unsigned_psbt, descriptors=descs, finalize=True)
    427+        assert_equal(result["complete"], True)
    428+        assert_equal("hex" in result, True)
    429+        
    


    achow101 commented at 0:23 am on December 24, 2025:

    In 7e468a6fb01b36edb6955ccb2b869ec1063a891c “rpc: Fix descriptorprocesspsbt internal bug on invalid signatures”

    Extra whitespace on this line

  31. in src/rpc/rawtransaction.cpp:2051 in 7e468a6fb0
    2047@@ -2048,10 +2048,12 @@ RPCHelpMan descriptorprocesspsbt()
    2048         sighash_type,
    2049         finalize);
    2050 
    2051-    // Check whether or not all of the inputs are now signed
    2052+    // Check whether or not all of the inputs are now correctly signed 
    


    achow101 commented at 0:23 am on December 24, 2025:

    In 7e468a6fb01b36edb6955ccb2b869ec1063a891c “rpc: Fix descriptorprocesspsbt internal bug on invalid signatures”

    Trailing whitespace on this line

  32. in src/rpc/rawtransaction.cpp:2056 in 7e468a6fb0
    2054-    for (const auto& input : psbtx.inputs) {
    2055-        complete &= PSBTInputSigned(input);
    2056+    const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx);
    2057+    for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) {
    2058+        const bool verified = PSBTInputSignedAndVerified(psbtx, i, &txdata);
    2059+        complete = complete && verified; 
    


    achow101 commented at 0:23 am on December 24, 2025:

    In 7e468a6fb01b36edb6955ccb2b869ec1063a891c “rpc: Fix descriptorprocesspsbt internal bug on invalid signatures”

    Trailing whitespace on this line


    b-l-u-e commented at 2:29 pm on January 5, 2026:
    thank you for your feedback
  33. b-l-u-e force-pushed on Dec 24, 2025
  34. b-l-u-e requested review from rkrux on Dec 24, 2025
  35. b-l-u-e requested review from achow101 on Dec 24, 2025
  36. rkrux commented at 8:55 am on December 26, 2025: contributor

    re-ACK 222139e

    0git range-diff 7e468a6...222139e
    
  37. b-l-u-e force-pushed on Dec 27, 2025
  38. DrahtBot removed the label CI failed on Dec 31, 2025
  39. rkrux approved
  40. rkrux commented at 12:51 pm on January 5, 2026: contributor

    re-ACK 9f55be650e6181f7d3ea898e3ccfafedecc26869

    Removed the unrelated lint changes because PR #34154 is merged now.

    0git range-diff 222139e...9f55be6
    

    Please resolve the PR comments that have been addressed to make it easier to go through the PR.

  41. b-l-u-e commented at 2:45 pm on January 5, 2026: contributor

    re-ACK 9f55be6

    Removed the unrelated lint changes because PR #34154 is merged now.

    0git range-diff 222139e...9f55be6
    

    Please resolve the PR comments that have been addressed to make it easier to go through the PR.

    all the PR comments have been resolved

  42. b-l-u-e force-pushed on Jan 5, 2026
  43. b-l-u-e requested review from rkrux on Jan 5, 2026
  44. b-l-u-e requested review from maflcko on Jan 5, 2026
  45. rkrux commented at 3:11 pm on January 5, 2026: contributor
    0git range-diff 9f55be6...e995c85
    

    I don’t see any changes in the latest range-diff, why was this latest push done after this #33014#pullrequestreview-3626721928?

  46. b-l-u-e commented at 3:16 pm on January 5, 2026: contributor
    0git range-diff 9f55be6...e995c85
    

    I don’t see any changes in the latest range-diff, why was this latest push done after this #33014 (review)?

    the review my bad i was suppose to comment after i rebased the branch that i resolved the comments the trailing spaces was sorted in https://github.com/bitcoin/bitcoin/commit/9f55be650e6181f7d3ea898e3ccfafedecc26869

  47. DrahtBot added the label CI failed on Jan 5, 2026
  48. in test/functional/rpc_psbt.py:446 in e995c85c4e outdated
    432+        valid_sig = flawed_psbt.i[0].map.get(PSBT_IN_FINAL_SCRIPTWITNESS)
    433+        invalid_sig = bitflipper(valid_sig)
    434+        flawed_psbt.i[0].map[PSBT_IN_FINAL_SCRIPTWITNESS] = invalid_sig
    435+        invalid_sig_psbt = flawed_psbt.to_base64()
    436+
    437+        result = self.nodes[0].descriptorprocesspsbt(psbt=invalid_sig_psbt, descriptors=descs, finalize=True)
    


    DrahtBot commented at 7:05 am on January 6, 2026:
     0                                      File "/home/admin/actions-runner/_work/_temp/build/test/functional/rpc_psbt.py", line 1287, in run_test
     1                                       self.test_psbt_with_invalid_signature()
     2                                     File "/home/admin/actions-runner/_work/_temp/build/test/functional/rpc_psbt.py", line 437, in test_psbt_with_invalid_signature
     3                                       result = self.nodes[0].descriptorprocesspsbt(psbt=invalid_sig_psbt, descriptors=descs, finalize=True)
     4                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     5                                     File "/home/admin/actions-runner/_work/_temp/test/functional/test_framework/test_node.py", line 917, in __call__
     6                                       return self.cli.send_cli(self.command, *args, **kwargs)
     7                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     8                                     File "/home/admin/actions-runner/_work/_temp/test/functional/test_framework/test_node.py", line 1000, in send_cli
     9                                       raise JSONRPCException(dict(code=int(code), message=message))
    10                                   test_framework.authproxy.JSONRPCException: TX decode failed DataStream::read(): end of data: iostream error (-22)
    
  49. b-l-u-e force-pushed on Jan 10, 2026
  50. DrahtBot removed the label CI failed on Jan 11, 2026
  51. in test/functional/rpc_psbt.py:438 in 5a04a06f1f
    433+        # The witness format is [num_items: CompactSize] [item1_len: CompactSize] [item1_data] ...
    434+        # For taproot key-path spend [0x01] [0x40 or 0x41] [64 or 65 byte signature]
    435+        # We need to flip a bit only in the signature, not in the length prefixes
    436+        # Skip first 2 bytes (num_items=0x01, sig_len=0x40/0x41) and flip a bit in the signature
    437+        prefix = valid_witness[:2]
    438+        signature = valid_witness[2:]
    


    achow101 commented at 11:02 pm on January 19, 2026:

    This potentially includes the sighash flag, and we shouldn’t flip bits in that sighash flag as that does not guarantee that signature will be invalidated. So this slice should be bounded:

    0        signature = valid_witness[2:66]
    
  52. achow101 commented at 11:03 pm on January 19, 2026: member
    ACK 5a04a06f1f782b7d19a35b44cc8fe81f2b4d4c55
  53. b-l-u-e force-pushed on Jan 20, 2026
  54. rkrux commented at 2:09 pm on January 20, 2026: contributor

    ISTM that the Windows, ucrt, test cross-built CI failure is unrelated, a restore wallet test case related to pruned blockchain in wallet_assumeutxo.py is failing.

     02026-01-20T13:41:07.2385503Z 2026-01-20T13:41:04.152353Z TestFramework (INFO): Ensuring wallet can't be restored from a backup that was created before the pruneheight (pruned node)
     12026-01-20T13:41:07.2386535Z 
     22026-01-20T13:41:07.2386839Z 2026-01-20T13:41:06.479889Z TestFramework (ERROR): Unexpected exception
     32026-01-20T13:41:07.2387306Z 
     42026-01-20T13:41:07.2387503Z Traceback (most recent call last):
     52026-01-20T13:41:07.2387815Z 
     62026-01-20T13:41:07.2388269Z   File "D:\a\bitcoin\bitcoin\test\functional\test_framework\util.py", line 166, in try_rpc
     72026-01-20T13:41:07.2388901Z 
     82026-01-20T13:41:07.2389049Z     fun(*args, **kwds)
     92026-01-20T13:41:07.2389318Z 
    102026-01-20T13:41:07.2389432Z     ~~~^^^^^^^^^^^^^^^
    112026-01-20T13:41:07.2389660Z 
    122026-01-20T13:41:07.2390180Z   File "D:\a\bitcoin\bitcoin\test\functional\test_framework\coverage.py", line 50, in __call__
    132026-01-20T13:41:07.2390808Z 
    142026-01-20T13:41:07.2391156Z     return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
    152026-01-20T13:41:07.2391651Z 
    162026-01-20T13:41:07.2392093Z   File "D:\a\bitcoin\bitcoin\test\functional\test_framework\authproxy.py", line 156, in __call__
    172026-01-20T13:41:07.2392771Z 
    182026-01-20T13:41:07.2392996Z     raise JSONRPCException(response['error'], status)
    192026-01-20T13:41:07.2393424Z 
    202026-01-20T13:41:07.2395532Z test_framework.authproxy.JSONRPCException: filesystem error: cannot remove: The process cannot access the file because it is being used by another process [D:\a\_temp\test_runner_₿_🏃_20260120_132914\wallet_assumeutxo_56\node3\regtest\w2\wallet.dat] (-1)
    212026-01-20T13:41:07.2397271Z 
    222026-01-20T13:41:07.2397277Z 
    232026-01-20T13:41:07.2397281Z 
    242026-01-20T13:41:07.2397613Z During handling of the above exception, another exception occurred:
    252026-01-20T13:41:07.2398102Z 
    262026-01-20T13:41:07.2398107Z 
    272026-01-20T13:41:07.2398112Z 
    282026-01-20T13:41:07.2398334Z Traceback (most recent call last):
    292026-01-20T13:41:07.2398622Z 
    302026-01-20T13:41:07.2399135Z   File "D:\a\bitcoin\bitcoin\test\functional\test_framework\test_framework.py", line 142, in main
    312026-01-20T13:41:07.2399796Z 
    322026-01-20T13:41:07.2399922Z     self.run_test()
    332026-01-20T13:41:07.2400161Z 
    342026-01-20T13:41:07.2400286Z     ~~~~~~~~~~~~~^^
    352026-01-20T13:41:07.2400474Z 
    362026-01-20T13:41:07.2400948Z   File "D:\a\bitcoin\bitcoin/test/functional/wallet_assumeutxo.py", line 267, in run_test
    372026-01-20T13:41:07.2401587Z 
    382026-01-20T13:41:07.2401827Z     self.test_restore_wallet_pruneheight(n3)
    392026-01-20T13:41:07.2402338Z 
    402026-01-20T13:41:07.2402476Z     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^
    412026-01-20T13:41:07.2402806Z 
    422026-01-20T13:41:07.2403384Z   File "D:\a\bitcoin\bitcoin/test/functional/wallet_assumeutxo.py", line 99, in test_restore_wallet_pruneheight
    432026-01-20T13:41:07.2404151Z 
    442026-01-20T13:41:07.2404691Z     assert_raises_rpc_error(-4, error_message, n3.restorewallet, "w2", "backup_w2.dat")
    452026-01-20T13:41:07.2405294Z 
    462026-01-20T13:41:07.2405553Z     ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    472026-01-20T13:41:07.2405970Z 
    482026-01-20T13:41:07.2406496Z   File "D:\a\bitcoin\bitcoin\test\functional\test_framework\util.py", line 157, in assert_raises_rpc_error
    492026-01-20T13:41:07.2407238Z 
    502026-01-20T13:41:07.2407544Z     assert try_rpc(code, message, fun, *args, **kwds), "No exception raised"
    512026-01-20T13:41:07.2408097Z 
    522026-01-20T13:41:07.2408261Z            ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    532026-01-20T13:41:07.2408598Z 
    542026-01-20T13:41:07.2409048Z   File "D:\a\bitcoin\bitcoin\test\functional\test_framework\util.py", line 170, in try_rpc
    552026-01-20T13:41:07.2409645Z 
    562026-01-20T13:41:07.2410041Z     raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
    572026-01-20T13:41:07.2410583Z 
    582026-01-20T13:41:07.2410797Z AssertionError: Unexpected JSONRPC error code -1
    592026-01-20T13:41:07.2411215Z 
    602026-01-20T13:41:07.2411925Z 2026-01-20T13:41:06.484138Z TestFramework (INFO): Not stopping nodes as test failed. The dangling processes will be cleaned up later.
    612026-01-20T13:41:07.2412795Z 
    622026-01-20T13:41:07.2413676Z 2026-01-20T13:41:06.484270Z TestFramework (WARNING): Not cleaning up dir D:\a\_temp\test_runner_₿_🏃_20260120_132914\wallet_assumeutxo_56
    632026-01-20T13:41:07.2414525Z 
    642026-01-20T13:41:07.2415646Z 2026-01-20T13:41:06.484375Z TestFramework (ERROR): Test failed. Test logging available at D:\a\_temp\test_runner_₿_🏃_20260120_132914\wallet_assumeutxo_56/test_framework.log
    652026-01-20T13:41:07.2416737Z 
    66
    672026-01-20T13:42:33.3464929Z wallet_assumeutxo.py                                                             | ✖ Failed  | 8 s
    682026-01-20T13:42:33.3465209Z 
    692026-01-20T13:42:33.3465460Z ALL                                                                              | ✖ Failed  | 2893 s (accumulated) 
    702026-01-20T13:42:33.3465784Z Runtime: 796 s
    
  55. DrahtBot added the label CI failed on Jan 20, 2026
  56. rkrux commented at 1:37 pm on January 22, 2026: contributor
    The #34354 issue that caused the CI to fail is fixed, can we re-run the CI on this PR?
  57. DrahtBot removed the label CI failed on Jan 22, 2026
  58. in test/functional/rpc_psbt.py:417 in f25db06d06
    412+        self.log.info("Test descriptorprocesspsbt with invalid signature in signed PSBT")
    413+
    414+        def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    415+        outputs = [{def_wallet.getnewaddress(address_type="bech32m"): 1}]
    416+        def_wallet.send(outputs)
    417+        self.generate(self.nodes[0], 6)
    


    rkrux commented at 11:45 am on January 23, 2026:

    I realise that generating 1 block should be enough for this case.

    0@@ -414,7 +414,7 @@ class PSBTTest(BitcoinTestFramework):
    1         def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    2         outputs = [{def_wallet.getnewaddress(address_type="bech32m"): 1}]
    3         def_wallet.send(outputs)
    4-        self.generate(self.nodes[0], 6)
    5+        self.generate(self.nodes[0], 1)
    
  59. in test/functional/rpc_psbt.py:440 in f25db06d06
    435+        # We need to flip a bit only in the signature, not in the length prefixes
    436+        # Skip first 2 bytes (num_items=0x01, sig_len=0x40/0x41) and flip a bit in the signature
    437+        prefix = valid_witness[:2]
    438+        signature = valid_witness[2:66]
    439+        invalid_sig = bitflipper(signature)
    440+        invalid_witness = prefix + invalid_sig
    


    rkrux commented at 11:49 am on January 23, 2026:

    Thanks for adding this verbose comment, it’s helpful.

    Agree with bounding it to :66, this works in both the cases of the signature being 64 or 65 bytes. I don’t think we should lose the remaining signature portion though while coming up with the invalid witness. So, I feel the following diff can be added:

     0@@ -434,10 +434,9 @@ class PSBTTest(BitcoinTestFramework):
     1         # For taproot key-path spend [0x01] [0x40 or 0x41] [64 or 65 byte signature]
     2         # We need to flip a bit only in the signature, not in the length prefixes
     3         # Skip first 2 bytes (num_items=0x01, sig_len=0x40/0x41) and flip a bit in the signature
     4-        prefix = valid_witness[:2]
     5-        signature = valid_witness[2:66]
     6-        invalid_sig = bitflipper(signature)
     7-        invalid_witness = prefix + invalid_sig
     8+        signature_bytes = valid_witness[2:66]
     9+        invalid_sig_bytes = bitflipper(signature_bytes)
    10+        invalid_witness = valid_witness[:2] + invalid_sig_bytes + valid_witness[66:]
    11         flawed_psbt.i[0].map[PSBT_IN_FINAL_SCRIPTWITNESS] = invalid_witness
    12         invalid_sig_psbt = flawed_psbt.to_base64()
    

    b-l-u-e commented at 2:10 pm on January 23, 2026:

    just realized that what happens if signature is 65 bytes because if we slicing at byte 66 is when we assume the signature is 64 bytes only.. i feel like this code below might work for both 64 and 65

     0@@ -435,9 +435,10 @@ class PSBTTest(BitcoinTestFramework):
     1         # We need to flip a bit only in the signature, not in the length prefixes
     2         # Skip first 2 bytes (num_items=0x01, sig_len=0x40/0x41) and flip a bit in the signature
     3         prefix = valid_witness[:2]
     4-        signature = valid_witness[2:66]
     5+        sig_len = valid_witness[1]
     6+        signature = valid_witness[2:2+sig_len]
     7         invalid_sig = bitflipper(signature)
     8-        invalid_witness = prefix + invalid_sig
     9+        invalid_witness = prefix + invalid_sig + valid_witness[2+sig_len:]
    10         flawed_psbt.i[0].map[PSBT_IN_FINAL_SCRIPTWITNESS] = invalid_witness
    11         invalid_sig_psbt = flawed_psbt.to_base64() 
    

    rkrux commented at 3:03 pm on January 23, 2026:

    The present code should work too for a sig with length 65 bytes. Only one bit needs to be flipped to make it invalid, doesn’t matter if it’s done in full sig (65) or partial sig (64). But yes, the above^ suggestion is more expressive and thorough, and can be implemented.

    Might as well add an assert for keypath spend, we already assume it as per the code comments.

    0@@ -434,6 +434,7 @@ class PSBTTest(BitcoinTestFramework):
    1         # For taproot key-path spend [0x01] [0x40 or 0x41] [64 or 65 byte signature]
    2         # We need to flip a bit only in the signature, not in the length prefixes
    3         # Skip first 2 bytes (num_items=0x01, sig_len=0x40/0x41) and flip a bit in the signature
    4+        assert_equal(valid_witness[0], 1) # keypath spend
    5         prefix = valid_witness[:2]
    6         signature = valid_witness[2:66]
    7         invalid_sig = bitflipper(signature)
    8(END)
    
  60. rkrux commented at 11:50 am on January 23, 2026: contributor
    +1 on fixing the error #33014 (review) highlighted by Drahbot, I didn’t think of this scenario earlier.
  61. rpc: Fix descriptorprocesspsbt internal bug on invalid signatures
    Use PSBTInputSignedAndVerified instead of PSBTInputSigned to properly
    validate signatures before marking PSBT as complete.
    
    Signed-off-by: b-l-u-e <winnie.gitau282@gmail.com>
    2fd79d85ef
  62. b-l-u-e force-pushed on Jan 23, 2026
  63. b-l-u-e requested review from rkrux on Jan 23, 2026
  64. rkrux approved
  65. rkrux commented at 8:27 am on January 30, 2026: contributor

    ACK 2fd79d8 Thanks for following up on all the previous comments.

    0git range-diff 5a04a06...2fd79d8
    
  66. DrahtBot requested review from achow101 on Jan 30, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-02-01 15:13 UTC

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