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