wallet: Add importdescriptors interface #34861

pull polespinasa wants to merge 12 commits into bitcoin:master from polespinasa:2026-03-19-importdescriptors changing 15 files +619 −263
  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 GUI menu here: https://github.com/polespinasa/bitcoin/pull/7 so it can be tested. I will open a PR against the main GUI repo, once this gets merged.

  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.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34861.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

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

    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:

    • #36143 (descriptor: add CreateMultisigDescriptor() by rxbryan)
    • #36133 (wallet: store multipath descriptor by Sjors)
    • #36087 (util: Add and use AssertUnreachable by maflcko)
    • #36074 (scripted-diff: [test] Add util/check.h includes for assertions by maflcko)
    • #35998 (wallet: Handle or explicitly ignore WalletBatch write failures by achow101)
    • #35989 (wallet: fix crash on importdescriptors with a range ending at 2^31-1 by shuv-amp)
    • #35752 (wallet: make encryption state updates atomic by l0rinc)
    • #35436 (wallet: Add addHDkey interface by pseudoramdom)
    • #35377 (wallet: Allow importing of descriptors without private keys when the wallet has the private keys by achow101)
    • #34681 (wallet: move rescan logic into ChainScanner and wallet/scan by Eunovo)
    • #31668 (Added rescan option for import descriptors by saikiran57)
    • #29278 (Wallet: Add maxfeerate wallet startup option by ismaelsadeeq)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • passing a timestamp bellow \0`->passing a timestamp below `0`` [misspelling]
    • Returns the import timestamp in in64_t -> Returns the import timestamp in int64_t [misspelled type name]
    • // ExpandPrivate to whether the descriptor can be derived at the first index. -> // Expand to determine whether the descriptor can be derived at the first index. [broken English; intent is unclear]

    <sup>2026-09-04 08:59:07</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.


    rkrux commented at 10:12 AM on June 8, 2026:

    I assume this review comment is resolved now?

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

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


    ViniciusCestarii commented at 1:21 PM on May 15, 2026:

    I believe it would help to add a short comment documenting that nullopt timestamp means "use the current best block's MTP time" rather than "no timestamp".

  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.


    pseudoramdom commented at 8:51 PM on May 20, 2026:

    Sorry for the late reply. I've been meaning to get to this. You're right util::Result may not work here. But.. looks like there's a util::Expected. Internally it uses std::variant. So it takes the same shape as ImportDescriptorsResult idea, but it has useful API for has_value(), error().

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

    The change in backup.cpp might read slightly cleaner?

    if (!import_results) {
        const auto& err = import_results.error();
        const RPCErrorCode rpc_code = err.reason == FailureReason::WALLET_UNLOCK_NEEDED
              ? RPC_WALLET_UNLOCK_NEEDED
              : RPC_WALLET_ERROR;
        throw JSONRPCError(rpc_code, err.message);
    }
    
  85. polespinasa force-pushed on May 1, 2026
  86. polespinasa force-pushed on May 1, 2026
  87. 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.

  88. DrahtBot added the label CI failed on May 1, 2026
  89. DrahtBot removed the label CI failed on May 1, 2026
  90. rxbryan commented at 4:39 PM on May 9, 2026: none

    Looking at extending this pattern to getdescriptorinfo. This would let the GUI and anything downstream consume structured descriptor objects directly, especially Sjors' BIP-388 work.

  91. DrahtBot added the label Needs rebase on May 13, 2026
  92. ViniciusCestarii commented at 8:38 PM on May 14, 2026: contributor

    Bringing the discussion from #35181

    "wallet: refactor importdescriptors RPC (346fc3d6bd508370b5e299d1044d8de4a03f6d93)" applies the breaking change behavior discussed: a missing or invalid timestamp field now returns a per-item error instead of throwing the whole RPC, because ProcessUniValueDescriptor timestamps exceptions are catched and added as a per-item error instead of surfacing as a top-level RPC error, matching the pre-refactor behaviour.

    As @polespinasa raised in #35181 what's the correct approach: Is it worth fixing the timestamp error per query, instead of throwing if a single timestamp is wrong? It's a breaking change.

  93. in src/wallet/imports.h:22 in 677b73fb7e outdated
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    bool success{false};
      22 | +    FailureReason reason{FailureReason::NONE};
    


    ViniciusCestarii commented at 1:41 PM on May 15, 2026:

    677b73fb7e0eda69d4b897dbedaec7c4f2258aed

    bool success is redundant if we already have reason. This also remove the risk of desyncing success and reason (the default constructor already start these member variables out of sync: success = false but reason = NONE).


    polespinasa commented at 4:21 PM on May 18, 2026:

    I am not sure. FailureReason tells you the reason why it failed, but not if it failed. FailureReason::NONE should not be used, it's just an initializer, but it's does not mean that it succeed.

    But after your comment I am thinking on making FailureReason an std::optional and drop FailureReason::NONE. I don't know if it is worth the handling of optional tho.


    ViniciusCestarii commented at 4:55 PM on May 18, 2026:

    I mean FailureReason::NONE already encodes success: if there's no failure reason, the operation succeeded. You could drop the boolean success and add an IsSuccess() function to the struct that just checks reason == NONE.


    polespinasa commented at 11:07 AM on May 28, 2026:

    Solved

  94. polespinasa force-pushed on May 18, 2026
  95. polespinasa commented at 4:09 PM on May 18, 2026: member

    Rebased on top of master

  96. polespinasa force-pushed on May 18, 2026
  97. DrahtBot added the label CI failed on May 18, 2026
  98. DrahtBot removed the label CI failed on May 18, 2026
  99. DrahtBot removed the label Needs rebase on May 18, 2026
  100. in src/wallet/imports.cpp:43 in 6ed501fe7e
      38 | +            return result.Error(ImportDescriptorResult::FailureReason::INVALID_PARAMETER,
      39 | +                "Range should not be specified for an un-ranged descriptor");
      40 | +        } else if (parsed_descs.at(0)->IsRange()) {
      41 | +            if (range.has_value()) {
      42 | +                range_start = range->first;
      43 | +                range_end = range->second + 1; // Specified range end is inclusive, but we need range end as exclusive
    


    pseudoramdom commented at 9:08 PM on May 20, 2026:

    nit: This is probably minor if this is the only place we do this. A safer way would be to define a type that has this behavior baked in.

    struct DescriptorRange {
      int64_t begin;  // inclusive
      int64_t end;    // exclusive
    
      static DescriptorRange FromInclusive(int64_t first, int64_t last) {
        return {first, last + 1};
      }
    };
    

    We can call DescriptorRange::FromInclusive(r.first, r.second) in backup.cpp


    polespinasa commented at 8:31 AM on May 28, 2026:

    Leaving at is for know, it can be done in a follow up. I rather keep this PR focused on only refactoring to allow the new interface.

  101. in src/wallet/imports.h:14 in 6ed501fe7e
       9 | +
      10 | +namespace wallet {
      11 | +
      12 | +struct ImportDescriptorResult {
      13 | +    enum class FailureReason {
      14 | +        NONE,
    


    pseudoramdom commented at 9:09 PM on May 20, 2026:

    This case will go away if you go with the util::expected suggestion.


  102. in src/wallet/imports.h:21 in bd29190bdd
      16 | +        INVALID_PARAMETER,
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    bool success{false};
    


    achow101 commented at 7:16 PM on May 25, 2026:

    In bd29190bddfe159755e380707c7a08c1539cf90a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    success should be synonmous with FailureReason::None. FailureReason could be renamed to ImportResultCode (or something like that) and used as a result code rather than just indicating failure types.


    polespinasa commented at 11:07 AM on May 28, 2026:

    Done

  103. in src/wallet/rpc/backup.cpp:168 in da866a3eae
     172 | +    const std::optional<bool>& internal,
     173 | +    const std::optional<std::string>& label,
     174 | +    int64_t timestamp,
     175 | +    const std::optional<std::pair<int64_t, int64_t>>& range,
     176 | +    const std::optional<int64_t>& next_index_arg) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
     177 | +{
    


    achow101 commented at 7:35 PM on May 25, 2026:

    In da866a3eaeb6bb8c3bb4c2d23e1ae13c45a7b200 "wallet: refactor importdescriptors RPC"

    Why doesn't this take a ImportDescriptorRequest? Most of the arguments are the members of the struct.


    polespinasa commented at 8:50 AM on May 28, 2026:

    Done

  104. in src/wallet/rpc/backup.cpp:433 in da866a3eae
     448 | +                // range, or if the import result already has an error set, let
     449 | +                // the result stand unmodified. Otherwise replace the result
     450 | +                // with an error message.
     451 | +                const int64_t timestamp{resolved_timestamps.at(i)};
     452 | +                if (scanned_time <= timestamp || !result.success) continue;
     453 | +                const int64_t error_timestamp{requests.at(i).timestamp.value_or(timestamp)};
    


    achow101 commented at 7:40 PM on May 25, 2026:

    In da866a3eaeb6bb8c3bb4c2d23e1ae13c45a7b200 "wallet: refactor importdescriptors RPC"

    I think this is unnecessary and timestamp could be used wherever error_timestamp is. timestamp is already the result of requests.at(i).timestamp.value_or().


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

    error_timestamp does not have the same value as timestamp. This is because resolved_timestamps stores the "corrected" timestamp while error_timestamp stores the original timestamp request.

    The edge case here that makes this two variables needed is if the user enters a descriptor with timestamp=0 which is below our minimum_timestamp=1. So error_timestamp=0 while timestamp=1 because of std::max(request.timestamp.value_or(now), minimum_timestamp);.

    Our test coverage already tests that in the wallet_assumeutxo.py.

    <details> <summary>See error logs after change</summary>

     build/test/functional/wallet_assumeutxo.py 
    2026-05-28T11:13:49.371236Z TestFramework (INFO): PRNG seed is: 8610115919761428835
    2026-05-28T11:13:49.422134Z TestFramework (INFO): Initializing test directory /tmp/bitcoin_func_test_iedpmzw6
    2026-05-28T11:13:51.223312Z TestFramework (INFO): -- Testing assumeutxo
    2026-05-28T11:13:51.226248Z TestFramework (INFO): Creating a UTXO snapshot at height 299
    2026-05-28T11:13:51.741927Z TestFramework (INFO): Loading snapshot into second node from /tmp/bitcoin_func_test_iedpmzw6/node0/regtest/utxos.dat
    2026-05-28T11:13:51.894492Z TestFramework (INFO): Backup from the snapshot height can be loaded during background sync
    2026-05-28T11:13:51.903778Z TestFramework (INFO): Backup from before the snapshot height can't be loaded during background sync
    2026-05-28T11:13:51.911199Z TestFramework (INFO): Backup from the snapshot height can be loaded during background sync (pruned node)
    2026-05-28T11:13:52.068538Z TestFramework (INFO): Backup from before the snapshot height can't be loaded during background sync (pruned node)
    2026-05-28T11:13:52.078886Z TestFramework (INFO): Test loading descriptors during background sync
    2026-05-28T11:13:52.113878Z TestFramework (ERROR): Unexpected exception:
    Traceback (most recent call last):
      File "/home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/test/functional/test_framework/test_framework.py", line 143, in main
        self.run_test()
      File "/home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/build/test/functional/wallet_assumeutxo.py", line 223, in run_test
        assert_equal(result[0]['error']['message'], expected_error_message)
      File "/home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/test/functional/test_framework/util.py", line 83, in assert_equal
        raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
    AssertionError: not(Rescan failed for descriptor with timestamp 1. There was an error reading a block from time 1296688654, 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. == Rescan failed for descriptor with timestamp 0. There was an error reading a block from time 1296688654, 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.)
    2026-05-28T11:13:52.165566Z TestFramework (INFO): Not stopping nodes as test failed. The dangling processes will be cleaned up later.
    2026-05-28T11:13:52.165864Z TestFramework (WARNING): Not cleaning up dir /tmp/bitcoin_func_test_iedpmzw6
    2026-05-28T11:13:52.165975Z TestFramework (ERROR): Test failed. Test logging available at /tmp/bitcoin_func_test_iedpmzw6/test_framework.log
    2026-05-28T11:13:52.166217Z TestFramework (ERROR): 
    2026-05-28T11:13:52.166485Z TestFramework (ERROR): Hint: Call /home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/test/functional/combine_logs.py '/tmp/bitcoin_func_test_iedpmzw6' to consolidate all logs
    2026-05-28T11:13:52.166590Z TestFramework (ERROR): 
    2026-05-28T11:13:52.166676Z TestFramework (ERROR): If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.
    2026-05-28T11:13:52.166817Z TestFramework (ERROR): https://github.com/bitcoin/bitcoin/issues
    2026-05-28T11:13:52.166994Z TestFramework (ERROR): 
    [node 3] Cleaning up leftover process
    [node 2] Cleaning up leftover process
    [node 1] Cleaning up leftover process
    [node 0] Cleaning up leftover process
    
    

    </details>


    achow101 commented at 6:29 PM on May 28, 2026:

    I'm pretty sure allowing a timestamp of 0 doesn't have a negative effect. The default value for the field that the timestamp ends up in is 0 already. We should disallow invalid timestamps (e.g. negatives), not rewriting them into a valid value.


    polespinasa commented at 2:27 PM on May 31, 2026:

    Maybe I am not following what you say, but I don't think it can ever be 0 as we pick the max between 1 and the mtp time. So 1 is the minimum timestamp.

    The only reason we keep that 0 value is because error_timestamp is the "user input" timestamp and it is only used for error message purpose. resolved_timestamps stores the "real" value of timestamps used in each import. So in case the user did not specify a timestamp we can use it on error logging.

    In case what you mean is that we should be able to lower the minimum timestamp to 0 instead of 1. I would say that is out of scope for this. I would rather keep this PR in the scope of refactoring and allowing a new interface. Behavior changes can be done in a separate PR.


    achow101 commented at 11:08 PM on June 2, 2026:

    I mean that we can lower the minimum to 0, and I think that doing so would be in scope for this PR as it makes many things simpler, including the interface.

    If you disagree, I think the interface should at least not have to take the timestamp as a separate parameter and a different solution is necessary to preserve the current behavior but also pass in the resolved timestamp in the request.


    polespinasa commented at 8:05 PM on June 3, 2026:

    Lowered to 0

  105. in src/wallet/imports.cpp:311 in 41dda70864 outdated
     306 | +                }
     307 | +            }
     308 | +        }
     309 | +        return response;
     310 | +    }
     311 | +}
    


    achow101 commented at 7:44 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    The closing bracket of a namespace needs to have a comment telling us what namespace that bracket is closing.

    } // namespace wallet
    

    polespinasa commented at 8:51 AM on May 28, 2026:

    done

  106. in src/wallet/imports.cpp:9 in 41dda70864
       0 | @@ -0,0 +1,311 @@
       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 | +#include <chain.h>
       6 | +#include <wallet/imports.h>
       7 | +
       8 | +namespace wallet{
       9 | +    ImportDescriptorResult ImportDescriptor(CWallet& wallet, const std::string& descriptor,
    


    achow101 commented at 7:45 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    Generally things inside of a namespace do not need to be indented. Unindenting all of the moved lines in this file also lets git detect that these lines were moved without using --color-moved-ws.


    polespinasa commented at 8:52 AM on May 28, 2026:

    done

  107. in src/wallet/imports.cpp:219 in 41dda70864
     214 | +        int64_t now = 0;
     215 | +        int64_t lowest_timestamp = 0;
     216 | +        bool rescan = false;
     217 | +        {
     218 | +            LOCK(wallet.cs_wallet);
     219 | +
    


    achow101 commented at 7:52 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    nit: whitespace introduced in move commit


    polespinasa commented at 8:53 AM on May 28, 2026:

    done

  108. in src/wallet/imports.cpp:212 in 41dda70864
     207 | +        }
     208 | +
     209 | +        // Ensure that the wallet is not locked for the remainder of this call,
     210 | +        // as the passphrase is used to top up the keypool.
     211 | +        LOCK(wallet.m_relock_mutex);
     212 | +
    


    achow101 commented at 7:53 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    nit: whitespace introduced in move


    polespinasa commented at 8:53 AM on May 28, 2026:

    done

  109. in src/wallet/rpc/backup.cpp:383 in da866a3eae
     391 | +
     392 | +        CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), interfaces::FoundBlock().time(lowest_timestamp).mtpTime(now)));
     393 | +
     394 | +        for (ImportDescriptorRequest& request : requests) {
     395 | +            const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
     396 | +            resolved_timestamps.push_back(timestamp);
    


    achow101 commented at 7:56 PM on May 25, 2026:

    In da866a3eaeb6bb8c3bb4c2d23e1ae13c45a7b200 "wallet: refactor importdescriptors RPC"

    Instead of storing the resolved timestamp separately, it seems like it would be easier to overwrite the timestamp in the struct, and then pass the whole struct into ImportDescriptor. This would also resolve some of my comments below about timestamp handling during rescan.


    polespinasa commented at 11:22 AM on May 28, 2026:

    For similar reasons to #34861 (review) this cannot be done. There is also some test coverage missing for the timestamps that I added here: #35179 Would be good if that gets merged prior to this PR so the CI tests that we are not missing anything.

    See also #34861 (review) for context

  110. in src/wallet/imports.cpp:188 in 41dda70864
     183 | +
     184 | +        result.success = true;
     185 | +        return result;
     186 | +    }
     187 | +
     188 | +    std::vector<wallet::ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
    


    achow101 commented at 7:58 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    wallet:: namespace specifiers are not necessary when everything in the file is already in the wallet namespace.


    polespinasa commented at 8:54 AM on May 28, 2026:

    done

  111. polespinasa force-pushed on May 28, 2026
  112. polespinasa force-pushed on May 28, 2026
  113. DrahtBot added the label CI failed on May 28, 2026
  114. DrahtBot commented at 11:29 AM on May 28, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/26570718078/job/78277655091</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy reported a readability-avoid-const-params-in-decls error (const-qualified parameter in declaration, e.g. wallet/imports.h:48) and treated it as a failure.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  115. DrahtBot removed the label CI failed on May 28, 2026
  116. in src/wallet/imports.cpp:246 in 53a0e162e5 outdated
     241 | +    if (rescan) {
     242 | +        const int64_t scanned_time = wallet.RescanFromTime(lowest_timestamp, reserver);
     243 | +        wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     244 | +
     245 | +        if (wallet.IsAbortingRescan()) {
     246 | +            // Mark all previously successful imports as aborted
    


    w0xlt commented at 8:15 PM on June 3, 2026:

    The current code as of 53a0e162e531f2393b63517515d3522ff90afac1 changes importdescriptors behavior when abortrescan is called during the rescan.

    On master, the RPC fails with a top-level RPC_MISC_ERROR: Rescan aborted by user.

    On this branch, the RPC returns a normal response array with per-descriptor failures instead.

    Is this intentional?

    If not, here is a suggestion to preserve the existing behavior. If it is intentional, it should probably be documented in the release notes, since some clients may rely on the previous behavior.

    <details> <summary>diff</summary>

    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index a46a6b7094..4ef81adc91 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -243,13 +243,12 @@ std::vector<ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
             wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     
             if (wallet.IsAbortingRescan()) {
    -            // Mark all previously successful imports as aborted
    -            for (ImportDescriptorResult& r : response) {
    -                if (r.result_code == ImportDescriptorResult::ImportResultCode::OK) {
    -                    r.Error(ImportDescriptorResult::ImportResultCode::MISC_ERROR,
    -                        "Rescan aborted by user.");
    -                }
    -            }
    +            ImportDescriptorResult result;
    +            result.Error(ImportDescriptorResult::ImportResultCode::MISC_ERROR,
    +                "Rescan aborted by user.",
    +                /*g_e=*/true);
    +            response.clear();
    +            response.push_back(result);
                 return response;
             }
     
    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index cf08e52cf7..be83d55b3e 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -242,9 +242,17 @@ RPCMethod importdescriptors()
         // 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].result_code == ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED)
    -         ? RPC_WALLET_UNLOCK_NEEDED
    -         : RPC_WALLET_ERROR;
    +        RPCErrorCode rpc_error_code{RPC_WALLET_ERROR};
    +        switch (import_results[0].result_code) {
    +            case ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED:
    +                rpc_error_code = RPC_WALLET_UNLOCK_NEEDED;
    +                break;
    +            case ImportDescriptorResult::ImportResultCode::MISC_ERROR:
    +                rpc_error_code = RPC_MISC_ERROR;
    +                break;
    +            default:
    +                break;
    +        }
             throw JSONRPCError(rpc_error_code, import_results[0].error);
         }
     
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 3e8b05a28d..9af9c4f809 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -789,6 +789,20 @@ class ImportDescriptorsTest(BitcoinTestFramework):
     
             assert_equal(temp_wallet.getbalance(), encrypted_wallet.getbalance())
     
    +        self.log.info("Aborting an importdescriptors rescan should fail the RPC call")
    +        self.nodes[0].createwallet("abort_import_wallet", blank=True)
    +        abort_import_wallet = self.nodes[0].get_wallet_rpc("abort_import_wallet")
    +        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
    +            with self.nodes[0].assert_debug_log(expected_msgs=["Rescan started from block"], timeout=10):
    +                importing = thread.submit(abort_import_wallet.importdescriptors, requests=[descriptor])
    +            self.nodes[0].cli("-rpcwallet=abort_import_wallet").abortrescan()
    +            try:
    +                importing.result(timeout=30)
    +                raise AssertionError("importdescriptors unexpectedly succeeded")
    +            except JSONRPCException as e:
    +                assert_equal(e.error["code"], -1)
    +                assert_equal(e.error["message"], "Rescan aborted by user.")
    +
             self.log.info("Multipath descriptors")
             self.nodes[1].createwallet(wallet_name="multipath", blank=True)
             w_multipath = self.nodes[1].get_wallet_rpc("multipath")
    

    </details>


    polespinasa commented at 8:48 AM on June 8, 2026:

    Nice catch! Fixed in the last push. Will add the test in #35179

  117. polespinasa force-pushed on Jun 3, 2026
  118. in src/wallet/rpc/backup.cpp:154 in 53a0e162e5 outdated
     328 | +    if (data.exists("label")) request.label = LabelFromValue(data["label"]);
     329 | +    request.timestamp = GetImportTimestamp(data);
     330 | +    if (data.exists("active")) request.active = data["active"].get_bool();
     331 | +    if (data.exists("internal")) request.internal = data["internal"].get_bool();
     332 | +    if (data.exists("range")) {
     333 | +        auto r = ParseDescriptorRange(data["range"]);
    


    w0xlt commented at 8:59 PM on June 3, 2026:

    If the importdescriptors request has more than one problem, for example a missing descriptor checksum and an invalid range, the PR code reports the range problem first. On master, it reports the descriptor problem (Missing checksum) first.


    polespinasa commented at 7:53 AM on June 8, 2026:

    I don't think there is a clean solution for that. The invalid range check is a RPC check, while the checksum check is a importdescriptor check, as we are separating them it is hard to mix the order.

    In any case I don't think this is a problem, if both problems are present in the request, it will fail anyway, the request will never be successful without both fixed. @achow101 thoughts?


    w0xlt commented at 8:34 AM on June 8, 2026:

    "In any case I don't think this is a problem"

    Yes, I agree that it does not need to be addressed. I also tried a few solutions, but none of them were simpler or cleaner.

    The comment was only meant to confirm the behavioral difference.

  119. w0xlt commented at 8:59 PM on June 3, 2026: contributor

    A few review comments:

  120. polespinasa force-pushed on Jun 8, 2026
  121. in src/wallet/rpc/backup.cpp:142 in 45fb860ae3
     140 |      }
     141 |      throw JSONRPCError(RPC_TYPE_ERROR, "Missing required timestamp field for key");
     142 |  }
     143 |  
     144 | -static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
     145 | +static wallet::ImportDescriptorRequest ProcessUniValueDescriptor(const UniValue& data)
    


    rkrux commented at 9:14 AM on June 8, 2026:

    In 45fb860ae3299929831d1c3cd067811cd37c7842 "wallet: refactor importdescriptors RPC"

    All of these are within the wallet namepsace, scope resolution is not required that should make the code succinct.

    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index be83d55b3e..c452497a1f 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -139,9 +139,9 @@ static std::optional<int64_t> GetImportTimestamp(const UniValue& data)
         throw JSONRPCError(RPC_TYPE_ERROR, "Missing required timestamp field for key");
     }
     
    -static wallet::ImportDescriptorRequest ProcessUniValueDescriptor(const UniValue& data)
    +static ImportDescriptorRequest ProcessUniValueDescriptor(const UniValue& data)
     {
    -    wallet::ImportDescriptorRequest request;
    +    ImportDescriptorRequest request;
         if (!data.exists("desc")) {
             throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found.");
         }
    @@ -221,7 +221,7 @@ RPCMethod importdescriptors()
     
         UniValue response(UniValue::VARR);
         const UniValue& univalue_requests = main_request.params[0];
    -    std::vector<wallet::ImportDescriptorRequest> requests;
    +    std::vector<ImportDescriptorRequest> requests;
     
         // Collect per-item parse errors so that malformed inputs (e.g. invalid
         // label) are returned as per-item failures.
    @@ -234,9 +234,9 @@ RPCMethod importdescriptors()
             }
         }
     
    -    std::vector<wallet::ImportDescriptorResult> import_results;
    +    std::vector<ImportDescriptorResult> import_results;
         if (!requests.empty()) {
    -        import_results = wallet::ProcessDescriptorsImport(wallet, requests);
    +        import_results = ProcessDescriptorsImport(wallet, requests);
         }
     
         // Wallet-wide precondition failure (e.g. already rescanning, or locked):
    @@ -271,28 +271,28 @@ RPCMethod importdescriptors()
                 continue;
             }
     
    -        wallet::ImportDescriptorResult import_result = import_results[result_idx++];
    +        ImportDescriptorResult import_result = import_results[result_idx++];
             auto writeError = [&result, error = import_result.error](int code) {
                 result.pushKV("success", false);
                 result.pushKV("error", JSONRPCError(code, error));
             };
             switch (import_result.result_code) {
    -            case wallet::ImportDescriptorResult::ImportResultCode::OK:
    +            case ImportDescriptorResult::ImportResultCode::OK:
                     result.pushKV("success", true);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::INVALID_DESCRIPTOR:
    +            case ImportDescriptorResult::ImportResultCode::INVALID_DESCRIPTOR:
                     writeError(RPC_INVALID_ADDRESS_OR_KEY);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::INVALID_PARAMETER:
    +            case ImportDescriptorResult::ImportResultCode::INVALID_PARAMETER:
                     writeError(RPC_INVALID_PARAMETER);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::MISC_ERROR:
    +            case ImportDescriptorResult::ImportResultCode::MISC_ERROR:
                     writeError(RPC_MISC_ERROR);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED:
    +            case ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED:
                     NONFATAL_UNREACHABLE();
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::WALLET_ERROR:
    +            case ImportDescriptorResult::ImportResultCode::WALLET_ERROR:
                     writeError(RPC_WALLET_ERROR);
                     break;
             }
    
    

    polespinasa commented at 10:43 AM on June 8, 2026:

    done

  122. in src/wallet/rpc/backup.cpp:1 in f8f55b3405 outdated


    rkrux commented at 9:20 AM on June 8, 2026:

    In f8f55b3405544906563101c791cc14930e64c48b "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    Nit: It can (should) be mentioned in the commit message that this commit should be reviewed with the --color-moved=dimmed-zebra git option, it greys out almost all the diff in this commit.


    polespinasa commented at 10:43 AM on June 8, 2026:

    taken

  123. in src/wallet/imports.h:28 in 934939a3b3
      23 | +    std::vector<std::string> warnings;
      24 | +    //! Set to true when a wallet-wide precondition failed before any descriptor was
      25 | +    //! processed (e.g. wallet is already rescanning, or wallet is locked).
      26 | +    //! Callers that support top-level errors should surface this as a
      27 | +    //! top-level / call-wide error rather than a per-descriptor failure.
      28 | +    bool global_error{false};
    


    rkrux commented at 9:45 AM on June 8, 2026:

    In 934939a3b30cccf21e26d6044d1dada6044c6dff "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    s/global_error/is_wallet_error

    Global doesn't necessarily mean limited to wallet, instead of relying on the documentation comment, the variable name can signify it itself.

    The "is_" portion is added to the reflect that argument is bool.


    polespinasa commented at 10:44 AM on June 8, 2026:

    good idea, taken

  124. in src/wallet/imports.cpp:245 in f8f55b3405
     240 | +        const int64_t scanned_time = wallet.RescanFromTime(lowest_timestamp, reserver);
     241 | +        wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     242 | +
     243 | +        if (wallet.IsAbortingRescan()) {
     244 | +            ImportDescriptorResult result;
     245 | +            result.Error(ImportDescriptorResult::ImportResultCode::MISC_ERROR, "Rescan aborted by user.", /*g_e=*/ true);
    


    rkrux commented at 9:45 AM on June 8, 2026:

    In f8f55b3405544906563101c791cc14930e64c48b "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    s//*g_e=*/ true//*g_e=*/true
    

    Nit: There is an extra space in between unlike other such calls.


    polespinasa commented at 10:46 AM on June 8, 2026:

    fixed

  125. in src/wallet/rpc/backup.cpp:521 in 45fb860ae3
     563 | +    }
     564 |  
     565 | -        if (pwallet->IsAbortingRescan()) {
     566 | -            throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
     567 | +    // Wallet-wide precondition failure (e.g. already rescanning, or locked):
     568 | +    // surface as a top-level RPC error, matching the pre-refactor behaviour.
    


    rkrux commented at 9:52 AM on June 8, 2026:

    In 45fb860ae3299929831d1c3cd067811cd37c7842 "wallet: refactor importdescriptors RPC"

    , matching the pre-refactor behaviour.

    I don't think this should be a part of the code comment, it's more of a part of commit message or a review comment.


    polespinasa commented at 10:49 AM on June 8, 2026:

    fixed

  126. in src/wallet/imports.h:21 in 934939a3b3 outdated
      16 | +        INVALID_PARAMETER,
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    ImportResultCode result_code{ImportResultCode::OK};
    


    rkrux commented at 10:03 AM on June 8, 2026:

    In 934939a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    I think we are moving away from this style where the "success" and "error" portion are both clubbed within the same struct. Refer PR #32958 that replaces this style with util::Expected<void, *Error> (and the corresponding util::Unexpected).


    pseudoramdom commented at 6:10 PM on June 11, 2026:

    Agree, this is the same direction discussed in this thread. Clubbing success and error in the same enum looks like an anti-pattern we should avoid IMO. FWIW, I used util::Expected in #35436 (reviews welcome :) )


    polespinasa commented at 6:20 PM on June 11, 2026:

    I'll take a look on how to use it, didn't have time yet

    I used util::Expected in #35436 (reviews welcome :) )

    Will do :)


    davidgumberg commented at 5:59 PM on June 23, 2026:

    If retouching would be nice to fix this, but I think OK as-is.

  127. in src/wallet/imports.h:13 in 934939a3b3 outdated
       8 | +#include <wallet/wallet.h>
       9 | +
      10 | +namespace wallet {
      11 | +
      12 | +struct ImportDescriptorResult {
      13 | +    enum class ImportResultCode {
    


    rkrux commented at 10:05 AM on June 8, 2026:

    In 934939a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    s/ImportResultCode/ResultCode

    "import" within the second struct ImportResultCode that's nested inside ImportDescriptorResult seems redundant and reading its usages in backup.cpp is quite verbose such as the one below.

    case ImportDescriptorResult::ImportResultCode::INVALID_DESCRIPTOR:
    

    rkrux commented at 10:06 AM on June 8, 2026:

    Also, using the util::Expected approach allows us to rename it to ErrorCode that seems more appropriate as well because all the codes are errors except the first one that seems to have forced its way into the struct. So the case mentioned in the above comment would read like below that seems short and to the point imo.

    case ImportDescriptorResult::ErrorCode::INVALID_DESCRIPTOR:
    
  128. in src/wallet/imports.h:22 in 934939a3b3
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    ImportResultCode result_code{ImportResultCode::OK};
      22 | +    std::string error;
    


    rkrux commented at 10:16 AM on June 8, 2026:

    In 934939a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    s/error/error_message

    To be explicit in the variable name itself.


    polespinasa commented at 11:07 AM on June 8, 2026:

    done

  129. rkrux commented at 10:16 AM on June 8, 2026: contributor

    Code review 4fd7d5ed819c5fa5285948c8cb5c740f46bf7738

  130. polespinasa force-pushed on Jun 8, 2026
  131. DrahtBot added the label CI failed on Jun 8, 2026
  132. DrahtBot commented at 12:36 PM on June 8, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/27133600610/job/80080414973</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy reported bugprone-argument-comment errors (argument comment name mismatch is_wallet_error vs parameter w_e in wallet/imports.cpp), treated as warnings-as-errors.</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>

  133. polespinasa force-pushed on Jun 8, 2026
  134. DrahtBot removed the label CI failed on Jun 8, 2026
  135. in src/wallet/imports.cpp:64 in e446f9bc81
      59 | +    if (parsed_descs.size() > 1 && request.label.has_value()) {
      60 | +        return result.Error(ImportDescriptorResult::ImportResultCode::INVALID_PARAMETER,
      61 | +            "Multipath descriptors should not have a label");
      62 | +    }
      63 | +
      64 | +    // Ranged descrptors should not have a label
    


    maflcko commented at 12:25 PM on June 9, 2026:

    typos (from the llm):

    • // Ranged descrptors should not have a label -> descriptors [misspelled word]
    • // ... as we didnt import the descriptor in the request. -> didn't [missing apostrophe in contraction]
  136. polespinasa force-pushed on Jun 9, 2026
  137. achow101 referenced this in commit 216b50c9a6 on Jun 11, 2026
  138. w0xlt commented at 6:05 AM on June 14, 2026: contributor

    Regarding #34861 (comment), is that behavior change intentional for this PR, or should timestamp parse errors remain top-level RPC errors to preserve the existing behavior?

  139. in src/wallet/imports.h:47 in 389c3059da outdated
      42 | +    std::optional<bool> internal;
      43 | +    std::optional<std::pair<int64_t,int64_t>> range;
      44 | +    std::optional<int64_t> next_index;
      45 | +};
      46 | +
      47 | +ImportDescriptorResult ImportDescriptor(CWallet& wallet,
    


    w0xlt commented at 6:21 AM on June 14, 2026:

    There is a declaration mismatch for ImportDescriptor() between the header and implementation.

    Header:

    ImportDescriptorResult ImportDescriptor(CWallet& wallet, ImportDescriptorRequest request, int64_t timestamp);
    

    Implementation:

    ImportDescriptorResult ImportDescriptor(CWallet& wallet, ImportDescriptorRequest request);
    

    The extra int64_t timestamp parameter in the header looks stale.

    Since ImportDescriptor() only seems to be used inside imports.cpp by ProcessDescriptorsImport(), maybe the better fix is to remove the declaration from imports.h entirely and keep ImportDescriptor() private to imports.cpp.


    polespinasa commented at 10:19 AM on July 1, 2026:

    Dropped from imports.h

  140. in src/wallet/rpc/backup.cpp:240 in 389c3059da outdated
     268 | -                lowest_timestamp = timestamp;
     269 | -            }
     270 | +    std::vector<ImportDescriptorResult> import_results;
     271 | +    if (!requests.empty()) {
     272 | +        import_results = ProcessDescriptorsImport(wallet, requests);
     273 | +    }
    


    w0xlt commented at 6:40 AM on June 14, 2026:

    ProcessDescriptorsImport() performs wallet-wide precondition checks, including whether the wallet is locked or already rescanning.

    It should not be skipped even when the parsed request list is empty, because an empty importdescriptors call on a locked wallet would otherwise return normally instead of raising the existing locked-wallet RPC error

    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index d32faa1c6a..07f5aa0d17 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -234,10 +234,7 @@ RPCMethod importdescriptors()
             }
         }
     
    -    std::vector<ImportDescriptorResult> import_results;
    -    if (!requests.empty()) {
    -        import_results = ProcessDescriptorsImport(wallet, requests);
    -    }
    +    std::vector<ImportDescriptorResult> import_results{ProcessDescriptorsImport(wallet, requests)};
     
         // Wallet-wide precondition failure (e.g. already rescanning, or locked):
         // surface as a top-level RPC error.
    

    Test:

    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 9a3a180298..67146ede19 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -762,6 +762,9 @@ class ImportDescriptorsTest(BitcoinTestFramework):
             self.nodes[0].createwallet("encrypted_wallet", blank=True, passphrase="passphrase")
             encrypted_wallet = self.nodes[0].get_wallet_rpc("encrypted_wallet")
     
    +        self.log.info("A locked wallet rejects an empty importdescriptors request")
    +        assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", encrypted_wallet.importdescriptors, [])
    +
             descriptor["timestamp"] = 0
             descriptor["next_index"] = 0
    

    polespinasa commented at 10:43 AM on July 1, 2026:

    Good catch! Taken :)

  141. Sjors commented at 9:18 AM on June 22, 2026: member

    should timestamp parse errors remain top-level RPC errors to preserve the existing behavior?

    I haven't looked at the implementation yet, but I would prefer to not make breaking changes to the RPC. At the same time we shouldn't saddle IPC with suboptimal behavior, so the RPC code may have to transform the result a bit to match the old behavior.

  142. in src/wallet/rpc/backup.cpp:412 in 7e99dede80
     428 | +                // If the descriptor timestamp is within the successfully scanned
     429 | +                // range, or if the import result already has an error set, let
     430 | +                // the result stand unmodified. Otherwise replace the result
     431 | +                // with an error message.
     432 | +                const int64_t timestamp{requests.at(i).timestamp.value()};
     433 | +                if (scanned_time <= timestamp || result.result_code != ImportDescriptorResult::ImportResultCode::OK) continue;
    


    davidgumberg commented at 7:02 PM on June 23, 2026:

    https://github.com/bitcoin/bitcoin/pull/34861/changes/7e99dede8028da4c9f06c0fd471cad183e6c5d36 (wallet: refactor importdescriptors RPC) I think this is pretty dangerous / brittle since it depends on the code above doing the right thing before we get here, I think something like:

                    const std::optional<int64_t>& timestamp = requests.at(i).timestamp;
                    if (!timestamp || scanned_time <= timestamp.value() || result.result_code != ImportDescriptorResult::ImportResultCode::OK) {
                        continue;
                    }
    

    is safer.


    polespinasa commented at 10:19 AM on July 1, 2026:

    Timestamp must have a value as if it does not, we use now. Would you consider adding this right before this code a good approach?

    Assert(requests.at(i).timestamp.hasValue());
    
  143. in src/wallet/rpc/backup.cpp:149 in 7e99dede80 outdated
     154 | +    ImportDescriptorRequest request;
     155 | +    if (!data.exists("desc")) {
     156 | +        throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found.");
     157 | +    }
     158 | +    request.descriptor = data["desc"].get_str();
     159 | +    if (data.exists("label")) request.label = LabelFromValue(data["label"]);
    


    davidgumberg commented at 10:27 PM on June 23, 2026:
       request.label = LabelFromValue(data["label"]);
    

    This should probably be fixed separately, but, since LabelFromValue checks for null:


    polespinasa commented at 10:12 AM on July 1, 2026:

    Done

  144. in src/wallet/imports.cpp:151 in 389c3059da outdated
     146 | +                std::transform(extpubs.begin(), extpubs.end(), std::inserter(pubkeys, pubkeys.begin()), [](const CExtPubKey& xpub) { return xpub.pubkey; });
     147 | +                CHECK_NONFATAL(pubkeys.size() == 1);
     148 | +                if (wallet.GetKey(pubkeys.begin()->GetID())) {
     149 | +                    return result.Error(ImportDescriptorResult::ImportResultCode::WALLET_ERROR, "Cannot import an unused() descriptor when its private key is already in the wallet");
     150 | +                }
     151 | +            }
    


    davidgumberg commented at 2:17 AM on June 24, 2026:

    whitespace !


    polespinasa commented at 10:12 AM on July 1, 2026:

    I think this should be resolved

  145. in src/wallet/rpc/backup.cpp:511 in 7e99dede80 outdated
     545 | +    std::vector<std::optional<UniValue>> parse_errors(univalue_requests.size());
     546 | +    for (size_t i = 0; i < univalue_requests.size(); ++i) {
     547 | +        try {
     548 | +            requests.push_back(ProcessUniValueDescriptor(univalue_requests[i]));
     549 | +        } catch (const UniValue& e) {
     550 | +            parse_errors[i] = e;
    


    davidgumberg commented at 2:29 AM on June 24, 2026:

    https://github.com/bitcoin/bitcoin/pull/34861/changes/7e99dede8028da4c9f06c0fd471cad183e6c5d36

    Drop the parse_errors and just handle the errors here:

                UniValue result(UniValue::VOBJ);
                result.pushKV("success", UniValue(false));
                result.pushKV("error", e);
                response.push_back(std::move(result));
    

    this allows the following diff:

    -    // Collect per-item parse errors so that malformed inputs (e.g. invalid
    -    // label) are returned as per-item failures.
    -    std::vector<std::optional<UniValue>> parse_errors(univalue_requests.size());
         for (size_t i = 0; i < univalue_requests.size(); ++i) {
             try {
                 requests.push_back(ProcessUniValueDescriptor(univalue_requests[i]));
             } catch (const UniValue& e) {
    -            parse_errors[i] = e;
    +            UniValue result(UniValue::VOBJ);
    +            result.pushKV("success", UniValue(false));
    +            result.pushKV("error", e);
    +            response.push_back(std::move(result));
             }
         }
    
    -    std::vector<ImportDescriptorResult> import_results;
         if (!requests.empty()) {
             import_results = ProcessDescriptorsImport(wallet, requests);
         }
    @@ -256,22 +256,11 @@ RPCMethod importdescriptors()
             throw JSONRPCError(rpc_error_code, import_results[0].error_message);
         }
    
    -    size_t result_idx = 0;
    -    for (size_t i = 0; i < univalue_requests.size(); ++i)
    +    for (const auto& import_result : import_results)
         {
             UniValue result(UniValue::VOBJ);
             UniValue warnings(UniValue::VARR);
    
    -        // If parsing this request failed, return a per-item error and skip it
    -        // as we didn't import the descriptor in the request.
    -        if (parse_errors[i].has_value()) {
    -            result.pushKV("success", UniValue(false));
    -            result.pushKV("error", parse_errors[i].value());
    -            response.push_back(std::move(result));
    -            continue;
    -        }
    -
    -        ImportDescriptorResult import_result = import_results[result_idx++];
             auto writeError = [&result, error = import_result.error_message](int code) {
                 result.pushKV("success", false);
                 result.pushKV("error", JSONRPCError(code, error));
    

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

    Unless I am missing something, I think this is wrong. It would break the order of responses.

    If we have 5 descriptors and their result are [success, fail, success, success, fail] doing this change would return: [fail, fail, success, success, success] instead of the correct order: [success, fail, success, success, fail]

    I added a comment to make it more clear that the response follows the input order.

  146. davidgumberg commented at 2:44 AM on June 24, 2026: contributor

    Reviewed https://github.com/bitcoin/bitcoin/pull/34861/changes/389c3059daff2bfd1c61c52017dae75a9ce210fb

    Left some comments, I think the "refactor importdescriptors" commit (https://github.com/bitcoin/bitcoin/pull/34861/changes/7e99dede8028da4c9f06c0fd471cad183e6c5d36) should be split up, as there are >3 refactors happening simultaneously, and, 3 being the largest number I can count to in my head, I found it pretty much impossible to follow.

    One idea of how to break it up into atomic commits (with a few misc. minor changes) here: https://github.com/davidgumberg/bitcoin/tree/34861-breakup-refactor .


    I'm not 100% sure about the way that optionals are being in ImportDescriptors and ProcessDescriptorsImport have a few spots where they assume .HasValue() when that might not be true for other consumers of these API's, e.g. timestamp, but I don't have a specific recommendation at the moment of how to make it better.

  147. polespinasa force-pushed on Jun 26, 2026
  148. polespinasa force-pushed on Jul 1, 2026
  149. polespinasa commented at 10:03 AM on July 1, 2026: member

    should timestamp parse errors remain top-level RPC errors to preserve the existing behavior?

    Fixed in https://github.com/bitcoin/bitcoin/compare/389c3059daff2bfd1c61c52017dae75a9ce210fb..ccfe93962f87be78ac139c9b835e937f85acdeb6

  150. polespinasa commented at 10:04 AM on July 1, 2026: member

    Last force-push splits the PR in multiple small commits to make it more easy to review. Thanks for the feedback @davidgumberg.

  151. polespinasa force-pushed on Jul 1, 2026
  152. DrahtBot added the label CI failed on Jul 1, 2026
  153. DrahtBot removed the label CI failed on Jul 1, 2026
  154. in src/wallet/imports.h:21 in adf25dde26 outdated
      16 | +    enum class ImportResultCode {
      17 | +        OK,
      18 | +        INVALID_DESCRIPTOR,
      19 | +        INVALID_PARAMETER,
      20 | +        WALLET_ERROR,
      21 | +        WALLET_UNLOCK_NEEDED,
    


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

    In adf25dde26b2dee3a02a7154bb8887a7af84c8a2 wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs: in #35436 there's been some back and forth on the use of enums. Commit 090edbb635fed4006274c868e463ce98d1e6f9d1 seems to be going in a better direction, introducing WalletErrorCode::WALLET_UNLOCK_NEEDED, which is worth considering here.

  155. DrahtBot added the label Needs rebase on Jul 3, 2026
  156. polespinasa force-pushed on Jul 3, 2026
  157. DrahtBot removed the label Needs rebase on Jul 3, 2026
  158. in src/wallet/imports.h:15 in 28cce72b30
      10 | +#include <string>
      11 | +#include <vector>
      12 | +
      13 | +namespace wallet {
      14 | +
      15 | +struct ImportDescriptorResult {
    


    achow101 commented at 9:48 PM on July 7, 2026:

    In 28cce72b306e71488a8bd44036644b7ae747f656 "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    Since the structs introduced here are used by the interface, they should live in wallet/types.h as that is where wallet internal structs that get passed across the interface live.

    But also, as is being discussed in #35436, it would be preferable to avoid having single function specific structs and enums, especially for error codes.

  159. polespinasa commented at 12:09 PM on July 9, 2026: member

    @pseudoramdom re #35436 (comment)

    Some calls like importdescriptors have two types of behavior for errors, an error that throws the entire operation or a per-item error. What should we do with those? We need a way to tell the interfaces (rpc, etc) what to do with them. This does not affect this PR, but for #34861 was done is add a bool signaling per-item or not. That could be part of struct WalletError and default to true. Imho that is the easiest way to do it without adding extra complexity to the interfaces.

    This sounds very importdescriptors specific request. I think the best approach for that would be something like this?

    using ImportDescriptorsResult = util::Expected<std::vector<ImportDescriptorResult>, WalletError>;
    

    I am not sure, in the proposed code both items ImportDescriptorResult and WalletError can be Errors. And even contain same error codes. I though the idea of using Expected was to have Expected<success, failure>, not Expected<(Success or failure), failure>

  160. polespinasa force-pushed on Jul 9, 2026
  161. polespinasa commented at 11:31 PM on July 9, 2026: member

    Last force push https://github.com/bitcoin/bitcoin/compare/4b9379723e07c46266cc0f8c29bcb9befeed816e..4c076bd0ff55c46ceb21c1530c8ae2d61520cd7c

    Rebased it on top of #35690

    The location and names of the three new errors does not need to be there, I have just added them there temporary so we can discuss.

    Also simplified a lot the result struct and removed the OK error code. @pseudoramdom I started rebasing using util::Expected, but at some point I though this was easier because warnings are shared in both the success and the error case and did not want to duplicate structs.

  162. DrahtBot added the label CI failed on Jul 10, 2026
  163. DrahtBot commented at 12:28 AM on July 10, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/29057319093/job/86251426488</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy reported an error (bugprone-argument-comment) where a comment parameter name (is_general_error) didn’t match the function parameter (is_wallet_error) in wallet/imports.cpp, causing the job to exit non-zero.</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>

  164. polespinasa force-pushed on Jul 10, 2026
  165. DrahtBot removed the label CI failed on Jul 10, 2026
  166. polespinasa commented at 9:27 PM on July 10, 2026: member

    @achow101 re: #35690 (comment)

    How do you translate them if we use the same INVALID_PARAMETER for different codes?

    I would be okay with changing the error codes.

    We consider those breaking change acceptable in this case?

  167. sedited referenced this in commit fe1cb6e40d on Jul 13, 2026
  168. DrahtBot added the label Needs rebase on Jul 13, 2026
  169. achow101 commented at 6:38 PM on July 22, 2026: member

    I would be okay with changing the error codes.

    We consider those breaking change acceptable in this case?

    I do.

    Callers already need to handle INVALID_PARAMETER; INVALID_ADDRESS_OR_KEY is really just a more specific variant of INVALID_PARAMETER.

    The breaking changes we want to avoid changing are introducing new error codes that callers would not be expecting, and changing where/how errors get reported.

  170. polespinasa force-pushed on Jul 27, 2026
  171. polespinasa commented at 9:13 AM on July 27, 2026: member

    Rebased on top of master after #35690 was merged.

    For the moment added a few more error codes, in case we take the "breaking change" and we simplify all to INVALID_PARAMETER that's an easy change to do.

  172. DrahtBot removed the label Needs rebase on Jul 27, 2026
  173. sedited referenced this in commit 465196d015 on Aug 5, 2026
  174. DrahtBot added the label Needs rebase on Aug 11, 2026
  175. polespinasa force-pushed on Aug 12, 2026
  176. polespinasa commented at 8:55 AM on August 12, 2026: member

    Rebased on top of master to fix conflicts with #35493

  177. DrahtBot removed the label Needs rebase on Aug 12, 2026
  178. in src/wallet/imports.cpp:50 in c4a268df6c
      45 | +            WalletErrorCode::InvalidParameter,
      46 | +            "Range should not be specified for an un-ranged descriptor"
      47 | +        );
      48 | +    } else if (parsed_descs.at(0)->IsRange()) {
      49 | +        if (request.range.has_value()) {
      50 | +            range_start = request.range->first;
    


    w0xlt commented at 6:48 PM on August 12, 2026:

    ImportDescriptor currently accepts invalid ranges.

    <details> <summary>suggestion</summary>

    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 992de81f0b..5bd31403dd 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -5,6 +5,8 @@
     #include <chain.h>
     #include <wallet/imports.h>
     
    +#include <limits>
    +
     namespace wallet {
     
     ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& request) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
    @@ -47,8 +49,34 @@ ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& re
             );
         } else if (parsed_descs.at(0)->IsRange()) {
             if (request.range.has_value()) {
    -            range_start = request.range->first;
    -            range_end = request.range->second + 1; // Specified range end is inclusive, but we need range end as exclusive
    +            const auto [range_begin, range_last] = request.range.value();
    +            if (range_begin > range_last) {
    +                return make_error(
    +                    WalletErrorCode::InvalidParameter,
    +                    "Range specified as [begin,end] must not have begin after end"
    +                );
    +            }
    +            if (range_begin < 0) {
    +                return make_error(
    +                    WalletErrorCode::InvalidParameter,
    +                    "Range should be greater or equal than 0"
    +                );
    +            }
    +            // WalletDescriptor stores an exclusive range end in an int32_t.
    +            if (range_last >= std::numeric_limits<int32_t>::max()) {
    +                return make_error(
    +                    WalletErrorCode::InvalidParameter,
    +                    "End of range is too high"
    +                );
    +            }
    +            if (range_last - range_begin >= 1000000) {
    +                return make_error(
    +                    WalletErrorCode::InvalidParameter,
    +                    "Range is too large"
    +                );
    +            }
    +            range_start = range_begin;
    +            range_end = range_last + 1; // Specified range end is inclusive, but we need range end as exclusive
             } else {
                 result.warnings.emplace_back("Range not given, using default keypool range");
                 range_start = 0;
    diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
    index 1c3047727d..be2618f635 100644
    --- a/src/wallet/test/wallet_tests.cpp
    +++ b/src/wallet/test/wallet_tests.cpp
    @@ -4,9 +4,15 @@
     
     #include <wallet/wallet.h>
     
    +#include <array>
    +#include <cstddef>
     #include <cstdint>
     #include <future>
    +#include <limits>
     #include <memory>
    +#include <optional>
    +#include <string>
    +#include <utility>
     #include <vector>
     
     #include <addresstype.h>
    @@ -16,6 +22,7 @@
     #include <node/types.h>
     #include <policy/policy.h>
     #include <rpc/server.h>
    +#include <script/descriptor.h>
     #include <script/solver.h>
     #include <test/util/common.h>
     #include <test/util/logging.h>
    @@ -26,6 +33,7 @@
     #include <validationinterface.h>
     #include <wallet/coincontrol.h>
     #include <wallet/context.h>
    +#include <wallet/imports.h>
     #include <wallet/receive.h>
     #include <wallet/spend.h>
     #include <wallet/test/util.h>
    @@ -71,6 +79,47 @@ static void AddKey(CWallet& wallet, const CKey& key)
         Assert(wallet.AddWalletDescriptor(w_desc, provider, "", false));
     }
     
    +BOOST_AUTO_TEST_CASE(reject_invalid_descriptor_ranges)
    +{
    +    const int height{*Assert(m_node.chain->getHeight())};
    +    {
    +        LOCK(m_wallet.cs_wallet);
    +        m_wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
    +        m_wallet.SetLastBlockProcessed(height, m_node.chain->getBlockHash(height));
    +    }
    +
    +    CExtKey ext_key;
    +    ext_key.SetSeed(std::array<std::byte, 32>{});
    +    const std::string descriptor_without_checksum{"wpkh(" + EncodeExtPubKey(ext_key.Neuter()) + "/*)"};
    +    const std::string descriptor{descriptor_without_checksum + "#" + GetDescriptorChecksum(descriptor_without_checksum)};
    +
    +    const std::array invalid_ranges{
    +        std::pair{std::pair<int64_t, int64_t>{2, 1}, "Range specified as [begin,end] must not have begin after end"},
    +        std::pair{std::pair<int64_t, int64_t>{-1, 10}, "Range should be greater or equal than 0"},
    +        std::pair{std::pair<int64_t, int64_t>{0, 1'000'000}, "Range is too large"},
    +        std::pair{std::pair<int64_t, int64_t>{0, std::numeric_limits<int64_t>::max()}, "End of range is too high"},
    +        std::pair{std::pair<int64_t, int64_t>{std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::max()}, "End of range is too high"},
    +    };
    +
    +    for (const auto& [range, expected_error] : invalid_ranges) {
    +        std::vector requests{ImportDescriptorRequest{
    +            .descriptor = descriptor,
    +            .label = {},
    +            .timestamp = 0,
    +            .active = false,
    +            .internal = std::nullopt,
    +            .range = range,
    +            .next_index = std::nullopt,
    +        }};
    +        const auto results{ProcessDescriptorsImport(m_wallet, requests)};
    +        BOOST_REQUIRE_EQUAL(results.size(), 1U);
    +        BOOST_REQUIRE(results.front().error.has_value());
    +        BOOST_CHECK(results.front().error->wallet_error.code == WalletErrorCode::InvalidParameter);
    +        BOOST_CHECK_EQUAL(results.front().error->wallet_error.message.original, expected_error);
    +        BOOST_CHECK(!results.front().error->is_general_error);
    +    }
    +}
    +
     BOOST_FIXTURE_TEST_CASE(update_non_range_descriptor, TestingSetup)
     {
         CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
    

    </details>


    polespinasa commented at 7:21 AM on August 13, 2026:

    Sorry I don't follow you. I don't think the current implementation allows this wrong values, see some manual testing I just did:

    $ ./build/bin/bitcoin-cli -testnet4 -rpcwallet="a" importdescriptors '[{
          "desc": "wpkh([930ebdba/84h/1h/0h]tpubDDDGjbGJ1GAHzbPKUtxXuatEze5f7qS7ftBqTNxrG4LWQkLFCvo7M8ZWvtTNMFqiPNJNywCGRn2zFDaDi8vpoLYsdeSZwn2Yo6K5ieSJ55V/1/*)#zndut4ua",
          "timestamp": 1785857256,
          "active": true,
          "internal": true,
          "range": [
            -10,1007]}]'
    [
      {
        "success": false,
        "error": {
          "code": -8,
          "message": "Range should be greater or equal than 0"
        }
      }
    ]
    sliv3r@sliv3r-tuxedo:~/Documentos/Projectes/BitcoinCore/bitcoin$ ./build/bin/bitcoin-cli -testnet4 -rpcwallet="a" importdescriptors '[{
          "desc": "wpkh([930ebdba/84h/1h/0h]tpubDDDGjbGJ1GAHzbPKUtxXuatEze5f7qS7ftBqTNxrG4LWQkLFCvo7M8ZWvtTNMFqiPNJNywCGRn2zFDaDi8vpoLYsdeSZwn2Yo6K5ieSJ55V/1/*)#zndut4ua",
          "timestamp": 1785857256,
          "active": true,
          "internal": true,
          "range": [
            0,100000007]}]'
    [
      {
        "success": false,
        "error": {
          "code": -8,
          "message": "Range is too large"
        }
      }
    ]
    sliv3r@sliv3r-tuxedo:~/Documentos/Projectes/BitcoinCore/bitcoin$ ./build/bin/bitcoin-cli -testnet4 -rpcwallet="a" importdescriptors '[{
          "desc": "wpkh([930ebdba/84h/1h/0h]tpubDDDGjbGJ1GAHzbPKUtxXuatEze5f7qS7ftBqTNxrG4LWQkLFCvo7M8ZWvtTNMFqiPNJNywCGRn2zFDaDi8vpoLYsdeSZwn2Yo6K5ieSJ55V/1/*)#zndut4ua",
          "timestamp": 1785857256,
          "active": true,
          "internal": true,
          "range": [
            0,2147483648]}]'
    [
      {
        "success": false,
        "error": {
          "code": -8,
          "message": "End of range is too high"
        }
      }
    ]
    
    

    Actually ranges are already tested in the functional suite case. See wallet_importdescriptors.py arround line 517.


    w0xlt commented at 3:09 PM on August 13, 2026:

    The RPC rejects these invalid ranges because ProcessUniValueDescriptor() calls ParseDescriptorRange() before the request reaches the wallet code. The new wallet interface receives an already constructed ImportDescriptorRequest, bypassing that RPC-layer validation.

    The two paths are:

    RPC: JSON → ProcessUniValueDescriptor (calls ParseDescriptorRange) → ProcessDescriptorsImport → ImportDescriptor

    New wallet interface: ImportDescriptorRequest → Wallet::importDescriptors → ProcessDescriptorsImport → ImportDescriptor


    polespinasa commented at 4:27 PM on August 14, 2026:

    Oh right, I see what you mean.

    Fixed :)

  179. w0xlt commented at 7:36 PM on August 12, 2026: contributor

    Currently explicit empty or null labels are treated as omitted and accepted, allowing them for ranged, internal, and multipath descriptors.

    The suggestion bellow restores master behavior.

    <details> <summary>diff</summary>

    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 992de81f0b..28b95ccd0b 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -74,7 +74,7 @@ ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& re
         }
     
         // Multipath descriptors should not have a label
    -    if (parsed_descs.size() > 1 && !request.label.empty()) {
    +    if (parsed_descs.size() > 1 && request.label.has_value()) {
             return make_error(
                 WalletErrorCode::InvalidParameter,
                 "Multipath descriptors should not have a label"
    @@ -82,7 +82,7 @@ ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& re
         }
     
         // Ranged descriptors should not have a label
    -    if (is_ranged && !request.label.empty()) {
    +    if (is_ranged && request.label.has_value()) {
             return make_error(
                 WalletErrorCode::InvalidParameter,
                 "Ranged descriptors should not have a label"
    @@ -91,7 +91,7 @@ ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& re
     
         bool desc_internal = request.internal.value_or(false);
         // Internal addresses should not have a label either
    -    if (desc_internal && !request.label.empty()) {
    +    if (desc_internal && request.label.has_value()) {
             return make_error(
                 WalletErrorCode::InvalidParameter,
                 "Internal addresses should not have a label"
    @@ -177,7 +177,7 @@ ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& re
             WalletDescriptor w_desc(std::move(parsed_desc), request.timestamp.value(), range_start, range_end, next_index);
     
             // Add descriptor to the wallet
    -        auto spk_manager_res = wallet.AddWalletDescriptor(w_desc, keys, request.label, desc_internal);
    +        auto spk_manager_res = wallet.AddWalletDescriptor(w_desc, keys, request.label.value_or(""), desc_internal);
     
             if (!spk_manager_res) {
                 return make_error(
    diff --git a/src/wallet/imports.h b/src/wallet/imports.h
    index 0d3c506ae0..ad45aabd15 100644
    --- a/src/wallet/imports.h
    +++ b/src/wallet/imports.h
    @@ -42,7 +42,8 @@ struct ImportResult {
     //! Information about a descriptor to be imported.
     struct ImportDescriptorRequest {
         std::string descriptor;
    -    std::string label;
    +    //! Set if a label was explicitly provided, including an empty label.
    +    std::optional<std::string> label;
         std::optional<int64_t> timestamp;
         bool active{false};
         std::optional<bool> internal;
    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index b90d90c387..b8bfa67475 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -147,7 +147,7 @@ static ImportDescriptorRequest ProcessUniValueDescriptor(const UniValue& data, s
             throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found.");
         }
         request.descriptor = data["desc"].get_str();
    -    request.label = LabelFromValue(data["label"]);
    +    if (data.exists("label")) request.label = LabelFromValue(data["label"]);
         request.timestamp = timestamp;
         if (data.exists("active")) request.active = data["active"].get_bool();
         if (data.exists("internal")) request.internal = data["internal"].get_bool();
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 78378e59c2..bcf8571276 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -352,10 +352,11 @@ class ImportDescriptorsTest(BitcoinTestFramework):
             test_address(w1, key.p2pkh_addr, solvable=True, ismine=True, labels=["Updated label"])
     
             self.log.info("Internal addresses cannot have labels")
    -        self.test_importdesc({**import_request, "internal": True},
    -                             success=False,
    -                             error_code=-8,
    -                             error_message="Internal addresses should not have a label")
    +        for label in ["Descriptor import test", "", None]:
    +            self.test_importdesc({**import_request, "internal": True, "label": label},
    +                                 success=False,
    +                                 error_code=-8,
    +                                 error_message="Internal addresses should not have a label")
     
             self.log.info("External non-ranged addresses can have labels")
             self.test_importdesc({**import_request, "internal": False}, success=True)
    @@ -455,13 +456,14 @@ class ImportDescriptorsTest(BitcoinTestFramework):
             desc = "sh(wpkh(" + xpub + "/0/0/*" + "))"
     
             self.log.info("Ranged descriptors cannot have labels")
    -        self.test_importdesc({"desc":descsum_create(desc),
    -                              "timestamp": "now",
    -                              "range": [0, 100],
    -                              "label": "test"},
    -                              success=False,
    -                              error_code=-8,
    -                              error_message='Ranged descriptors should not have a label')
    +        for label in ["test", "", None]:
    +            self.test_importdesc({"desc":descsum_create(desc),
    +                                  "timestamp": "now",
    +                                  "range": [0, 100],
    +                                  "label": label},
    +                                  success=False,
    +                                  error_code=-8,
    +                                  error_message='Ranged descriptors should not have a label')
     
             self.log.info("Ranged descriptors cannot have labels - even if range not provided by user and only implied by asterisk (*)")
             self.test_importdesc({"desc":descsum_create("wpkh(" + xpub + "/100/0/*)"),
    @@ -1026,15 +1028,16 @@ class ImportDescriptorsTest(BitcoinTestFramework):
             w_multisplit = self.nodes[1].get_wallet_rpc("multipath_split")
             timestamp = int(time.time())
     
    -        self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/<10;20>/0/*)"),
    -                              "active": True,
    -                              "range": 10,
    -                              "timestamp": "now",
    -                              "label": "some label"},
    -                              success=False,
    -                              error_code=-8,
    -                              error_message="Multipath descriptors should not have a label",
    -                              wallet=w_multipath)
    +        for label in ["some label", "", None]:
    +            self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/<10;20>/0/*)"),
    +                                  "active": True,
    +                                  "range": 10,
    +                                  "timestamp": "now",
    +                                  "label": label},
    +                                  success=False,
    +                                  error_code=-8,
    +                                  error_message="Multipath descriptors should not have a label",
    +                                  wallet=w_multipath)
             self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/<10;20>/0/*)"),
                                   "active": True,
                                   "range": 10,
    

    </details>

  180. polespinasa commented at 9:08 AM on August 13, 2026: member

    Currently explicit empty or null labels are treated as omitted and accepted, allowing them for ranged, internal, and multipath descriptors.

    I don't think that is a problem. See that if a label is not set (null) they are converted to empty. So the result of the user setting an empty or null label in here gives the same result as in master. Master does fail the import because an RPC param is set, but it does not makes sense at all. See:

    std::string LabelFromValue(const UniValue& value)
    {
        static const std::string empty_string;
        if (value.isNull()) return empty_string;
    
        const std::string& label{value.get_str()};
        if (label == "*")
            throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME, "Invalid label name");
        return label;
    }
    

    Also I think the patch you propose has a problem and is, if label is unset, then LabelFromValue converts label into empty string, and then the new comparison request.label.has_value() will return true and fail, while in master it would succeed.

  181. w0xlt commented at 3:40 PM on August 13, 2026: contributor

    Master does fail the import because an RPC param is set, but it does not makes sense at all.

    If the PR intentionally changes the current behavior, this should be documented.

  182. polespinasa force-pushed on Aug 14, 2026
  183. polespinasa force-pushed on Aug 14, 2026
  184. DrahtBot added the label CI failed on Aug 14, 2026
  185. polespinasa force-pushed on Aug 14, 2026
  186. polespinasa commented at 6:37 PM on August 14, 2026: member

    Force pushed to address some comments:

    • added a missing descriptor range check on the interface path -> #34861 (review)
    • Document a small behavior change that this PR introduces -> #34861 (comment)
  187. DrahtBot removed the label CI failed on Aug 14, 2026
  188. in src/wallet/rpc/backup.cpp:198 in 9deb9f4f65
     204 | +        return make_error(
     205 | +            WalletErrorCode::InvalidParameter,
     206 | +            "Range should not be specified for an un-ranged descriptor"
     207 | +        );
     208 | +    } else if (parsed_descs.at(0)->IsRange()) {
     209 | +        if (request.range.has_value()) {
    


    achow101 commented at 8:25 PM on August 26, 2026:

    In 9deb9f4f6584ceb51ea451dc22775b8d485309c5 "wallet: rpc: refactor: ProcessDescriptorImport returns ImportDescriptorResult"

    This should be in the previous commit where checking request.range is introduced.


    polespinasa commented at 9:20 AM on August 27, 2026:

    Done

  189. in src/wallet/rpc/backup.cpp:402 in 6e081b6a30
     397 | +        }
     398 | +
     399 | +        CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), interfaces::FoundBlock().time(lowest_timestamp).mtpTime(now)));
     400 | +
     401 | +        for (ImportDescriptorRequest& request : requests) {
     402 | +            const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
    


    achow101 commented at 8:27 PM on August 26, 2026:

    In 6e081b6a306e6a88131918c9122821e8ddfee046 "wallet: rename ProcessDescriptorImport to ImportDescriptor and add ProcessDescriptorsImport"

    Why is this changed to use a temporary rather than request.timestamp as in the previous commit?


    polespinasa commented at 9:55 AM on August 27, 2026:

    good catch, fixed, probably missed it during some rebasing

  190. in src/wallet/rpc/backup.cpp:552 in 6e081b6a30
     621 | -            }
     622 | +    std::vector<ImportResult> import_results{ProcessDescriptorsImport(wallet, requests)};
     623 | +
     624 | +    // Wallet-wide precondition failure (e.g. already rescanning, or locked):
     625 | +    // surface as a top-level RPC error.
     626 | +    if (import_results.size() == 1 && import_results[0].has_error() && import_results[0].error.value().is_general_error) {
    


    achow101 commented at 8:29 PM on August 26, 2026:

    In 6e081b6a306e6a88131918c9122821e8ddfee046 "wallet: rename ProcessDescriptorImport to ImportDescriptor and add ProcessDescriptorsImport"

    nit: use -> instead of .value()


    polespinasa commented at 9:29 AM on August 27, 2026:

    done

  191. achow101 commented at 8:30 PM on August 26, 2026: member

    ACK 9ebb6ff2592ebfa5682c1936706a0bae9b5742f6

  192. DrahtBot requested review from xyzconstant on Aug 26, 2026
  193. DrahtBot requested review from davidgumberg on Aug 26, 2026
  194. DrahtBot requested review from w0xlt on Aug 26, 2026
  195. DrahtBot requested review from Sjors on Aug 26, 2026
  196. DrahtBot requested review from rkrux on Aug 26, 2026
  197. polespinasa force-pushed on Aug 27, 2026
  198. polespinasa force-pushed on Aug 27, 2026
  199. DrahtBot added the label CI failed on Aug 27, 2026
  200. polespinasa commented at 10:17 AM on August 27, 2026: member

    Force pushed to address some small nits. No behavior change.

  201. DrahtBot removed the label CI failed on Aug 27, 2026
  202. achow101 commented at 10:56 PM on August 27, 2026: member

    ACK 26589578824c0f57f3df39dabf649bde8ae946e2

  203. w0xlt commented at 10:03 PM on September 1, 2026: contributor

    ACK 26589578824c0f57f3df39dabf649bde8ae946e2

    Non-blocking suggestion: for clarity, the two make_error lambdas introduced in this PR could use explicit captures:

    auto make_error = [&response](...) { ... };
    auto make_error = [&result](...) { ... };
    

    Feel free to ignore unless you retouch this code.

  204. in src/wallet/rpc/backup.cpp:135 in ca3b84ca7a outdated
     132 |          const UniValue& timestamp = data["timestamp"];
     133 |          if (timestamp.isNum()) {
     134 |              return timestamp.getInt<int64_t>();
     135 |          } else if (timestamp.isStr() && timestamp.get_str() == "now") {
     136 | -            return now;
     137 | +            return std::nullopt; // std::nullopt means use the current best block's MTP time
    


    Sjors commented at 2:02 PM on September 2, 2026:

    In ca3b84ca7ab737b0b29c6ee3b5185a60b3369ee4 wallet: rpc: Use std::optional in GetImportTimestamp nit: could document the return value in a new doxygen function doc.


    polespinasa commented at 7:17 AM on September 3, 2026:

    done

  205. in src/wallet/rpc/backup.cpp:386 in 3e18c7885b
     382 | @@ -383,7 +383,7 @@ RPCMethod importdescriptors()
     383 |      LOCK(pwallet->m_relock_mutex);
     384 |  
     385 |      const UniValue& requests = main_request.params[0];
     386 | -    const int64_t minimum_timestamp = 1;
     387 | +    const int64_t minimum_timestamp = 0;
    


    Sjors commented at 2:32 PM on September 2, 2026:

    In 3e18c7885bed473dc515bea3d4149817be75ea86 wallet: lower the minimum timestamp to 0: let's also stop clamping negative values:

    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index b173da9b9b..6e18128027 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -131,5 +131,9 @@ static std::optional<int64_t> GetImportTimestamp(const UniValue& data)
             const UniValue& timestamp = data["timestamp"];
             if (timestamp.isNum()) {
    -            return timestamp.getInt<int64_t>();
    +            const int64_t value{timestamp.getInt<int64_t>()};
    +            if (value < 0) {
    +                throw JSONRPCError(RPC_INVALID_PARAMETER, "Timestamp must not be negative");
    +            }
    +            return value;
             } else if (timestamp.isStr() && timestamp.get_str() == "now") {
                 return std::nullopt; // std::nullopt means use the current best block's MTP time
    @@ -384,5 +388,4 @@ RPCMethod importdescriptors()
    
         const UniValue& requests = main_request.params[0];
    -    const int64_t minimum_timestamp = 0;
         int64_t now = 0;
         int64_t lowest_timestamp = 0;
    @@ -398,5 +401,5 @@ RPCMethod importdescriptors()
             for (const UniValue& request : requests.getValues()) {
                 // This throws an error if "timestamp" doesn't exist
    -            const int64_t timestamp = std::max(GetImportTimestamp(request).value_or(now), minimum_timestamp);
    +            const int64_t timestamp = GetImportTimestamp(request).value_or(now);
                 const UniValue result = ProcessDescriptorImport(*pwallet, request, timestamp);
                 response.push_back(result);
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 78378e59c2..6171083a77 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -331,4 +331,13 @@ class ImportDescriptorsTest(BitcoinTestFramework):
                 error_message='Expected number or "now" timestamp value for key. got type string')
    
    +        import_request = {"desc": descsum_create("pkh(" + key.pubkey + ")"),
    +            "timestamp": -1,
    +            "label": "Descriptor import test"}
    +        self.test_importdesc(import_request,
    +            success=False,
    +            global_error=True,
    +            error_code=-8,
    +            error_message="Timestamp must not be negative")
    +
             # # Test importing of a P2PKH descriptor
             key = get_generate_key()
    

    polespinasa commented at 8:45 AM on September 3, 2026:

    taken

  206. in src/wallet/rpc/backup.cpp:25 in f5d46773b7 outdated
      21 | @@ -22,6 +22,7 @@
      22 |  #include <util/time.h>
      23 |  #include <util/translation.h>
      24 |  #include <wallet/export.h>
      25 | +#include <wallet/imports.h>
    


    Sjors commented at 2:38 PM on September 2, 2026:

    In f5d46773b773dd8538979c51504f2353e80de68a wallet: rpc: refactor: Extract UniValue processing from ProcessDescriptorImport: might as well squash bddd917fc721eddf0166eeeda7a548a9d27ce6fb into this commit, where you actually use the new struct.


    polespinasa commented at 8:47 AM on September 3, 2026:

    I rather keep it separated, imho it's easier to read.

  207. in src/wallet/rpc/backup.cpp:209 in f5d46773b7
     235 |              throw JSONRPCError(RPC_INVALID_PARAMETER, "Multipath descriptors should not have a label");
     236 |          }
     237 |  
     238 |          // Ranged descriptors should not have a label
     239 | -        if (is_ranged && data.exists("label")) {
     240 | +        if (is_ranged && !request.label.empty()) {
    


    Sjors commented at 2:45 PM on September 2, 2026:

    In f5d46773b773dd8538979c51504f2353e80de68a wallet: rpc: refactor: Extract UniValue processing from ProcessDescriptorImport: to add test coverage for the small behavior change described in the commit message:

    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 78378e59c2..bae441e06f 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -474,4 +474,11 @@ class ImportDescriptorsTest(BitcoinTestFramework):
                                   error_message='Ranged descriptors should not have a label')
    
    +        self.log.info("Ranged descriptors can have an explicitly empty label")
    +        self.test_importdesc({"desc":descsum_create("sh(wpkh(" + xpub + "/0/1/*))"),
    +                              "timestamp": "now",
    +                              "range": [0, 100],
    +                              "label": ""},
    +                              success=True)
    +
             self.log.info("Private keys required for private keys enabled wallet")
             self.test_importdesc({"desc":descsum_create(desc),
    

    polespinasa commented at 8:48 AM on September 3, 2026:

    taken, thanks :)

  208. in src/wallet/rpc/backup.cpp:483 in f5d46773b7 outdated
     481 | +
     482 |              }
     483 |          }
     484 |      }
     485 |  
     486 | +    // Build the final response in input order.
    


    Sjors commented at 3:02 PM on September 2, 2026:

    In f5d46773b773dd8538979c51504f2353e80de68a wallet: rpc: refactor: Extract UniValue processing from ProcessDescriptorImport: this double loop is awkward.

    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index aaff02eef1..7d53c78917 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -384,16 +384,23 @@ RPCMethod importdescriptors()
    
         const UniValue& univalue_requests = main_request.params[0];
    -    std::vector<ImportDescriptorRequest> requests;
    +    // One result per input, in input order.
    +    std::vector<UniValue> results(univalue_requests.size());
    +    // Successfully parsed requests, each with the index of the input it came from.
    +    struct ParsedRequest {
    +        size_t input_index;
    +        ImportDescriptorRequest request;
    +    };
    +    std::vector<ParsedRequest> requests;
    
    -    // Collect per-item parse errors so that malformed inputs (e.g. invalid
    -    // label) are returned as per-item failures.
    -    std::vector<std::optional<UniValue>> parse_errors(univalue_requests.size());
    +    // Malformed inputs (e.g. invalid label) are returned as per-item failures.
         for (size_t i = 0; i < univalue_requests.size(); ++i) {
             // Throws a top-level RPC error if "timestamp" is missing or invalid
             std::optional<int64_t> timestamp = GetImportTimestamp(univalue_requests[i]);
             try {
    -            requests.push_back(ProcessUniValueDescriptor(univalue_requests[i], timestamp));
    +            requests.push_back({i, ProcessUniValueDescriptor(univalue_requests[i], timestamp)});
             } catch (const UniValue& e) {
    -            parse_errors[i] = e;
    +            results[i] = UniValue(UniValue::VOBJ);
    +            results[i].pushKV("success", UniValue(false));
    +            results[i].pushKV("error", e);
             }
         }
    @@ -403,5 +410,4 @@ RPCMethod importdescriptors()
         int64_t lowest_timestamp = 0;
         bool rescan = false;
    -    std::vector<UniValue> import_results;
         {
             LOCK(pwallet->cs_wallet);
    @@ -411,8 +417,7 @@ RPCMethod importdescriptors()
    
             // Get all timestamps and extract the lowest timestamp
    -        for (auto& request : requests) {
    +        for (auto& [i, request] : requests) {
                 request.timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
    -            const UniValue result = ProcessDescriptorImport(*pwallet, request);
    -            import_results.push_back(std::move(result));
    +            results[i] = ProcessDescriptorImport(*pwallet, request);
    
                 if (lowest_timestamp > request.timestamp.value()) {
    @@ -421,5 +426,5 @@ RPCMethod importdescriptors()
    
                 // If we know the chain tip, and at least one request was successful then allow rescan
    -            if (!rescan && result["success"].get_bool()) {
    +            if (!rescan && results[i]["success"].get_bool()) {
                     rescan = true;
                 }
    @@ -440,10 +445,6 @@ RPCMethod importdescriptors()
             if (scanned_time > lowest_timestamp) {
                 // Compose the response
    -            size_t result_idx = 0;
    -            for (size_t i = 0; i < univalue_requests.size(); ++i) {
    -                // Skip items that had parse errors (they have no entry in import_results).
    -                if (parse_errors[i].has_value()) continue;
    -
    -                UniValue& result = import_results[result_idx];
    +            for (const auto& [i, request] : requests) {
    +                UniValue& result = results[i];
    
                     // If the descriptor timestamp is within the successfully scanned
    @@ -451,5 +452,5 @@ RPCMethod importdescriptors()
                     // the result stand unmodified. Otherwise replace the result
                     // with an error message.
    -                const int64_t timestamp = requests[result_idx].timestamp.value();
    +                const int64_t timestamp = request.timestamp.value();
                     if (scanned_time > timestamp && !result.exists("error")) {
                         std::string error_msg{strprintf("Rescan failed for descriptor with timestamp %d. There "
    @@ -474,25 +475,12 @@ RPCMethod importdescriptors()
                         result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, error_msg));
                     }
    -
    -                ++result_idx;
    -
                 }
             }
         }
    
    -    // Build the final response in input order.
         UniValue response(UniValue::VARR);
    -    size_t result_idx = 0;
    -    for (size_t i = 0; i < univalue_requests.size(); ++i) {
    -        if (parse_errors[i].has_value()) {
    -            UniValue result(UniValue::VOBJ);
    -            result.pushKV("success", UniValue(false));
    -            result.pushKV("error", parse_errors[i].value());
    -            response.push_back(std::move(result));
    -        } else {
    -            response.push_back(import_results[result_idx++]);
    -        }
    +    for (UniValue& result : results) {
    +        response.push_back(std::move(result));
         }
    -
         return response;
     },
    

    And then in the later commits you put the result back in the right position.


    davidgumberg commented at 5:26 PM on September 2, 2026:

    +1, alternatively you could do something similar with a vector util::expected, and then building the univalue response at the end


    polespinasa commented at 11:45 AM on September 3, 2026:

    Nice suggestion thanks, this looks much more clean. The rebase was a bit of a mess hope I didn't break anything 😅

  209. Sjors commented at 3:36 PM on September 2, 2026: member

    Code review 26589578824c0f57f3df39dabf649bde8ae946e2. I didn't look closely at bba644d202e9438431035c17265eab70e0a03384, 145a7a0e9468b22347bc3c0aadd2e44d8f42f46e.

    Some suggestions for readability, nothing blocking.

  210. DrahtBot requested review from Sjors on Sep 2, 2026
  211. in src/wallet/rpc/backup.cpp:169 in bba644d202
     175 | +
     176 | +    auto make_error = [&](WalletErrorCode code, std::string message, bool general_error = false) {
     177 | +        ImportError error{code, Untranslated(message), general_error};
     178 | +        result.error = std::move(error);
     179 | +        return result;
     180 | +    };
    


    davidgumberg commented at 5:43 PM on September 2, 2026:

    https://github.com/bitcoin/bitcoin/pull/34861/changes/bba644d202e9438431035c17265eab70e0a03384 (wallet: rpc: refactor: ProcessDescriptorImport returns ImportDescriptorResult) nit: I think this makes more sense as a constructor for ImportResult, would also avoid defining the lambda twice here and in ImportDescriptor above


    polespinasa commented at 2:07 PM on September 3, 2026:

    Taken :)

  212. DrahtBot requested review from davidgumberg on Sep 2, 2026
  213. in src/wallet/rpc/backup.cpp:165 in bba644d202
     171 | -            throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
     172 | +    AssertLockHeld(wallet.cs_wallet);
     173 | +
     174 | +    ImportResult result;
     175 | +
     176 | +    auto make_error = [&](WalletErrorCode code, std::string message, bool general_error = false) {
    


    davidgumberg commented at 5:47 PM on September 2, 2026:

    https://github.com/bitcoin/bitcoin/pull/34861/changes/bba644d202e9438431035c17265eab70e0a03384 (wallet: rpc: refactor: ProcessDescriptorImport returns ImportDescriptorResult)

    If keeping the lambda: (here and above)

        auto make_error = [&result](WalletErrorCode code, std::string message, bool general_error = false) {
    

    polespinasa commented at 2:25 PM on September 3, 2026:

    both lambdas were annihilated :sunglasses:

  214. DrahtBot requested review from davidgumberg on Sep 2, 2026
  215. wallet: rpc: Use std::optional in GetImportTimestamp e4d73194cb
  216. wallet: lower the minimum timestamp to 0
    Timestamp 0 is allowed and it does not seem a reason to have a minimum timestamp > 0
    other than some carried value from legacy wallet.
    
    Lowering simplifies importdescriptors because, before this change, if a timestamp 0 was set
    by the user it would be overwritten by the minimum timestamp, but error messages should still
    report timestamp 0.
    
    Negative timestamps are now invalid and throw.
    6b2cdafc2f
  217. wallet: refactor: make is_ranged no longer an optional e231799b14
  218. wallet: Add ImportDescriptorRequest structs
    ImportDescriptorRquest will replace the UniValue-based interface of ProcessDescriptorImport(),
    which will allow to extract the functionality to other interfaces such as the GUI, etc,
    in a subsequent commit.
    8e94603181
  219. polespinasa force-pushed on Sep 3, 2026
  220. wallet: rpc: refactor: Extract UniValue processing from ProcessDescriptorImport
    Adds a new function ProcessUniValueDescriptor that translates from UniValue to ImportDescriptorRequest.
    Refactors ProcessDescriptorImport to work with ImportDescriptorRequest objects instead of UniValue.
    Refactors RPCMethod importdescriptors() to use ProcessUniValueDescriptor.
    
    The commit introduces a small behavior change to make label handling more consistent.
    Previously, an unset label on a descriptor that cannot have a label would eventually be converted
    to an empty string. However, if the user explicitly set the label to an empty string,
    the importdescriptors RPC call would fail because the descriptor does not support labels.
    This inconsistency does not make sense, since an empty string (or null) effectively means that
    no label was set. With this change, null labels and empty strings are treated as valid and are
    considered equivalent to having no label at all.
    c88a879aec
  221. polespinasa force-pushed on Sep 3, 2026
  222. in src/wallet/rpc/backup.cpp:410 in c88a879aec outdated
     406 | +    // Malformed inputs (e.g. invalid label) are returned as per-item failures.
     407 | +    for (size_t i = 0; i < univalue_requests.size(); ++i) {
     408 | +        // Throws a top-level RPC error if "timestamp" is missing or invalid
     409 | +        std::optional<int64_t> timestamp = GetImportTimestamp(univalue_requests[i]);
     410 | +        try {
     411 | +            requests.push_back({i, ProcessUniValueDescriptor(univalue_requests[i], timestamp)});
    


    Sjors commented at 11:53 AM on September 3, 2026:

    In c88a879aec8521f54f3e961904bd723fdaee093f wallet: rpc: refactor: Extract UniValue processing from ProcessDescriptorImport: maybe keep requests.push_back out of try-catch.

    Ideally it shouldn't throw either.


    polespinasa commented at 4:01 PM on September 3, 2026:

    Are you thinking about changing from throwing to some util::Expected ?


    Sjors commented at 4:49 PM on September 3, 2026:

    Yes.


    polespinasa commented at 8:40 AM on September 4, 2026:

    I have checked and for now I will leave the try catch, maybe as a follow-up. Inside ProcessUniValueDescriptor there is LabelFromValue which also throws. So this would only move the try catch from one function to the other or would need a refactor for all functions using LabelFromValue to use util::Expected which is a bit off-scope of this PR. Maybe as a follow-up that could be done.

  223. polespinasa force-pushed on Sep 3, 2026
  224. DrahtBot added the label CI failed on Sep 3, 2026
  225. polespinasa commented at 12:09 PM on September 3, 2026: member

    Added a helper function to minimize the size of the main functions: https://github.com/bitcoin/bitcoin/compare/8724afc70ea93e29dabf1957b8f30fe406d212bf..b65792c32486acef25801fac1efdc3b989a8d1db

    This function cal also be used by other RPCs who rely on WalletErrorCodes. It avoid having duplicated switch cases around the codebase.

  226. polespinasa force-pushed on Sep 3, 2026
  227. polespinasa force-pushed on Sep 3, 2026
  228. polespinasa commented at 4:30 PM on September 3, 2026: member

    Force pushed to apply some comments by Sjors and David.

    • The two lambda functions are now extinct.
    • Negative timestamps are now treated as invalid. (small breaking change to the rpc interface)
    • Added some more test coverage for the label change behavior.
    • Cleaned a bit how the response was created taking into consideration request order and errors.

    Giving all this a prettier and cleaner RPC interface :)

  229. DrahtBot removed the label CI failed on Sep 3, 2026
  230. in src/wallet/imports.h:44 in d207e3f4fb
      39 | +
      40 | +    ImportResult() = default;
      41 | +    ImportResult(WalletErrorCode code, std::string message, bool general_error = false)
      42 | +        : error{ImportError{code, Untranslated(message), general_error}}
      43 | +    {}
      44 | +    ImportResult(WalletErrorCode code, std::string message, std::vector<std::string> warnings, bool general_error = false)
    


    Sjors commented at 4:56 PM on September 3, 2026:

    In d207e3f4fbee1ff5fa246fd53413b4e96694a3e9 wallet: Add ImportError struct and new WalletError codes: I prefer one constructor:

    ImportResult(WalletErrorCode code, std::string message,
                 std::vector<std::string> warnings = {}, bool general_error = false)
    

    Claude also suggests:

    drop the warnings parameter altogether and have ImportDescriptor use a small local lambda that captures the accumulated warnings


    polespinasa commented at 8:48 AM on September 4, 2026:

    taken (only the non-claude comment)

  231. in src/wallet/rpc/backup.cpp:584 in 649ced7e06
     629 | -        if (pwallet->IsAbortingRescan()) {
     630 | -            throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
     631 | +    // Translate each ImportResult into the per-input UniValue result, in input
     632 | +    // order. Inputs that failed to parse already hold an error in results[i]
     633 | +    // and are not part of `requests`, so they are skipped here.
     634 | +    size_t result_idx = 0;
    


    Sjors commented at 5:07 PM on September 3, 2026:

    In 649ced7e0637bb40b629874cbb90f7fb4b4db438 wallet: rename ProcessDescriptorImport to ImportDescriptor and add ProcessDescriptorsImport: marginally nicer imo:

    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index 2587af8609..f6f9c16b94 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -269,11 +269,12 @@ RPCMethod importdescriptors()
         }
    
    -    // Translate each ImportResult into the per-input UniValue result, in input
    -    // order. Inputs that failed to parse already hold an error in results[i]
    -    // and are not part of `requests`, so they are skipped here.
    -    size_t result_idx = 0;
    -    for (const auto& parsed : requests) {
    -        UniValue& result = results[parsed.input_index];
    -        const ImportResult& import_result = import_results.at(result_idx);
    +    // Translate each ImportResult into the per-input UniValue result. Inputs
    +    // that failed to parse already hold an error in results[] and are not
    +    // part of `requests`, so use input_index to map each import_results[k]
    +    // back to its slot.
    +    CHECK_NONFATAL(import_results.size() == requests.size());
    +    for (size_t k = 0; k < requests.size(); ++k) {
    +        UniValue& result = results[requests[k].input_index];
    +        const ImportResult& import_result = import_results[k];
             result = UniValue(UniValue::VOBJ);
             UniValue warnings(UniValue::VARR);
    @@ -293,5 +294,4 @@ RPCMethod importdescriptors()
             }
             PushWarnings(warnings, result);
    -        ++result_idx;
         }
    

    polespinasa commented at 8:57 AM on September 4, 2026:

    I like it, taken

  232. Sjors commented at 5:14 PM on September 3, 2026: member

    utACK cd360352442b849872ce35d95b2f1d1925dce326

  233. DrahtBot requested review from w0xlt on Sep 3, 2026
  234. DrahtBot requested review from achow101 on Sep 3, 2026
  235. wallet: Add ImportError struct and new WalletError codes
    ImportError struct will substitute the UniValue based response of ProcessDescriptorImport
    in a subsequent commit. This will allow other interfaces such as the GUI to consume those function.
    
    Added an extra constructors that will be useful in following commits.
    
    Also add new error codes to the WalletError codes enum, this is necessary in order to propagate
    error codes to the new interfaces and to translate errors to the rpc wallet error codes.
    536f6cfceb
  236. wallet, util: Add HandleWalletErrorCode
    Helper function to translate from WalletErrorCode to RPC error codes.
    abbe1a1f06
  237. wallet: rpc: refactor: ProcessDescriptorImport returns ImportDescriptorResult
    Make ProcessDescriptorImport return a ImportDescriptorResult object instead of a UniValue.
    
    This commit gets rid of a try-catch that generates a big diff of identation changes,
    it is recommended to review it with --ignore-space-change
    f50d56b1fc
  238. wallet: rename ProcessDescriptorImport to ImportDescriptor and add ProcessDescriptorsImport
    Rename ProcessDescriptorImport() to ImportDescriptor().
    ImportDescriptor() takes a single descriptor request and imports it into the wallet.
    
    Add ProcessDescriptorsImport() (descriptors in plural) to handle wallet locking and rescanning
    over a vector of ImportDescriptorRequest items. It is also in charge of collecting all responses
    of imports from ImportDescriptor.
    
    This allows a next commit to extract ImportDescriptor() and
    ProcessDescriptorsImport() out of the RPC code so they can be used by
    other future interfaces.
    de925a1f6a
  239. wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp
    Extract ImportDescriptor() and ProcessDescriptorsImport() out of backup.cpp and move them into imports.cpp so other future interfaces
    can use them without needing to know about RPC code.
    
    The commit can be reviewed with the --color-moved=dimmed-zebra option for an easier review.
    b323403f16
  240. wallet: add CheckDescriptorRangeBounds
    Co-authored-by: w0xlt <woltx@protonmail.com>
    
    ProcessUniValueDescriptor() checks for the descriptor range correctness, but since a
    future commit adds an interface to import descriptors, those checks must be extracted
    from RPC code. This commit adds a helper function to check ranges in descriptors and
    checks for the range inside ImportDescriptor().
    ProcessUniValueDescriptor keeps also the check as other RPC calls other than importdescriptors
    also needs to check for ranges. This introduces a double check on the RPC path,
    but the cost of it is minimal and can be accepted.
    89becfe065
  241. wallet: Add an importDescriptors() interface for the wallet d1036b8abe
  242. polespinasa force-pushed on Sep 4, 2026
  243. polespinasa commented at 8:59 AM on September 4, 2026: member

    Force pushed to address some nits :)


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-04 09:51 UTC

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