Changeset View
Changeset View
Standalone View
Standalone View
src/wallet/rpcdump.cpp
Show First 20 Lines • Show All 1,822 Lines • ▼ Show 20 Lines | if (fRescan && fRunScan && requests.size()) { | ||||
++i; | ++i; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
return response; | return response; | ||||
} | } | ||||
static UniValue ProcessDescriptorImport(CWallet *const pwallet, | |||||
const UniValue &data, | |||||
const int64_t timestamp) | |||||
EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) { | |||||
UniValue warnings(UniValue::VARR); | |||||
UniValue result(UniValue::VOBJ); | |||||
try { | |||||
if (!data.exists("desc")) { | |||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found."); | |||||
} | |||||
const std::string &descriptor = data["desc"].get_str(); | |||||
const bool active = | |||||
data.exists("active") ? data["active"].get_bool() : false; | |||||
const bool internal = | |||||
data.exists("internal") ? data["internal"].get_bool() : false; | |||||
const std::string &label = | |||||
data.exists("label") ? data["label"].get_str() : ""; | |||||
// Parse descriptor string | |||||
FlatSigningProvider keys; | |||||
std::string error; | |||||
auto parsed_desc = | |||||
Parse(descriptor, keys, error, /* require_checksum = */ true); | |||||
if (!parsed_desc) { | |||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); | |||||
} | |||||
// Range check | |||||
int64_t range_start = 0, range_end = 1, next_index = 0; | |||||
if (!parsed_desc->IsRange() && data.exists("range")) { | |||||
throw JSONRPCError( | |||||
RPC_INVALID_PARAMETER, | |||||
"Range should not be specified for an un-ranged descriptor"); | |||||
} else if (parsed_desc->IsRange()) { | |||||
if (data.exists("range")) { | |||||
auto range = ParseDescriptorRange(data["range"]); | |||||
range_start = range.first; | |||||
// Specified range end is inclusive, but we need range end as | |||||
// exclusive | |||||
range_end = range.second + 1; | |||||
} else { | |||||
warnings.push_back( | |||||
"Range not given, using default keypool range"); | |||||
range_start = 0; | |||||
range_end = gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE); | |||||
} | |||||
next_index = range_start; | |||||
if (data.exists("next_index")) { | |||||
next_index = data["next_index"].get_int64(); | |||||
// bound checks | |||||
if (next_index < range_start || next_index >= range_end) { | |||||
throw JSONRPCError(RPC_INVALID_PARAMETER, | |||||
"next_index is out of range"); | |||||
} | |||||
} | |||||
} | |||||
// Active descriptors must be ranged | |||||
if (active && !parsed_desc->IsRange()) { | |||||
throw JSONRPCError(RPC_INVALID_PARAMETER, | |||||
"Active descriptors must be ranged"); | |||||
} | |||||
// Ranged descriptors should not have a label | |||||
if (data.exists("range") && data.exists("label")) { | |||||
throw JSONRPCError(RPC_INVALID_PARAMETER, | |||||
"Ranged descriptors should not have a label"); | |||||
} | |||||
// Internal addresses should not have a label either | |||||
if (internal && data.exists("label")) { | |||||
throw JSONRPCError(RPC_INVALID_PARAMETER, | |||||
"Internal addresses should not have a label"); | |||||
} | |||||
// Combo descriptor check | |||||
if (active && !parsed_desc->IsSingleType()) { | |||||
throw JSONRPCError(RPC_WALLET_ERROR, | |||||
"Combo descriptors cannot be set to active"); | |||||
} | |||||
// If the wallet disabled private keys, abort if private keys exist | |||||
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && | |||||
!keys.keys.empty()) { | |||||
throw JSONRPCError(RPC_WALLET_ERROR, | |||||
"Cannot import private keys to a wallet with " | |||||
"private keys disabled"); | |||||
} | |||||
// Need to ExpandPrivate to check if private keys are available for all | |||||
// pubkeys | |||||
FlatSigningProvider expand_keys; | |||||
std::vector<CScript> scripts; | |||||
parsed_desc->Expand(0, keys, scripts, expand_keys); | |||||
parsed_desc->ExpandPrivate(0, keys, expand_keys); | |||||
// 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 (!pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { | |||||
if (keys.keys.empty()) { | |||||
throw JSONRPCError( | |||||
RPC_WALLET_ERROR, | |||||
"Cannot import descriptor without private keys to a wallet " | |||||
"with private keys enabled"); | |||||
} | |||||
if (!have_all_privkeys) { | |||||
warnings.push_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); | |||||
// Check if the wallet already contains the descriptor | |||||
auto existing_spk_manager = | |||||
pwallet->GetDescriptorScriptPubKeyMan(w_desc); | |||||
if (existing_spk_manager) { | |||||
LOCK(existing_spk_manager->cs_desc_man); | |||||
if (range_start > | |||||
existing_spk_manager->GetWalletDescriptor().range_start) { | |||||
throw JSONRPCError( | |||||
RPC_INVALID_PARAMS, | |||||
strprintf( | |||||
"range_start can only decrease; current range = " | |||||
"[%d,%d]", | |||||
existing_spk_manager->GetWalletDescriptor().range_start, | |||||
existing_spk_manager->GetWalletDescriptor().range_end)); | |||||
} | |||||
} | |||||
// Add descriptor to the wallet | |||||
auto spk_manager = pwallet->AddWalletDescriptor(w_desc, keys, label); | |||||
if (spk_manager == nullptr) { | |||||
throw JSONRPCError( | |||||
RPC_WALLET_ERROR, | |||||
strprintf("Could not add descriptor '%s'", descriptor)); | |||||
} | |||||
// Set descriptor as active if necessary | |||||
if (active) { | |||||
if (!w_desc.descriptor->GetOutputType()) { | |||||
warnings.push_back( | |||||
"Unknown output type, cannot set descriptor to active."); | |||||
} else { | |||||
pwallet->SetActiveScriptPubKeyMan( | |||||
spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), | |||||
internal); | |||||
} | |||||
} | |||||
result.pushKV("success", UniValue(true)); | |||||
} catch (const UniValue &e) { | |||||
result.pushKV("success", UniValue(false)); | |||||
result.pushKV("error", e); | |||||
} catch (...) { | |||||
result.pushKV("success", UniValue(false)); | |||||
result.pushKV("error", | |||||
JSONRPCError(RPC_MISC_ERROR, "Missing required fields")); | |||||
} | |||||
if (warnings.size()) { | |||||
result.pushKV("warnings", warnings); | |||||
} | |||||
return result; | |||||
} | |||||
UniValue importdescriptors(const Config &config, | |||||
const JSONRPCRequest &main_request) { | |||||
// Acquire the wallet | |||||
std::shared_ptr<CWallet> const wallet = | |||||
GetWalletForJSONRPCRequest(main_request); | |||||
CWallet *const pwallet = wallet.get(); | |||||
if (!EnsureWalletIsAvailable(pwallet, main_request.fHelp)) { | |||||
return NullUniValue; | |||||
} | |||||
RPCHelpMan{ | |||||
"importdescriptors", | |||||
"\nImport descriptors. This will trigger a rescan of the blockchain " | |||||
"based on the earliest timestamp of all descriptors being imported. " | |||||
"Requires a new wallet backup.\n" | |||||
"\nNote: This call can take over an hour to complete if using an early " | |||||
"timestamp; during that time, other rpc calls\n" | |||||
"may report that the imported keys, addresses or scripts exist but " | |||||
"related transactions are still missing.\n", | |||||
{ | |||||
{"requests", | |||||
RPCArg::Type::ARR, | |||||
RPCArg::Optional::NO, | |||||
"Data to be imported", | |||||
{ | |||||
{ | |||||
"", | |||||
RPCArg::Type::OBJ, | |||||
RPCArg::Optional::OMITTED, | |||||
"", | |||||
{ | |||||
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, | |||||
"Descriptor to import."}, | |||||
{"active", RPCArg::Type::BOOL, /* default */ "false", | |||||
"Set this descriptor to be the active descriptor for " | |||||
"the corresponding output type/externality"}, | |||||
{"range", RPCArg::Type::RANGE, | |||||
RPCArg::Optional::OMITTED, | |||||
"If a ranged descriptor is used, this specifies the " | |||||
"end or the range (in the form [begin,end]) to " | |||||
"import"}, | |||||
{"next_index", RPCArg::Type::NUM, | |||||
RPCArg::Optional::OMITTED, | |||||
"If a ranged descriptor is set to active, this " | |||||
"specifies the next index to generate addresses " | |||||
"from"}, | |||||
{"timestamp", | |||||
RPCArg::Type::NUM, | |||||
RPCArg::Optional::NO, | |||||
"Time from which to start rescanning the blockchain " | |||||
"for this descriptor, in " + | |||||
UNIX_EPOCH_TIME + | |||||
"\n" | |||||
" " | |||||
" Use the string \"now\" to " | |||||
"substitute the current synced blockchain time.\n" | |||||
" " | |||||
" \"now\" can be specified to " | |||||
"bypass scanning, for outputs which are known to " | |||||
"never have been used, and\n" | |||||
" " | |||||
" 0 can be specified to scan the " | |||||
"entire blockchain. Blocks up to 2 hours before " | |||||
"the earliest timestamp\n" | |||||
" " | |||||
" of all descriptors being imported " | |||||
"will be scanned.", | |||||
/* oneline_description */ "", | |||||
{"timestamp | \"now\"", "integer / string"}}, | |||||
{"internal", RPCArg::Type::BOOL, /* default */ "false", | |||||
"Whether matching outputs should be treated as not " | |||||
"incoming payments (e.g. change)"}, | |||||
{"label", RPCArg::Type::STR, /* default */ "''", | |||||
"Label to assign to the address, only allowed with " | |||||
"internal=false"}, | |||||
}, | |||||
}, | |||||
}, | |||||
"\"requests\""}, | |||||
}, | |||||
RPCResult{RPCResult::Type::ARR, | |||||
"", | |||||
"Response is an array with the same size as the input that " | |||||
"has the execution result", | |||||
{ | |||||
{RPCResult::Type::OBJ, | |||||
"", | |||||
"", | |||||
{ | |||||
{RPCResult::Type::BOOL, "success", ""}, | |||||
{RPCResult::Type::ARR, | |||||
"warnings", | |||||
/* optional */ true, | |||||
"", | |||||
{ | |||||
{RPCResult::Type::STR, "", ""}, | |||||
}}, | |||||
{RPCResult::Type::OBJ, | |||||
"error", | |||||
/* optional */ true, | |||||
"", | |||||
{ | |||||
{RPCResult::Type::ELISION, "", "JSONRPC error"}, | |||||
}}, | |||||
}}, | |||||
}}, | |||||
RPCExamples{ | |||||
HelpExampleCli("importdescriptors", | |||||
"'[{ \"desc\": \"<my descriptor>\", " | |||||
"\"timestamp\":1455191478, \"internal\": true }, " | |||||
"{ \"desc\": \"<my desccriptor 2>\", \"label\": " | |||||
"\"example 2\", \"timestamp\": 1455191480 }]'") + | |||||
HelpExampleCli( | |||||
"importdescriptors", | |||||
"'[{ \"desc\": \"<my descriptor>\", \"timestamp\":1455191478, " | |||||
"\"active\": true, \"range\": [0,100], \"label\": \"<my " | |||||
"cashaddr wallet>\" }]'")}, | |||||
} | |||||
.Check(main_request); | |||||
// Make sure wallet is a descriptor wallet | |||||
if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { | |||||
throw JSONRPCError( | |||||
RPC_WALLET_ERROR, | |||||
"importdescriptors is not available for non-descriptor wallets"); | |||||
} | |||||
RPCTypeCheck(main_request.params, {UniValue::VARR, UniValue::VOBJ}); | |||||
WalletRescanReserver reserver(*pwallet); | |||||
if (!reserver.reserve()) { | |||||
throw JSONRPCError( | |||||
RPC_WALLET_ERROR, | |||||
"Wallet is currently rescanning. Abort existing rescan or wait."); | |||||
} | |||||
const UniValue &requests = main_request.params[0]; | |||||
const int64_t minimum_timestamp = 1; | |||||
int64_t now = 0; | |||||
int64_t lowest_timestamp = 0; | |||||
bool rescan = false; | |||||
UniValue response(UniValue::VARR); | |||||
{ | |||||
LOCK(pwallet->cs_wallet); | |||||
EnsureWalletIsUnlocked(pwallet); | |||||
CHECK_NONFATAL(pwallet->chain().findBlock( | |||||
pwallet->GetLastBlockHash(), | |||||
FoundBlock().time(lowest_timestamp).mtpTime(now))); | |||||
// Get all timestamps and extract the lowest timestamp | |||||
for (const UniValue &request : requests.getValues()) { | |||||
// This throws an error if "timestamp" doesn't exist | |||||
const int64_t timestamp = | |||||
std::max(GetImportTimestamp(request, now), minimum_timestamp); | |||||
const UniValue result = | |||||
ProcessDescriptorImport(pwallet, request, timestamp); | |||||
response.push_back(result); | |||||
if (lowest_timestamp > timestamp) { | |||||
lowest_timestamp = timestamp; | |||||
} | |||||
// If we know the chain tip, and at least one request was successful | |||||
// then allow rescan | |||||
if (!rescan && result["success"].get_bool()) { | |||||
rescan = true; | |||||
} | |||||
} | |||||
pwallet->ConnectScriptPubKeyManNotifiers(); | |||||
} | |||||
// Rescan the blockchain using the lowest timestamp | |||||
if (rescan) { | |||||
int64_t scanned_time = pwallet->RescanFromTime( | |||||
lowest_timestamp, reserver, true /* update */); | |||||
{ | |||||
LOCK(pwallet->cs_wallet); | |||||
pwallet->ReacceptWalletTransactions(); | |||||
} | |||||
if (pwallet->IsAbortingRescan()) { | |||||
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user."); | |||||
} | |||||
if (scanned_time > lowest_timestamp) { | |||||
std::vector<UniValue> results = response.getValues(); | |||||
response.clear(); | |||||
response.setArray(); | |||||
// Compose the response | |||||
for (unsigned int i = 0; i < requests.size(); ++i) { | |||||
const UniValue &request = requests.getValues().at(i); | |||||
// If the descriptor timestamp is within the successfully | |||||
// scanned 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 <= GetImportTimestamp(request, now) || | |||||
results.at(i).exists("error")) { | |||||
response.push_back(results.at(i)); | |||||
} else { | |||||
UniValue result = UniValue(UniValue::VOBJ); | |||||
result.pushKV("success", UniValue(false)); | |||||
result.pushKV( | |||||
"error", | |||||
JSONRPCError( | |||||
RPC_MISC_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 desc. As a " | |||||
"result, transactions and coins using this " | |||||
"desc may not appear in the wallet. 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 and -rescan " | |||||
"options).", | |||||
GetImportTimestamp(request, now), | |||||
scanned_time - TIMESTAMP_WINDOW - 1, | |||||
TIMESTAMP_WINDOW))); | |||||
response.push_back(std::move(result)); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
return response; | |||||
} | |||||
// clang-format off | // clang-format off | ||||
static const CRPCCommand commands[] = { | static const CRPCCommand commands[] = { | ||||
// category name actor (function) argNames | // category name actor (function) argNames | ||||
// ------------------- ------------------------ ---------------------- ---------- | // ------------------- ------------------------ ---------------------- ---------- | ||||
{ "wallet", "abortrescan", abortrescan, {} }, | { "wallet", "abortrescan", abortrescan, {} }, | ||||
{ "wallet", "dumpprivkey", dumpprivkey, {"address"} }, | { "wallet", "dumpprivkey", dumpprivkey, {"address"} }, | ||||
{ "wallet", "dumpwallet", dumpwallet, {"filename"} }, | { "wallet", "dumpwallet", dumpwallet, {"filename"} }, | ||||
{ "wallet", "importdescriptors", importdescriptors, {"requests"} }, | |||||
{ "wallet", "importmulti", importmulti, {"requests","options"} }, | { "wallet", "importmulti", importmulti, {"requests","options"} }, | ||||
{ "wallet", "importprivkey", importprivkey, {"privkey","label","rescan"} }, | { "wallet", "importprivkey", importprivkey, {"privkey","label","rescan"} }, | ||||
{ "wallet", "importwallet", importwallet, {"filename"} }, | { "wallet", "importwallet", importwallet, {"filename"} }, | ||||
{ "wallet", "importaddress", importaddress, {"address","label","rescan","p2sh"} }, | { "wallet", "importaddress", importaddress, {"address","label","rescan","p2sh"} }, | ||||
{ "wallet", "importprunedfunds", importprunedfunds, {"rawtransaction","txoutproof"} }, | { "wallet", "importprunedfunds", importprunedfunds, {"rawtransaction","txoutproof"} }, | ||||
{ "wallet", "importpubkey", importpubkey, {"pubkey","label","rescan"} }, | { "wallet", "importpubkey", importpubkey, {"pubkey","label","rescan"} }, | ||||
{ "wallet", "removeprunedfunds", removeprunedfunds, {"txid"} }, | { "wallet", "removeprunedfunds", removeprunedfunds, {"txid"} }, | ||||
}; | }; | ||||
Show All 9 Lines |