wallet: Add addHDkey interface #35436

pull pseudoramdom wants to merge 4 commits into bitcoin:master from pseudoramdom:addhdkey-interface changing 9 files +199 −34
  1. pseudoramdom commented at 9:20 PM on June 1, 2026: none

    This PR adds a wallet interface for addhdkey.

    The motivation is same as #34861 - while we have addhdkey RPC, Bitcoin Core GUI does not use the RPC interface. Having a dedicated wallet interface is helpful for GUI when performing multisig setup. When used in tandem with a similar interface for derivehdkey (#32784), the GUI can produce a shareable xpub during multisig setup.

    Key changes:

    • Move the wallet logic from the addhdkey RPC into CWallet::AddHDKey().
    • Update addhdkey RPC to call CWallet::AddHDKey() while keeping RPC-specific argument parsing
    • Introduce WalletError, a generic wallet-layer error type carrying a machine-readable WalletErrorCode and a translated user-facing message.
    • Update addhdkey RPC to use the above wallet helper. The RPC also exposes the master fingerprint in hex as fingerprint field.
    • Add interfaces::Wallet::addHDKey(), which generates and adds a new HD key and returns the master xpub.
    • Add unit test coverage for interfaces::Wallet::addHDKey().
  2. DrahtBot added the label Wallet on Jun 1, 2026
  3. DrahtBot commented at 9:20 PM on June 1, 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/35436.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK w0xlt, Sjors, davidgumberg

    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:

    • #35444 (wallet: make descriptor SPKM mutex non-recursive by w0xlt)
    • #35201 (Refactor: Updated signMessage to use util::Expected and removed SigningResult::OK by kevkevinpal)
    • #34909 (wallet, refactor: modularise wallet by extracting out legacy wallet migration by rkrux)
    • #34617 (fees: wallet: remove block policy fee estimator internals from wallet by ismaelsadeeq)
    • #34075 (fees: Introduce Mempool Based Fee Estimation to reduce overestimation by ismaelsadeeq)
    • #32861 (Have createwalletdescriptor auto-detect an unused(KEY) by Sjors)
    • #25722 (refactor: Use util::Result class for wallet loading 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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. DrahtBot added the label CI failed on Jun 1, 2026
  5. DrahtBot commented at 10:42 PM on June 1, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/26782790523/job/78951203063</sub> <sub>LLM reason (✨ experimental): CI failed due to a new circular dependency detected in the lint check (“interfaces/wallet.h -> wallet/hd_keys -> wallet/wallet -> interfaces/wallet”).</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>

  6. pseudoramdom force-pushed on Jun 2, 2026
  7. pseudoramdom force-pushed on Jun 2, 2026
  8. DrahtBot removed the label CI failed on Jun 2, 2026
  9. pseudoramdom marked this as ready for review on Jun 2, 2026
  10. w0xlt commented at 8:58 PM on June 2, 2026: contributor

    Concept ACK

  11. in src/interfaces/wallet.h:108 in edae6b048c
     104 | @@ -102,6 +105,9 @@ class Wallet
     105 |      //! Get public key.
     106 |      virtual bool getPubKey(const CScript& script, const CKeyID& address, CPubKey& pub_key) = 0;
     107 |  
     108 | +    //! Add a new HD key to the wallet. Returns the master fingerprint only.
    


    w0xlt commented at 9:52 PM on June 3, 2026:

    nit: That is the fingerprint of the HD key being added.

        //! Add a new HD key to the wallet. Returns the added key's fingerprint only.
    
  12. in src/wallet/rpc/wallet.cpp:857 in edae6b048c
     853 | @@ -853,6 +854,7 @@ RPCMethod addhdkey()
     854 |          RPCResult{
     855 |              RPCResult::Type::OBJ, "", "",
     856 |              {
     857 | +                {RPCResult::Type::STR_HEX, "fingerprint", "Master key fingerprint as hex"},
    


    w0xlt commented at 9:54 PM on June 3, 2026:

    nit: same as above

                    {RPCResult::Type::STR_HEX, "fingerprint", "Fingerprint of the HD key that was added as hex"},
    
  13. in src/wallet/hd_keys.cpp:54 in edae6b048c
      49 | +    } else {
      50 | +        CKey seed_key = GenerateRandomKey();
      51 | +        hdkey.SetSeed(seed_key);
      52 | +    }
      53 | +
      54 | +    LOCK(wallet.cs_wallet);
    


    w0xlt commented at 6:07 AM on June 11, 2026:

    Consider taking LOCK(wallet.cs_wallet) before the IsLocked() check rather than after key generation. It matches the existing convention in the wallet RPCs, where LOCK(pwallet->cs_wallet) is consistently taken before EnsureWalletIsUnlocked(*pwallet) (e.g. signmessage.cpp, backup.cpp, spend.cpp).

    diff --git a/src/wallet/hd_keys.cpp b/src/wallet/hd_keys.cpp
    index 20dbf647a2..b7be8d246c 100644
    --- a/src/wallet/hd_keys.cpp
    +++ b/src/wallet/hd_keys.cpp
    @@ -35,6 +35,8 @@ bilingual_str AddHDKeyErrorString(AddHDKeyError error)
     
     util::Expected<AddHDKeyResult, AddHDKeyError> AddHDKey(CWallet &wallet, const std::optional<CExtKey> &existing_key)
     {
    +    LOCK(wallet.cs_wallet);
    +
         if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
             return util::Unexpected{AddHDKeyError::PRIVATE_KEYS_DISABLED};
         }
    @@ -51,8 +53,6 @@ util::Expected<AddHDKeyResult, AddHDKeyError> AddHDKey(CWallet &wallet, const st
             hdkey.SetSeed(seed_key);
         }
     
    -    LOCK(wallet.cs_wallet);
    -
         std::string descriptor_str = "unused(" + EncodeExtKey(hdkey) + ")";
         FlatSigningProvider keys;
         std::string error;
    

    pseudoramdom commented at 5:32 PM on June 11, 2026:

    That's a good catch! Probably an oversight when I ported this over from rpc. I'll update it.

  14. pseudoramdom force-pushed on Jun 11, 2026
  15. Sjors commented at 4:14 PM on June 16, 2026: member

    Concept ACK

    Have you considered making the RPC code go through the interface rather than using CWallet directly? As a side-effect, that gives you test coverage.

  16. pseudoramdom commented at 5:06 PM on June 16, 2026: none

    Have you considered making the RPC code go through the interface rather than using CWallet directly?

    I did consider this briefly but my only hesitation was that I couldn't find any precedent for wallet RPCs going through interfaces::Wallet. That said, if we'd like to head in a direction where RPC becomes a thin layer and calls the interface, I'd be happy to do it here.

    As a side-effect, that gives you test coverage.

    Agreed!

  17. Sjors commented at 7:42 AM on June 17, 2026: member

    Some mining RPC calls go through the Mining interface.

    Thoughts @ryanofsky / @achow101?

  18. ryanofsky commented at 11:09 AM on June 17, 2026: contributor

    Thoughts @ryanofsky / @achow101?

    I'm neutral on the idea of having RPC methods call src/interfaces/ class methods. It seems ok to me, but not ideal, because interfaces classes are not supposed to be implementing any real functionality, they are just supposed to be glue code.

    In this case if you look at the WalletImpl::addHDKey method, you can see it just a simple wrapper around the wallet::AddHDKey function.

    So IMO, it is more ideal for the RPC method to just call wallet::AddHDKey. But also ok for it call Wallet::addHDKey.

    I do understand there could be benefits to doing the opposite, like increasing test coverage, enforcing feature parity between IPC and RPC interfaces, or potentially allowing the RPC server to live in a separate process than the process running wallet code. But so far those benefits don't seem as nice as keeping code simpler and removing a level of indirection.

    I'm fine with either approach though, and it also seems ok to have different approaches for different areas of code.

  19. Sjors commented at 1:15 PM on June 17, 2026: member

    We could also get the extra code coverage by having (at least) one unit test go through the interface. And then avoid the indirection in RPC code until we actually want to split it off into a separate process.

  20. pseudoramdom commented at 10:42 PM on June 18, 2026: none

    Thanks for the input @Sjors & @ryanofsky. Based on @achow101's comment on another PR, having the RPC call the interface feels out of scope for this PR. Instead, I added a unit test to cover the interface path.

  21. in src/wallet/rpc/wallet.cpp:871 in a63b6024b8 outdated
     865 | @@ -864,18 +866,11 @@ RPCMethod addhdkey()
     866 |              std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
     867 |              if (!wallet) return UniValue::VNULL;
     868 |  
     869 | -            if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
     870 | -                throw JSONRPCError(RPC_WALLET_ERROR, "addhdkey is not available for wallets without private keys");
     871 | -            }
     872 | +            std::optional<CExtKey> existing_key;
     873 |  
     874 | -            EnsureWalletIsUnlocked(*wallet);
    


    davidgumberg commented at 1:07 AM on June 19, 2026:

    https://github.com/bitcoin/bitcoin/pull/35436/changes/a63b6024b89425b2706e9f3bb3419ebb32df5f76 (wallet, test: Refactor addhdkey RPC to use wallet::AddHDKey helper)


    Maybe not important, but just calling out: there might be a bit of a behavior change here since now the wallet unlock check happens after the DecodeExtPubKey() check.


    pseudoramdom commented at 5:51 AM on June 24, 2026:

    Good catch. But since addhdkey has not been included in a release yet, I don’t think this is a compatibility concern, if that's what you're hinting at.


    achow101 commented at 9:36 PM on June 24, 2026:

    EnsureWalletIsUnlocked should not be removed, even if the underlying function checks it too.

  22. in src/wallet/rpc/wallet.cpp:885 in a63b6024b8 outdated
     890 | -            CHECK_NONFATAL(!descs.empty());
     891 | -            WalletDescriptor w_desc(std::move(descs.at(0)), GetTime(), 0, 0, 0);
     892 | -            if (wallet->GetDescriptorScriptPubKeyMan(w_desc) != nullptr) {
     893 | -                throw JSONRPCError(RPC_WALLET_ERROR, "HD key already exists");
     894 | +                existing_key = hdkey;
     895 |              }
    


    davidgumberg commented at 1:18 AM on June 19, 2026:

    https://github.com/bitcoin/bitcoin/pull/35436/changes/a63b6024b89425b2706e9f3bb3419ebb32df5f76 (wallet, test: Refactor addhdkey RPC to use wallet::AddHDKey helper)


    Feel free to disregard style suggestion:

    --- a/src/wallet/rpc/wallet.cpp
    +++ b/src/wallet/rpc/wallet.cpp
    @@ -869,9 +869,9 @@ RPCMethod addhdkey()
                 std::optional<CExtKey> existing_key;
    
                 if (!request.params[0].isNull()) {
    -                CExtKey hdkey = DecodeExtKey(request.params[0].get_str());
    +                existing_key = DecodeExtKey(request.params[0].get_str());
    
    -                if (!hdkey.key.IsValid()) {
    +                if (!existing_key || !existing_key->key.IsValid()) {
                         // Check if the user gave us an xpub and give a more descriptive error if so
                         CExtPubKey xpub = DecodeExtPubKey(request.params[0].get_str());
                         if (xpub.pubkey.IsValid()) {
    @@ -880,8 +880,6 @@ RPCMethod addhdkey()
                             throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Could not parse HD key");
                         }
                     }
    -
    -                existing_key = hdkey;
                 }
    
                 auto res = wallet::AddHDKey(*wallet, existing_key);
    
    

    pseudoramdom commented at 4:44 PM on June 24, 2026:

    Took this feedback in 21b04fa32ee3dd544fe186bd71bc22fe5defdcc5

  23. davidgumberg commented at 1:55 AM on June 19, 2026: contributor

    Concept ACK, I suggested something similar about having the RPC code call the interface in #34861. (https://github.com/bitcoin/bitcoin/pull/34861#issuecomment-4138778665)

    I tend to think having RPC code call src/interface functions and leaning on all of the existing functional tests makes a little bit more sense than adding unit tests that exercise the interface and look like/duplicate functional tests.

    I think if it became the typical style of RPC code to call interfaces:

    • There would be less business logic in the RPC code in general
    • Non-RPC callers like the GUI can have confidence that the interface is well tested.
    • PR's like this that add an interface with the intent of having it used by a consumer that doesn't exist yet have an immediate consumer that can be dogfed the new interface.

    Separately, I wonder if it might be better to squash commits one and two, just a matter of personal preference but I think this makes it easier to see that commit 1 (wallet: Introduce AddHDKey helper in wallet) + commit 2 (wallet, test: Refactor addhdkey RPC to use wallet::AddHDKey helper) = mostly a move-only change.

    E.g. https://github.com/bitcoin/bitcoin/compare/master...davidgumberg:bitcoin:2026-06-18-addhdkey-squash / https://github.com/davidgumberg/bitcoin/commit/cb9074faa3b8c709c80461ec83b9b879dc8295d6

    Here I also tweak AddHDKey a little bit to more closely match the RPC code where reasonable, and reviewing with --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change you can see that the change is mostly just moving code.

  24. ryanofsky commented at 11:29 AM on June 19, 2026: contributor

    re: #35436 (comment)

    • There would be less business logic in the RPC code in general

    I guess the point I am trying to make is that src/wallet/interfaces.cpp and src/node/interfaces.cpp should not contain any business logic. Ideally they would not even have any control flow, and would just be whitelists of wallet and node functions that are ok to call externally. Benefits of using interfaces as an extra hop between RPC code and wallet code may exist, but I think are more minor than others seem to think.

  25. in src/wallet/hd_keys.cpp:21 in 963b519473
      16 | +#include <string>
      17 | +#include <vector>
      18 | +
      19 | +namespace wallet {
      20 | +
      21 | +bilingual_str AddHDKeyErrorString(AddHDKeyError error)
    


    Sjors commented at 6:39 PM on June 23, 2026:

    In 963b5194737052c3fb7d8104e6cd2fab0cc370a2 wallet: Introduce AddHDKey helper in wallet: the first two errors are generic wallet RPC failures, so I wouldn't make them AddHDKeyError specific.


    Sjors commented at 6:46 PM on June 23, 2026:

    I would leave these first two checks to the caller, document the requirement in the header and assert here.


    pseudoramdom commented at 11:05 PM on June 23, 2026:

    I would leave these first two checks to the caller, document the requirement in the header and assert here.

    While I agree that WALLET_LOCKED and WALLET_FLAG_DISABLE_PRIVATE_KEYS are not addHDKey specific errors (in the sense that other wallet operations can fail for the same reason), addHDKey is a stateful operation on the wallet, and the operation cannot be performed with either of these states. So I think it is reasonable for them to be part of the operation’s failure contract.

    Also, I’d prefer this to remain a recoverable error that GUI or IPC callers can act on, rather than assertion failures. Otherwise a caller that forgets to check whether the wallet is unlocked could turn a normal wallet state into a process abort.

    If the concern is specifically that these generic wallet states should not live in AddHDKeyError, maybe a shared wallet-level error type could be considered.

    <details> <summary>details</summary>

    enum class WalletError {
        PRIVATE_KEYS_DISABLED,
        WALLET_LOCKED,
    };
    
    enum class AddHDKeyError {
        DUPLICATE_KEY,
        DESCRIPTOR_ADD_FAILED,
    };
    
    using AddHDKeyFailure = std::variant<WalletError, AddHDKeyError>;
    
    util::Expected<AddHDKeyResult, AddHDKeyFailure> AddHDKey(
          CWallet& wallet,
          const std::optional<CExtKey>& existing_key);
    

    </details>


    Sjors commented at 8:00 AM on June 24, 2026:

    Otherwise a caller that forgets to check whether the wallet is unlocked could turn a normal wallet state into a process abort.

    That would imply missing test coverage. On the RPC side we can easily cover every assert, but GUI doesn't have much automated testing which is indeed a problem.

    The issue I have with AddHDKeyFailure is that implies we have to create a unique struct for every RPC call (or for every method in the wallet). The two failure reasons you have left could just be error strings.

    Expanding WalletError with PRIVATE_KEYS_DISABLED and WALLET_LOCKED does seem fine, as it might be reusable in other RPC methods.

    I still think it's better to just have both the GUI and RPC call perform the (mostly familiar) guards.


    pseudoramdom commented at 4:43 PM on June 24, 2026:

    That would imply missing test coverage. On the RPC side we can easily cover every assert, but GUI doesn't have much automated testing which is indeed a problem.

    Yes, most of my concern here is from the GUI perspective :) On the RPC side I agree the guards are familiar and easy to cover with tests, but GUI paths have much less coverage.

    Error strings work well enough for RPC responses, where the main consumer is the user. But for GUI/IPC callers, I’d prefer not to rely only on strings. Strings are user-facing, translated, don't offer type safety and not a stable contract for programmatic handling.

    I also agree we should avoid growing a bespoke error hierarchy for every wallet method. Looking at https://github.com/bitcoin/bitcoin/pull/34861/changes#diff-c43409a71c57e16b203417c045c8982bf658af8a0db7c210c35f86d8acefcc36R13 in #34861, we should probably fold them all to one WalletError? If so, I should probably introduce that in a separate PR?

    (And thanks for the review 🙏)

  26. in src/wallet/hd_keys.cpp:69 in 963b519473
      64 | +        return util::Unexpected{AddHDKeyError::DUPLICATE_KEY};
      65 | +    }
      66 | +
      67 | +    auto spkm = wallet.AddWalletDescriptor(wallet_descriptor, keys, /*label=*/"", /*internal=*/false);
      68 | +    if(!spkm) {
      69 | +        return util::Unexpected{AddHDKeyError::DESCRIPTOR_ADD_FAILED};
    


    Sjors commented at 6:48 PM on June 23, 2026:

    963b5194737052c3fb7d8104e6cd2fab0cc370a2: it seems problematic to fail without reason.


    pseudoramdom commented at 11:14 PM on June 23, 2026:

    Good catch. Didn't realize wallet.AddWalletDescriptor returns a util::Result. We shouldn't be swallowing the underlying error here. I'll see if I can preserve it.

  27. Sjors commented at 6:54 PM on June 23, 2026: member

    Having looked at this a bit more, and while trying to base #32784 on it, I think the problem is that hd_keys.{h.cpp} is too RPC-ish. I left more concrete suggestions above.

  28. pseudoramdom force-pushed on Jun 24, 2026
  29. pseudoramdom commented at 5:39 AM on June 24, 2026: none

    Separately, I wonder if it might be better to squash commits one and two, just a matter of personal preference but I think this makes it easier to see that commit 1 (wallet: Introduce AddHDKey helper in wallet) + commit 2 (wallet, test: Refactor addhdkey RPC to use wallet::AddHDKey helper) = mostly a move-only change.

    Force-pushed to squash the helper/RPC changes, keep the interface work separate. Also updated the error handling to preserve the underlying descriptor-add failure reason to address #35436 (review)

  30. in src/wallet/test/hd_keys_tests.cpp:8 in 18825b7ca8
       0 | @@ -0,0 +1,46 @@
       1 | +// Copyright (c) 2026-present The Bitcoin Core developers
       2 | +// Distributed under the MIT software license, see the accompanying
       3 | +// file COPYING or https://www.opensource.org/licenses/mit-license.php.
       4 | +
       5 | +#include <interfaces/wallet.h>
       6 | +#include <test/util/setup_common.h>
       7 | +#include <wallet/context.h>
       8 | +#include <wallet/hd_keys.h>
    


    w0xlt commented at 8:06 PM on June 24, 2026:

    Currently, a caller that includes only <interfaces/wallet.h> and uses Wallet::addHDKey() can fail to compile unless it also includes <wallet/hd_keys.h>.

    The reason is that addHDKey() is declared as:

    virtual util::Expected<wallet::Fingerprint, wallet::AddHDKeyError> addHDKey() = 0;
    

    But wallet::AddHDKeyError is only forward-declared in interfaces/wallet.h.

    Because util::Expected stores the error type, callers need the complete definition of AddHDKeyError when they instantiate or inspect the result.

    If the intent is for callers to use the wallet interface by including only <interfaces/wallet.h>, a possible fix is to move the public AddHDKey result and error types into a lightweight public header and include that from interfaces/wallet.h.

    Suggested patch:

    <details> <summary>diff</summary>

    diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h
    index 993a0f694f..8930d2b120 100644
    --- a/src/interfaces/wallet.h
    +++ b/src/interfaces/wallet.h
    @@ -18,6 +18,7 @@
     #include <util/fs.h>
     #include <util/result.h>
     #include <util/ui_change_type.h>
    +#include <wallet/hd_key_types.h>
     #include <wallet/types.h>
     
     #include <cstdint>
    @@ -43,7 +44,6 @@ namespace node {
     enum class TransactionError;
     } // namespace node
     namespace wallet {
    -struct AddHDKeyError;
     struct CreatedTransactionResult;
     class CCoinControl;
     class CWallet;
    diff --git a/src/wallet/hd_key_types.h b/src/wallet/hd_key_types.h
    new file mode 100644
    index 0000000000..b8bea0ba41
    --- /dev/null
    +++ b/src/wallet/hd_key_types.h
    @@ -0,0 +1,34 @@
    +// Copyright (c) 2026-present The Bitcoin Core developers
    +// Distributed under the MIT software license, see the accompanying
    +// file COPYING or http://www.opensource.org/licenses/mit-license.php.
    +
    +#ifndef BITCOIN_WALLET_HD_KEY_TYPES_H
    +#define BITCOIN_WALLET_HD_KEY_TYPES_H
    +
    +#include <util/translation.h>
    +#include <wallet/types.h>
    +
    +#include <string>
    +
    +namespace wallet {
    +
    +struct AddHDKeyResult {
    +    Fingerprint fingerprint;
    +    std::string xpub;
    +};
    +
    +enum class AddHDKeyErrorCode {
    +    PRIVATE_KEYS_DISABLED,
    +    WALLET_LOCKED,
    +    DUPLICATE_KEY,
    +    DESCRIPTOR_ADD_FAILED
    +};
    +
    +struct AddHDKeyError {
    +    AddHDKeyErrorCode code;
    +    bilingual_str message;
    +};
    +
    +} // namespace wallet
    +
    +#endif // BITCOIN_WALLET_HD_KEY_TYPES_H
    diff --git a/src/wallet/hd_keys.h b/src/wallet/hd_keys.h
    index a4baedc730..29fdb7cff2 100644
    --- a/src/wallet/hd_keys.h
    +++ b/src/wallet/hd_keys.h
    @@ -7,33 +7,14 @@
     
     #include <key.h>
     #include <util/expected.h>
    -#include <util/translation.h>
    -#include <wallet/types.h>
    +#include <wallet/hd_key_types.h>
     
     #include <optional>
    -#include <string>
     
     namespace wallet {
     
     class CWallet;
     
    -struct AddHDKeyResult {
    -    Fingerprint fingerprint;
    -    std::string xpub;
    -};
    -
    -enum class AddHDKeyErrorCode {
    -    PRIVATE_KEYS_DISABLED,
    -    WALLET_LOCKED,
    -    DUPLICATE_KEY,
    -    DESCRIPTOR_ADD_FAILED
    -};
    -
    -struct AddHDKeyError {
    -    AddHDKeyErrorCode code;
    -    bilingual_str message;
    -};
    -
     util::Expected<AddHDKeyResult, AddHDKeyError> AddHDKey(CWallet& wallet, const std::optional<CExtKey>& existing_key);
     
     } // namespace wallet
    diff --git a/src/wallet/test/hd_keys_tests.cpp b/src/wallet/test/hd_keys_tests.cpp
    index 219a5870ad..485d64dd5b 100644
    --- a/src/wallet/test/hd_keys_tests.cpp
    +++ b/src/wallet/test/hd_keys_tests.cpp
    @@ -5,7 +5,6 @@
     #include <interfaces/wallet.h>
     #include <test/util/setup_common.h>
     #include <wallet/context.h>
    -#include <wallet/hd_keys.h>
     #include <wallet/test/util.h>
     #include <wallet/wallet.h>
    

    </details>


    pseudoramdom commented at 9:32 PM on June 25, 2026:

    I removed Fingerprint and forward decl for AddHDKeyError. So we should no longer need this?

  31. in src/wallet/hd_keys.h:35 in 21b04fa32e
      30 | +};
      31 | +
      32 | +struct AddHDKeyError {
      33 | +    AddHDKeyErrorCode code;
      34 | +    bilingual_str message;
      35 | +};
    


    achow101 commented at 9:10 PM on June 24, 2026:

    In 21b04fa32ee3dd544fe186bd71bc22fe5defdcc5 "wallet: Extract addhdkey RPC to a helper in wallet"

    I don't think it is necessary to be adding an error code enum or a this error struct, it unnecessarily complicates the interface and requires interface users to include yet more headers (likely accidentally crossing interface boundaries), or end up with more code duplication.

  32. in src/wallet/hd_keys.h:37 in 21b04fa32e
      32 | +struct AddHDKeyError {
      33 | +    AddHDKeyErrorCode code;
      34 | +    bilingual_str message;
      35 | +};
      36 | +
      37 | +util::Expected<AddHDKeyResult, AddHDKeyError> AddHDKey(CWallet& wallet, const std::optional<CExtKey>& existing_key);
    


    achow101 commented at 9:10 PM on June 24, 2026:

    In 21b04fa32ee3dd544fe186bd71bc22fe5defdcc5 "wallet: Extract addhdkey RPC to a helper in wallet"

    It's unclear to me why a new code enum and error struct are necessary, istm this could use util::Result.


    pseudoramdom commented at 9:54 PM on June 24, 2026:

    As mentioned in this comment (https://github.com/bitcoin/bitcoin/pull/35436#discussion_r3468762211), util::Result works well when the main consumer is RPC, but less so for GUI/IPC callers. GUI should not rely on error strings to make programmatic decisions. For example if GUI received a WALLET_LOCKED, it may want to show the passphrase alert. Typed errors help with that, hence util::Expected


    pseudoramdom commented at 9:57 PM on June 24, 2026:

    That said I'm leaning towards the idea of removing AddHDKeyErrorCode and expanding to a more generic WalletErrorCode & WalletError that other methods (like import descriptors) can also use. We can start with what we have in RPC_WALLET_* errors and map to them?


    achow101 commented at 10:02 PM on June 24, 2026:

    We should not be adding these for every new function. That is unsustainable.


    ryanofsky commented at 10:36 PM on June 24, 2026:

    re: #35436 (review)

    We should not be adding these for every new function. That is unsustainable.

    Yeah ideal thing would be to add minimum number of enum values & types needed to provide a good GUI experience. Should avoid having enum values that would only be used for display or logging.

    Looking at the current types:

    • Maybe AddHDKeyResult could be renamed to something like HDKeyId and be a more generic struct used other places?
    • Unclear if AddHDKeyErrorCode is necessary. If it really is necessary maybe it should be a more generic KeyError type that is not specific to this method.
    • Also if it is necessary, it could be a use-case for #25665 which would allow util::Result<HDKeyId, KeyError> and avoid the need for a AddHDKeyError struct.

    pseudoramdom commented at 10:53 PM on June 24, 2026:

    Maybe AddHDKeyResult could be renamed to something like HDKeyId and be a more generic struct used other places

    Good idea

    Yeah ideal thing would be to add minimum number of enum values & types needed to provide a good GUI experience. Should avoid having enum values that would only be used for #25308 (comment).

    Unclear if AddHDKeyErrorCode is necessary. If it really is necessary maybe it should be a more generic KeyError type that is not specific to this method.

    After ingesting all the feedback, I agree with this. I can only think of WALLET_LOCKED being a useful error for the GUI. The rest can be an assert or folded to a single error type if needed.

    That said, we'll probably need a more general purpose WalletError or KeyError that the interface callers can consume.

    Also if it is necessary, it could be a use-case for #25665 which would allow util::Result<HDKeyId, KeyError> and avoid the need for a AddHDKeyError struct.

    At quick glance, it looks great. It's what I wish util::Result should've been :)


    davidgumberg commented at 1:31 AM on June 25, 2026:

    More generic error types like WalletError / KeyError seems like a good idea.

    I'm kind of just reiterating what @pseudoramdom and others have said but:

    It seems smart to be conservative and only give unique error codes to the kinds of errors that a caller can do something interesting/useful with, e.g. WALLET_LOCKED and prompting the user for the password. Any error states that aren't actionable on by the caller in any interesting way will probably just end up being handed off to the end-user, so they probably don't deserve unique error codes/states and a generic code + string will suffice. (don't assert)


    Sjors commented at 7:12 AM on June 25, 2026:

    There are some conditions that should never happen if the code is correct, which includes WALLET_LOCKED. We probably shouldn't use assert for them, but in RPC land we have CHECK_NON_FATAL and the GUI can also throw non-fatal errors with an arbitrary message.

    That's why I don't think it belongs in an enum that generally has messages of things that are expected to happen.


    pseudoramdom commented at 5:30 PM on June 25, 2026:

    the GUI can also throw non-fatal errors with an arbitrary message

    For GUI, I think WALLET_LOCKED is better treated as an actionable operation failure. Even if the GUI checked isLocked() before calling, the state can change before the callee mutates the wallet, especially across async UI. And if the callee detects that, the GUI should be able to prompt for passphrase and retry, not parse or display an error message.

    There are some conditions that should never happen if the code is correct

    If we are designing an API that may be used over IPC, the caller code we are assuming to be correct is outside the interface boundary. Shouldn’t the callee be defensive here? :)


    achow101 commented at 5:37 PM on June 25, 2026:

    the state can change before the callee mutates the wallet, especially across async UI

    Then callers should hold m_relock_mutex to prevent the wallet from being locked while they are doing something.


    davidgumberg commented at 12:22 AM on June 26, 2026:

    There are some conditions that should never happen if the code is correct, which includes WALLET_LOCKED. We probably shouldn't use assert for them, but in RPC land we have CHECK_NON_FATAL and the GUI can also throw non-fatal errors with an arbitrary message.

    The check has to exist in the called function anyways, and returning an error is a contract that's enforced by the API: where expecting the caller to have done something and not enforcing it in any way other than a CHECK_NON_FATAL is just hoping no-one ever does the wrong thing. I think the current approach in the PR strikes a good balance between no error codes and the other extreme of every interface getting it's own error enum with unique members for each possible error.


    Sjors commented at 4:16 PM on July 1, 2026:

    I like the new approach in 090edbb635fed4006274c868e463ce98d1e6f9d1 of expanding WalletError / WalletErrorCode. This seems reusable.

    I only suggest splitting it for easier review, see Sjors/bitcoin/2026/07/addhdkey. That branch also adds a test for unlocking behavior, and a WalletErrorCode for methods that require a wallet with private keys.


    pseudoramdom commented at 5:52 PM on July 1, 2026:

    Thanks, I'll try to include the suggestions.

    Re: WALLET_PRIVATE_KEYS_DISABLED in https://github.com/Sjors/bitcoin/commit/c4be2be526a14bc2142cd2b583ae5ff644a57c02

    //! The wallet has private keys disabled (WALLET_FLAG_DISABLE_PRIVATE_KEYS) //! but the operation requires them. Callers may present the accompanying //! message explaining that the operation is unavailable for this wallet.

    While I prefer granular errors, I don't foresee this being an error that the GUI might need to act on. GUI should ideally not perform an operation on a wallet if it doesn't have private keys. It's something that's in the control of the GUI, unlike WALLET_UNLOCK_NEEDED.
    Let me know if I'm missing something. Happy to consider it otherwise.

  33. in src/wallet/hd_keys.h:5 in 21b04fa32e
       0 | @@ -0,0 +1,41 @@
       1 | +// Copyright (c) 2026-present The Bitcoin Core developers
       2 | +// Distributed under the MIT software license, see the accompanying
       3 | +// file COPYING or http://www.opensource.org/licenses/mit-license.php.
       4 | +
       5 | +#ifndef BITCOIN_WALLET_HD_KEYS_H
    


    achow101 commented at 9:31 PM on June 24, 2026:

    In 21b04fa32ee3dd544fe186bd71bc22fe5defdcc5 "wallet: Extract addhdkey RPC to a helper in wallet"

    Naming this file "hd_keys" implies to me that it is doing more than it actually does. In actuality, AddHDKey is almost entirely unrelated to doing any HD key operations, it is doing entirely wallet operations which happen to involve having a HD key. I would prefer this to be in a file named imports.{h/cpp} or something similar.


    pseudoramdom commented at 9:46 PM on June 24, 2026:

    Fair enough. I was hoping deriveHDKey would also end up in this file. Would wallet_hd_keys.{h/cpp} be better?


    pseudoramdom commented at 9:32 PM on June 25, 2026:

    Moved to CWallet

  34. in src/wallet/hd_keys.cpp:78 in 21b04fa32e
      73 | +    desc_spkm.GetWalletDescriptor().descriptor->GetPubKeys(pubkeys, extpubs);
      74 | +    CHECK_NONFATAL(pubkeys.empty());
      75 | +    CHECK_NONFATAL(extpubs.size() == 1);
      76 | +
      77 | +    Fingerprint fingerprint;
      78 | +    std::copy_n(hdkey.key.GetPubKey().GetID().begin(), 4, fingerprint.begin());
    


    achow101 commented at 9:33 PM on June 24, 2026:

    In 21b04fa32ee3dd544fe186bd71bc22fe5defdcc5 "wallet: Extract addhdkey RPC to a helper in wallet"

    At this point, we should add a function to CExtPubKey to get the fingerprint instead of copying the same couple lines over and over.


    davidgumberg commented at 1:03 AM on June 25, 2026:

    pseudoramdom commented at 9:34 PM on June 25, 2026:

    Removed fingerprint in c18b9162a166bd7e3c7981429bd8b40a1d01c5b3

  35. in src/wallet/types.h:47 in 21b04fa32e
      43 | @@ -42,6 +44,8 @@ struct CreatedTransactionResult
      44 |              : tx(_tx), fee(_fee), fee_calc(_fee_calc), change_pos(_change_pos) {}
      45 |  };
      46 |  
      47 | +using Fingerprint = std::array<unsigned char, 4>;
    


    achow101 commented at 9:34 PM on June 24, 2026:

    In 21b04fa32ee3dd544fe186bd71bc22fe5defdcc5 "wallet: Extract addhdkey RPC to a helper in wallet"

    This should be in src/pubkey.h next to CExtPubKey.


    davidgumberg commented at 12:04 AM on June 25, 2026:

    +1 here is a commit that does that and changes CExtPubKey and CPubKey to use Fingerprint: https://github.com/bitcoin/bitcoin/commit/2bbfc0a7ddb0f5c1951b2979cc965e32effd8c50

  36. davidgumberg commented at 1:15 AM on June 25, 2026: contributor

    I'm not sure about adding fingerprint to the RPC or to the interface.

    I'm assuming the motivation is to be able to show fingerprints in the GUI but I'm not sure if/when that's a good idea given that fingerprints can be collided and this is probably not interesting for RPC users. More importantly computing this doesn't require wallet state so adding this to the interface is not necessary when a helper function will do.

    If it does stay in this PR, it should still get a helper, and the Fingerprint type should be used by the various C.*Key.* types, and the commit should probably be separate where that is added as an RPC return, here is a branch that looks like that:

    https://github.com/bitcoin/bitcoin/compare/master...davidgumberg:bitcoin:35436-alt

  37. Sjors commented at 7:16 AM on June 25, 2026: member

    @davidgumberg some hardware wallets do show the fingerprint to users (e.g. ColdCard), and some software wallets (e.g. Sparrow) use it in their UX. See https://coldcard.com/docs/ultra-quick/

    Especially in multisig it's useful debugging info. It's not intended for adversarial scenarios, for that you need things like displaying the address.

  38. pseudoramdom commented at 4:07 PM on June 25, 2026: none

    I'm not sure about adding fingerprint to the RPC or to the interface.

    We'll most likely need the fingerprint. It'll be good to give a handle on the key that was generated so that the GUI can perform operations on it like deriveHDKey(fingerprint, path). Fingerprints are safer than sharing master xpubs.

    If it does stay in this PR, it should still get a helper, and the Fingerprint type should be used by the various C.Key. types, and the commit should probably be separate where that is added as an RPC return, here is a branch that looks like that: https://github.com/bitcoin/bitcoin/compare/master...davidgumberg:bitcoin:35436-alt

    Thanks! Will do that 📝

  39. davidgumberg commented at 6:06 PM on June 25, 2026: contributor

    Even if the fingerprint is needed, which I think is an orthogonal discussion I don't mean to dismiss the possibility that it's useful in some scenarios, but this doesn't need to happen over an interface, it can be computed locally from the xpub without the help of the wallet.

  40. pseudoramdom commented at 6:20 PM on June 25, 2026: none

    Even if the fingerprint is needed, which I think is an orthogonal discussion I don't mean to dismiss the possibility that it's useful in some scenarios, but this doesn't need to happen over an interface, it can be computed locally from the xpub without the help of the wallet.

    After checking derivehdkey PR #32784 , it looks like derivehdkey takes an xpub. I'll remove the fingerprint from this PR. Will maybe add the helper in a separate PR if GUI needs to show one.

  41. pseudoramdom force-pushed on Jun 25, 2026
  42. pseudoramdom force-pushed on Jun 25, 2026
  43. pseudoramdom commented at 9:21 PM on June 25, 2026: none

    Force-pushed to c18b9162a166bd7e3c7981429bd8b40a1d01c5b3 to update the approach based on review feedback

    • Moved the addhdkey wallet logic into CWallet::AddHDKey(). Removed hd_keys.{h, cpp}.
    • Introduced WalletError as a generic wallet-layer error type
    • Replaced the addhdkey-specific result/error types with util::Expected<CExtPubKey, WalletError>. It no longer returns the fingerprint.
  44. in src/wallet/types.h:60 in c18b9162a1 outdated
      55 | +
      56 | +struct WalletError {
      57 | +    //! Machine-readable error code for callers that need programmatic handling.
      58 | +    WalletErrorCode code;
      59 | +    //! User-facing translated error message
      60 | +    bilingual_str message;
    


    pseudoramdom commented at 9:23 PM on June 25, 2026:

    @achow101 Let me know if this is not the right place to have the WalletError struct

  45. test: expand addhdkey coverage for locked wallet and bad key
    Add functional coverage for two addhdkey paths that were previously
    untested:
    - an encrypted, locked wallet returns RPC_WALLET_UNLOCK_NEEDED (-13),
      and succeeds once the wallet is unlocked;
    - an unparseable extended key string returns "Could not parse HD key".
    
    This pins the existing RPC behavior before the addhdkey logic is moved
    into CWallet::AddHDKey().
    08de750a11
  46. wallet: Move addhdkey logic into CWallet
    Move wallet related logic from the addhdkey RPC into
    CWallet::AddHDKey(), and make the RPC call the wallet method.
    
    The wallet method returns a `util::Result<CExtPubKey>`, carrying the
    master xpub on success and a translated error message on failure. The
    RPC continues to enforce the unlocked-wallet precondition via
    EnsureWalletIsUnlocked() and maps any wallet error to RPC_WALLET_ERROR.
    
    Review hint:
    
        git show --color-moved=dimmed-zebra \
                 --color-moved-ws=allow-indentation-change
    fa83b4ffd0
  47. wallet: Introduce WalletError with machine-readable error code
    Introduce WalletError as a generic wallet-layer error type that can
    carry a machine-readable WalletErrorCode and a translated user-facing
    message.
    
    Change CWallet::AddHDKey() to return a `util::Expected<CExtPubKey,
    WalletError>` and distinguish the locked-wallet case with a dedicated
    WALLET_UNLOCK_NEEDED code, so non-RPC callers can react to it
    programmatically. The addhdkey RPC maps WALLET_UNLOCK_NEEDED to
    RPC_WALLET_UNLOCK_NEEDED and any other error to RPC_WALLET_ERROR.
    d5529428bc
  48. wallet: Add addhdkey interface
    Expose addHDKey through interfaces::Wallet and add unit coverage for the
    interface path.
    3a926acf64
  49. in src/wallet/wallet.cpp:3878 in 090edbb635
    3873 | +
    3874 | +    std::string desc_str = "unused(" + EncodeExtKey(hdkey) + ")";
    3875 | +    FlatSigningProvider keys;
    3876 | +    std::string error;
    3877 | +    std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_str, keys, error, /*require_checksum=*/false);
    3878 | +    CHECK_NONFATAL(!descs.empty());
    


    davidgumberg commented at 11:43 PM on June 25, 2026:

    this should probably return an error, since descs.at(0) in the next line will throw an exception anyways.


    davidgumberg commented at 11:44 PM on June 25, 2026:

    c++ exceptions when they happen to an RPC call look like:

    error code: -1
    error message:
    vector::_M_range_check: __n (which is 0) >= this->size() (which is 0)
    

    davidgumberg commented at 11:48 PM on June 25, 2026:

    I have no idea what would happen if the wallet process threw an exception while processing an interface call today / in a multiprocess world.


    Sjors commented at 3:50 PM on July 1, 2026:

    CHECK_NONFATAL relies on being caught, so it's not a good idea to use it outside of RPC code.


    pseudoramdom commented at 5:27 PM on July 1, 2026:

    Got it, I should've read the docs for CHECK_NONFATAL. TIL I'll update this (and the check for extpubs) to throw a WalletError to stay consistent.

  50. pseudoramdom force-pushed on Jul 1, 2026
  51. pseudoramdom commented at 6:21 PM on July 1, 2026: none

    Force-pushed to 3a926acf6498300aee6df62425a0fb1e15d36e1a

    • Take @Sjors 's commit for extra test coverage
    • The move from RPC to CWallet and introduction of WalletError are separate commits now.

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-07-02 09:51 UTC

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