wallet: Add deriveHDKey interface #36070

pull PraneethGunas wants to merge 3 commits into bitcoin:master from PraneethGunas:derivehdkey-interface changing 7 files +163 −55
  1. PraneethGunas commented at 5:59 PM on August 24, 2026: none

    This PR adds a wallet interface for derivehdkey.

    The motivation is the same as #35436 and #34861. The derivehdkey RPC exists (#32784), but the GUI does not go through RPC, so the logic is currently out of its reach. A dedicated wallet interface makes it available for multisig setup. Alongside the addhdkey interface, this lets the GUI add an HD key and derive a shareable xpub from it. Tracked as the deriveHDKey item in #35645.

    Key changes:

    • Add CWallet::DeriveHDKey(), which selects the HD key and derives it at the requested path.
    • Update the derivehdkey RPC to call it, rather than repeating the hardened path check, the key selection and the derivation. The watch-only and unlock checks stay in the RPC, along with the xpub argument parsing.
    • Add interfaces::Wallet::deriveHDKey(), which returns the derived xpub and its key origin. Private key material does not cross the interface.
    • Add unit test coverage for interfaces::Wallet::deriveHDKey().

    The RPC maps every WalletError to -4, moving the unhardened path and failed derivation cases from -8 and the key selection cases from -5. The watch-only -4 and locked wallet -13 are unchanged.

  2. DrahtBot renamed this:
    wallet: Add deriveHDKey interface
    wallet: Add deriveHDKey interface
    on Aug 24, 2026
  3. DrahtBot added the label Wallet on Aug 24, 2026
  4. DrahtBot commented at 5:59 PM on August 24, 2026: 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/36070.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Concept ACK polespinasa, jeanpablojp, pseudoramdom

    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

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  5. polespinasa commented at 7:54 AM on August 25, 2026: member

    concept ACK

    will review soon :)

  6. DrahtBot added the label Needs rebase on Aug 27, 2026
  7. jeanpablojp commented at 4:39 PM on August 27, 2026: contributor

    Concept ACK

    Left some comments.

  8. in src/wallet/test/wallet_interfaces_tests.cpp:32 in a1250d2f41
      27 | +
      28 | +    std::vector<uint32_t> path{87 | BIP32_HARDENED_FLAG};
      29 | +    auto result = interface->deriveHDKey(path, std::nullopt);
      30 | +    BOOST_REQUIRE(result);
      31 | +    BOOST_CHECK(result->first.pubkey.IsValid());
      32 | +    BOOST_CHECK(result->second.path == path);
    


    jeanpablojp commented at 4:39 PM on August 27, 2026:

    This case passes even when the derivation comes from a different key: origin.path just echoes the argument, and IsValid() holds for any derived key. I made SelectHDKey return a CExtKey the wallet doesn't hold and all six cases still passed, while wallet_derivehdkey.py failed on the same mutation.

    Comparing against the wallet's own active HD key would catch this, the way bip32_derive_ext_key does for DeriveExtKey:

        LOCK(wallet->cs_wallet);
        const auto active{wallet->GetHDPubKeys(CWallet::HDKeyFilter::Active)};
        BOOST_REQUIRE_EQUAL(active.size(), 1U);
        const auto xprv{wallet->GetExtKey(active.begin()->first)};
        BOOST_REQUIRE(xprv);
        const auto expected{DeriveExtKey(*xprv, path)};
        BOOST_REQUIRE(expected);
        BOOST_CHECK(result->first == expected->first.Neuter());
        BOOST_CHECK(result->second == expected->second);
    
  9. in src/wallet/rpc/wallet.cpp:1019 in a1250d2f41
    1054 |  
    1055 | -            std::optional<CExtKey> xprv{wallet->GetExtKey(xpub)};
    1056 | +            util::Expected<CExtKey, WalletError> xprv{wallet->SelectHDKey(selected_hdkey)};
    1057 |              if (!xprv) {
    1058 | -                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Private key for %s is not known", EncodeExtPubKey(xpub)));
    1059 | +                if (xprv.error().code == WalletErrorCode::UnlockNeeded) {
    


    jeanpablojp commented at 4:39 PM on August 27, 2026:

    Is this branch reachable? EnsureWalletIsUnlocked runs a few lines above with cs_wallet already held, and vMasterKey is GUARDED_BY(cs_wallet), so nothing can lock the wallet in between. Asking because the commit message says the RPC maps it.


    PraneethGunas commented at 12:25 AM on August 29, 2026:

    Right, removed this branch

  10. in src/wallet/rpc/wallet.cpp:1019 in 5de0c29e06
    1013 | @@ -1014,9 +1014,12 @@ RPCMethod derivehdkey()
    1014 |                  selected_hdkey = xpub;
    1015 |              }
    1016 |  
    1017 | -            util::Result<CExtKey> xprv{wallet->SelectHDKey(selected_hdkey)};
    1018 | +            util::Expected<CExtKey, WalletError> xprv{wallet->SelectHDKey(selected_hdkey)};
    1019 |              if (!xprv) {
    1020 | -                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, util::ErrorString(xprv).original);
    1021 | +                if (xprv.error().code == WalletErrorCode::UnlockNeeded) {
    


    polespinasa commented at 11:21 AM on August 28, 2026:

    in 5de0c29e06f41f6901fd0d0ec69c9359612371e5 wallet: Return WalletError from SelectHDKey

    This is dead code, it can never be executed because if the wallet is locked, EnsureWalletIsUnlocked(*wallet); (L1006) will throw before.

  11. in src/wallet/wallet.h:1101 in 11953500e9
    1095 | @@ -1096,6 +1096,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
    1096 |      //! no descriptor has the private key, or the wallet is locked.
    1097 |      std::optional<CExtKey> GetExtKey(const CExtPubKey& xpub) const;
    1098 |  
    1099 | +    //! Return the extended private key of the HD key to derive from. Without
    1100 | +    //! hdkey, unused(KEY) descriptors are preferred over active ones.
    1101 | +    util::Result<CExtKey> SelectHDKey(const std::optional<CExtPubKey>& hdkey) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    


    polespinasa commented at 11:23 AM on August 28, 2026:

    in 11953500e91920bc6357190bcd8956212913743d wallet: Move derivehdkey HD key selection into CWallet

    I would make this return Expected since the beginning. Probably commit 1 and 2 should be squashed, you are already changing the return type here, so why not go directly to the "final" version?


    PraneethGunas commented at 12:26 AM on August 29, 2026:

    Squashed, SelectHDKey returns util::Expected from the start now.

  12. in src/wallet/wallet.cpp:4672 in a1250d2f41 outdated
    4668 | @@ -4668,6 +4669,37 @@ util::Expected<CExtKey, WalletError> CWallet::SelectHDKey(const std::optional<CE
    4669 |      return *xprv;
    4670 |  }
    4671 |  
    4672 | +util::Expected<std::pair<CExtKey, KeyOriginInfo>, WalletError> CWallet::DeriveHDKey(const std::vector<uint32_t>& path, const std::optional<CExtPubKey>& hdkey) const
    


    polespinasa commented at 11:28 AM on August 28, 2026:

    in a1250d2f413016c4b4524d50823ef816af9aff40 wallet: Add deriveHDKey interface

    All this is duplicated from RPC code. Why not just make both the interface and the RPC rely on the same code? Because of both using different code functions, they both return different error codes for same errors.

    Also probably this commit could be split in different commits. First a preparatory commit that creates the DeriveHDKey function + testing, then a commit rebasing the RPC to use it, then a commit creating the interface. You can check #34861 for a commit structure idea :)


    PraneethGunas commented at 12:28 AM on August 29, 2026:

    Done. Split into "Add CWallet::DeriveHDKey" and "Add deriveHDKey interface".

    On sharing code with the RPC: it already calls SelectHDKey(). What's left is the watch-only, hardened-path and unlock guards, which pin the error codes wallet_derivehdkey.py asserts (-4, -8, -13).

    DeriveHDKey() returns GenericError for every failure, so routing the rest through it would turn "Unable to derive HD key at the requested path" from -8 into -5. Keeping -8 would need a new WalletErrorCode just so one caller can pick a different number, which src/wallet/types.h:48-50 warns against.


    PraneethGunas commented at 8:48 PM on September 10, 2026:

    Now both the interface and the RPC rely on the same CWallet::DeriveHDKey

  13. polespinasa commented at 11:29 AM on August 28, 2026: member

    did a first swift review

  14. PraneethGunas force-pushed on Aug 29, 2026
  15. DrahtBot removed the label Needs rebase on Aug 29, 2026
  16. jeanpablojp commented at 4:24 PM on August 29, 2026: contributor

    Reviewed again. Left one more comment on the new test.

  17. in src/wallet/test/wallet_tests.cpp:771 in 6131eab5b9
     766 | +
     767 | +    const std::vector<uint32_t> path{87 | BIP32_HARDENED_FLAG};
     768 | +    const auto result{wallet->DeriveHDKey(path, std::nullopt)};
     769 | +    BOOST_REQUIRE(result);
     770 | +    BOOST_CHECK(result->first.key.IsValid());
     771 | +    BOOST_CHECK(result->second.path == path);
    


    jeanpablojp commented at 4:24 PM on August 29, 2026:

    Same problem the interface test had: origin.path just echoes the argument, and IsValid() holds for any key. With SelectHDKey returning a key the wallet doesn't hold, this one passes while the interface test fails; same for blanking the origin fingerprint or returning the key before derivation.

        const auto active{wallet->GetHDPubKeys(CWallet::HDKeyFilter::Active)};
        BOOST_REQUIRE_EQUAL(active.size(), 1U);
        const auto xprv{wallet->GetExtKey(active.begin()->first)};
        BOOST_REQUIRE(xprv);
        const auto expected{DeriveExtKey(*xprv, path)};
        BOOST_REQUIRE(expected);
        BOOST_CHECK(result->first == expected->first);
        BOOST_CHECK(result->second == expected->second);
    

    PraneethGunas commented at 4:39 PM on September 1, 2026:

    Fixed

  18. DrahtBot added the label CI failed on Sep 1, 2026
  19. DrahtBot commented at 12:54 PM on September 1, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/33223334574/job/99831020528</sub> <sub>LLM reason (✨ experimental): CI failed because the IWYU (include-what-you-use) check reported include issues and intentionally exited with a failure.</sub>

    <details><summary>Hints</summary>

    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.

    </details>

  20. PraneethGunas force-pushed on Sep 1, 2026
  21. hebasto commented at 9:21 AM on September 2, 2026: member

    From https://github.com/bitcoin/bitcoin/actions/runs/33532997440/job/100190403622?pr=36070:

    --- a/src/interfaces/wallet.h
    +++ b/src/interfaces/wallet.h
    @@ -34,19 +34,16 @@
     #include <vector>
     
     class ArgsManager;
    -class CKeyID;
    -class CPubKey;
     class CScript;
     class PartiallySignedTransaction;
     class uint256;
     enum class FeeReason;
     enum class OutputType;
     struct bilingual_str;
    +
     namespace wallet {
    -struct CreatedTransactionResult;
     class CCoinControl;
     class CWallet;
    -enum class AddressPurpose;
     struct CRecipient;
     struct WalletContext;
     } // namespace wallet
    
  22. PraneethGunas force-pushed on Sep 9, 2026
  23. DrahtBot removed the label CI failed on Sep 9, 2026
  24. achow101 commented at 11:03 PM on September 9, 2026: member

    Why is SelectHDKey being split into a separate function? This PR should only create one function: DeriveHDKey, and that should be called by RPC. There should not need to be a RPC that is essentially duplicating code of CWallet::DeriveHDKey. This also allows the unit tests to be dropped as the functional tests (can) cover those cases.

  25. PraneethGunas commented at 6:25 PM on September 10, 2026: none

    Why is SelectHDKey being split into a separate function? This PR should only create one function: DeriveHDKey, and that should be called by RPC. There should not need to be a RPC that is essentially duplicating code of CWallet::DeriveHDKey. This also allows the unit tests to be dropped as the functional tests (can) cover those cases.

    It was done so that the the RPC error codes and messages are preserved. Also, looking at the discussions in #35436 (review) it makes sense to collapse the error codes down based on how the errors are logged/displayed #35436 (review)

  26. PraneethGunas force-pushed on Sep 10, 2026
  27. PraneethGunas commented at 8:46 PM on September 10, 2026: none

    Rebased on master and rebuilt the history

  28. DrahtBot added the label Needs rebase on Sep 14, 2026
  29. in src/wallet/wallet.cpp:4598 in cc9db19551 outdated
    4594 | @@ -4593,6 +4595,102 @@ std::optional<CExtKey> CWallet::GetExtKey(const CExtPubKey& xpub) const
    4595 |      return std::nullopt;
    4596 |  }
    4597 |  
    4598 | +util::Expected<std::pair<CExtKey, KeyOriginInfo>, WalletError> CWallet::DeriveHDKey(const std::vector<uint32_t>& path, const std::optional<CExtPubKey>& hdkey) const
    


    pseudoramdom commented at 7:15 PM on September 14, 2026:

    In wallet: Add CWallet::DeriveHDKey -

    There's a lot going on in this commit - extracting to wallet.cpp, changing the RPC error contracts. It would still be beneficial to have 2 commits

    • Extract the wallet method whilst preserving existing behavior, including RPC code.
    • Adopt WalletError, making the error related changes explicit.

    PraneethGunas commented at 10:35 PM on September 15, 2026:

    I've split this into separate commits for better readability


    polespinasa commented at 1:11 PM on September 16, 2026:

    I don't think the current structure is what Ram meant. First commit duplicates code now, and the 2ndo removes it which is confusing. Let me suggest another approach:

    A way to do this in a clean and easily readable way is to first extract the RPC functionality into some helper function inside the rpc/wallet.cpp, then have another move-only commit that moves that helper function into wallet.cpp. And then another one changing the error codes.

    So tldr;

    1. Extract in the same rpc file the functionality of DeriveHDKey into a DeriveHDKey helper function
    2. Change the error codes.
    3. Move the DeriveHDKey function to wallet.cpp.
    4. Add the interface

    This is easy to review because the extracting code is in the same file and does not change much. Both old code and the new function are in the same commit. The change errors is pretty straight forward. The move commit can easily be reviewed with --color-moved=dimmed-zebra

    Left a proposal here: https://github.com/polespinasa/bitcoin/commits/pr-36070/

  30. in src/wallet/test/wallet_interfaces_tests.cpp:1 in 431f0d976d
       0 | @@ -0,0 +1,60 @@
       1 | +// Copyright (c) 2026-present The Bitcoin Core developers
    


    pseudoramdom commented at 7:19 PM on September 14, 2026:

    Now that #35436, can you rebase to that this doesn't look like a new file is added :)


    pseudoramdom commented at 7:19 PM on September 14, 2026:

    i.e. this commit might be easier to review after rebasing on master.


    PraneethGunas commented at 10:36 PM on September 15, 2026:

    Rebased and resolved.

  31. in test/functional/wallet_derivehdkey.py:118 in cc9db19551 outdated
     114 | @@ -115,7 +115,7 @@ def test_multiple_unused_keys(self):
     115 |          master_xpub_1 = wallet.addhdkey()['xpub']
     116 |          master_xpub_2 = wallet.addhdkey()['xpub']
     117 |          assert_raises_rpc_error(
     118 | -            -5,
     119 | +            -4,
    


    pseudoramdom commented at 7:23 PM on September 14, 2026:

    In wallet: Add CWallet::DeriveHDKey

    I'm not sure if we're okay with changing the error contract since derivehdkey will ship in v32. If not, we likely have to duplicate the checks to retain the error codes. @achow101


    PraneethGunas commented at 10:43 PM on September 15, 2026:

    Restored the watch-only and unlock checks in the RPC, so -4 and -13 are unchanged from v32.

    -8 and -5 are now collapsed to -4, or we can duplicate the checks in the RPC to preserve the v32 behaviour.


    polespinasa commented at 10:15 AM on September 16, 2026:

    Haven't reviewed yet, but re changing error codes: #35690 (comment)

  32. pseudoramdom commented at 7:24 PM on September 14, 2026: contributor

    Concept ACK. Left some comments

  33. wallet: Add CWallet::DeriveHDKey
    The derivehdkey RPC selects the HD key and derives it inline, which the GUI
    cannot reach. Move both into the wallet.
    132dd25ad4
  34. wallet: Have derivehdkey use CWallet::DeriveHDKey
    The RPC now consumes CWallet::DeriveHDKey instead of repeating the hardened
    path check, the key selection and the derivation. Those errors now come from
    the wallet and are reported as -4.
    c18cc35876
  35. wallet: Add deriveHDKey interface
    Only the xpub is returned. interfaces::Wallet is not authenticated, so the
    private key is not passed over it.
    462038a1a1
  36. PraneethGunas force-pushed on Sep 15, 2026
  37. DrahtBot removed the label Needs rebase on Sep 16, 2026
  38. DrahtBot added the label CI failed on Sep 18, 2026
  39. DrahtBot closed this on Sep 18, 2026

  40. DrahtBot reopened this on Sep 18, 2026

  41. DrahtBot closed this on Sep 19, 2026

  42. DrahtBot reopened this on Sep 19, 2026

  43. DrahtBot removed the label CI failed on Sep 19, 2026
  44. DrahtBot added the label CI failed on Sep 19, 2026
  45. DrahtBot removed the label CI failed on Sep 19, 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-09-20 21:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me