Have createwalletdescriptor auto-detect an unused(KEY) #32861

pull Sjors wants to merge 5 commits into bitcoin:master from Sjors:2025/07/smart-createwalletdescriptor changing 6 files +71 −9
  1. Sjors commented at 8:55 AM on July 3, 2025: member

    The createwalletdescriptor was introduced in #29130 to let users add a tr() descriptor to an existing SegWit wallet. The new addhdkey method from #29136 introduces a new potential workflow: start from a blank wallet, generate an HD and then add only the descriptors you need, e.g.:

    bitcoin rpc createwallet TaprootMaxi blank=true
    bitcoin rpc addhdkey
    bitcoin rpc createwalletdescriptor bech32m
    

    Before this PR the last line would fail, requiring the user to call gethdkeys and copy-paste the xpub.

    This PR makes createwalletdescriptor a bit smarter so it just finds the unused(KEY) generated by hdkey and uses that.

    If multiple unused(KEY) descriptors are present the user still has to pick one.

    A potential followup is to make our multisig tutorial slightly safer to use. Rather than creating a full wallet, the instruction could be changed to start with a blank wallet and only generate the default legacy descriptor. This avoids accidental use of single sig p2sh-segwit and bech32 addresses.

    --

    The first two commits are quick followups for #29136.

  2. DrahtBot commented at 8:55 AM on July 3, 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/32861.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK w0xlt, jeanpablojp
    Approach ACK adyshimony

    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:

    • #36133 (wallet: store multipath descriptor by Sjors)
    • #35436 (wallet: Add addHDkey interface by pseudoramdom)

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

  3. rkrux commented at 9:38 AM on July 18, 2025: contributor

    This PR makes createwalletdescriptor a bit smarter so it just finds the unused(KEY) generated by hdkey and uses that.

    Once the unused(KEY) descriptor is used, doesn't it become used? Keeping it as an unused descriptor later might come across as confusing.

  4. in src/wallet/rpc/wallet.cpp:None in e8b1440aa3 outdated
     908 | +                    }
     909 | +
     910 | +                    if (wallet_xpubs.empty()) {
     911 | +                        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No HD key found. Please generate one with 'addhdkey' or import an active descriptor.");
     912 | +                    } else if (wallet_xpubs.size() > 1) {
     913 | +                        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to determine which HD key to use. Please specify with 'hdkey'");
    


    rkrux commented at 9:44 AM on July 18, 2025:

    In e8b1440aa3ed7efe3a9c2d10afb05e7247fff1f6 "rpc: make createwalletdescriptor smarter"

    Though this check here seems more thorough, but the presence of more than one spkm also seems sufficient to throw this error?

  5. Sjors commented at 9:50 AM on July 18, 2025: member

    Keeping it as an unused descriptor later might come across as confusing.

    I agree, and it was also brought up here: #29136 (comment). It's orthogonal to this PR.

  6. rkrux commented at 10:19 AM on July 18, 2025: contributor

    Thanks, I recall reading this comment earlier but forgot about it later. If I am not missing anything, I don't suppose this point is orthogonal to this PR?

    The linked comment also states that any RPC using the unused descriptor should delete it immediately afterwords.

    add logic to importdescriptors and generatewalletdescriptor to delete any unused(KEY) descriptor as soon as the KEY which it references is used by an new descriptor (taking into account both public and private parts of the key)

    I don't believe generatewalletdescriptor is present as an RPC in the codebase, and createwalletdescriptor stores the newly created descriptor in the database as I verified from the below patch. I can see the unused descriptor present after this RPC as well. Shouldn't we add the deletion of the unused descriptor in this PR itself?

    diff --git a/test/functional/wallet_createwalletdescriptor.py b/test/functional/wallet_createwalletdescriptor.py
    index 6de0ca4782..a7086c5b9e 100755
    --- a/test/functional/wallet_createwalletdescriptor.py
    +++ b/test/functional/wallet_createwalletdescriptor.py
    @@ -125,7 +125,11 @@ class WalletCreateDescriptorTest(BitcoinTestFramework):
     
             # Create unused(KEY) descriptor and try again
             w1.addhdkey()
    +        print("listdescriptors: ", w1.listdescriptors())
    +        print("gethdkeys: ", w1.gethdkeys())
             w1.createwalletdescriptor(type="bech32")
    +        print("listdescriptors: ", w1.listdescriptors())
    +        print("gethdkeys: ", w1.gethdkeys())
     
             self.nodes[0].createwallet("w2", blank=True)
             w2 = self.nodes[0].get_wallet_rpc("w2")
    (END)
    
  7. Sjors force-pushed on Jul 25, 2025
  8. Sjors commented at 7:12 AM on July 25, 2025: member

    The linked comment also states that any RPC using the unused descriptor should delete it immediately afterwords.

    That should be done in #29136 which introduces createwalletdescriptor , or in a followup, but not in this PR (which consists of only the last two commits).

  9. DrahtBot added the label Needs rebase on Jan 3, 2026
  10. Sjors force-pushed on Jan 5, 2026
  11. DrahtBot removed the label Needs rebase on Jan 5, 2026
  12. Sjors force-pushed on Feb 2, 2026
  13. in src/test/descriptor_tests.cpp:1357 in c8b6d4ee2a
    1352 | +    BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub2), "Public ser: " + pub2 + " Public desc: " + pub);
    1353 | +
    1354 | +    // Check both only have one pubkey
    1355 | +    std::set<CPubKey> prv_pubkeys;
    1356 | +    std::set<CExtPubKey> prv_extpubs;
    1357 | +    parse_pub->GetPubKeys(prv_pubkeys, prv_extpubs);
    


    adyshimony commented at 10:29 PM on February 21, 2026:

    Should this call use parse_priv instead of parse_pub?


    Sjors commented at 12:28 PM on May 14, 2026:

    Thanks, fixed in a new commit (even though it pertains to #29136).

  14. in src/wallet/rpc/wallet.cpp:919 in c8b6d4ee2a outdated
     914 | +                        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Could not parse HD key");
     915 | +                    }
     916 | +                }
     917 | +            }
     918 | +
     919 | +            LOCK(wallet->cs_wallet);
    


    adyshimony commented at 10:44 PM on February 21, 2026:

    I think a duplicate check is needed. What if the wallet already has this xprv through an active descriptor and user calls addhdkey with the same xprv? The descriptor check would not catch that, because unused(xprv) is a different descriptor string. A check is needed here to prevent adding the same private key twice.

    const CExtPubKey hd_xpub{hdkey.Nexuter()};
    if (wallet->GetKey(hd_xpub.pubkey.GetID())) {
        throw JSONRPCError(RPC_WALLET_ERROR, "HD key already exists");
    }
    
  15. in test/functional/wallet_hd.py:37 in c8b6d4ee2a outdated
      32 | +        def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
      33 | +        self.nodes[0].createwallet("hdkey")
      34 | +        wallet = self.nodes[0].get_wallet_rpc("hdkey")
      35 | +
      36 | +        assert_equal(len(wallet.gethdkeys()), 1)
      37 | +
    


    adyshimony commented at 10:46 PM on February 21, 2026:

    Cover the case where addhdkey is called with existing xprv. Expected behavior is to reject it as "HD key already exists"

    existing_wallet_xprv = wallet.gethdkeys(private=True)[0]["xprv"]
    assert_raises_rpc_error(-4, "HD key already exists", wallet.addhdkey, existing_wallet_xprv)
    
  16. adyshimony commented at 10:57 PM on February 21, 2026: none

    Review and tested c8b6d4ee2abbd1271e44aa66da6079cee6323529

    Concept ACK, Approach ACK.

    I think I have found a small issue where a xprv already in wallet via active descriptors, and addhdkey(xprv) is called from RPC with the same xprv.

    Suggested a fix in the review comments.

  17. DrahtBot added the label Needs rebase on May 14, 2026
  18. Sjors force-pushed on May 14, 2026
  19. Sjors commented at 12:35 PM on May 14, 2026: member

    Rebased after #29136 landed and included suggestions from @adyshimony.

  20. Sjors marked this as ready for review on May 14, 2026
  21. DrahtBot removed the label Needs rebase on May 14, 2026
  22. DrahtBot added the label Needs rebase on May 28, 2026
  23. Sjors force-pushed on May 29, 2026
  24. Sjors commented at 8:49 AM on May 29, 2026: member

    Rebased after #28333.

  25. DrahtBot removed the label Needs rebase on May 29, 2026
  26. DrahtBot added the label CI failed on May 29, 2026
  27. DrahtBot removed the label CI failed on Jun 2, 2026
  28. w0xlt commented at 9:12 PM on June 8, 2026: contributor

    Concept ACK

  29. Sjors force-pushed on Jul 3, 2026
  30. Sjors referenced this in commit fbb2870e4e on Jul 3, 2026
  31. sedited requested review from adyshimony on Jul 24, 2026
  32. sedited requested review from rkrux on Jul 24, 2026
  33. Sjors force-pushed on Aug 6, 2026
  34. DrahtBot added the label Needs rebase on Aug 15, 2026
  35. Sjors commented at 12:25 PM on August 17, 2026: member

    Rebased after #32784.

  36. Sjors force-pushed on Aug 17, 2026
  37. DrahtBot removed the label Needs rebase on Aug 17, 2026
  38. Sjors force-pushed on Aug 20, 2026
  39. jeanpablojp commented at 10:38 PM on August 24, 2026: contributor

    Concept ACK

    I noticed that on a wallet with two active keys plus one from addhdkey, derivehdkey uses the unused one while createwalletdescriptor errors with "Unable to determine which HD key to use from active descriptors". Master does the same. But now that this RPC knows about unused(KEY), shouldn't that lookup also apply when there's more than one active key?

    The hdkey hint is stale now too. It still says "The HD key used by all other active descriptors", where derivehdkey's mentions both cases.

  40. test: check unused xprv descriptor pubkeys
    Co-authored-by: adyshimony <6388409+adyshimony@users.noreply.github.com>
    331f87613f
  41. wallet: reject duplicate addhdkey xprvs
    Co-authored-by: adyshimony <6388409+adyshimony@users.noreply.github.com>
    4daf376b5e
  42. rpc: make createwalletdescriptor smarter
    When a wallet contains only an unused(KEY) descriptor, use it. Previously the user would have to call listdescriptors and manually specify it.
    f36bf793e3
  43. doc: use a blank wallet in the multisig tutorial
    The participant wallets are only used to derive an xpub and sign PSBTs
    for the multisig wallet. Creating them blank and adding just the legacy
    descriptor avoids handing the reader singlesig addresses they are not
    supposed to use.
    
    The legacy descriptor can't be dropped entirely because the signing code
    only considers descriptors whose derivation paths cover the input.
    
    To further discourage use of the singlesig descriptors, the PSBT example
    now pays a separate recipient wallet rather than participant_1.
    97522077bf
  44. doc: add release note for #32861 9d4eb1f50d
  45. in src/wallet/rpc/wallet.cpp:801 in 9d8e530f52
     797 | +                    xpub = active_xpubs.begin()->first;
     798 | +                } else if (active_xpubs.size() > 1) {
     799 |                      throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to determine which HD key to use from active descriptors. Please specify with 'hdkey'");
     800 | +                } else {
     801 | +                    // Look for an unused(KEY) descriptor
     802 | +                    std::set<DescriptorScriptPubKeyMan*> spkms{pwallet->GetScriptlessSPKMs()};
    


    jeanpablojp commented at 10:38 PM on August 24, 2026:

    GetHDPubKeys(HDKeyFilter::UnusedKey) already returns this set, and derivehdkey uses it further down in this same file. I swapped this block for a call to it, the wallet tests still pass and GetScriptlessSPKMs() goes away entirely. Is there a reason for the separate helper that I'm missing?


    Sjors commented at 8:12 AM on August 25, 2026:

    I dropped f336199336f619f6c361e56da06e36aa46f776c8, as it's indeed superseded by GetHDPubKeys(HDKeyFilter::UnusedKey) from #32784.

  46. Sjors force-pushed on Aug 25, 2026
  47. Sjors commented at 8:12 AM on August 25, 2026: member

    The rule and help text is now:

    The HD key used by all other active descriptors, or, if there are none, the HD key of an unused(KEY) descriptor.

    In the ambiguous case @jeanpablojp describes, it's better to err on the side of failing, because descriptors can't be deleted. In that sense it's different from derivehdkey.

    The hdkey hint is stale now too.

    Fixed

    I also added a commit to use improve the multisig tutorial.


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-09-09 07:56 UTC