Skip to content

Commit

Permalink
add importdescriptors RPC and tests for native descriptor wallets
Browse files Browse the repository at this point in the history
Co-authored-by: Andrew Chow <[email protected]>
  • Loading branch information
hugohn and achow101 committed Apr 23, 2020
1 parent ce24a94 commit f193ea8
Show file tree
Hide file tree
Showing 10 changed files with 905 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "importpubkey", 2, "rescan" },
{ "importmulti", 0, "requests" },
{ "importmulti", 1, "options" },
{ "importdescriptors", 0, "requests" },
{ "verifychain", 0, "checklevel" },
{ "verifychain", 1, "nblocks" },
{ "getblockstats", 0, "hash_or_height" },
Expand Down
294 changes: 294 additions & 0 deletions src/wallet/rpcdump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1458,3 +1458,297 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)

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;
range_end = range.second + 1; // Specified range end is inclusive, but we need range end as exclusive
} 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 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 bech32 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);
{
auto locked_chain = pwallet->chain().lock();
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 */);
{
auto locked_chain = pwallet->chain().lock();
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;
}
2 changes: 2 additions & 0 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4289,6 +4289,7 @@ UniValue importwallet(const JSONRPCRequest& request);
UniValue importprunedfunds(const JSONRPCRequest& request);
UniValue removeprunedfunds(const JSONRPCRequest& request);
UniValue importmulti(const JSONRPCRequest& request);
UniValue importdescriptors(const JSONRPCRequest& request);

void RegisterWalletRPCCommands(interfaces::Chain& chain, std::vector<std::unique_ptr<interfaces::Handler>>& handlers)
{
Expand Down Expand Up @@ -4318,6 +4319,7 @@ static const CRPCCommand commands[] =
{ "wallet", "getbalances", &getbalances, {} },
{ "wallet", "getwalletinfo", &getwalletinfo, {} },
{ "wallet", "importaddress", &importaddress, {"address","label","rescan","p2sh"} },
{ "wallet", "importdescriptors", &importdescriptors, {"requests"} },
{ "wallet", "importmulti", &importmulti, {"requests","options"} },
{ "wallet", "importprivkey", &importprivkey, {"privkey","label","rescan"} },
{ "wallet", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} },
Expand Down
41 changes: 41 additions & 0 deletions src/wallet/scriptpubkeyman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1745,6 +1745,15 @@ void DescriptorScriptPubKeyMan::MarkUnusedAddresses(const CScript& script)
}
}

void DescriptorScriptPubKeyMan::AddDescriptorKey(const CKey& key, const CPubKey &pubkey)
{
LOCK(cs_desc_man);
WalletBatch batch(m_storage.GetDatabase());
if (!AddDescriptorKeyWithDB(batch, key, pubkey)) {
throw std::runtime_error(std::string(__func__) + ": writing descriptor private key failed");
}
}

bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey)
{
AssertLockHeld(cs_desc_man);
Expand Down Expand Up @@ -2121,3 +2130,35 @@ bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKe
m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key);
return true;
}

bool DescriptorScriptPubKeyMan::HasWalletDescriptor(const WalletDescriptor& desc) const
{
LOCK(cs_desc_man);
return m_wallet_descriptor.descriptor != nullptr && desc.descriptor != nullptr && m_wallet_descriptor.descriptor->ToString() == desc.descriptor->ToString();
}

void DescriptorScriptPubKeyMan::WriteDescriptor()
{
LOCK(cs_desc_man);
WalletBatch batch(m_storage.GetDatabase());
if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) {
throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
}
}

const WalletDescriptor DescriptorScriptPubKeyMan::GetWalletDescriptor() const
{
return m_wallet_descriptor;
}

const std::vector<CScript> DescriptorScriptPubKeyMan::GetScriptPubKeys() const
{
LOCK(cs_desc_man);
std::vector<CScript> script_pub_keys;
script_pub_keys.reserve(m_map_script_pub_keys.size());

for (auto const& script_pub_key: m_map_script_pub_keys) {
script_pub_keys.push_back(script_pub_key.first);
}
return script_pub_keys;
}
7 changes: 7 additions & 0 deletions src/wallet/scriptpubkeyman.h
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,13 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan

bool AddKey(const CKeyID& key_id, const CKey& key);
bool AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector<unsigned char>& crypted_key);

bool HasWalletDescriptor(const WalletDescriptor& desc) const;
void AddDescriptorKey(const CKey& key, const CPubKey &pubkey);
void WriteDescriptor();

const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
const std::vector<CScript> GetScriptPubKeys() const;
};

#endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H
Loading

0 comments on commit f193ea8

Please sign in to comment.