Coverage Report

Created: 2026-10-19 15:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/tmp/bitcoin/src/wallet/imports.cpp
Line
Count
Source
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
#include <wallet/scan.h>
8
9
#include <cstdint>
10
#include <limits>
11
12
namespace wallet {
13
14
ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& request) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
15
770
{
16
770
    AssertLockHeld(wallet.cs_wallet);
17
18
770
    std::vector<std::string> warnings;
19
20
    // Parse descriptor string
21
770
    FlatSigningProvider keys;
22
770
    std::string error;
23
770
    auto parsed_descs = Parse(request.descriptor, keys, error, /*require_checksum=*/true);
24
770
    if (parsed_descs.empty()) {
25
9
        return ImportResult(WalletErrorCode::InvalidDescriptor, error, warnings);
26
9
    }
27
28
761
    if (request.internal.has_value() && parsed_descs.size() > 1) {
29
1
        return ImportResult(
30
1
            WalletErrorCode::InvalidDescriptor,
31
1
            "Cannot have multipath descriptor while also specifying 'internal'",
32
1
            warnings
33
1
        );
34
1
    }
35
36
    // Range check
37
760
    bool is_ranged{false};
38
760
    int64_t range_start = 0, range_end = 1, next_index = 0;
39
760
    if (!parsed_descs.at(0)->IsRange() && request.range.has_value()) {
40
1
        return ImportResult(
41
1
            WalletErrorCode::InvalidParameter,
42
1
            "Range should not be specified for an un-ranged descriptor",
43
1
            warnings
44
1
        );
45
759
    } else if (parsed_descs.at(0)->IsRange()) {
46
483
        if (request.range.has_value()) {
47
126
            int64_t low = request.range->first;
48
126
            int64_t high = request.range->second;
49
126
            if (auto res = CheckDescriptorRangeBounds(low, high); !res) {
50
6
                return ImportResult(WalletErrorCode::InvalidParameter,
51
6
                    res.error());
52
6
            }
53
120
            range_start = low;
54
120
            range_end = high + 1; // Specified range end is inclusive, but we need range end as exclusive
55
357
        } else {
56
357
            warnings.emplace_back("Range not given, using default keypool range");
57
357
            range_start = 0;
58
357
            range_end = wallet.m_keypool_size;
59
357
        }
60
        // WalletDescriptor stores the range as int32_t with an exclusive end, so an
61
        // end past the maximum index would be truncated to a negative range.
62
477
        if (range_end > std::numeric_limits<int32_t>::max()) {
63
2
            return ImportResult(WalletErrorCode::InvalidParameter, "End of range is too high", warnings);
64
2
        }
65
475
        next_index = request.next_index.value_or(range_start);
66
475
        is_ranged = true;
67
68
475
        if (next_index < range_start || next_index >= range_end) {
69
0
            return ImportResult(
70
0
                WalletErrorCode::InvalidParameter,
71
0
                "next_index is out of range",
72
0
                warnings
73
0
            );
74
0
        }
75
475
    }
76
77
    // Active descriptors must be ranged
78
751
    if (request.active && !parsed_descs.at(0)->IsRange()) {
79
1
        return ImportResult(
80
1
            WalletErrorCode::InvalidParameter,
81
1
            "Active descriptors must be ranged",
82
1
            warnings
83
1
        );
84
1
    }
85
86
    // Multipath descriptors should not have a label
87
750
    if (parsed_descs.size() > 1 && !request.label.empty()) {
88
1
        return ImportResult(
89
1
            WalletErrorCode::InvalidParameter,
90
1
            "Multipath descriptors should not have a label",
91
1
            warnings
92
1
        );
93
1
    }
94
95
    // Ranged descriptors should not have a label
96
749
    if (is_ranged && !request.label.empty()) {
97
2
        return ImportResult(
98
2
            WalletErrorCode::InvalidParameter,
99
2
            "Ranged descriptors should not have a label",
100
2
            warnings
101
2
        );
102
2
    }
103
104
747
    bool desc_internal = request.internal.value_or(false);
105
    // Internal addresses should not have a label either
106
747
    if (desc_internal && !request.label.empty()) {
107
2
        return ImportResult(
108
2
            WalletErrorCode::InvalidParameter,
109
2
            "Internal addresses should not have a label",
110
2
            warnings
111
2
        );
112
2
    }
113
114
    // Combo descriptor check
115
745
    if (request.active && !parsed_descs.at(0)->IsSingleType()) {
116
1
        return ImportResult(
117
1
            WalletErrorCode::GenericError,
118
1
            "Combo descriptors cannot be set to active",
119
1
            warnings
120
1
        );
121
1
    }
122
123
    // If the wallet disabled private keys, abort if private keys exist
124
744
    if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
125
3
        return ImportResult(
126
3
            WalletErrorCode::GenericError,
127
3
            "Cannot import private keys to a wallet with private keys disabled",
128
3
            warnings
129
3
        );
130
3
    }
131
132
1.54k
    for (size_t j = 0; j < parsed_descs.size(); ++j) {
133
813
        auto parsed_desc = std::move(parsed_descs[j]);
134
813
        if (parsed_descs.size() == 2) {
135
132
            desc_internal = j == 1;
136
681
        } else if (parsed_descs.size() > 2) {
137
9
            CHECK_NONFATAL(!desc_internal);
138
9
        }
139
        // ExpandPrivate to whether the descriptor can be derived at the first index.
140
813
        FlatSigningProvider expand_keys;
141
813
        std::vector<CScript> scripts;
142
813
        if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) {
143
1
            return ImportResult(
144
1
                WalletErrorCode::GenericError,
145
1
                "Cannot expand descriptor. Probably because of hardened derivations without private keys provided",
146
1
                warnings
147
1
            );
148
1
        }
149
150
812
        for (const auto& w : parsed_desc->Warnings()) {
151
2
            warnings.push_back(w);
152
2
        }
153
154
        // If private keys are enabled, check some things.
155
812
        if (!wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
156
617
            if (keys.keys.empty()) {
157
4
                return ImportResult(
158
4
                    WalletErrorCode::GenericError,
159
4
                    "Cannot import descriptor without private keys to a wallet with private keys enabled",
160
4
                    warnings
161
4
                );
162
4
            }
163
613
            if (!parsed_desc->HavePrivateKeys(keys)) {
164
240
                warnings.emplace_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
165
240
            }
166
613
        }
167
        // If this is an unused(KEY) descriptor, check that the wallet doesn't already have other descriptors with this key
168
808
        if (!parsed_desc->HasScripts()) {
169
3
            if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
170
1
                return ImportResult(
171
1
                    WalletErrorCode::GenericError,
172
1
                    "Cannot import unused() to wallet without private keys enabled",
173
1
                    warnings
174
1
                );
175
1
            }
176
            // Unused descriptors must contain a single key.
177
            // Earlier checks will have enforced that this key is either a private key when private keys are enabled,
178
            // or that this key is a public key when private keys are disabled.
179
            // If we can retrieve the corresponding private key from the wallet, then this key is already in the wallet
180
            // and we should not import it.
181
2
            std::set<CPubKey> pubkeys;
182
2
            std::set<CExtPubKey> extpubs;
183
2
            parsed_desc->GetPubKeys(pubkeys, extpubs);
184
2
            std::transform(extpubs.begin(), extpubs.end(), std::inserter(pubkeys, pubkeys.begin()), [](const CExtPubKey& xpub) { return xpub.pubkey; });
185
2
            CHECK_NONFATAL(pubkeys.size() == 1);
186
2
            if (wallet.GetKey(pubkeys.begin()->GetID())) {
187
1
                return ImportResult(
188
1
                    WalletErrorCode::GenericError,
189
1
                    "Cannot import an unused() descriptor when its private key is already in the wallet",
190
1
                    warnings
191
1
                );
192
1
            }
193
2
        }
194
195
806
        Assume(request.timestamp.has_value());
196
806
        WalletDescriptor w_desc(std::move(parsed_desc), request.timestamp.value(), range_start, range_end, next_index);
197
198
        // Add descriptor to the wallet
199
806
        auto spk_manager_res = wallet.AddWalletDescriptor(w_desc, keys, request.label, desc_internal);
200
201
806
        if (!spk_manager_res) {
202
3
            return ImportResult(
203
3
                WalletErrorCode::GenericError,
204
3
                strprintf("Could not add descriptor '%s': %s", request.descriptor, util::ErrorString(spk_manager_res).original),
205
3
                warnings
206
3
            );
207
3
        }
208
209
803
        auto& spk_manager = spk_manager_res.value().get();
210
211
        // Set descriptor as active if necessary
212
803
        if (request.active) {
213
388
            if (!w_desc.descriptor->GetOutputType()) {
214
1
                warnings.emplace_back("Unknown output type, cannot set descriptor to active.");
215
387
            } else {
216
387
                wallet.AddActiveScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
217
387
            }
218
415
        } else {
219
415
            if (w_desc.descriptor->GetOutputType()) {
220
239
                wallet.DeactivateScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
221
239
            }
222
415
        }
223
803
    }
224
225
731
    ImportResult result;
226
731
    result.warnings = warnings;
227
731
    return result;
228
741
}
229
230
std::vector<ImportResult> ProcessDescriptorsImport(CWallet& wallet,
231
    std::vector<ImportDescriptorRequest>& requests)
232
686
{
233
686
    std::vector<ImportResult> response;
234
235
686
    WalletRescanReserver reserver(wallet);
236
686
    if (!reserver.reserve(/*with_passphrase=*/true)) {
237
1
        return {ImportResult{
238
1
            WalletErrorCode::GenericError,
239
1
            "Wallet is currently rescanning. Abort existing rescan or wait.",
240
1
            /*warnings=*/{},
241
1
            /*general_error=*/true
242
1
        }};
243
1
    }
244
245
    // Make sure the results are valid at least up to the most recent block
246
    // the user could have gotten from another RPC command prior to now
247
685
    wallet.BlockUntilSyncedToCurrentChain();
248
249
    // Ensure that the wallet is not locked for the remainder of this call,
250
    // as the passphrase is used to top up the keypool.
251
685
    LOCK(wallet.m_relock_mutex);
252
685
    int64_t now = 0;
253
685
    int64_t lowest_timestamp = 0;
254
685
    bool rescan = false;
255
685
    {
256
685
        LOCK(wallet.cs_wallet);
257
685
        if (wallet.IsLocked()) {
258
2
            return {ImportResult{
259
2
                WalletErrorCode::UnlockNeeded,
260
2
                "Error: Please enter the wallet passphrase with walletpassphrase first.",
261
2
                /*warnings=*/{},
262
2
                /*general_error=*/true
263
2
            }};
264
2
        }
265
266
683
        CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), interfaces::FoundBlock().time(lowest_timestamp).mtpTime(now)));
267
268
770
        for (ImportDescriptorRequest& request : requests) {
269
770
            request.timestamp = request.timestamp.value_or(now);
270
770
            const ImportResult& import_result = ImportDescriptor(wallet, request);
271
272
770
            if (lowest_timestamp > request.timestamp.value()) {
273
541
                lowest_timestamp = request.timestamp.value();
274
541
            }
275
770
            if (!import_result.has_error()) {
276
                // At least one request succeeded, so we need to rescan
277
731
                rescan = true;
278
731
            }
279
770
            response.push_back(import_result);
280
770
        }
281
683
        wallet.ConnectScriptPubKeyManNotifiers();
282
683
        wallet.RefreshAllTXOs();
283
683
    }
284
285
683
    if (rescan) {
286
639
        const int64_t scanned_time = wallet.Scanner().ScanFromTime(lowest_timestamp, reserver);
287
639
        wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
288
289
639
        if (wallet.Scanner().IsAborting()) {
290
1
            return {ImportResult{
291
1
                WalletErrorCode::MiscError,
292
1
                "Rescan aborted by user.",
293
1
                /*warnings=*/{},
294
1
                /*general_error=*/true
295
1
            }};
296
1
        }
297
298
638
        if (scanned_time > lowest_timestamp) {
299
            // Compose the response
300
4
            for (size_t i = 0; i < requests.size(); ++i) {
301
2
                ImportResult& result = response.at(i);
302
303
                // If the descriptor timestamp is within the successfully scanned
304
                // range, or if the import result already has an error set, let
305
                // the result stand unmodified. Otherwise replace the result
306
                // with an error message.
307
2
                const int64_t timestamp{requests.at(i).timestamp.value()};
308
2
                if (scanned_time > timestamp && !result.has_error()) {
309
2
                    std::string error_msg = strprintf("Rescan failed for descriptor with timestamp %d. There "
310
2
                        "was an error reading a block from time %d, which is after or within %d seconds "
311
2
                        "of key creation, and could contain transactions pertaining to the desc. As a "
312
2
                        "result, transactions and coins using this desc may not appear in the wallet.",
313
2
                        timestamp, scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW);
314
2
                    if (wallet.chain().havePruned()) {
315
0
                        error_msg += strprintf(" This error could be caused by pruning or data corruption "
316
0
                            "(see bitcoind log for details) and could be dealt with by downloading and "
317
0
                            "rescanning the relevant blocks (see -reindex option and rescanblockchain RPC).");
318
2
                    } else if (wallet.chain().hasAssumedValidChain()) {
319
2
                        error_msg += strprintf(" This error is likely caused by an in-progress assumeutxo "
320
2
                            "background sync. Check logs or getchainstates RPC for assumeutxo background "
321
2
                            "sync progress and try again later.");
322
2
                    } else {
323
0
                        error_msg += strprintf(" This error could potentially caused by data corruption. If "
324
0
                            "the issue persists you may want to reindex (see -reindex option).");
325
0
                    }
326
2
                    result.error = ImportError{
327
2
                        WalletErrorCode::MiscError,
328
2
                        Untranslated(error_msg),
329
2
                        /*is_wallet_error=*/false
330
2
                    };
331
2
                }
332
2
            }
333
2
        }
334
638
    }
335
682
    return response;
336
683
}
337
338
} // namespace wallet