Skip to content

wallet: derivehdkey RPC to get xpub at arbitrary path#32784

Draft
Sjors wants to merge 6 commits into
bitcoin:masterfrom
Sjors:2025/06/gethdkey
Draft

wallet: derivehdkey RPC to get xpub at arbitrary path#32784
Sjors wants to merge 6 commits into
bitcoin:masterfrom
Sjors:2025/06/gethdkey

Conversation

@Sjors

@Sjors Sjors commented Jun 20, 2025

Copy link
Copy Markdown
Member

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.

@DrahtBot DrahtBot changed the title wallet: derivehdkey RPC to get xpub at arbitrary path wallet: derivehdkey RPC to get xpub at arbitrary path Jun 20, 2025
@DrahtBot

DrahtBot commented Jun 20, 2025

Copy link
Copy Markdown
Contributor

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

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK rkrux, pseudoramdom

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:

  • #bitcoin-core/gui/872 (Menu action to export a watchonly wallet by achow101)
  • #35462 (test: remove unnecessary nodes from wallet_multisig_descriptor_psbt by rkrux)
  • #35069 (Refactor keypath parser by pythcoiner)
  • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet by achow101)
  • #31260 (scripted-diff: Type-safe settings retrieval by ryanofsky)

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:

  • # Get the activate wpkh() receive descriptor -> # Get the active wpkh() receive descriptor [“activate” is the wrong word here; “active” is needed for the intended meaning.]

2026-05-14 12:47:27

@DrahtBot

Copy link
Copy Markdown
Contributor

🚧 At least one of the CI tasks failed.
Task lint: https://github.com/bitcoin/bitcoin/runs/44475268764
LLM reason (✨ experimental): The CI failure is caused by errors from the lint check 'py_lint', specifically due to unused imports flagged by ruff.

Hints

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.

Comment thread src/rpc/util.h Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In util.h: “@params[in] path” → “@param[in] path” [Doxygen tag typo]

Comment thread doc/multisig-tutorial.md Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

addressesd -> addresses [extra “d” makes “addresses” misspelled]

@rkrux

rkrux commented Jul 18, 2025

Copy link
Copy Markdown
Contributor

Very nice, Concept ACK.
I will review this PR.

@Sjors Sjors force-pushed the 2025/06/gethdkey branch 2 times, most recently from 380a57f to 017fb68 Compare August 1, 2025 10:52
@DrahtBot

Copy link
Copy Markdown
Contributor

🚧 At least one of the CI tasks failed.
Task test ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/25052455086/job/73383604351
LLM reason (✨ experimental): CI failed because the CMake build errored while compiling src/wallet (bitcoin_wallet target), stopping with make/gmake exit code 2.

Hints

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.

@Sjors

Sjors commented Apr 28, 2026

Copy link
Copy Markdown
Member Author

I pushed an extra commit 9814b33 to pre-empt IWYU warnings (at least in Sjors#91, probably here too). That caused CI to pick up the latest master, which fails as expected. I'll push again after #29136 is updated.

@Sjors Sjors force-pushed the 2025/06/gethdkey branch 2 times, most recently from d05788e to 2e6b696 Compare May 1, 2026 11:28
@Sjors

Sjors commented May 1, 2026

Copy link
Copy Markdown
Member Author

Rebased after #29136 updated and squashed 9814b33 IWYU fixup into the first commit.

@DrahtBot DrahtBot removed the CI failed label May 1, 2026
Sjors added 6 commits May 14, 2026 14:38
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.
@Sjors Sjors force-pushed the 2025/06/gethdkey branch from 2e6b696 to a457779 Compare May 14, 2026 12:47
@Sjors

Sjors commented May 14, 2026

Copy link
Copy Markdown
Member Author

Rebased now that #29136 landed.

@Sjors Sjors marked this pull request as ready for review May 14, 2026 13:50
@sedited sedited requested a review from polespinasa May 22, 2026 12:12
@pseudoramdom

Copy link
Copy Markdown

Concept ACK

Hi @Sjors, could the implementation move out of the RPC into a wallet::DeriveHDKey() helper, similar to #34861 ?
The use case I have in mind is multisig setup in gui-qml, where each cosigner needs to share a derived xpub. A single interfaces::Wallet method that combines addhdkey & derivehdkey would let the GUI get a shareable xpub in one call.

@pseudoramdom

Copy link
Copy Markdown

A single interfaces::Wallet method that combines addhdkey & derivehdkey would let the GUI get a shareable xpub in one call.

After chatting with @achow101, these might be separate interfaces. We'll still need the logic to be moved out of the RPC for derivehdkey.
I have PR for addHDKey interface #35436

@w0xlt w0xlt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few review comments:

Comment thread src/rpc/util.cpp
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread src/wallet/rpc/wallet.cpp
CExtKey xprv(xpub, *key);

std::optional<std::pair<CExtKey, KeyOriginInfo>> child{DeriveExtKey(xprv, path)};
CHECK_NONFATAL(child);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@Sjors

Sjors commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

I'll mark this draft pending #35436.

@DrahtBot

DrahtBot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants