Coverage Report

Created: 2026-09-21 19:49

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