Skip to content

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

Draft
Sjors wants to merge 8 commits intobitcoin:masterfrom
Sjors:2025/07/smart-createwalletdescriptor
Draft

Have createwalletdescriptor auto-detect an unused(KEY)#32861
Sjors wants to merge 8 commits intobitcoin:masterfrom
Sjors:2025/07/smart-createwalletdescriptor

Conversation

@Sjors
Copy link
Member

@Sjors Sjors commented Jul 3, 2025

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

Builds on #29136.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jul 3, 2025

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/32861.

Reviews

See the guideline for information on the review process.

Type Reviewers
Approach ACK adyshimony

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:

  • #33874 (wallet: refactor ProcessDescriptorImport by naiyoma)
  • #33392 (wallet, rpc: add UTXO set check and incremental rescan to importdescriptors by musaHaruna)
  • #33008 (wallet: support bip388 policy with external signer by Sjors)
  • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet by achow101)
  • #31668 (Added rescan option for import descriptors by saikiran57)
  • #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.

LLM Linter (✨ experimental)

Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

  • [Parse(desc_str, keys, error, false)] in src/wallet/rpc/wallet.cpp

2026-02-02 15:54:16

@rkrux
Copy link
Contributor

rkrux commented Jul 18, 2025

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@Sjors
Copy link
Member Author

Sjors commented Jul 18, 2025

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.

@rkrux
Copy link
Contributor

rkrux commented Jul 18, 2025

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)

@Sjors Sjors force-pushed the 2025/07/smart-createwalletdescriptor branch from e8b1440 to 6049553 Compare July 25, 2025 07:06
@Sjors
Copy link
Member Author

Sjors commented Jul 25, 2025

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

Sjors added 2 commits February 2, 2026 16:52
A helper method to obtain all unused(key) descriptor SPKMs.
When a wallet contains only an unused(KEY) descriptor, use it. Previously the user would have to call listdescriptors and manually specify it.
@Sjors Sjors force-pushed the 2025/07/smart-createwalletdescriptor branch from f2752ef to c8b6d4e Compare February 2, 2026 15:53
// Check both only have one pubkey
std::set<CPubKey> prv_pubkeys;
std::set<CExtPubKey> prv_extpubs;
parse_pub->GetPubKeys(prv_pubkeys, prv_extpubs);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this call use parse_priv instead of parse_pub?

}
}

LOCK(wallet->cs_wallet);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");
}

wallet = self.nodes[0].get_wallet_rpc("hdkey")

assert_equal(len(wallet.gethdkeys()), 1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@adyshimony
Copy link

Review and tested c8b6d4e

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants