wallet: allow skipping script paths #32857

pull Sjors wants to merge 8 commits into bitcoin:master from Sjors:2025/07/no_script_path changing 12 files +201 −26
  1. Sjors commented at 2:13 PM on July 2, 2025: member

    While testing MuSig2 integration in #29675 I ran into a Ledger bug https://github.com/LedgerHQ/app-bitcoin-new/issues/329 that was triggered because we signed for a script path in addition to the key path. The bug itself will be fixed, but it seems useful in general to be able to skip script path signing.

    For example, you may have a 2-of-2 musig() descriptor with some sort of fallback after coins are N blocks old. For both on chain privacy and fee reasons, you don't want to use this path unless absolutely necessary. But our wallet can't know if our co-signer still has their key, so we sign the script path at all times. This could lead to accidental broadcast.

    This PR introduces a keypath_only option (default false for now) to the following RPC methods:

    • send
    • sendall
    • walletprocesspsbt
    • descriptorprocesspsbt

    The resulting PSBT won't add taproot_scripts or taproot_script_path_sigs. It does still have taproot_bip32_derivs and musig2_participant_pubkeys.

    It will not remove taproot_scripts from an existing PSBT, but it won't add a corresponding entry to taproot_script_path_sigs.

    The wallet_taproot.py tests are expanded to cover this behavior.

    It can also be tested with https://github.com/Sjors/bitcoin/pull/91.

  2. DrahtBot added the label Wallet on Jul 2, 2025
  3. DrahtBot commented at 2:13 PM on July 2, 2025: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK rkrux, w0xlt

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35370 (rpc: add stripbip32derivs to PSBT processing RPCs by junbyjun1238)
    • #34697 (descriptor: fix musig duplicate checks and origin handling by shuv-amp)
    • #32958 (wallet/refactor: Update SignPSBTInput to return util::Expected<void, PSBTError> and remove PSBTError:Ok by kevkevinpal)

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

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. DrahtBot added the label CI failed on Jul 2, 2025
  5. DrahtBot commented at 3:53 PM on July 2, 2025: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task ARM, unit tests, no functional tests: https://github.com/bitcoin/bitcoin/runs/45219351756</sub> <sub>LLM reason (✨ experimental): Compilation error due to passing a bool where an int* is expected in fillPSBT function.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  6. fanquake commented at 4:04 PM on July 2, 2025: member
  7. Sjors commented at 4:35 PM on July 2, 2025: member

    @fanquake because it's in the fuzzer code, I incorrectly changed one of the function signatures there.

  8. Sjors force-pushed on Jul 2, 2025
  9. Sjors commented at 4:53 PM on July 2, 2025: member

    From the original description:

    Under the hood it adds m_hide_script_paths to the HidingSigningProvider which triggers an early return in GetTaprootSpendData() when set.

    It's also missing taproot_scripts which might be a bit too much. At least in initial testing Ledger wouldn't sign it.

    I probably need to implement this at the SigningProvider level rather than HidingSigningProvider. The early return needs to be inside SignTaproot() right before // Try script path spending rather than in GetTaprootSpendData(). The goal isn't to hide leaf scripts from co-signers, only to make sure we don't sign them ourselves.

  10. Sjors force-pushed on Jul 2, 2025
  11. Sjors commented at 6:31 PM on July 2, 2025: member

    Shoehorning this into SigningProvider got ugly real fast. So instead I endup up passing avoid_script_path along the call stack from SignPSBTInput to SignTaproot. Which is also ugly...

    Well, it compiles and works. Thoughts on how to implement this elegantly?

  12. Sjors force-pushed on Jul 3, 2025
  13. Sjors commented at 12:48 PM on July 3, 2025: member

    The walletprocesspsbt RPC now also has this option. Added test coverage by expanding wallet_taproot.py.

  14. Sjors force-pushed on Jul 3, 2025
  15. Sjors commented at 2:01 PM on July 3, 2025: member

    I split off a non-behavior-changing commit that adds avoid_script_path to FillPSBT. I think it makes sense to pass this option here, although in general we may want to refactor FillPSBT to take an options struct instead of this ever expanding argument list. That could be an independent or prerequisite PR.

    I then did the same for SignPSBTInput, where I also think struct would be better.

    Rinse and repeat for ::SignTransaction.

    Will continue later to further split 09334ae0bd7939ed7476eb2b36f3bf8085885fe7. It might make sense to add a property to BaseSignatureCreator or it subclasses?

  16. DrahtBot removed the label CI failed on Jul 3, 2025
  17. Sjors referenced this in commit ea493972d2 on Jul 4, 2025
  18. Sjors referenced this in commit edbb5d8df3 on Jul 4, 2025
  19. Sjors referenced this in commit 2829cb78c5 on Jul 4, 2025
  20. Sjors force-pushed on Jul 4, 2025
  21. Sjors commented at 1:45 PM on July 4, 2025: member

    I opened #32876 and rebased this PR on top of it. That allowed me to drop 1486f05c8eded02d72690bf34d983a03e4ad748b + efc34a514b59cc1bab6e4d0f80c197481561c429.

  22. Sjors force-pushed on Jul 4, 2025
  23. Sjors commented at 4:02 PM on July 4, 2025: member

    I expanded #32876 with SignOptions. This shrinks 93fb607af3141ff1e22693588f498764401fc2c4 to just e2a151c3dc35997ac0f515feb292fc4543093593 which is very small ... but very ugly: casting BaseSignatureCreator to MutableTransactionSignatureCreator to access its avoid_script_path option.

  24. Sjors commented at 7:57 AM on July 7, 2025: member

    This may also be useful for Silent Payments, cc @josibake.

  25. Sjors commented at 12:59 PM on July 7, 2025: member

    Sidenote: currently it's unlikely you'll accidentally spend via an older() or after() script path. This is because you need to manually set the input sequence field in the send (etc.) RPC (and manually adjust the fee rate). Found out the hard way while testing 🤦 (there's currently no useful feedback when this happens).

  26. DrahtBot added the label Needs rebase on Jul 7, 2025
  27. Sjors referenced this in commit 2923087479 on Jul 8, 2025
  28. Sjors force-pushed on Jul 8, 2025
  29. DrahtBot removed the label Needs rebase on Jul 8, 2025
  30. Sjors referenced this in commit 576da4c7dd on Jul 8, 2025
  31. Sjors force-pushed on Jul 8, 2025
  32. Sjors referenced this in commit 8660b55c88 on Jul 11, 2025
  33. Sjors force-pushed on Jul 14, 2025
  34. DrahtBot added the label Needs rebase on Jul 29, 2025
  35. Sjors force-pushed on Jul 29, 2025
  36. Sjors referenced this in commit 8c268ff1d7 on Jul 29, 2025
  37. DrahtBot removed the label Needs rebase on Jul 29, 2025
  38. Sjors referenced this in commit 6703cc7743 on Aug 1, 2025
  39. DrahtBot added the label Needs rebase on Aug 19, 2025
  40. Sjors commented at 11:51 AM on August 25, 2025: member

    Holding off on rebasing until #32876 is merged or I have to touch it.

  41. rkrux commented at 12:39 PM on October 6, 2025: contributor

    Sidenote: currently it's unlikely you'll accidentally spend via an older() or after() script path. This is because you need to manually set the input sequence field in the send (etc.) RPC (and manually adjust the fee rate). Found out the hard way while testing 🤦 (there's currently no useful feedback when this happens).

    Scenario that I have not tested but expect to behave in this way: 3 signers (A, B, C) form a MuSig (ABC) in the keypath and also have 3 non-Musig multi-sig script paths (AB, BC, AC) without any timelocks. They intend to MuSig sign in order from A to C to spend via keypath and start the signing flow first by producing nonces in the mentioned order, and then by adding partial signatures. By the time B adds its nonce, there'd would be a fully signed spendable script path? If yes, seems unusual to have this when the intent of the signers was for keypath spending, which makes a good case for this PR.

    I did notice unconditional key and script path signing within SignTaproot function when I was reviewing #29675. We tried to simulate an intentional script path spending like above in #29675 by making A a no-signing wallet in the last test case in functional tests: https://github.com/bitcoin/bitcoin/pull/29675/commits/ac599c4a9cb3b2d424932d3fd91f9eed17426827#diff-c94f9555a2569240d362a00a82764f4714f6c65283e3f96725cf2a1043a296b5R238. Though the script path in that case is using MuSig but the overall unconditional script signing principle is still applicable.

  42. rkrux commented at 12:42 PM on October 6, 2025: contributor

    If #32876 is not garnering reviews, can consider undrafting this PR itself because it contains 7 commits only. Allowing skipping script path spending optionally seems reasonable to me and a ready-to-review PR is more inviting than a draft one.

  43. Sjors commented at 8:10 AM on October 7, 2025: member

    @rkrux I'm not too worried about reviews just yet, let's focus on getting #29675 in.

  44. Sjors referenced this in commit 4c11b25943 on Oct 16, 2025
  45. Sjors force-pushed on Oct 16, 2025
  46. DrahtBot removed the label Needs rebase on Oct 16, 2025
  47. Sjors referenced this in commit a2a20b97ca on Nov 4, 2025
  48. Sjors force-pushed on Nov 4, 2025
  49. DrahtBot added the label Needs rebase on Nov 18, 2025
  50. Sjors referenced this in commit 643e6dc59a on Jan 5, 2026
  51. Sjors force-pushed on Jan 5, 2026
  52. DrahtBot removed the label Needs rebase on Jan 5, 2026
  53. Sjors referenced this in commit e4a1397fea on Jan 22, 2026
  54. Sjors force-pushed on Jan 22, 2026
  55. DrahtBot added the label CI failed on Jan 22, 2026
  56. DrahtBot added the label Needs rebase on Mar 25, 2026
  57. Sjors referenced this in commit 66ce046ab6 on Mar 26, 2026
  58. Sjors force-pushed on Mar 26, 2026
  59. DrahtBot removed the label CI failed on Mar 26, 2026
  60. DrahtBot removed the label Needs rebase on Mar 26, 2026
  61. DrahtBot added the label Needs rebase on Apr 19, 2026
  62. Sjors referenced this in commit b3788bf2fc on Apr 22, 2026
  63. Sjors force-pushed on Apr 22, 2026
  64. DrahtBot added the label CI failed on Apr 22, 2026
  65. DrahtBot removed the label Needs rebase on Apr 23, 2026
  66. Sjors referenced this in commit dc4a5d1270 on Apr 28, 2026
  67. sedited referenced this in commit cef58341a0 on Apr 29, 2026
  68. DrahtBot added the label Needs rebase on Apr 29, 2026
  69. Sjors force-pushed on Apr 30, 2026
  70. Sjors commented at 9:45 AM on April 30, 2026: member

    Rebased after #32876 landed. I need to check a few more things before marking this as ready for review.

  71. DrahtBot removed the label CI failed on Apr 30, 2026
  72. DrahtBot removed the label Needs rebase on Apr 30, 2026
  73. Sjors force-pushed on May 1, 2026
  74. Sjors force-pushed on May 15, 2026
  75. Sjors commented at 3:22 PM on May 15, 2026: member

    Rebased after #29136 landed.

    Added test coverage for the send RPC (ad364249baebe9f75f24e32358d1f1f514c98f1d -> 8bd15fd779f8ac43c15f32d8b52a6439ec738d9f)

    Redesigned ed89145afb12138b8e6664bc9acdda4772028115 wallet: add option to avoid script path spends to pass SignOptions through ProduceSignature into SignTaproot (9bfdfaea2dbd62b8f86be90df18fb9cd61bac60f + f77a74743ececdf98dff9188c19f13c725b21801).

    SignTaproot now also clears spenddata.scripts when avoid_script_path is set.

  76. Sjors marked this as ready for review on May 15, 2026
  77. sedited requested review from rkrux on Jun 7, 2026
  78. in src/rpc/client.cpp:218 in 52a5ace371 outdated
     213 | @@ -214,6 +214,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
     214 |      { "walletprocesspsbt", 2, "sighashtype", ParamFormat::STRING },
     215 |      { "walletprocesspsbt", 3, "bip32derivs" },
     216 |      { "walletprocesspsbt", 4, "finalize" },
     217 | +    { "walletprocesspsbt", 5, "keypath_only"},
     218 |      { "descriptorprocesspsbt", 0, "psbt", ParamFormat::STRING },
    


    rkrux commented at 1:25 PM on June 8, 2026:

    In 52a5ace3718dae474ce09c1e06aa26293a4e82dc "rpc: add keypath_only to walletprocesspsbt"

    descriptorprocesspsbt also does signing, it won't be far fetched to allow this option in that RPC too.


    Sjors commented at 3:17 PM on June 9, 2026:

    Done in 8d819f7d8e6e93b8132ae0e621ad39740a9493df.

  79. in src/rpc/client.cpp:272 in 8bd15fd779 outdated
     266 | @@ -267,6 +267,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
     267 |      { "send", 4, "replaceable"},
     268 |      { "send", 4, "solving_data"},
     269 |      { "send", 4, "max_tx_weight"},
     270 | +    { "send", 4, "keypath_only"},
     271 |      { "send", 5, "version"},
     272 |      { "sendall", 0, "recipients" },
    


    rkrux commented at 1:27 PM on June 8, 2026:

    In 8bd15fd779f8ac43c15f32d8b52a6439ec738d9f "rpc: add keypath_only to send"

    FinishTransaction is updated in this PR to have this option. It's used by send and sendall, so it seems reasonable to have this option in sendall too.


    rkrux commented at 1:32 PM on June 8, 2026:

    In 8bd15fd "rpc: add keypath_only to send"

    Along similar lines, sendmany and sendtoaddress also have similar expectations as send, which includes signing. Although it seems congruent to add this option in those RPCs too but it might involve a little bit more refactoring because of a slightly different flow taken by them. Though ultimately they end up calling SignTransaction that already has been updated in this PR.


    Sjors commented at 2:19 PM on June 8, 2026:

    I'll add sendall, but I'm inclined leave the older sendmany and sendtoaddress alone. They also can't be used with external signers.


    rkrux commented at 2:23 PM on June 8, 2026:

    Oh, I didn't know they were older.

    Seems fine to me to not update them in this PR, they don't use the common internal signing/processing functions as well.

  80. rkrux commented at 1:53 PM on June 8, 2026: contributor

    Concept ACK 8bd15fd779f8ac43c15f32d8b52a6439ec738d9f

    Seems like a useful feature to have, the example in the PR description is helpful for the motivation. Asked a couple questions around updating other similar RPCs.

  81. in test/functional/wallet_taproot.py:467 in fec2019bdb outdated
     464 |          self.do_test_addr(comment, pattern, privmap, treefn, keys[0:nkeys])
     465 |          self.do_test_sendtoaddress(comment, pattern, privmap, treefn, keys[0:nkeys], keys[nkeys:2*nkeys])
     466 | -        self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys])
     467 | +        self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys], avoid_script_path=False)
     468 | +        if 'tr' in pattern:
     469 | +            self.do_test_psbt(comment, pattern, privmap, treefn, keys[4*nkeys:5*nkeys], keys[5*nkeys:6*nkeys], avoid_script_path=True)
    


    rkrux commented at 2:15 PM on June 8, 2026:

    In fec2019bdb3f97482fba54a1a4540f9e4ec44efd "test: cover keypath_only in wallet_taproot.py"

    https://github.com/bitcoin/bitcoin/blob/5f33da9aa30ff80876f2f450b4370c446555bd71/test/functional/wallet_musig.py#L352

    There's a very interesting test case in wallet_musig.py here where we contrive it to not be able to sign the 3-key-musig-keypath because of nosign_wallets=[0]. A new test case can be added there as well by not passing the nosign option and by instead passing this new option to not sign the script path. This test automatically generates all the required signers and is quite extensible. It would be nice to test the MuSig flow in this PR as well as this use case has been given as an example in the PR description too.

    A diff like the below one works. I could not find a quick way to determine if the musig placeholders are within the script path so as to not count them in expected pubnonces and partialsigs.

    diff --git a/test/functional/wallet_musig.py b/test/functional/wallet_musig.py
    index 0e3e4377c0..43c2de0436 100755
    --- a/test/functional/wallet_musig.py
    +++ b/test/functional/wallet_musig.py
    @@ -168,7 +168,7 @@ class WalletMuSigTest(BitcoinTestFramework):
             assert "musig2_pubnonces" in dec["inputs"][0]
             assert "musig2_partial_sigs" not in dec["inputs"][0]
     
    -    def test_success_case(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None, only_one_musig_wallet=False):
    +    def test_success_case(self, comment, pattern, sighash_type=None, scriptpath=None, nosign_wallets=None, only_one_musig_wallet=False, expected_pubnonces_partialsigs=None):
             self.log.info(f"Testing {comment}")
             has_internal = MULTIPATH_TWO_RE.search(pattern) is not None
     
    @@ -176,22 +176,26 @@ class WalletMuSigTest(BitcoinTestFramework):
             wallets, keys = self.create_wallets_and_keys_from_pattern(pat)
             self.construct_and_import_musig_descriptor_in_wallets(pat, wallets, keys, only_one_musig_wallet)
     
    -        expected_pubnonces = 0
    -        expected_partial_sigs = 0
    -        for musig in MUSIG_RE.findall(pat):
    -            musig_partial_sigs = 0
    -            for placeholder in PLACEHOLDER_RE.findall(musig):
    -                wallet_index = int(placeholder[1:])
    -                if nosign_wallets is None or wallet_index not in nosign_wallets:
    -                    expected_pubnonces += 1
    -                else:
    -                    musig_partial_sigs = None
    +        if expected_pubnonces_partialsigs != None:
    +            expected_pubnonces = expected_pubnonces_partialsigs['pubnonces']
    +            expected_partial_sigs = expected_pubnonces_partialsigs['partialsigs']
    +        else:
    +            expected_pubnonces = 0
    +            expected_partial_sigs = 0
    +            for musig in MUSIG_RE.findall(pat):
    +                musig_partial_sigs = 0
    +                for placeholder in PLACEHOLDER_RE.findall(musig):
    +                    wallet_index = int(placeholder[1:])
    +                    if nosign_wallets is None or wallet_index not in nosign_wallets:
    +                        expected_pubnonces += 1
    +                    else:
    +                        musig_partial_sigs = None
    +                    if musig_partial_sigs is not None:
    +                        musig_partial_sigs += 1
    +                    if wallet_index < len(wallets):
    +                        continue
                     if musig_partial_sigs is not None:
    -                    musig_partial_sigs += 1
    -                if wallet_index < len(wallets):
    -                    continue
    -            if musig_partial_sigs is not None:
    -                expected_partial_sigs += musig_partial_sigs
    +                    expected_partial_sigs += musig_partial_sigs
     
             # Check that the wallets agree on the same musig address
             addr = None
    @@ -252,7 +256,10 @@ class WalletMuSigTest(BitcoinTestFramework):
             for i, wallet in enumerate(wallets):
                 if nosign_wallets and i in nosign_wallets:
                     continue
    -            proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
    +            if scriptpath == None:
    +                proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
    +            else:
    +                proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type, keypath_only=not scriptpath)
                 assert_equal(proc["complete"], False)
                 nonce_psbts.append(proc["psbt"])
     
    @@ -280,7 +287,10 @@ class WalletMuSigTest(BitcoinTestFramework):
             for i, wallet in enumerate(wallets):
                 if nosign_wallets and i in nosign_wallets:
                     continue
    -            proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type)
    +            if scriptpath == None:
    +                proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type)
    +            else:
    +                proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type, keypath_only=not scriptpath)
                 assert_equal(proc["complete"], False)
                 psig_psbts.append(proc["psbt"])
     
    @@ -333,6 +343,7 @@ class WalletMuSigTest(BitcoinTestFramework):
             self.test_success_case("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", scriptpath=True)
             self.test_success_case("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
             self.test_success_case("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", scriptpath=True, nosign_wallets=[0])
    +        self.test_success_case("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", scriptpath=False, expected_pubnonces_partialsigs={'pubnonces':3, 'partialsigs':3})
             self.test_success_case("tr(H,and(pk(musig/*),after(1)))", "tr($H,and_v(v:pk(musig($0,$1,$2)/<0;1>/*),after(1)))", scriptpath=True)
             self.test_success_case("tr(H,and(pk_k(musig/*),after(1)))", "tr($H,and_v(vc:pk_k(musig($0,$1,$2)/<0;1>/*),after(1)))", scriptpath=True)
             self.test_success_case("tr(H,and(pkh(musig/*),after(1)))", "tr($H,and_v(v:pkh(musig($0,$1,$2)/<0;1>/*),after(1)))", scriptpath=True)
    
    

    Sjors commented at 3:15 PM on June 9, 2026:

    Taken with some tweaks for readability in 349462b7a97adc221fb2a1d6948f9197e4a1fef1.


    Sjors commented at 3:29 PM on June 9, 2026:

    Touching that file requires a rebase, so will do that now.


    rkrux commented at 10:59 AM on June 15, 2026:

    Taken with some tweaks for readabilityin https://github.com/bitcoin/bitcoin/commit/349462b7a97adc221fb2a1d6948f9197e4a1fef1.

    Nice, the changes in the commit are cleaner than what I suggested.

  82. rkrux commented at 2:16 PM on June 8, 2026: contributor

    The test reminded me of wallet_musig.py functional test that I believe can also be updated here because it works over walletprocesspsbt RPC.

  83. Sjors force-pushed on Jun 9, 2026
  84. Sjors force-pushed on Jun 9, 2026
  85. Sjors commented at 3:45 PM on June 9, 2026: member

    Rebased after #33636, because the test I added in my previous push touches the same file. Also further simplified that test.

  86. DrahtBot added the label CI failed on Jun 9, 2026
  87. DrahtBot removed the label CI failed on Jun 10, 2026
  88. w0xlt commented at 8:11 AM on June 14, 2026: contributor

    Concept ACK

  89. rkrux commented at 11:00 AM on June 15, 2026: contributor

    I believe the changes in the multiple RPCs require a release note.

  90. Sjors commented at 4:04 PM on June 16, 2026: member

    @rkrux pushed a release note commit.

  91. in test/functional/rpc_psbt.py:1428 in 03541dff24 outdated
    1423 | +            descriptors=[taproot_descriptor],
    1424 | +            finalize=False,
    1425 | +            keypath_only=True,
    1426 | +        )
    1427 | +        decoded = self.nodes[2].decodepsbt(keypath_only_psbt["psbt"])
    1428 | +        assert "taproot_scripts" in decoded["inputs"][0]
    


    rkrux commented at 12:19 PM on June 19, 2026:

    In 2d8d02d4da89b5e928dd7007365f31ab2f8f9761 "rpc: add keypath_only to descriptorprocesspsbt"

    From the PR description:

    The resulting PSBT won't include taproot_scripts or taproot_script_path_sigs.

    The PR description is outdated?


    Sjors commented at 1:29 PM on June 19, 2026:

    When the fields are present, we should not sign them, that was already correct. When the fields are absent, we should not add them. This prevents other signers from accidentally signing them (or get confused by them, as I found with earlier Ledger firmware).

    (update: it worked correctly, but PR description was wrong)

    I also added some code comments.

  92. in src/common/types.h:56 in fa971a09d5
      48 | @@ -49,6 +49,11 @@ struct PSBTFillOptions {
      49 |       * Whether to fill in bip32 derivation information if available.
      50 |       */
      51 |      bool bip32_derivs{true};
      52 | +
      53 | +    /**
      54 | +     * Only sign the key path (for taproot inputs).
      55 | +     */
      56 | +    bool avoid_script_path{false};
    


    rkrux commented at 12:22 PM on June 19, 2026:

    In fa971a09d5227d29aea57836d46538d6e11ea911 "wallet: add option to avoid script path spends"

    I am not seeing any benefit of having two kinds of naming for the same feature - keypath_only in the RPCs and avoid_script_path within the code.

    Would really prefer to have only one kind of naming, I'd let keypath_only stay.


    rkrux commented at 12:25 PM on June 19, 2026:

    In fa971a0 "wallet: add option to avoid script path spends"

    PSBTFillOptions is a more generic struct and not having taproot in the avoid_script_path name can be confusing for the uninitiated (though its present in the documentation comment). Maybe prefix it with taproot_?


    Sjors commented at 1:24 PM on June 19, 2026:

    Good point, I'll switch over to keypath_only.

  93. in src/script/sign.h:70 in fa971a09d5


    rkrux commented at 12:27 PM on June 19, 2026:

    In fa971a0 "wallet: add option to avoid script path spends"

    There are 2 private and public sections each in the class. Is this intentional? Or can these be clubbed?


    Sjors commented at 2:01 PM on June 19, 2026:

    It is, but it's ugly. I'm adding a TaprootKeypathOnly() helper so we can keep m_options private and drop the annotations.


    Sjors commented at 2:19 PM on June 19, 2026:

    On second thought, exposing Options() seems generic.


    rkrux commented at 9:08 AM on June 27, 2026:

    It's not related to this PR but I noticed this dead code while reviewing. It can be removed here because this file is being touched, I'm not sure if raising a separate PR only for this would be worth a whole review & merge process.

    --- a/test/functional/wallet_taproot.py
    +++ b/test/functional/wallet_taproot.py
    @@ -64,9 +64,6 @@ class WalletTaprootTest(BitcoinTestFramework):
         def setup_network(self):
             self.setup_nodes()
     
    -    def init_wallet(self, *, node):
    -        pass
    - [@staticmethod](/bitcoin-bitcoin/contributor/staticmethod/)
         def make_desc(pattern, privmap, keys, pub_only = False):
             pat = pattern.replace("$H", H_POINT)
    

    Sjors commented at 2:19 PM on June 29, 2026:

    @rkrux I'll add a commit here, but it's probably worth its own PR, because it might sit here for a while otherwise.


    rkrux commented at 2:30 PM on June 29, 2026:

    I see, I can raise a separate PR for it then.


    rkrux commented at 2:35 PM on June 29, 2026:
  94. in src/script/sign.h:37 in fa971a09d5 outdated
      32 | @@ -33,6 +33,7 @@ struct SignatureData;
      33 |  
      34 |  struct SignOptions {
      35 |      int sighash_type{SIGHASH_DEFAULT};
      36 | +    bool avoid_script_path{false};
      37 |  };
    


    rkrux commented at 12:28 PM on June 19, 2026:

    In fa971a0 "wallet: add option to avoid script path spends"

    Same taproot_ prefix suggestion for this generic SignOptions struct as well.

  95. in src/wallet/rpc/spend.cpp:1227 in 03541dff24
    1223 | @@ -1221,6 +1224,7 @@ RPCMethod send()
    1224 |                            }},
    1225 |                          },
    1226 |                      },
    1227 | +                    {"keypath_only", RPCArg::Type::BOOL, RPCArg::Default{false}, "Only sign the key path (for taproot inputs)."},
    


    rkrux commented at 12:35 PM on June 19, 2026:

    In multiple commits:

    The false default for all this option in all the affected RPCs can be a wallet constant named DEFAULT_SIGN_TAPROOT_KEYPATH_ONLY. It needs to be in the wallet namespace, either in this file wallet/rpc/spend.cpp where its used almost exclusively, or in the wallet/wallet.h file where other defaults also are (not my preference though).


    Sjors commented at 1:54 PM on June 19, 2026:

    I'll move it to wallet/wallet.h since one day the GUI might have a similar toggle.

    descriptorprocesspsbt keeps a hardcoded {false}, since it doesn't include the wallet.

    Ultimately it doesn't matter that much, because changing the default would be a breaking change for older RPC clients, so it seems unlikely to happen.

  96. in src/wallet/rpc/spend.cpp:1654 in e6b5cef884
    1650 |      bool complete = true;
    1651 |  
    1652 |      if (sign) EnsureWalletIsUnlocked(*pwallet);
    1653 |  
    1654 | -    const auto err{wallet.FillPSBT(psbtx, {.sign = sign, .sighash_type = nHashType, .finalize = finalize, .bip32_derivs = bip32derivs}, complete)};
    1655 | +    const auto err{wallet.FillPSBT(psbtx, {.sign = sign, .sighash_type = nHashType, .finalize = finalize, .bip32_derivs = bip32derivs, .avoid_script_path = keypath_only}, complete)};
    


    rkrux commented at 12:41 PM on June 19, 2026:

    In e6b5cef884b9b728954b4e3d3a483b5d41132066 "rpc: add keypath_only to walletprocesspsbt"

    Related to an earlier suggestion, seeing this double naming together seems unnecessary to me.

  97. in test/functional/wallet_taproot.py:162 in 7ab4224ceb outdated
     153 | @@ -154,6 +154,56 @@
     154 |              "6b8747a6bbe4440d7386658476da51f6e49a220508a7ec77fe7bccc3e7baa916",
     155 |              "4674bf4d9ebbe01bf0aceaca2472f63198655ecf2df810f8d69b38421972318e",
     156 |          ]
     157 | +    },
     158 | +    {
     159 | +        "xprv": "tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK",
     160 | +        "xpub": "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B",
     161 | +        "pubs": [
     162 | +            "146846eeb5a7533abb594ba734bc243fc7b6349499b8311c8fc13b0112ba8a77",
    


    rkrux commented at 12:48 PM on June 19, 2026:

    From the comment few lines above:

    xprvs/xpubs, and m/* derived x-only pubkeys (created using independent implementation)

    1. Where were these xprvs and xpubs gotten from?
    2. Do the 4 pubs here correspond to m/0, m/1, m/2, m/3 respectively?

    I don't prefer to have hardcoded extended keys that are created via some outside implementation in the testing framework because it clutters the testing code, is not self documenting, and makes the tests difficult to update - that's why I raised #35543 recently.

    If it gets merged before this PR (this one doesn't need to be dependent on it), it'd be really nice to use the ExtendedPrivate(|Public)Key class(es) from there so that we can have programmatic test cases generation that would make these tests self-documenting.


    Sjors commented at 2:00 PM on June 19, 2026:
    1. Lifted from various other tests. Happy to rebase after #35543 and get them from a more canonical source.
    2. I believe so, yes.

    rkrux commented at 7:44 AM on June 27, 2026:

    PR #35543 is merged now, only the following change would be required now I believe:

    diff --git a/test/functional/wallet_taproot.py b/test/functional/wallet_taproot.py
    index b312d4e153..89d45f0ca5 100755
    --- a/test/functional/wallet_taproot.py
    +++ b/test/functional/wallet_taproot.py
    @@ -256,7 +256,7 @@ class WalletTaprootTest(BitcoinTestFramework):
             self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys])
     
         def generate_test_keys(self):
    -        xprvs = [ExtendedPrivateKey.generate() for _ in range(0, 13)]
    +        xprvs = [ExtendedPrivateKey.generate() for _ in range(0, 18)]
             return [{
                 "xprv": xprv.to_string(),
                 "xpub": xprv.pubkey().to_string(),
    

    Sjors commented at 2:38 PM on June 29, 2026:

    Done!

  98. rkrux commented at 12:49 PM on June 19, 2026: contributor

    Code review 03541dff2424352b8e366c8deb25ed36190388e0

    I'm reviewing the wallet_taproot changes at the moment. I've shared the points that I have till now.

    Also the PR description can be updated to mention the remaining changed RPCs.

  99. in src/wallet/rpc/spend.cpp:1648 in e6b5cef884
    1644 | @@ -1644,11 +1645,13 @@ RPCMethod walletprocesspsbt()
    1645 |      bool sign = request.params[1].isNull() ? true : request.params[1].get_bool();
    1646 |      bool bip32derivs = request.params[3].isNull() ? true : request.params[3].get_bool();
    1647 |      bool finalize = request.params[4].isNull() ? true : request.params[4].get_bool();
    1648 | +    bool keypath_only{request.params[5].isNull() ? false : request.params[5].get_bool()};
    


    rkrux commented at 2:18 PM on June 19, 2026:

    In e6b5cef "rpc: add keypath_only to walletprocesspsbt"

    This point is specific to walletprocesspsbt RPC only because all the 3 other affected RPCs don't provide an option to not sign.

    Having both sign and keypath_only booleans might be confusing for the end user as the latter is a form of former. I was reviewing #35370 and noticed multiple overlapping boolean RPC options over there and believe the same point is applicable here as well (to some extent).

    One way could be to make this sign option an enum (ALL, NONE, KEYPATH_ONLY), though that would be a breaking change for the end users. An alternative could be to add a condition in the code that sign needs to be true if keypath_only is passed true because keypath_only:true with sign:false is conflicting.

    Ideally I prefer the enum approach because it allows us to have fewer RPC arguments but given that it'd be a breaking change and only one RPC needs to be updated at the moment, I'm inclining towards the latter.

    WDYT?


    Sjors commented at 2:23 PM on June 19, 2026:

    I think it's better to keep similar RPC methods consistent, even if it's a bit awkward for one of them.

  100. Sjors force-pushed on Jun 19, 2026
  101. Sjors commented at 2:36 PM on June 19, 2026: member

    Addressed feedback from @rkrux. Most importantly:

    • added Options() to BaseSignatureCreator to avoid having to make m_options public. See #32857 (review)

    Clarified in PR description as well code comments that we don't add new taproot_scripts. We also don't remove exiting ones, but don't sign them. See #32857 (review)

    Rebased just in case after #35504.

  102. DrahtBot added the label CI failed on Jun 19, 2026
  103. DrahtBot removed the label CI failed on Jun 26, 2026
  104. DrahtBot added the label Needs rebase on Jun 26, 2026
  105. wallet: add option to avoid script path spends 3dfe347989
  106. rpc: add keypath_only to walletprocesspsbt 5e312ffed8
  107. test: cover keypath_only in wallet_taproot.py
    Expand taproot tests to cover avoid_script_path in  walletprocesspsbt.
    
    When avoiding script paths, there's no need for the workaround that increases fee_rate to compensate for the wallet's inability to estimate fees for script path spends. We use this to indirectly test that key path was used.
    
    We also check that taproot_script_path_sigs is not set.
    
    Finally, for transactions that can't be signed using their key path, we try again by allowing the script path.
    
    Additional test extended private keys were extracted from other tests.
    
    Co-authored-by: rkrux <rkrux.connect@gmail.com>
    9460402e90
  108. rpc: add keypath_only to send and sendall 201da29135
  109. rpc: add keypath_only to descriptorprocesspsbt 0a89ada4d1
  110. test: cover keypath_only in wallet_musig.py
    Extract the descriptor with both MuSig key and script paths into
    key_and_script_path_musigs. Reuse it for the existing cases and
    give those cases descriptions.
    
    Expand the same scenario with keypath_only=true. The new case asks
    walletprocesspsbt to avoid script-path signing.
    
    Co-authored-by: rkrux <rkrux.connect@gmail.com>
    d04ee7541c
  111. doc: add release note for keypath_only 1070ba8c10
  112. test: remove unused wallet_taproot init_wallet 707b6132af
  113. Sjors force-pushed on Jun 29, 2026
  114. Sjors commented at 2:38 PM on June 29, 2026: member

    Rebased after #35543 and took advantage of it in the test, see #32857 (review).

    Also added the minor test cleanup suggested in #32857 (review).

  115. DrahtBot removed the label Needs rebase on Jun 29, 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-07-03 23:51 UTC

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