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-
b-l-u-e commented at 5:15 am on July 19, 2025: contributorFixes #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.
-
DrahtBot added the label RPC/REST/ZMQ on Jul 19, 2025
-
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.
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.
-
achow101 commented at 10:47 pm on July 22, 2025: member
The
CHECK_NONFATALis correct.complete == truemeans that the PSBT is complete and can be finalized. The invariant here is thatcomplete == truemeansFinalizeAndExtractmust 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
PSBTInputSignedAndVerifiedinstead. It may even be worth it to combinePSBTInputSignedwithPSBTInputSignedAndVerifiedand replacingPSBTInputSignedwherever it is used. Although that may not be possible to do. -
b-l-u-e force-pushed on Jul 24, 2025
-
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.
-
b-l-u-e force-pushed on Jul 24, 2025
-
achow101 requested review from achow101 on Oct 22, 2025
-
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 circuitsin 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 signedrkrux commented at 1:39 pm on October 24, 2025: contributorI don’t think the corresponding bug is still present after v30, see: #32849 (comment)
Also, a test case in
rpc_psbt.pywould be good to verify the fix.rkrux commented at 11:50 am on October 25, 2025: contributorConcept 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
bitflipperfunction 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 to0x42always 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 fileb-l-u-e force-pushed on Dec 7, 2025b-l-u-e requested review from l0rinc on Dec 7, 2025b-l-u-e requested review from rkrux on Dec 7, 2025in 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) 33rkrux approvedrkrux commented at 10:24 am on December 10, 2025: contributorlgtm ACK ac99af4f80191caa24badcfbb5cca6811fc92cf1
Thanks for accepting the suggestion adding the functional test.
DrahtBot added the label CI failed on Dec 15, 2025DrahtBot 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.
b-l-u-e force-pushed on Dec 22, 2025b-l-u-e requested review from rkrux on Dec 22, 2025in 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.
rkrux approvedrkrux commented at 1:17 pm on December 22, 2025: contributorcode review re-ACK 7e468a6
0git range-diff ac99af4...7e468a6Thanks for accepting the nit suggestion.
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
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
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
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 feedbackb-l-u-e force-pushed on Dec 24, 2025b-l-u-e requested review from rkrux on Dec 24, 2025b-l-u-e requested review from achow101 on Dec 24, 2025rkrux commented at 8:55 am on December 26, 2025: contributorre-ACK 222139e
0git range-diff 7e468a6...222139eb-l-u-e force-pushed on Dec 27, 2025DrahtBot removed the label CI failed on Dec 31, 2025rkrux approvedrkrux commented at 12:51 pm on January 5, 2026: contributorre-ACK 9f55be650e6181f7d3ea898e3ccfafedecc26869
Removed the unrelated lint changes because PR #34154 is merged now.
0git range-diff 222139e...9f55be6Please resolve the PR comments that have been addressed to make it easier to go through the PR.
b-l-u-e commented at 2:45 pm on January 5, 2026: contributorb-l-u-e force-pushed on Jan 5, 2026b-l-u-e requested review from rkrux on Jan 5, 2026b-l-u-e requested review from maflcko on Jan 5, 2026b-l-u-e commented at 3:16 pm on January 5, 2026: contributor0git range-diff 9f55be6...e995c85I 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
DrahtBot added the label CI failed on Jan 5, 2026in 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)b-l-u-e force-pushed on Jan 10, 2026DrahtBot removed the label CI failed on Jan 11, 2026in 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]achow101 commented at 11:03 pm on January 19, 2026: memberACK 5a04a06f1f782b7d19a35b44cc8fe81f2b4d4c55b-l-u-e force-pushed on Jan 20, 2026rkrux commented at 2:09 pm on January 20, 2026: contributorISTM that the Windows, ucrt, test cross-built CI failure is unrelated, a restore wallet test case related to pruned blockchain in
wallet_assumeutxo.pyis 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 sDrahtBot added the label CI failed on Jan 20, 2026DrahtBot removed the label CI failed on Jan 22, 2026in 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)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
keypathspend, 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)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.2fd79d85efrpc: 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>
b-l-u-e force-pushed on Jan 23, 2026b-l-u-e requested review from rkrux on Jan 23, 2026rkrux approvedrkrux commented at 8:27 am on January 30, 2026: contributorACK 2fd79d8 Thanks for following up on all the previous comments.
0git range-diff 5a04a06...2fd79d8DrahtBot 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