wallet: derivehdkey RPC to get xpub at arbitrary path#32784
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32784. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please copy-paste ConflictsReviewers, this pull request conflicts with the following ones:
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 typos and grammar issues:
2026-05-14 12:47:27 |
|
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
de7f5c6 to
3f35b02
Compare
There was a problem hiding this comment.
In util.h: “@params[in] path” → “@param[in] path” [Doxygen tag typo]
There was a problem hiding this comment.
addressesd -> addresses [extra “d” makes “addresses” misspelled]
|
Very nice, Concept ACK. |
380a57f to
017fb68
Compare
2d40e1e to
18cbcb1
Compare
|
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
d05788e to
2e6b696
Compare
Use derivehdkey instead of extracting each participant xpub (and derivation info) from the listdescriptors output. Additionally use the new <0;1> descriptor syntax. Finally this commits adds a few debug log lines, and expand the explanation for why we use m/44h/1h/0h.
Use derivehdkey instead of extracting each participant xpub from the listdescriptors output. Additionally use the new <0;1> descriptor syntax. Also use bitcoin rpc instead of bitcoin-cli.
|
Rebased now that #29136 landed. |
|
Concept ACK Hi @Sjors, could the implementation move out of the RPC into a |
After chatting with @achow101, these might be separate interfaces. We'll still need the logic to be moved out of the RPC for |
| std::optional<std::pair<CExtKey, KeyOriginInfo>> DeriveExtKey(const CExtKey& ext_key, const std::vector<uint32_t>& path) { | ||
| CExtKey descendant = ext_key; | ||
| KeyOriginInfo origin; | ||
| origin.clear(); // Prevent spurious uninitialized variable warning |
There was a problem hiding this comment.
derivehdkey("m") currently leaves the origin fingerprint as [00000000] because the fingerprint is only populated inside the child-derivation loop, which does not run for an empty path.
diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp
index 9cae5b8ecd..1754d26cfa 100644
--- a/src/rpc/util.cpp
+++ b/src/rpc/util.cpp
@@ -1386,14 +1386,11 @@ std::optional<std::pair<CExtKey, KeyOriginInfo>> DeriveExtKey(const CExtKey& ext
CExtKey descendant = ext_key;
KeyOriginInfo origin;
origin.clear(); // Prevent spurious uninitialized variable warning
+ const CKeyID id = ext_key.key.GetPubKey().GetID();
+ std::copy(id.begin(), id.begin() + sizeof(origin.fingerprint), origin.fingerprint);
origin.path = path;
- bool first = true;
for (uint32_t i : path) {
if (!descendant.Derive(descendant, i)) return std::nullopt;
- if (first) {
- memcpy(origin.fingerprint, descendant.vchFingerprint, 4);
- first = false;
- }
}
return std::make_pair(descendant, origin);
}
diff --git a/src/rpc/util.h b/src/rpc/util.h
index d00920a9eb..70bde19dfe 100644
--- a/src/rpc/util.h
+++ b/src/rpc/util.h
@@ -159,7 +159,7 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
//! Get extended key and origin info for a given path
//! @param[in] ext_key The extended private key to derive from
//! @param[in] path The BIP 32 path
-//! @return the resulting extended private key and origin info (blank if path is empty)
+//! @return the resulting extended private key and origin info
std::optional<std::pair<CExtKey, KeyOriginInfo>> DeriveExtKey(const CExtKey& ext_key, const std::vector<uint32_t>& path);
//! Parse BIP32 path
diff --git a/test/functional/wallet_derivehdkey.py b/test/functional/wallet_derivehdkey.py
index 118726b94b..0bedc985a8 100755
--- a/test/functional/wallet_derivehdkey.py
+++ b/test/functional/wallet_derivehdkey.py
@@ -35,6 +35,8 @@ class WalletDeriveHDKeyTest(BitcoinTestFramework):
xpub_info = wallet.derivehdkey("m")
assert "xprv" not in xpub_info
xpub = xpub_info["xpub"]
+ root_fingerprint = wallet.derivehdkey("m/0")["origin"][1:9]
+ assert_equal(xpub_info["origin"], f"[{root_fingerprint}]")
xpub_info = wallet.derivehdkey("m", private=True)
xprv = xpub_info["xprv"]| CExtKey xprv(xpub, *key); | ||
|
|
||
| std::optional<std::pair<CExtKey, KeyOriginInfo>> child{DeriveExtKey(xprv, path)}; | ||
| CHECK_NONFATAL(child); |
There was a problem hiding this comment.
Could derivehdkey() return RPC_INVALID_PARAMETER when DeriveExtKey() returns null for a user-provided path, such as one exceeding BIP32’s maximum depth?
That would avoid surfacing invalid input through CHECK_NONFATAL(), which looks more like an internal bug path.
diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp
index f086ead493..87a860dbf8 100644
--- a/src/wallet/rpc/wallet.cpp
+++ b/src/wallet/rpc/wallet.cpp
@@ -1027,7 +1027,9 @@ RPCMethod derivehdkey()
CExtKey xprv(xpub, *key);
std::optional<std::pair<CExtKey, KeyOriginInfo>> child{DeriveExtKey(xprv, path)};
- CHECK_NONFATAL(child);
+ if (!child) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Unable to derive HD key at the requested path");
+ }
UniValue res{UniValue::VOBJ};
diff --git a/test/functional/wallet_derivehdkey.py b/test/functional/wallet_derivehdkey.py
index 118726b94b..13220a5aba 100755
--- a/test/functional/wallet_derivehdkey.py
+++ b/test/functional/wallet_derivehdkey.py
@@ -35,6 +35,13 @@ class WalletDeriveHDKeyTest(BitcoinTestFramework):
xpub_info = wallet.derivehdkey("m")
assert "xprv" not in xpub_info
xpub = xpub_info["xpub"]
+ too_deep_path = "m/" + "/".join(["0"] * 256)
+ assert_raises_rpc_error(
+ -8,
+ "Unable to derive HD key at the requested path",
+ wallet.derivehdkey,
+ too_deep_path,
+ )
xpub_info = wallet.derivehdkey("m", private=True)
xprv = xpub_info["xprv"]|
I'll mark this draft pending #35436. |
|
🐙 This pull request conflicts with the target branch and needs rebase. |
Given a (blank) wallet with an
unused(KEY)descriptor,derivehdkey "m/87h/0h/0h"gets the xpub or xprv at any given path.This is particularly useful for multisig setup where it's not desirable to use our default derivations (e.g. 44h).
I updated the multisig tutorial and the functional test.