wallet: Add importdescriptors interface #34861

pull polespinasa wants to merge 4 commits into bitcoin:master from polespinasa:2026-03-19-importdescriptors changing 6 files +452 −243
  1. polespinasa commented at 9:24 AM on March 19, 2026: member

    This PR adds an interface for importing descriptors.

    The motivation behind this is that currently, importing descriptors is only possible via RPC. Bitcoin Core GUI doesn't use the RPC interface so it cannot offer descriptor import functionality, which is needed to support more complex wallet setups such as multisig.

    This PR also adds a refactor by moving the importdescriptors logic from the RPC layer into CWallet::ImportDescriptor, making it reusable by both the RPC and this new interface.

    The main changes are:

    • Introduces CWallet::ImportDescriptor() containing the core import logic, previously embedded in the RPC ProcessDescriptorImport function.
    • Introduces wallet::ImportDescriptorResult, a new result struct that carries success status, error message, warnings, and a FailureReason enum. The RPC layer uses FailureReason to map results back to the appropriate JSON-RPC error codes, keeping RPC concerns out of CWallet.
    • Updates ProcessDescriptorImport in rpc/backup.cpp to delegate to CWallet::ImportDescriptor.
    • Adds interfaces::Wallet::importDescriptors() as a new interface method, allowing the GUI to import descriptors without going through RPC.

    I have a vibe coded PoC commit for how the GUI could consume it here: https://github.com/polespinasa/bitcoin/commit/d1cb120a1106103234a91e693f6e2d3ba653f5eb It might be useful to cherry-pick the commit to test the interface.

  2. DrahtBot commented at 9:24 AM on March 19, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK Sjors, sedited, stickies-v, rkrux, davidgumberg
    Approach ACK w0xlt
    Stale ACK xyzconstant

    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:

    • #35185 (wallet: fix importdescriptors batch abort on timestamp error by shuv-amp)
    • #35123 (wallet: remove outdated arguments from wallet scanning methods by rkrux)
    • #33392 (wallet, rpc: add UTXO set check and incremental rescan to importdescriptors by musaHaruna)
    • #32861 (Have createwalletdescriptor auto-detect an unused(KEY) by Sjors)
    • #32857 (wallet: allow skipping script paths by Sjors)
    • #32784 (wallet: derivehdkey RPC to get xpub at arbitrary path by Sjors)
    • #31668 (Added rescan option for import descriptors by saikiran57)
    • #29136 (wallet: addhdkey RPC to add just keys to wallets via new unused(KEY) descriptor by achow101)

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

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • // Ranged descrptors should not have a label -> // Ranged descriptors should not have a label [“descrptors” is misspelled]

    <sup>2026-05-01 09:08:21</sup>

  3. polespinasa renamed this:
    wallet, refactor, ipc: Add importdescriptors IPC interface
    wallet, ipc: Add importdescriptors IPC interface
    on Mar 19, 2026
  4. polespinasa force-pushed on Mar 19, 2026
  5. DrahtBot added the label CI failed on Mar 19, 2026
  6. DrahtBot removed the label CI failed on Mar 19, 2026
  7. stickies-v commented at 1:57 PM on March 19, 2026: contributor

    currently, importing descriptors is only possible via RPC. This makes it impossible for the GUI to offer descriptor import functionality,

    Why can a GUI not use the RPC interface?

  8. polespinasa commented at 3:03 PM on March 19, 2026: member

    Why can a GUI not use the RPC interface?

    Sorry, a GUI can. I ment Bitcoin Core GUI needs it because it doesn't use RPC. Will rephrase to make it more clear.

  9. Sjors commented at 3:41 PM on March 19, 2026: member

    Concept ACK

    We'll eventually want a way to import descriptors in the GUI, and moving this code out of src/wallet/rpc is a sensible refactor in any case.

    Let's update the PR description to make it clear this isn't an IPC change. Until #10102 lands it's just an interface change, used internally.

  10. sedited commented at 3:49 PM on March 19, 2026: contributor

    Concept ACK

    Makes sense to move this into the wallet interface, but this does indeed have nothing to do with ipc for now.

  11. polespinasa renamed this:
    wallet, ipc: Add importdescriptors IPC interface
    wallet, ipc: Add importdescriptors interface
    on Mar 19, 2026
  12. polespinasa commented at 4:56 PM on March 19, 2026: member

    Updated the title and description to avoid confusion

  13. polespinasa renamed this:
    wallet, ipc: Add importdescriptors interface
    wallet: Add importdescriptors interface
    on Mar 19, 2026
  14. DrahtBot added the label Wallet on Mar 19, 2026
  15. stickies-v commented at 9:52 AM on March 20, 2026: contributor

    Thanks for the clarification, Concept ACK.

  16. w0xlt commented at 8:08 AM on March 22, 2026: contributor

    Approach ACK

  17. in src/wallet/interfaces.cpp:452 in 5e9dab43fd outdated
     447 | +                        r.error = "Rescan aborted by user.";
     448 | +                        r.reason = wallet::ImportDescriptorResult::FailureReason::MISC_ERROR;
     449 | +                    }
     450 | +                }
     451 | +                return response;
     452 | +            }
    


    rxbryan commented at 9:41 PM on March 22, 2026:
    • Adds interfaces::Wallet::importDescriptors() as a new interface method, allowing the GUI to import descriptors without going through RPC.

    Looking at importDescriptors() , it uses the same scaffolding as the RPC path in src/wallet/rpc/backup.cpp, but RescanFromTime is never called when rescan == true. The RPC calls RescanFromTime before ResubmitWalletTransactions, but the interface only calls ResubmitWalletTransactions. Would importing a descriptor with a historical timestamp via this interface scan for past transactions? The IsAbortingRescan() check also appears unreachable as written, no rescan was started.


    polespinasa commented at 10:03 PM on March 22, 2026:

    Yep you are right, good catch thanks! I tried to keep the same structure to make sure the behavior is as close as possible to the RPC one and for some reason I missed that function call.

    Force pushed to add it.


    rxbryan commented at 11:07 PM on March 22, 2026:

    Looks correct.

  18. polespinasa force-pushed on Mar 22, 2026
  19. in src/wallet/wallet.cpp:578 in 1571cdb005
     574 | @@ -575,6 +575,183 @@ void CWallet::UpgradeDescriptorCache()
     575 |      SetWalletFlag(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED);
     576 |  }
     577 |  
     578 | +wallet::ImportDescriptorResult CWallet::ImportDescriptor(const std::string& descriptor,
    


    rkrux commented at 7:42 AM on March 23, 2026:

    In 1571cdb00517a62769f635e077581c2095289646 "refactor: Add ImportDescriptor to CWallet"

    This is close to a 200 line method being added in a file that's already more than 4500 lines long and hard enough to follow - I don't believe we should add more lengthy code in this file anymore. An alternative can be to add this implementation in a separate file while being in the same wallet namespace.

    Something like below in a src/wallet/descriptor.cpp file, this is a starting point and not a full solution. PR #34681 does a simlar refactoring and can be referred to.

    <details open> <summary>Same method impementation but in a different fle</summary>

    #include <wallet/wallet.h>
    
    namespace wallet {
      ImportDescriptorResult CWallet::ImportDescriptor(const std::string& descriptor,
        bool active,
        const std::optional<bool>& internal,
        const std::optional<std::string>& label,
        int64_t timestamp,
        const std::optional<std::pair<int64_t, int64_t>>& range,
        const std::optional<int64_t>& next_idx)
      {
        AssertLockHeld(cs_wallet);
    
        ImportDescriptorResult result;
    
        // Parse descriptor string
        FlatSigningProvider keys;
        std::string error;
        auto parsed_descs = Parse(descriptor, keys, error, /*require_checksum=*/true);
        if (parsed_descs.empty()) {
            result.error = error;
            result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
            return result;
        }
        if (internal.has_value() && parsed_descs.size() > 1) {
            result.error = "Cannot have multipath descriptor while also specifying 'internal'";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
            return result;
        }
    
        // Range check
        std::optional<bool> is_ranged;
        int64_t range_start = 0, range_end = 1, next_index = 0;
        if (!parsed_descs.at(0)->IsRange() && range.has_value()) {
            result.error = "Range should not be specified for an un-ranged descriptor";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        } else if (parsed_descs.at(0)->IsRange()) {
            if (range.has_value()) {
                range_start = range->first;
                range_end   = range->second + 1;
            } else {
                result.warnings.emplace_back("Range not given, using default keypool range");
                result.used_default_range = true;
                range_start = 0;
                range_end = m_keypool_size;
            }
            next_index  = range_start;
            is_ranged = true;
    
            if (next_idx.has_value()) {
                next_index = next_idx.value();
                if (next_index < range_start || next_index >= range_end) {
                    result.error = "next_index is out of range";
                    result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
                    return result;
                }
            }
        }
    
        // Active descriptors must be ranged
        if (active && !parsed_descs.at(0)->IsRange()) {
            result.error = "Active descriptors must be ranged";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        // Multipath descriptors should not have a label
        if (parsed_descs.size() > 1 && label.has_value()) {
            result.error = "Multipath descriptors should not have a label";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        // Ranged descrptors should not have a label
        if (is_ranged.has_value() && is_ranged.value() && label.has_value()) {
            result.error = "Ranged descriptors should not have a label";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        // Internal descriptors should not have a label
        bool desc_internal = internal.value_or(false);
        if (desc_internal && label.has_value()) {
            result.error = "Internal addresses should not have a label";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        //Combo descriptor check
        if (active && !parsed_descs.at(0)->IsSingleType()) {
            result.error = "Combo descriptors cannot be set to active";
            result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
            return result;
        }
    
        // If the wallet disabled private keys, abort if private keys exist
        if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
            result.error = "Cannot import private keys to a wallet with private keys disabled";
            result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
            return result;
        }
    
        const std::string label_str = label.value_or(std::string{});
    
        for (size_t j = 0; j < parsed_descs.size(); ++j) {
            auto parsed_desc = std::move(parsed_descs[j]);
            if (parsed_descs.size() == 2) {
                desc_internal = j == 1;
            } else if (parsed_descs.size() > 2) {
                CHECK_NONFATAL(!desc_internal);
            }
            // Need to ExpandPrivate to check if private keys are available for all pubkeys
            FlatSigningProvider expand_keys;
            std::vector<CScript> scripts;
            if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) {
                result.error = "Cannot expand descriptor. Probably because of hardened derivations without private keys provided";
                result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
                return result;
            }
            parsed_desc->ExpandPrivate(0, keys, expand_keys);
    
            for (const auto& w : parsed_desc->Warnings()) {
                result.warnings.push_back(w);
            }
    
            // Check if all private keys are provided
            bool have_all_privkeys = !expand_keys.keys.empty();
            for (const auto& entry : expand_keys.origins) {
                const CKeyID& key_id = entry.first;
                CKey key;
                if (!expand_keys.GetKey(key_id, key)) {
                    have_all_privkeys = false;
                    break;
                }
            }
    
            // If private keys are enabled, check some things.
            if (!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
                if (keys.keys.empty()) {
                    result.error = "Cannot import descriptor without private keys to a wallet with private keys enabled";
                    result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
                    return result;
                }
                if (!have_all_privkeys) {
                    result.warnings.emplace_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
                }
            }
    
            WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
    
            // Add descriptor to wallet
            auto spk_manager_res = AddWalletDescriptor(w_desc, keys, label_str, desc_internal);
    
            if (!spk_manager_res) {
                result.error = strprintf("Could not add descriptor '%s': %s", descriptor, util::ErrorString(spk_manager_res).original);
                result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
                return result;
            }
    
            auto& spk_manager = spk_manager_res.value().get();
    
            // Set descriptor as active if necessary
            if (active) {
                if (!w_desc.descriptor->GetOutputType()) {
                    result.warnings.emplace_back("Unknown output type, cannot set descriptor to active.");
                } else {
                    AddActiveScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
                }
            } else {
                if (w_desc.descriptor->GetOutputType()) {
                    DeactivateScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
                }
            }
        }
    
        result.success = true;
        return result;
      }
    }
    

    </details>


    rkrux commented at 1:56 PM on March 24, 2026:

    I've raised an extraction PR #34909 to support this point of mine. It's an attempt to reduce the size of this file.


    achow101 commented at 11:24 PM on March 26, 2026:

    This function does not need to be a member of CWallet. In the interest of not making wallet.cpp any bigger than it already is, I would suggest moving this to a new file, maybe imports.cpp and having it take a CWallet& as an argument.


    davidgumberg commented at 12:55 AM on March 27, 2026:

    One other alternative is to split the stateful part of the function that modifies / queries CWallet from the stateless error parsing stuff


    polespinasa commented at 4:41 PM on April 9, 2026:

    Done :)


    polespinasa commented at 5:08 PM on April 9, 2026:

    Done :)

  20. rkrux changes_requested
  21. rkrux commented at 7:44 AM on March 23, 2026: contributor

    Concept ACK b88462d758e2d24f278607dc5fbd9dcee6c1a076

    I have not reviewed the PR fully but only looked at one portion that was conspicuous in the diff.

  22. davidgumberg commented at 10:39 PM on March 26, 2026: contributor

    Concept ACK, this refactor is a nice win for maintainability, and makes adding importdescriptors to the GUI and/or to an IPC interface or any other interface trivial.

    I could be mistaken as I haven't tried yet to make this change myself, but I think that the RPC code should consume the interface you are adding, because it deduplicates code and increases confidence that the interface added in this PR (which in the current branch is untested) works correctly since we reuse all of our functional tests on it.

    If that's not feasible, the interface commit should probably be held off until the PR that introduces its consumer in the GUI.

  23. in src/wallet/interfaces.cpp:456 in b88462d758 outdated
     451 | +                }
     452 | +                return response;
     453 | +            }
     454 | +        }
     455 | +        return response;
     456 | +    }
    


    davidgumberg commented at 10:56 PM on March 26, 2026:

    Alot of this business logic should be moved out of the interface and into the wallet, the interface functions should probably be as thin as possible.

  24. achow101 commented at 11:22 PM on March 26, 2026: member

    but I think that the RPC code should consume the interface you are adding

    That is not how RPCs access the wallet, and making RPCs access the wallet through this interface is a pretty significant change that is massively outside of the scope of this PR. It would also mean that a bunch of things use the interface, while other things access CWallet directly, which is very weird.


    I think the layout of the commits could be changed to make this easier to review. In general, making a new function that is mostly a copy of another function that will end up replacing the other function makes review more difficult as it does not allow git to show reviewers where things are moved. It also runs the risk of things that are added to the original function not making it into the copy when the PR is rebased, although this is less likely to be an issue for this PR. Since the difference between the 2 functions is primarily in the data types, I would suggest having a first commit that separates ImportDescriptor from ProcessDescriptorImport so that the type changes can be reviewed in that commit. Then having a second commit that moves ImportDescriptor to its new location so that the move diff is minimal.

  25. in src/wallet/interfaces.cpp:388 in b88462d758 outdated
     384 | @@ -373,6 +385,75 @@ class WalletImpl : public Wallet
     385 |      {
     386 |          return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs, n_signed);
     387 |      }
     388 | +    std::vector<wallet::ImportDescriptorResult> importDescriptors(const std::vector<interfaces::ImportDescriptorRequest>& requests) override
    


    achow101 commented at 11:25 PM on March 26, 2026:

    I don't think most of the logic that this interface function is doing needs to be in the interface. It seems like it could probably be pushed into the underlying ImportDescriptors and the RPC refactored so that it can also use this logic rather than duplicating code.

  26. in src/wallet/wallet.cpp:597 in b88462d758
     592 | +    std::string error;
     593 | +    auto parsed_descs = Parse(descriptor, keys, error, /*require_checksum=*/true);
     594 | +    if (parsed_descs.empty()) {
     595 | +        result.error = error;
     596 | +        result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
     597 | +        return result;
    


    achow101 commented at 11:28 PM on March 26, 2026:

    Instead of having errors take up 3 lines, this could be consolidated into a return result.Error() function in a similar way to how ValidationState works.


    polespinasa commented at 5:08 PM on April 9, 2026:

    Nice idea, done! :)

  27. in src/wallet/wallet.cpp:615 in 1571cdb005
     610 | +        result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     611 | +        return result;
     612 | +    } else if (parsed_descs.at(0)->IsRange()) {
     613 | +        if (range.has_value()) {
     614 | +            range_start = range->first;
     615 | +            range_end   = range->second + 1;
    


    davidgumberg commented at 12:01 AM on March 27, 2026:

    dropped a comment:

                range_end = range->second + 1; // Specified range end is inclusive, but we need range end as exclusive
    

    polespinasa commented at 5:13 PM on April 9, 2026:

    Done

  28. in src/wallet/wallet.cpp:633 in 1571cdb005
     628 | +                result.error = "next_index is out of range";
     629 | +                result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     630 | +                return result;
     631 | +            }
     632 | +        }
     633 | +    }
    


    davidgumberg commented at 12:18 AM on March 27, 2026:

    can be simplified (the arg also has to be renamed above)

            next_index = next_index_arg.value_or(range_start);
            is_ranged = true;
            if (next_index < range_start || next_index >= range_end) {
                result.error = "next_index is out of range";
                result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
                return result;
            }
    

    polespinasa commented at 5:19 PM on April 9, 2026:

    nice catch, done :)

  29. davidgumberg commented at 12:54 AM on March 27, 2026: contributor

    One approach to simplifying review here is with a prologue commit to make ProcessDescriptorImport and ImportDescriptor more similar, and squashing the commits where ImportDescriptor is added and where it ProcessDescriptorImport is simplified....something like: https://github.com/bitcoin/bitcoin/compare/master...davidgumberg:bitcoin:2026-03-26-34861-squashed-move (https://github.com/bitcoin/bitcoin/commit/e8f2604372df9bba9f5bf22bfcf7243ebb6546b4)

    This makes most lines more obviously moves if you do:

    git show 7eb1e638ae1e7510bdc0e895ca7cc44e20e5deea --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space
    
  30. davidgumberg commented at 2:39 AM on March 27, 2026: contributor

    That is not how RPCs access the wallet, and making RPCs access the wallet through this interface is a pretty significant change that is massively outside of the scope of this PR.

    I don't think the change would be that significant, here is a rough draft as PoC (like the current pr branch some of the business logic of importDescriptors should probably be move out of the interface code)

    https://github.com/davidgumberg/bitcoin/commit/72b781d066cae3734ef30e4bbbbe66f20b500750 (https://github.com/bitcoin/bitcoin/compare/master...davidgumberg:bitcoin:2026-03-26-34861-rpc-use-interface)

    It seems like the best way to be reasonably confident that the importdescriptors of the wallet's RPC interface and the wallet's GUI interface are equivalent, and moves wallet business logic out of the RPC code.

  31. achow101 commented at 1:29 AM on March 28, 2026: member

    I don't think the change would be that significant

    It makes the RPC mix both of the public interfaces for CWallet, which I think is generally bad. Either all of the RPCs should use the CWallet, or they should all use interfaces::Wallet. Not mixing both, and definitely not mixing them within a single RPC. This constraint is less of a technical one and more on principle to not be mixing up how things are being accessed.

  32. in src/wallet/wallet.cpp:606 in b88462d758
     601 | +        result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
     602 | +        return result;
     603 | +    }
     604 | +
     605 | +    // Range check
     606 | +    std::optional<bool> is_ranged;
    


    xyzconstant commented at 12:37 PM on April 9, 2026:

    In 1571cdb00517a62769f635e077581c2095289646 "refactor: Add ImportDescriptor to CWallet":

    is_ranged has only 2 possible values: true or nullopt (never false), so std::optional<bool> is a bit misleading.

    A plain bool is_ranged{false} would be clearer and simplifies the check at line 650 to if (is_ranged && label.has_value())


    polespinasa commented at 5:25 PM on April 9, 2026:

    You are right, done! This comes from before this PR. It was done this way in backup.cpp.

  33. in src/wallet/wallet.cpp:607 in b88462d758
     602 | +        return result;
     603 | +    }
     604 | +
     605 | +    // Range check
     606 | +    std::optional<bool> is_ranged;
     607 | +    int64_t range_start = 0, range_end = 1, next_index = 0;
    


    xyzconstant commented at 12:40 PM on April 9, 2026:

    nit: next_idx and next_index in the same scope can be confusing. Either renaming the parameter to requested_next_index or the local to resolved_next_index would make their meanings clearer.


    polespinasa commented at 5:30 PM on April 9, 2026:

    Done! Used next_index_arg I liked it more and saw it in @davidgumberg 's comment: #34861 (review)

  34. polespinasa force-pushed on Apr 9, 2026
  35. polespinasa force-pushed on Apr 9, 2026
  36. DrahtBot added the label CI failed on Apr 9, 2026
  37. polespinasa force-pushed on Apr 9, 2026
  38. polespinasa commented at 4:40 PM on April 9, 2026: member

    Force pushed to move the ImportDescriptor(...) function out of CWallet and into a new file imports.h/cpp as suggested by @achow101 @rkrux and @davidgumberg

    https://github.com/bitcoin/bitcoin/compare/b88462d758e2d24f278607dc5fbd9dcee6c1a076..f6671d9ac5cec296fe20b311369b5a134a52d0fa

  39. polespinasa force-pushed on Apr 9, 2026
  40. polespinasa force-pushed on Apr 9, 2026
  41. polespinasa force-pushed on Apr 9, 2026
  42. polespinasa force-pushed on Apr 9, 2026
  43. in src/interfaces/wallet.h:55 in 7bff033c56 outdated
      51 | @@ -51,6 +52,7 @@ struct WalletContext;
      52 |  namespace interfaces {
      53 |  
      54 |  class Handler;
      55 | +struct ImportDescriptorRequest;
    


    w0xlt commented at 6:16 PM on April 9, 2026:

    ImportDescriptorResult can be defined in the public interface. That makes interfaces::Wallet::importDescriptors() usable from interfaces/wallet.h.

    diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h
    index eaf12e620a..83a3b5790b 100644
    --- a/src/interfaces/wallet.h
    +++ b/src/interfaces/wallet.h
    @@ -43,10 +43,32 @@ namespace wallet {
     struct CreatedTransactionResult;
     class CCoinControl;
     class CWallet;
    -struct ImportDescriptorResult;
     enum class AddressPurpose;
     struct CRecipient;
     struct WalletContext;
    +
    +//! Result of importing one descriptor.
    +struct ImportDescriptorResult {
    +    enum class FailureReason {
    +        NONE,
    +        INVALID_DESCRIPTOR,
    +        INVALID_PARAMETER,
    +        WALLET_ERROR,
    +        MISC_ERROR,
    +    };
    +    bool success{false};
    +    bool used_default_range{false};
    +    FailureReason reason{FailureReason::NONE};
    +    std::string error;
    +    std::vector<std::string> warnings;
    +
    +    ImportDescriptorResult& Error(FailureReason r, std::string e)
    +    {
    +        reason = r;
    +        error = std::move(e);
    +        return *this;
    +    }
    +};
     } // namespace wallet
     
     namespace interfaces {
    diff --git a/src/wallet/imports.h b/src/wallet/imports.h
    index fb2e296d5c..7f65afdc6f 100644
    --- a/src/wallet/imports.h
    +++ b/src/wallet/imports.h
    @@ -5,31 +5,10 @@
     #ifndef BITCOIN_WALLET_IMPORTS_H
     #define BITCOIN_WALLET_IMPORTS_H
     
    +#include <interfaces/wallet.h>
     #include <wallet/wallet.h>
     
     namespace wallet{
    -
    -    struct ImportDescriptorResult {
    -        enum class FailureReason {
    -            NONE,
    -            INVALID_DESCRIPTOR,
    -            INVALID_PARAMETER,
    -            WALLET_ERROR,
    -            MISC_ERROR
    -        };
    -        bool success{false};
    -        bool used_default_range{false};
    -        FailureReason reason{FailureReason::NONE};
    -        std::string error;
    -        std::vector<std::string> warnings;
    -
    -        ImportDescriptorResult& Error(FailureReason r, std::string e) {
    -            reason = r;
    -            error = std::move(e);
    -            return *this;
    -        }
    -    };
    -
         ImportDescriptorResult ImportDescriptor(CWallet& wallet, const std::string& descriptor,
             bool active,
             const std::optional<bool>& internal,
    

    polespinasa commented at 6:23 PM on April 9, 2026:

    But then I would need to be accessed by the RPC code. Which I don't have a strong opinion about if that's good or not, but @achow101 and @davidgumberg have been discussing it in this PR (see prior comments).

    In any case I think that rebase is out of the scope for this PR.

  44. DrahtBot removed the label CI failed on Apr 9, 2026
  45. in src/wallet/interfaces.cpp:418 in 7bff033c56 outdated
     413 | +        LOCK(m_wallet->m_relock_mutex);
     414 | +
     415 | +        const int64_t minimum_timestamp = 1;
     416 | +        int64_t now = 0;
     417 | +        int64_t lowest_timestamp = 0;
     418 | +        bool rescan = false;
    


    w0xlt commented at 6:35 PM on April 9, 2026:

    This patch below makes interfaces::Wallet::importDescriptors() consistent with the RPC importdescriptors behavior for locked-wallet imports and incomplete rescans.

    diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp
    index 447a098a95..f468f107b3 100644
    --- a/src/wallet/interfaces.cpp
    +++ b/src/wallet/interfaces.cpp
    @@ -4,6 +4,7 @@
     
     #include <interfaces/wallet.h>
     
    +#include <chain.h>
     #include <common/args.h>
     #include <consensus/amount.h>
     #include <interfaces/chain.h>
    @@ -419,6 +420,17 @@ public:
             {
                 LOCK(m_wallet->cs_wallet);
     
    +            if (m_wallet->IsLocked()) {
    +                wallet::ImportDescriptorResult result;
    +                result.success = false;
    +                result.error = "Error: Please enter the wallet passphrase with walletpassphrase first.";
    +                result.reason = wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR;
    +                for (size_t i = 0; i < requests.size(); ++i) {
    +                    response.push_back(result);
    +                }
    +                return response;
    +            }
    +
                 CHECK_NONFATAL(m_wallet->chain().findBlock(m_wallet->GetLastBlockHash(), FoundBlock().time(lowest_timestamp).mtpTime(now)));
     
                 for (const interfaces::ImportDescriptorRequest& request : requests) {
    @@ -439,7 +451,7 @@ public:
             }
     
             if (rescan) {
    -            m_wallet->RescanFromTime(lowest_timestamp, reserver, /*update=*/true);
    +            const int64_t scanned_time = m_wallet->RescanFromTime(lowest_timestamp, reserver, /*update=*/true);
                 m_wallet->ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     
                 if (m_wallet->IsAbortingRescan()) {
    @@ -453,6 +465,33 @@ public:
                     }
                     return response;
                 }
    +
    +            if (scanned_time > lowest_timestamp) {
    +                for (size_t i = 0; i < requests.size(); ++i) {
    +                    wallet::ImportDescriptorResult& result = response.at(i);
    +                    if (scanned_time <= requests.at(i).timestamp || !result.success) continue;
    +
    +                    result.success = false;
    +                    result.reason = wallet::ImportDescriptorResult::FailureReason::MISC_ERROR;
    +                    result.error = strprintf("Rescan failed for descriptor with timestamp %d. There "
    +                            "was an error reading a block from time %d, which is after or within %d seconds "
    +                            "of key creation, and could contain transactions pertaining to the descriptor. As a "
    +                            "result, transactions and coins using this descriptor may not appear in the wallet.",
    +                            requests.at(i).timestamp, scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW);
    +                    if (m_wallet->chain().havePruned()) {
    +                        result.error += " This error could be caused by pruning or data corruption "
    +                                "(see bitcoind log for details) and could be dealt with by downloading and "
    +                                "rescanning the relevant blocks (see -reindex option and rescanblockchain RPC).";
    +                    } else if (m_wallet->chain().hasAssumedValidChain()) {
    +                        result.error += " This error is likely caused by an in-progress assumeutxo "
    +                                "background sync. Check logs or getchainstates RPC for assumeutxo background "
    +                                "sync progress and try again later.";
    +                    } else {
    +                        result.error += " This error could potentially caused by data corruption. If "
    +                                "the issue persists you may want to reindex (see -reindex option).";
    +                    }
    +                }
    +            }
             }
             return response;
         }
    

    polespinasa commented at 8:57 PM on April 9, 2026:

    You are right, I forgot to include that last part. :)

    I have modified it a bit to make it consistent with the current PR implementation.

  46. polespinasa force-pushed on Apr 9, 2026
  47. polespinasa commented at 9:04 PM on April 9, 2026: member

    Made interfaces::Wallet::importDescriptors() consistent with the RPC importdescriptors behavior for locked-wallet imports and incomplete rescans as suggested in #34861 (review)

    https://github.com/bitcoin/bitcoin/compare/7bff033c566e8293dc8d22ac05402e9cd3fa3a6a..3be7a9a06d5608719ccb3f523cc9ac63b13a1466

  48. polespinasa force-pushed on Apr 10, 2026
  49. polespinasa commented at 1:04 PM on April 10, 2026: member

    @achow101 @davidgumberg as suggested I have extracted all the functionality from the wallet interface and moved it into imports.

    It included huge changes in the RPC code in order to use that code too. Changes can be seen in the diff.

    https://github.com/bitcoin/bitcoin/compare/3be7a9a06d5608719ccb3f523cc9ac63b13a1466..2672bd569a34d3abb386912465e4ad4e47d36661

    I will try to work on a new commit structure soon to make it easier to review..

  50. polespinasa force-pushed on Apr 14, 2026
  51. polespinasa force-pushed on Apr 14, 2026
  52. DrahtBot added the label CI failed on Apr 14, 2026
  53. polespinasa commented at 10:37 PM on April 14, 2026: member

    Force pushed to improve the commit order to make reviewing it easier :)

  54. in src/wallet/imports.cpp:70 in eec8483ae9 outdated
      65 | +        if (parsed_descs.size() > 1 && label.has_value()) {
      66 | +            return result.Error(ImportDescriptorResult::FailureReason::INVALID_PARAMETER,
      67 | +                "Multipath descriptors should not have a label");
      68 | +        }
      69 | +
      70 | +        // Ranged descrptors should not have a label
    


    DrahtBot commented at 9:29 AM on April 15, 2026:

    Possible typos and grammar issues:

    Range descrptors should not have a label -> Range descriptors should not have a label [typo: “descrptors” missing an “i”, which harms comprehension]
    as we didnt import the descriptor in the request. -> as we didn't import the descriptor in the request. [typo/grammar: “dident” should be “didn’t”; missing apostrophe makes the sentence harder to read]

    2026-04-14 22:34:36

  55. polespinasa force-pushed on Apr 15, 2026
  56. polespinasa force-pushed on Apr 15, 2026
  57. polespinasa commented at 1:10 PM on April 15, 2026: member

    Force pushed to fix a merge conflict with master

  58. DrahtBot removed the label CI failed on Apr 15, 2026
  59. xyzconstant commented at 3:02 PM on April 27, 2026: none

    ACK e2ed4a6929

  60. DrahtBot requested review from Sjors on Apr 27, 2026
  61. DrahtBot requested review from stickies-v on Apr 27, 2026
  62. DrahtBot requested review from davidgumberg on Apr 27, 2026
  63. DrahtBot requested review from rkrux on Apr 27, 2026
  64. DrahtBot requested review from sedited on Apr 27, 2026
  65. DrahtBot requested review from w0xlt on Apr 27, 2026
  66. in src/wallet/rpc/backup.cpp:340 in 03f892e6b3
     440 | +            "Wallet is currently rescanning. Abort existing rescan or wait.");
     441 | +        // Fill response with one error for each request
     442 | +        for (size_t i = 0; i < requests.size(); ++i) {
     443 | +            response.push_back(result);
     444 | +        }
     445 | +        return response;
    


    achow101 commented at 11:07 PM on April 28, 2026:

    In 03f892e6b34f7702e4974713317d1619bfee89a1 "wallet: refactor importdescriptors RPC"

    This changes the error that the user gets if there is an existing rescan. Instead of a single RPC failure error, they get an RPC success with the error existing inside the result object which may be surprising/unexpected. Ideally we would preserve the existing behavior for these errors that are outside of the imports.


    polespinasa commented at 8:04 AM on April 29, 2026:

    fixed

  67. in src/wallet/rpc/backup.cpp:362 in 03f892e6b3
     466 | +                "Error: Please enter the wallet passphrase with walletpassphrase first.");
     467 | +            // Fill response with one error for each request
     468 | +            for (size_t i = 0; i < requests.size(); ++i) {
     469 | +                response.push_back(result);
     470 |              }
     471 | +            return response;
    


    achow101 commented at 11:07 PM on April 28, 2026:

    In 03f892e6b34f7702e4974713317d1619bfee89a1 "wallet: refactor importdescriptors RPC"

    Same comment as the rescan error about preserving errors.


    polespinasa commented at 8:04 AM on April 29, 2026:

    fixed

  68. in src/wallet/imports.h:10 in 46bb20e1b5
       5 | +#ifndef BITCOIN_WALLET_IMPORTS_H
       6 | +#define BITCOIN_WALLET_IMPORTS_H
       7 | +
       8 | +#include <wallet/wallet.h>
       9 | +
      10 | +namespace wallet{
    


    achow101 commented at 11:12 PM on April 28, 2026:

    In 46bb20e1b524acdf4277834f549b02f2d7c5f400 "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    nit: space before {

    namespace wallet {
    

    Same in the .cpp file.


    polespinasa commented at 7:25 AM on April 29, 2026:

    done

  69. in src/wallet/imports.h:12 in 46bb20e1b5
       7 | +
       8 | +#include <wallet/wallet.h>
       9 | +
      10 | +namespace wallet{
      11 | +
      12 | +    struct ImportDescriptorResult {
    


    achow101 commented at 11:12 PM on April 28, 2026:

    In 46bb20e1b524acdf4277834f549b02f2d7c5f400 "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    nit: Things within a namespace do not need to be tabbed in one level.

    Same in the .cpp file.


    polespinasa commented at 7:25 AM on April 29, 2026:

    done

  70. polespinasa force-pushed on Apr 29, 2026
  71. polespinasa commented at 8:08 AM on April 29, 2026: member

    Note: This comment is hidden because the solution was wrong.

    ~Added a new global_error optional attribute to ImportDescriptorResult this is different from error as it's a global error in the import descriptor process and not a per-descriptor error. This helps maintaining the old behavior in the RPC call and at the same time is cleaner for the future interfaces as it doesn't make sense to report that error on each descriptor when it affects all of them.~

    ~Also addressed some small nits.~

    $ git diff e2ed4a69291bf3b2ae6f1279bbc45074738b53ea..021236429c8fbb08cfbd619018c4780c884e2e2c
    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 92213e034b..1e09fa6b5d 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -178,12 +178,8 @@ namespace wallet{
             WalletRescanReserver reserver(wallet);
             if (!reserver.reserve(/*with_passphrase=*/true)) {
                 wallet::ImportDescriptorResult result;
    -            result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                "Wallet is currently rescanning. Abort existing rescan or wait.");
    -            // Fill response with one error for each request
    -            for (size_t i = 0; i < requests.size(); ++i) {
    -                response.push_back(result);
    -            }
    +            result.global_error = "Wallet is currently rescanning. Abort existing rescan or wait.";
    +            response.push_back(result);
                 return response;
             }
     
    @@ -200,12 +196,8 @@ namespace wallet{
     
                 if (wallet.IsLocked()) {
                     wallet::ImportDescriptorResult result;
    -                result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                    "Error: Please enter the wallet passphrase with walletpassphrase first.");
    -                // Fill response with one error for each request
    -                for (size_t i = 0; i < requests.size(); ++i) {
    -                    response.push_back(result);
    -                }
    +                result.global_error = "Error: Please enter the wallet passphrase with walletpassphrase first.";
    +                response.push_back(result);
                     return response;
                 }
    
    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index 7144d9c604..8a9ca5d56a 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -240,6 +240,12 @@ RPCMethod importdescriptors()
             import_results = wallet::ProcessDescriptorsImport(wallet, requests);
         }
     
    +    // Wallet-wide precondition failure (e.g. already rescanning, or locked):
    +    // surface as a top-level RPC error, matching the pre-refactor behaviour.
    +    if (!import_results.empty() && import_results[0].global_error.has_value()) {
    +        throw JSONRPCError(RPC_WALLET_ERROR, import_results[0].global_error.value());
    +    }
    +
         size_t result_idx = 0;
         for (size_t i = 0; i < univalue_requests.size(); ++i)
         {
    
    
  72. polespinasa commented at 11:57 AM on April 29, 2026: member

    @achow101 as the functional tests didn't detect that those error where silently being broken I added test coverage here #35179

    Would be nice to have review there and rebase this PR on top of it to make sure it's not being broken in other places.

  73. polespinasa force-pushed on Apr 29, 2026
  74. polespinasa commented at 2:49 PM on April 29, 2026: member

    Added a new global_error optional attribute to ImportDescriptorResult this is different from error as it's a global error in the import descriptor process and not a per-descriptor error. This helps maintaining the old behavior in the RPC call and at the same time is cleaner for the future interfaces as it doesn't make sense to report that error on each descriptor when it affects all of them.

    Also addressed some small nits.

    <details> <summary>git diff</summary>

    $ git diff e2ed4a69291bf3b2ae6f1279bbc45074738b53ea..02c061737cadf6b02f0ed53ed8053dcbf30f6b8b
    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 92213e034b..1dff620298 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -177,13 +177,11 @@ namespace wallet{
     
             WalletRescanReserver reserver(wallet);
             if (!reserver.reserve(/*with_passphrase=*/true)) {
    -            wallet::ImportDescriptorResult result;
    -            result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                "Wallet is currently rescanning. Abort existing rescan or wait.");
    -            // Fill response with one error for each request
    -            for (size_t i = 0; i < requests.size(); ++i) {
    -                response.push_back(result);
    -            }
    +            ImportDescriptorResult result;
    +            result.Error(ImportDescriptorResult::FailureReason::WALLET_ERROR,
    +                "Wallet is currently rescanning. Abort existing rescan or wait.",
    +                /*g_e*/true);
    +            response.push_back(result);
                 return response;
             }
     
    @@ -199,13 +197,12 @@ namespace wallet{
                 LOCK(wallet.cs_wallet);
     
                 if (wallet.IsLocked()) {
    -                wallet::ImportDescriptorResult result;
    -                result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                    "Error: Please enter the wallet passphrase with walletpassphrase first.");
    -                // Fill response with one error for each request
    -                for (size_t i = 0; i < requests.size(); ++i) {
    -                    response.push_back(result);
    -                }
    +                ImportDescriptorResult result;
    +                result.Error(
    +                    ImportDescriptorResult::FailureReason::WALLET_UNLOCK_NEEDED,
    +                    "Error: Please enter the wallet passphrase with walletpassphrase first.",
    +                    /*g_e=*/true);
    +                response.push_back(result);
                     return response;
                 }
     
    diff --git a/src/wallet/imports.h b/src/wallet/imports.h
    index 662a33e8aa..b47072e1a6 100644
    --- a/src/wallet/imports.h
    +++ b/src/wallet/imports.h
    @@ -7,51 +7,58 @@
     
     #include <wallet/wallet.h>
     
    -namespace wallet{
    -
    -    struct ImportDescriptorResult {
    -        enum class FailureReason {
    -            NONE,
    -            INVALID_DESCRIPTOR,
    -            INVALID_PARAMETER,
    -            WALLET_ERROR,
    -            MISC_ERROR
    -        };
    -        bool success{false};
    -        bool used_default_range{false};
    -        FailureReason reason{FailureReason::NONE};
    -        std::string error;
    -        std::vector<std::string> warnings;
    -
    -        ImportDescriptorResult& Error(FailureReason r, std::string e) {
    -            reason = r;
    -            error = std::move(e);
    -            return *this;
    -        }
    -    };
    +namespace wallet {
     
    -    //! Information about a descriptor to be imported.
    -    struct ImportDescriptorRequest {
    -        std::string descriptor;
    -        std::optional<std::string> label;
    -        std::optional<int64_t> timestamp;
    -        std::optional<bool> active;
    -        std::optional<bool> internal;
    -        std::optional<std::pair<int64_t,int64_t>> range;
    -        std::optional<int64_t> next_index;
    +struct ImportDescriptorResult {
    +    enum class FailureReason {
    +        NONE,
    +        INVALID_DESCRIPTOR,
    +        INVALID_PARAMETER,
    +        WALLET_ERROR,
    +        WALLET_UNLOCK_NEEDED,
    +        MISC_ERROR
         };
     
    -    ImportDescriptorResult ImportDescriptor(CWallet& wallet,
    -        const std::string& descriptor,
    -        bool active,
    -        const std::optional<bool>& internal,
    -        const std::optional<std::string>& label,
    -        int64_t timestamp,
    -        const std::optional<std::pair<int64_t, int64_t>>& range,
    -        const std::optional<int64_t>& next_idx) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
    -
    -    std::vector<ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
    -        std::vector<ImportDescriptorRequest> requests);
    +    bool success{false};
    +    bool used_default_range{false};
    +    FailureReason reason{FailureReason::NONE};
    +    std::string error;
    +    std::vector<std::string> warnings;
    +    //! Set to true when a wallet-wide precondition failed before any descriptor was
    +    //! processed (e.g. wallet is already rescanning, or wallet is locked).
    +    //! Callers that support top-level errors should surface this as a
    +    //! top-level / call-wide error rather than a per-descriptor failure.
    +    bool global_error;
    +    ImportDescriptorResult& Error(FailureReason r, std::string e, bool g_e = false) {
    +        reason = r;
    +        error = std::move(e);
    +        global_error = g_e;
    +        return *this;
    +    }
    +};
    +//! Information about a descriptor to be imported.
    +struct ImportDescriptorRequest {
    +    std::string descriptor;
    +    std::optional<std::string> label;
    +    std::optional<int64_t> timestamp;
    +    std::optional<bool> active;
    +    std::optional<bool> internal;
    +    std::optional<std::pair<int64_t,int64_t>> range;
    +    std::optional<int64_t> next_index;
    +};
    +
    +ImportDescriptorResult ImportDescriptor(CWallet& wallet,
    +    const std::string& descriptor,
    +    bool active,
    +    const std::optional<bool>& internal,
    +    const std::optional<std::string>& label,
    +    int64_t timestamp,
    +    const std::optional<std::pair<int64_t, int64_t>>& range,
    +    const std::optional<int64_t>& next_idx) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
    +
    +std::vector<ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
    +    std::vector<ImportDescriptorRequest> requests);
    +
     }
     
     #endif // BITCOIN_WALLET_IMPORTS_H
    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index 7144d9c604..af8e45865c 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -240,6 +240,15 @@ RPCMethod importdescriptors()
             import_results = wallet::ProcessDescriptorsImport(wallet, requests);
         }
     
    +    // Wallet-wide precondition failure (e.g. already rescanning, or locked):
    +    // surface as a top-level RPC error, matching the pre-refactor behaviour.
    +    if (!import_results.empty() && import_results[0].global_error) {
    +        RPCErrorCode rpc_error_code = (import_results[0].reason == ImportDescriptorResult::FailureReason::WALLET_UNLOCK_NEEDED)
    +         ? RPC_WALLET_UNLOCK_NEEDED
    +         : RPC_WALLET_ERROR;
    +        throw JSONRPCError(rpc_error_code, import_results[0].error);
    +    }
    +
         size_t result_idx = 0;
         for (size_t i = 0; i < univalue_requests.size(); ++i)
         {
    
    

    </details>

  75. polespinasa force-pushed on Apr 29, 2026
  76. DrahtBot added the label CI failed on Apr 29, 2026
  77. in src/wallet/imports.h:31 in 635b00bc76
      26 | +    std::vector<std::string> warnings;
      27 | +    //! Set to true when a wallet-wide precondition failed before any descriptor was
      28 | +    //! processed (e.g. wallet is already rescanning, or wallet is locked).
      29 | +    //! Callers that support top-level errors should surface this as a
      30 | +    //! top-level / call-wide error rather than a per-descriptor failure.
      31 | +    bool global_error;
    


    w0xlt commented at 6:01 PM on April 29, 2026:
        bool global_error{false};
    

    This will fix the CI error: runtime error: load of value 170, which is not a valid value for type 'bool' SUMMARY: UndefinedBehaviorSanitizer: invalid-bool-load.

    ImportDescriptorResult result; default-initializes the object. Members with default member initializers are initialized, but bool global_error; has none, so it is default-initialized as a scalar, which performs no initialization. Reading it is undefined behavior.


    polespinasa commented at 6:07 PM on April 29, 2026:

    Done, thanks!

  78. polespinasa force-pushed on Apr 29, 2026
  79. in src/wallet/imports.cpp:260 in 525c5592bc
     255 | +
     256 | +                    // If the descriptor timestamp is within the successfully scanned
     257 | +                    // range, or if the import result already has an error set, let
     258 | +                    // the result stand unmodified. Otherwise replace the result
     259 | +                    // with an error message.
     260 | +                    if (scanned_time <= requests.at(i).timestamp.value() || !result.success) continue;
    


    w0xlt commented at 6:54 PM on April 29, 2026:

    In this PR, "timestamp": "now" is represented as std::nullopt in ImportDescriptorRequest::timestamp (master resolves "now" immediately to an int64_t). So this branch can’t call requests.at(i).timestamp.value(), because there is no value for "now".

    The code needs to use the resolved timestamp computed earlier from std::max(request.timestamp.value_or(now), minimum_timestamp).

    Suggested patch (with a new test to cover this case):

    <details> <summary>diff</summary>

    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 5f9c4ca99b..4a268b3f3d 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -170,6 +170,8 @@ namespace wallet{
             std::vector<wallet::ImportDescriptorRequest> requests)
         {
             std::vector<wallet::ImportDescriptorResult> response;
    +        std::vector<int64_t> resolved_timestamps;
    +        resolved_timestamps.reserve(requests.size());
     
             // Make sure the results are valid at least up to the most recent block
             // the user could have gotten from another RPC command prior to now
    @@ -210,6 +212,7 @@ namespace wallet{
     
                 for (wallet::ImportDescriptorRequest& request : requests) {
                     const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
    +                resolved_timestamps.push_back(timestamp);
                     wallet::ImportDescriptorResult result = wallet::ImportDescriptor(
                         wallet,
                         request.descriptor,
    @@ -257,14 +260,16 @@ namespace wallet{
                         // range, or if the import result already has an error set, let
                         // the result stand unmodified. Otherwise replace the result
                         // with an error message.
    -                    if (scanned_time <= requests.at(i).timestamp.value() || !result.success) continue;
    +                    const int64_t timestamp{resolved_timestamps.at(i)};
    +                    if (scanned_time <= timestamp || !result.success) continue;
    +                    const int64_t error_timestamp{requests.at(i).timestamp.value_or(timestamp)};
     
                         result.success = false;
                         std::string error_msg = strprintf("Rescan failed for descriptor with timestamp %d. There "
                             "was an error reading a block from time %d, which is after or within %d seconds "
                             "of key creation, and could contain transactions pertaining to the desc. As a "
                             "result, transactions and coins using this desc may not appear in the wallet.",
    -                        requests.at(i).timestamp.value(), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW);
    +                        error_timestamp, scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW);
                         if (wallet.chain().havePruned()) {
                             error_msg += strprintf(" This error could be caused by pruning or data corruption "
                                 "(see bitcoind log for details) and could be dealt with by downloading and "
    diff --git a/test/functional/wallet_assumeutxo.py b/test/functional/wallet_assumeutxo.py
    index a8c97be778..9e05028316 100755
    --- a/test/functional/wallet_assumeutxo.py
    +++ b/test/functional/wallet_assumeutxo.py
    @@ -215,12 +215,18 @@ class AssumeutxoTest(BitcoinTestFramework):
             wallet_name = "w1"
             n1.createwallet(wallet_name, disable_private_keys=True)
             key = get_generate_key()
    -        time = n1.getblockchaininfo()['time']
    +        block_info = n1.getblockchaininfo()
    +        time = block_info['time']
    +        def expected_rescan_error(timestamp):
    +            return f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later."
             timestamp = 0
    -        expected_error_message = f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later."
             result = self.import_descriptor(n1, wallet_name, key, timestamp)
             assert_equal(result[0]['error']['code'], -1)
    -        assert_equal(result[0]['error']['message'], expected_error_message)
    +        assert_equal(result[0]['error']['message'], expected_rescan_error(timestamp))
    +        now_timestamp = block_info['mediantime']
    +        result = self.import_descriptor(n1, wallet_name, get_generate_key(), "now")
    +        assert_equal(result[0]['error']['code'], -1)
    +        assert_equal(result[0]['error']['message'], expected_rescan_error(now_timestamp))
     
             self.log.info("Test that rescanning blocks from before the snapshot fails when blocks are not available from the background sync yet")
             w1 = n1.get_wallet_rpc(wallet_name)
    

    </details>


    polespinasa commented at 8:05 PM on April 29, 2026:

    I think there is an easier patch that is just giving request.timestamp the value that std::max(request.timestamp.value_or(now), minimum_timestamp) gets.


    polespinasa commented at 8:26 PM on April 29, 2026:

    I've cherry-picked your test in #35179 with a small modification because it was failing even in master.


    polespinasa commented at 9:14 PM on April 29, 2026:

    I think there is an easier patch that is just giving request.timestamp the value that std::max(request.timestamp.value_or(now), minimum_timestamp) gets.

    Ok, you cannot hehe :)

    I picked you solution patch.

  80. DrahtBot removed the label CI failed on Apr 29, 2026
  81. in src/wallet/imports.h:42 in 26fc2eacdc outdated
      37 | +};
      38 | +//! Information about a descriptor to be imported.
      39 | +struct ImportDescriptorRequest {
      40 | +    std::string descriptor;
      41 | +    std::optional<std::string> label;
      42 | +    std::optional<int64_t> timestamp;
    


    pseudoramdom commented at 8:35 PM on April 29, 2026:

    26fc2eacdcc14aa497610b6a4c3437837c680e8d

    nit: Do we need the timestamp to be optional? It looks like we're overloading nullability - where nullopt means "now" and not absence of timestamp. Since timestamp is required input, can the RPC layer resolve "now" before constructing a ImportDescriptorRequest


    polespinasa commented at 8:27 AM on May 1, 2026:

    now is resolved in ProcessDescriptorsImport this way that logic is shared for all interfaces not only RPC.

    We could make it just a int64_t and make GetImportTimestamp return -1 (can't use 0 because it is a valid timestamp that resolves to 1) if timestamp = "now". But if we do that we are just changing:

    const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
    

    for:

    const int64_t timestamp;
    if (request.timestamp >= 0) {
        timestamp = std::max(request.timestamp, minimum_timestamp);
    } else {
        timestamp = std::max(now, minimum_timestamp);
    }
    

    Given that now is resolved in:

    CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), interfaces::FoundBlock().time(lowest_timestamp).mtpTime(now)));
    

    This populates the variable now with the mtptime of the current best block.

    Also I think using optional is fine, because then interfaces could implement logic to avoid setting a timestamp. "If timestamp is not set, then there's no re-scanning the blockchain".

  82. in src/wallet/imports.cpp:216 in 923dc043c5 outdated
     211 | +            for (wallet::ImportDescriptorRequest& request : requests) {
     212 | +                const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
     213 | +                wallet::ImportDescriptorResult result = wallet::ImportDescriptor(
     214 | +                    wallet,
     215 | +                    request.descriptor,
     216 | +                    request.active.value_or(false),
    


    pseudoramdom commented at 9:07 PM on April 29, 2026:

    923dc043c5c8bb2e47b69c664782d7f3878c84bf

    If we're falling back to false, can we drop the optionality and default it to false in ImportDescriptorRequest?


    polespinasa commented at 8:58 AM on May 1, 2026:

    done

  83. polespinasa force-pushed on Apr 29, 2026
  84. in src/wallet/imports.h:37 in d5811b43fd outdated
      32 | +    ImportDescriptorResult& Error(FailureReason r, std::string e, bool g_e = false) {
      33 | +        reason = r;
      34 | +        error = std::move(e);
      35 | +        global_error = g_e;
      36 | +        return *this;
      37 | +    }
    


    pseudoramdom commented at 10:28 PM on April 29, 2026:

    d5811b43fdc97e950a8aa83a4a069e107b257d5e

    This result type looks quite inconvenient to consume. It's a flat dump of three-mutually exclusive states - per-descriptor success, per-descriptor failure and a call-wide failure (global_error).

    Checking for import_results[0].global_error) is not intuitive looking at the API.

    Could we make the API more explicit by separating call-wide errors from per-descriptor results ? For call-wide error, can we use util::Result

    util::Result<std::vector<ImportDescriptorResult>> ProcessDescriptorsImport(...)
    

    and remove global_error.

    For per-descriptor result, we could do something like

    using ImportDescriptorResult = std::variant<ImportDescriptorSuccess, ImportDescriptorError>
    
    struct ImportDescriptorSuccess {
          bool used_default_range{false};
          std::vector<std::string> warnings;
    };
    
    struct ImportDescriptorError {
          FailureReason reason;
          std::string message;
    };
    

    polespinasa commented at 9:33 AM on May 1, 2026:

    Not really familiar with util::Result but I think the problem with it is that it only takes error messages and not error codes. That's what FailureReason takes care of.

    I think there's no other way to distinguish them than this, which is not clean. I don't like relying on strings to identify errors. (in backup.cpp)

        std::optional<util::Result<std::vector<wallet::ImportDescriptorResult>>> import_results;
        if (!requests.empty()) {
            import_results.emplace(wallet::ProcessDescriptorsImport(wallet, requests));
        }
    
        // Wallet-wide precondition failure (wallet locked or already rescanning):
        // surface as a top-level RPC error, matching the pre-refactor behaviour.
        if (import_results && !*import_results) {
            const std::string error_msg = util::ErrorString(*import_results).original;
    
            const RPCErrorCode rpc_code = error_msg.starts_with("Error: Please enter")
                ? RPC_WALLET_UNLOCK_NEEDED
                : RPC_WALLET_ERROR;
            throw JSONRPCError(rpc_code, error_msg);
        }
    

    polespinasa commented at 10:26 AM on May 1, 2026:

    Maybe I can pick the std::variant idea and do something like:

    using ImportDescriptorResult = std::variant<ImportDescriptorSuccess, ImportDescriptorError>
    using ImportDescriptorsResult = std::variant<std::vector<ImportDescriptorResult>, GeneralImportError>;
    
    struct ImportDescriptorSuccess {
          bool used_default_range{false};
          std::vector<std::string> warnings;
    };
    
    struct ImportDescriptorError {
          FailureReason reason;
          std::string message;
    };
    
    struct GeneralImportError {
        FailureReason reason;
        std::string message;
    }
    
    importDescriptorsResult ProcessDescriptorsImport(...)
    

    Or just add global_error{false} into ImportDescriptorError. It will not fix what you are commenting, but will reduce the three mutual exclusive states to two in one case.

  85. polespinasa force-pushed on May 1, 2026
  86. wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs
    Add two new structs used for creating requests and getting response when importing descriptors.
    
    These structs replace the UniValue-based interface of ProcessDescriptorImport()
    in a subsequent commit.
    677b73fb7e
  87. wallet: refactor importdescriptors RPC
    Rename ProcessDescriptorImport() to ImportDescriptor() and replace its
    UniValue-based arguments and return value with the ImportDescriptorRequest
    and ImportDescriptorResult structs introduced in the previous commit.
    
    Add ProcessDescriptorsImport() to handle wallet locking and rescanning
    over a vector of ImportDescriptorRequest items.
    
    Add ProcessUniValueDescriptor() to translat from the UniValue arguments
    used by the importdescriptors RPC into ImportDescriptorRequest.
    
    This allows a next commit to extract ImportDescriptor() and
    ProcessDescriptorsImport() out of the RPC code so they can be used by
    other future interfaces.
    346fc3d6bd
  88. wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp
    Extract ImportDescriptor() and ProcessDescriptorsImport() out of backup.cpp and move it into imports.cpp so other future interfaces
    can use them without needing to know about RPC code.
    a5463455b0
  89. wallet: Add an importDescriptors() interface for the wallet 16858d988b
  90. polespinasa force-pushed on May 1, 2026
  91. polespinasa commented at 9:09 AM on May 1, 2026: member

    Force pushed to address some nit comments and drop result.used_default_range as it was not used.

  92. DrahtBot added the label CI failed on May 1, 2026
  93. DrahtBot removed the label CI failed on May 1, 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-05-04 00:12 UTC

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