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

pull Sjors wants to merge 4 commits into bitcoin:master from Sjors:2025/07/smart-createwalletdescriptor changing 6 files +74 −4
  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.

    (I might do that in #32784, which introduces some other improvements to the tutorial. The situation is still not great; ideally the importdescriptors RPC is enhanced to detect when an xpub is a child of an hd key and then treats it as if an xpriv was imported.)

    --

    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 for information on the review process.

    Type Reviewers
    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:

    • #28333 (wallet: Construct ScriptPubKeyMans with all data rather than loaded progressively 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.

    <!--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. test: check unused xprv descriptor pubkeys
    Co-authored-by: adyshimony <6388409+adyshimony@users.noreply.github.com>
    438a0ad797
  19. wallet: reject duplicate addhdkey xprvs
    Co-authored-by: adyshimony <6388409+adyshimony@users.noreply.github.com>
    c680f9b30e
  20. wallet: add GetScriptlessSPKMs() helper
    A helper method to obtain all unused(key) descriptor SPKMs.
    524151fbf0
  21. 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.
    fb57795e14
  22. Sjors force-pushed on May 14, 2026
  23. Sjors commented at 12:35 PM on May 14, 2026: member

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

  24. Sjors marked this as ready for review on May 14, 2026
  25. DrahtBot removed the label Needs rebase on May 14, 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-05-20 06:52 UTC