wallet: Add deriveHDKey interface #36070

pull PraneethGunas wants to merge 3 commits into bitcoin:master from PraneethGunas:derivehdkey-interface changing 8 files +276 −40
  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:

    • Move the HD key selection logic from the derivehdkey RPC into CWallet::SelectHDKey().
    • Update derivehdkey RPC to call CWallet::SelectHDKey(), keeping its argument parsing, guards and error codes in the RPC.
    • Return util::Expected<CExtKey, WalletError> from CWallet::SelectHDKey(), using WalletErrorCode::UnlockNeeded for a locked wallet so callers can prompt for a passphrase and retry.
    • Add CWallet::DeriveHDKey(), which selects the HD key and derives it at the requested path.
    • 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().
  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

    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:

    • #36087 (util: Add and use AssertUnreachable by maflcko)
    • #36074 (scripted-diff: [test] Add util/check.h includes for assertions by maflcko)
    • #35998 (wallet: Handle or explicitly ignore WalletBatch write failures by achow101)
    • #35786 (wallet: drop spent parents redundant cache invalidation and notification by furszy)
    • #35752 (wallet: make encryption state updates atomic by l0rinc)
    • #35436 (wallet: Add addHDkey interface by pseudoramdom)
    • #34681 (wallet: move rescan logic into ChainScanner and wallet/scan by Eunovo)
    • #29278 (Wallet: Add maxfeerate wallet startup option by ismaelsadeeq)

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

  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.

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

    did a first swift review

  14. wallet: Move derivehdkey HD key selection into CWallet
    The GUI will need to select an HD key without going through the RPC, so move
    the selection out of derivehdkey into CWallet::SelectHDKey.
    
    It returns a WalletError so that a caller can tell a locked wallet apart from
    the other failures. The RPC checks for a locked wallet before calling it, so
    everything it gets back maps to RPC_INVALID_ADDRESS_OR_KEY.
    eac794f7ba
  15. wallet: Add CWallet::DeriveHDKey
    Requiring a hardened step means the parent xpub cannot be used to work out the
    sibling keys.
    39bd941d6f
  16. wallet: Add deriveHDKey interface
    Only the xpub is returned. interfaces::Wallet is not authenticated, so private
    keys should not be passed over it.
    6131eab5b9
  17. PraneethGunas force-pushed on Aug 29, 2026
  18. DrahtBot removed the label Needs rebase on Aug 29, 2026
  19. jeanpablojp commented at 4:24 PM on August 29, 2026: contributor

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

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

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-08-31 19:51 UTC

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