Branch data Line data Source code
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 : 775 : ImportResult ImportDescriptor(CWallet& wallet, const ImportDescriptorRequest& request) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
15 : : {
16 : 775 : AssertLockHeld(wallet.cs_wallet);
17 : :
18 : 775 : std::vector<std::string> warnings;
19 : :
20 : : // Parse descriptor string
21 : 775 : FlatSigningProvider keys;
22 [ - + ]: 775 : std::string error;
23 [ - + + - ]: 775 : auto parsed_descs = Parse(request.descriptor, keys, error, /*require_checksum=*/true);
24 [ + + ]: 775 : if (parsed_descs.empty()) {
25 [ + - + - ]: 27 : return ImportResult(WalletErrorCode::InvalidDescriptor, error, warnings);
26 : : }
27 : :
28 [ + + + + ]: 882 : if (request.internal.has_value() && parsed_descs.size() > 1) {
29 : 1 : return ImportResult(
30 : : WalletErrorCode::InvalidDescriptor,
31 [ + - ]: 1 : "Cannot have multipath descriptor while also specifying 'internal'",
32 : : warnings
33 [ + - + - ]: 3 : );
34 : : }
35 : :
36 : : // Range check
37 : 765 : bool is_ranged{false};
38 : 765 : int64_t range_start = 0, range_end = 1, next_index = 0;
39 [ + - + - : 765 : if (!parsed_descs.at(0)->IsRange() && request.range.has_value()) {
+ + + + ]
40 : 1 : return ImportResult(
41 : : WalletErrorCode::InvalidParameter,
42 [ + - ]: 1 : "Range should not be specified for an un-ranged descriptor",
43 : : warnings
44 [ + - + - ]: 3 : );
45 [ + - + - : 764 : } 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 [ - + + - ]: 18 : 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 : : } 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 : : }
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 [ + - + - : 4 : return ImportResult(WalletErrorCode::InvalidParameter, "End of range is too high", warnings);
+ - ]
64 : : }
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 : : WalletErrorCode::InvalidParameter,
71 [ # # ]: 0 : "next_index is out of range",
72 : : warnings
73 [ # # # # ]: 0 : );
74 : : }
75 : : }
76 : :
77 : : // Active descriptors must be ranged
78 [ + + + - : 1079 : if (request.active && !parsed_descs.at(0)->IsRange()) {
+ + ]
79 : 1 : return ImportResult(
80 : : WalletErrorCode::InvalidParameter,
81 [ + - ]: 1 : "Active descriptors must be ranged",
82 : : warnings
83 [ + - + - ]: 3 : );
84 : : }
85 : :
86 : : // Multipath descriptors should not have a label
87 [ - + + + : 755 : if (parsed_descs.size() > 1 && !request.label.empty()) {
+ + ]
88 : 1 : return ImportResult(
89 : : WalletErrorCode::InvalidParameter,
90 [ + - ]: 1 : "Multipath descriptors should not have a label",
91 : : warnings
92 [ + - + - ]: 3 : );
93 : : }
94 : :
95 : : // Ranged descriptors should not have a label
96 [ + + + + ]: 754 : if (is_ranged && !request.label.empty()) {
97 : 2 : return ImportResult(
98 : : WalletErrorCode::InvalidParameter,
99 [ + - ]: 2 : "Ranged descriptors should not have a label",
100 : : warnings
101 [ + - + - ]: 6 : );
102 : : }
103 : :
104 [ + + ]: 752 : bool desc_internal = request.internal.value_or(false);
105 : : // Internal addresses should not have a label either
106 [ + + + + ]: 115 : if (desc_internal && !request.label.empty()) {
107 : 2 : return ImportResult(
108 : : WalletErrorCode::InvalidParameter,
109 [ + - ]: 2 : "Internal addresses should not have a label",
110 : : warnings
111 [ + - + - ]: 6 : );
112 : : }
113 : :
114 : : // Combo descriptor check
115 [ + + + - : 1070 : if (request.active && !parsed_descs.at(0)->IsSingleType()) {
+ + ]
116 : 1 : return ImportResult(
117 : : WalletErrorCode::GenericError,
118 [ + - ]: 1 : "Combo descriptors cannot be set to active",
119 : : warnings
120 [ + - + - ]: 3 : );
121 : : }
122 : :
123 : : // If the wallet disabled private keys, abort if private keys exist
124 [ + - + + : 749 : if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
+ + ]
125 : 3 : return ImportResult(
126 : : WalletErrorCode::GenericError,
127 [ + - ]: 3 : "Cannot import private keys to a wallet with private keys disabled",
128 : : warnings
129 [ + - + - ]: 9 : );
130 : : }
131 : :
132 [ - + + + ]: 1554 : for (size_t j = 0; j < parsed_descs.size(); ++j) {
133 [ - + ]: 818 : auto parsed_desc = std::move(parsed_descs[j]);
134 [ - + + + ]: 818 : if (parsed_descs.size() == 2) {
135 : 132 : desc_internal = j == 1;
136 [ + + ]: 686 : } else if (parsed_descs.size() > 2) {
137 [ + - ]: 9 : CHECK_NONFATAL(!desc_internal);
138 : : }
139 : : // ExpandPrivate to whether the descriptor can be derived at the first index.
140 : 818 : FlatSigningProvider expand_keys;
141 : 818 : std::vector<CScript> scripts;
142 [ + - + + ]: 818 : if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) {
143 : 1 : return ImportResult(
144 : : WalletErrorCode::GenericError,
145 [ + - ]: 1 : "Cannot expand descriptor. Probably because of hardened derivations without private keys provided",
146 : : warnings
147 [ + - + - ]: 3 : );
148 : : }
149 : :
150 [ + - + + ]: 819 : for (const auto& w : parsed_desc->Warnings()) {
151 [ + - ]: 2 : warnings.push_back(w);
152 : 817 : }
153 : :
154 : : // If private keys are enabled, check some things.
155 [ + - + + ]: 817 : if (!wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
156 [ + + ]: 622 : if (keys.keys.empty()) {
157 : 4 : return ImportResult(
158 : : WalletErrorCode::GenericError,
159 [ + - ]: 4 : "Cannot import descriptor without private keys to a wallet with private keys enabled",
160 : : warnings
161 [ + - + - ]: 12 : );
162 : : }
163 [ + - + + ]: 618 : if (!parsed_desc->HavePrivateKeys(keys)) {
164 [ + - ]: 240 : warnings.emplace_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
165 : : }
166 : : }
167 : : // If this is an unused(KEY) descriptor, check that the wallet doesn't already have other descriptors with this key
168 [ + - + + ]: 813 : if (!parsed_desc->HasScripts()) {
169 [ + - + + ]: 3 : if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
170 : 1 : return ImportResult(
171 : : WalletErrorCode::GenericError,
172 [ + - ]: 1 : "Cannot import unused() to wallet without private keys enabled",
173 : : warnings
174 [ + - + - ]: 3 : );
175 : : }
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 [ + - ]: 4 : 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 : : WalletErrorCode::GenericError,
189 [ + - ]: 1 : "Cannot import an unused() descriptor when its private key is already in the wallet",
190 : : warnings
191 [ + - + - ]: 3 : );
192 : : }
193 : 2 : }
194 : :
195 [ + - ]: 811 : Assume(request.timestamp.has_value());
196 [ + - + - : 811 : WalletDescriptor w_desc(std::move(parsed_desc), request.timestamp.value(), range_start, range_end, next_index);
+ - ]
197 : :
198 : : // Add descriptor to the wallet
199 [ + - ]: 811 : auto spk_manager_res = wallet.AddWalletDescriptor(w_desc, keys, request.label, desc_internal);
200 : :
201 [ + + ]: 811 : if (!spk_manager_res) {
202 : 3 : return ImportResult(
203 : : WalletErrorCode::GenericError,
204 [ + - + - ]: 6 : strprintf("Could not add descriptor '%s': %s", request.descriptor, util::ErrorString(spk_manager_res).original),
205 : : warnings
206 [ + - + - ]: 9 : );
207 : : }
208 : :
209 [ + + ]: 808 : auto& spk_manager = spk_manager_res.value().get();
210 : :
211 : : // Set descriptor as active if necessary
212 [ + + ]: 808 : if (request.active) {
213 [ + - + + ]: 388 : if (!w_desc.descriptor->GetOutputType()) {
214 [ + - ]: 1 : warnings.emplace_back("Unknown output type, cannot set descriptor to active.");
215 : : } else {
216 [ + - + - : 387 : wallet.AddActiveScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
+ - ]
217 : : }
218 : : } else {
219 [ + - + + ]: 420 : if (w_desc.descriptor->GetOutputType()) {
220 [ + - + - : 239 : wallet.DeactivateScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
+ - ]
221 : : }
222 : : }
223 : 818 : }
224 : :
225 : 736 : ImportResult result;
226 [ + - ]: 736 : result.warnings = warnings;
227 : 736 : return result;
228 : 775 : }
229 : :
230 : 691 : std::vector<ImportResult> ProcessDescriptorsImport(CWallet& wallet,
231 : : std::vector<ImportDescriptorRequest>& requests)
232 : : {
233 : 691 : std::vector<ImportResult> response;
234 : :
235 [ + - ]: 691 : WalletRescanReserver reserver(wallet);
236 [ + - + + ]: 691 : if (!reserver.reserve(/*with_passphrase=*/true)) {
237 : 1 : return {ImportResult{
238 : : WalletErrorCode::GenericError,
239 [ + - ]: 2 : "Wallet is currently rescanning. Abort existing rescan or wait.",
240 : : /*warnings=*/{},
241 : : /*general_error=*/true
242 [ - + + + : 2 : }};
- - ]
243 : : }
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 [ + - ]: 690 : 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 [ + - ]: 690 : LOCK(wallet.m_relock_mutex);
252 : 690 : int64_t now = 0;
253 : 690 : int64_t lowest_timestamp = 0;
254 : 690 : bool rescan = false;
255 : 690 : {
256 [ + - ]: 690 : LOCK(wallet.cs_wallet);
257 [ + - + + ]: 690 : if (wallet.IsLocked()) {
258 : 2 : return {ImportResult{
259 : : WalletErrorCode::UnlockNeeded,
260 [ + - + - ]: 4 : "Error: Please enter the wallet passphrase with walletpassphrase first.",
261 : : /*warnings=*/{},
262 : : /*general_error=*/true
263 [ - + + + : 4 : }};
- - ]
264 : : }
265 : :
266 [ + - + - ]: 688 : CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), interfaces::FoundBlock().time(lowest_timestamp).mtpTime(now)));
267 : :
268 [ + + ]: 1463 : for (ImportDescriptorRequest& request : requests) {
269 [ + + + - ]: 1550 : request.timestamp = request.timestamp.value_or(now);
270 [ + - ]: 775 : const ImportResult& import_result = ImportDescriptor(wallet, request);
271 : :
272 [ + - + + ]: 775 : if (lowest_timestamp > request.timestamp.value()) {
273 : 544 : lowest_timestamp = request.timestamp.value();
274 : : }
275 [ + + ]: 775 : if (!import_result.has_error()) {
276 : : // At least one request succeeded, so we need to rescan
277 : 736 : rescan = true;
278 : : }
279 [ + - ]: 775 : response.push_back(import_result);
280 : 775 : }
281 [ + - ]: 688 : wallet.ConnectScriptPubKeyManNotifiers();
282 [ + - ]: 688 : wallet.RefreshAllTXOs();
283 : 2 : }
284 : :
285 [ + + ]: 688 : if (rescan) {
286 [ + - + - ]: 644 : const int64_t scanned_time = wallet.Scanner().ScanFromTime(lowest_timestamp, reserver);
287 [ + - ]: 644 : wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
288 : :
289 [ + - + + ]: 644 : if (wallet.Scanner().IsAborting()) {
290 : 1 : return {ImportResult{
291 : : WalletErrorCode::MiscError,
292 [ + - ]: 2 : "Rescan aborted by user.",
293 : : /*warnings=*/{},
294 : : /*general_error=*/true
295 [ - + + + : 2 : }};
- - ]
296 : : }
297 : :
298 [ + + ]: 643 : 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 : : "was an error reading a block from time %d, which is after or within %d seconds "
311 : : "of key creation, and could contain transactions pertaining to the desc. As a "
312 : : "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 : : "(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 [ + - ]: 4 : error_msg += strprintf(" This error is likely caused by an in-progress assumeutxo "
320 : : "background sync. Check logs or getchainstates RPC for assumeutxo background "
321 : 2 : "sync progress and try again later.");
322 : : } 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 : : }
326 [ - + ]: 4 : result.error = ImportError{
327 : : WalletErrorCode::MiscError,
328 [ + - ]: 4 : Untranslated(error_msg),
329 : : /*is_wallet_error=*/false
330 : 2 : };
331 : 2 : }
332 : : }
333 : : }
334 : : }
335 : 687 : return response;
336 [ + - + - : 695 : }
+ - ]
337 : :
338 : : } // namespace wallet
|