mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-04 13:55:23 -05:00
add importdescriptors RPC and tests for native descriptor wallets
Co-authored-by: Andrew Chow <achow101-github@achow101.com>
This commit is contained in:
parent
ce24a94494
commit
f193ea889d
10 changed files with 905 additions and 0 deletions
|
@ -131,6 +131,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||||
{ "importpubkey", 2, "rescan" },
|
{ "importpubkey", 2, "rescan" },
|
||||||
{ "importmulti", 0, "requests" },
|
{ "importmulti", 0, "requests" },
|
||||||
{ "importmulti", 1, "options" },
|
{ "importmulti", 1, "options" },
|
||||||
|
{ "importdescriptors", 0, "requests" },
|
||||||
{ "verifychain", 0, "checklevel" },
|
{ "verifychain", 0, "checklevel" },
|
||||||
{ "verifychain", 1, "nblocks" },
|
{ "verifychain", 1, "nblocks" },
|
||||||
{ "getblockstats", 0, "hash_or_height" },
|
{ "getblockstats", 0, "hash_or_height" },
|
||||||
|
|
|
@ -1458,3 +1458,297 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
|
||||||
|
|
||||||
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -4289,6 +4289,7 @@ UniValue importwallet(const JSONRPCRequest& request);
|
||||||
UniValue importprunedfunds(const JSONRPCRequest& request);
|
UniValue importprunedfunds(const JSONRPCRequest& request);
|
||||||
UniValue removeprunedfunds(const JSONRPCRequest& request);
|
UniValue removeprunedfunds(const JSONRPCRequest& request);
|
||||||
UniValue importmulti(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)
|
void RegisterWalletRPCCommands(interfaces::Chain& chain, std::vector<std::unique_ptr<interfaces::Handler>>& handlers)
|
||||||
{
|
{
|
||||||
|
@ -4318,6 +4319,7 @@ static const CRPCCommand commands[] =
|
||||||
{ "wallet", "getbalances", &getbalances, {} },
|
{ "wallet", "getbalances", &getbalances, {} },
|
||||||
{ "wallet", "getwalletinfo", &getwalletinfo, {} },
|
{ "wallet", "getwalletinfo", &getwalletinfo, {} },
|
||||||
{ "wallet", "importaddress", &importaddress, {"address","label","rescan","p2sh"} },
|
{ "wallet", "importaddress", &importaddress, {"address","label","rescan","p2sh"} },
|
||||||
|
{ "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", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} },
|
{ "wallet", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} },
|
||||||
|
|
|
@ -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)
|
bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey)
|
||||||
{
|
{
|
||||||
AssertLockHeld(cs_desc_man);
|
AssertLockHeld(cs_desc_man);
|
||||||
|
@ -2121,3 +2130,35 @@ bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKe
|
||||||
m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key);
|
m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key);
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -580,6 +580,13 @@ public:
|
||||||
|
|
||||||
bool AddKey(const CKeyID& key_id, const CKey& key);
|
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 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
|
#endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H
|
||||||
|
|
|
@ -4399,6 +4399,7 @@ void CWallet::SetupDescriptorScriptPubKeyMans()
|
||||||
|
|
||||||
void CWallet::SetActiveScriptPubKeyMan(uint256 id, OutputType type, bool internal, bool memonly)
|
void CWallet::SetActiveScriptPubKeyMan(uint256 id, OutputType type, bool internal, bool memonly)
|
||||||
{
|
{
|
||||||
|
WalletLogPrintf("Setting spkMan to active: id = %s, type = %d, internal = %d\n", id.ToString(), static_cast<int>(type), static_cast<int>(internal));
|
||||||
auto& spk_mans = internal ? m_internal_spk_managers : m_external_spk_managers;
|
auto& spk_mans = internal ? m_internal_spk_managers : m_external_spk_managers;
|
||||||
auto spk_man = m_spk_managers.at(id).get();
|
auto spk_man = m_spk_managers.at(id).get();
|
||||||
spk_man->SetType(type, internal);
|
spk_man->SetType(type, internal);
|
||||||
|
@ -4421,3 +4422,88 @@ bool CWallet::IsLegacy() const
|
||||||
auto spk_man = dynamic_cast<LegacyScriptPubKeyMan*>(m_internal_spk_managers.at(OutputType::LEGACY));
|
auto spk_man = dynamic_cast<LegacyScriptPubKeyMan*>(m_internal_spk_managers.at(OutputType::LEGACY));
|
||||||
return spk_man != nullptr;
|
return spk_man != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DescriptorScriptPubKeyMan* CWallet::GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const
|
||||||
|
{
|
||||||
|
for (auto& spk_man_pair : m_spk_managers) {
|
||||||
|
// Try to downcast to DescriptorScriptPubKeyMan then check if the descriptors match
|
||||||
|
DescriptorScriptPubKeyMan* spk_manager = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man_pair.second.get());
|
||||||
|
if (spk_manager != nullptr && spk_manager->HasWalletDescriptor(desc)) {
|
||||||
|
return spk_manager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptPubKeyMan* CWallet::AddWalletDescriptor(WalletDescriptor& desc, const FlatSigningProvider& signing_provider, const std::string& label)
|
||||||
|
{
|
||||||
|
if (!IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
|
||||||
|
WalletLogPrintf("Cannot add WalletDescriptor to a non-descriptor wallet\n");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCK(cs_wallet);
|
||||||
|
auto new_spk_man = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, desc));
|
||||||
|
|
||||||
|
// If we already have this descriptor, remove it from the maps but add the existing cache to desc
|
||||||
|
auto old_spk_man = GetDescriptorScriptPubKeyMan(desc);
|
||||||
|
if (old_spk_man) {
|
||||||
|
WalletLogPrintf("Update existing descriptor: %s\n", desc.descriptor->ToString());
|
||||||
|
|
||||||
|
{
|
||||||
|
LOCK(old_spk_man->cs_desc_man);
|
||||||
|
new_spk_man->SetCache(old_spk_man->GetWalletDescriptor().cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from maps of active spkMans
|
||||||
|
auto old_spk_man_id = old_spk_man->GetID();
|
||||||
|
for (bool internal : {false, true}) {
|
||||||
|
for (OutputType t : OUTPUT_TYPES) {
|
||||||
|
auto active_spk_man = GetScriptPubKeyMan(t, internal);
|
||||||
|
if (active_spk_man && active_spk_man->GetID() == old_spk_man_id) {
|
||||||
|
if (internal) {
|
||||||
|
m_internal_spk_managers.erase(t);
|
||||||
|
} else {
|
||||||
|
m_external_spk_managers.erase(t);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_spk_managers.erase(old_spk_man_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the private keys to the descriptor
|
||||||
|
for (const auto& entry : signing_provider.keys) {
|
||||||
|
const CKey& key = entry.second;
|
||||||
|
new_spk_man->AddDescriptorKey(key, key.GetPubKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top up key pool, the manager will generate new scriptPubKeys internally
|
||||||
|
new_spk_man->TopUp();
|
||||||
|
|
||||||
|
// Apply the label if necessary
|
||||||
|
// Note: we disable labels for ranged descriptors
|
||||||
|
if (!desc.descriptor->IsRange()) {
|
||||||
|
auto script_pub_keys = new_spk_man->GetScriptPubKeys();
|
||||||
|
if (script_pub_keys.empty()) {
|
||||||
|
WalletLogPrintf("Could not generate scriptPubKeys (cache is empty)\n");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
CTxDestination dest;
|
||||||
|
if (ExtractDestination(script_pub_keys.at(0), dest)) {
|
||||||
|
SetAddressBook(dest, label, "receive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the descriptor to memory
|
||||||
|
auto ret = new_spk_man.get();
|
||||||
|
m_spk_managers[new_spk_man->GetID()] = std::move(new_spk_man);
|
||||||
|
|
||||||
|
// Save the descriptor to DB
|
||||||
|
ret->WriteDescriptor();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
|
@ -1250,6 +1250,12 @@ public:
|
||||||
|
|
||||||
//! Create new DescriptorScriptPubKeyMans and add them to the wallet
|
//! Create new DescriptorScriptPubKeyMans and add them to the wallet
|
||||||
void SetupDescriptorScriptPubKeyMans();
|
void SetupDescriptorScriptPubKeyMans();
|
||||||
|
|
||||||
|
//! Return the DescriptorScriptPubKeyMan for a WalletDescriptor if it is already in the wallet
|
||||||
|
DescriptorScriptPubKeyMan* GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const;
|
||||||
|
|
||||||
|
//! Add a descriptor to the wallet, return a ScriptPubKeyMan & associated output type
|
||||||
|
ScriptPubKeyMan* AddWalletDescriptor(WalletDescriptor& desc, const FlatSigningProvider& signing_provider, const std::string& label);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,6 +13,10 @@ from test_framework.address import (
|
||||||
script_to_p2sh_p2wsh,
|
script_to_p2sh_p2wsh,
|
||||||
script_to_p2wsh,
|
script_to_p2wsh,
|
||||||
)
|
)
|
||||||
|
from test_framework.key import (
|
||||||
|
bytes_to_wif,
|
||||||
|
ECKey,
|
||||||
|
)
|
||||||
from test_framework.script import (
|
from test_framework.script import (
|
||||||
CScript,
|
CScript,
|
||||||
OP_0,
|
OP_0,
|
||||||
|
@ -66,6 +70,25 @@ def get_key(node):
|
||||||
p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(),
|
p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(),
|
||||||
p2sh_p2wpkh_addr=key_to_p2sh_p2wpkh(pubkey))
|
p2sh_p2wpkh_addr=key_to_p2sh_p2wpkh(pubkey))
|
||||||
|
|
||||||
|
def get_generate_key():
|
||||||
|
"""Generate a fresh key
|
||||||
|
|
||||||
|
Returns a named tuple of privkey, pubkey and all address and scripts."""
|
||||||
|
eckey = ECKey()
|
||||||
|
eckey.generate()
|
||||||
|
privkey = bytes_to_wif(eckey.get_bytes())
|
||||||
|
pubkey = eckey.get_pubkey().get_bytes().hex()
|
||||||
|
pkh = hash160(hex_str_to_bytes(pubkey))
|
||||||
|
return Key(privkey=privkey,
|
||||||
|
pubkey=pubkey,
|
||||||
|
p2pkh_script=CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]).hex(),
|
||||||
|
p2pkh_addr=key_to_p2pkh(pubkey),
|
||||||
|
p2wpkh_script=CScript([OP_0, pkh]).hex(),
|
||||||
|
p2wpkh_addr=key_to_p2wpkh(pubkey),
|
||||||
|
p2sh_p2wpkh_script=CScript([OP_HASH160, hash160(CScript([OP_0, pkh])), OP_EQUAL]).hex(),
|
||||||
|
p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(),
|
||||||
|
p2sh_p2wpkh_addr=key_to_p2sh_p2wpkh(pubkey))
|
||||||
|
|
||||||
def get_multisig(node):
|
def get_multisig(node):
|
||||||
"""Generate a fresh 2-of-3 multisig on node
|
"""Generate a fresh 2-of-3 multisig on node
|
||||||
|
|
||||||
|
|
|
@ -180,6 +180,7 @@ BASE_SCRIPTS = [
|
||||||
'mempool_expiry.py',
|
'mempool_expiry.py',
|
||||||
'wallet_import_rescan.py',
|
'wallet_import_rescan.py',
|
||||||
'wallet_import_with_label.py',
|
'wallet_import_with_label.py',
|
||||||
|
'wallet_importdescriptors.py',
|
||||||
'rpc_bind.py --ipv4',
|
'rpc_bind.py --ipv4',
|
||||||
'rpc_bind.py --ipv6',
|
'rpc_bind.py --ipv6',
|
||||||
'rpc_bind.py --nonloopback',
|
'rpc_bind.py --nonloopback',
|
||||||
|
|
444
test/functional/wallet_importdescriptors.py
Executable file
444
test/functional/wallet_importdescriptors.py
Executable file
|
@ -0,0 +1,444 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2019 The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
"""Test the importdescriptors RPC.
|
||||||
|
|
||||||
|
Test importdescriptors by generating keys on node0, importing the corresponding
|
||||||
|
descriptors on node1 and then testing the address info for the different address
|
||||||
|
variants.
|
||||||
|
|
||||||
|
- `get_generate_key()` is called to generate keys and return the privkeys,
|
||||||
|
pubkeys and all variants of scriptPubKey and address.
|
||||||
|
- `test_importdesc()` is called to send an importdescriptors call to node1, test
|
||||||
|
success, and (if unsuccessful) test the error code and error message returned.
|
||||||
|
- `test_address()` is called to call getaddressinfo for an address on node1
|
||||||
|
and test the values returned."""
|
||||||
|
|
||||||
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.descriptors import descsum_create
|
||||||
|
from test_framework.util import (
|
||||||
|
assert_equal,
|
||||||
|
assert_raises_rpc_error,
|
||||||
|
find_vout_for_address,
|
||||||
|
)
|
||||||
|
from test_framework.wallet_util import (
|
||||||
|
get_generate_key,
|
||||||
|
test_address,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ImportDescriptorsTest(BitcoinTestFramework):
|
||||||
|
def set_test_params(self):
|
||||||
|
self.num_nodes = 2
|
||||||
|
self.extra_args = [["-addresstype=legacy"],
|
||||||
|
["-addresstype=bech32", "-keypool=5"]
|
||||||
|
]
|
||||||
|
self.setup_clean_chain = True
|
||||||
|
|
||||||
|
def skip_test_if_missing_module(self):
|
||||||
|
self.skip_if_no_wallet()
|
||||||
|
|
||||||
|
def test_importdesc(self, req, success, error_code=None, error_message=None, warnings=None, wallet=None):
|
||||||
|
"""Run importdescriptors and assert success"""
|
||||||
|
if warnings is None:
|
||||||
|
warnings = []
|
||||||
|
wrpc = self.nodes[1].get_wallet_rpc('w1')
|
||||||
|
if wallet is not None:
|
||||||
|
wrpc = wallet
|
||||||
|
|
||||||
|
result = wrpc.importdescriptors([req])
|
||||||
|
observed_warnings = []
|
||||||
|
if 'warnings' in result[0]:
|
||||||
|
observed_warnings = result[0]['warnings']
|
||||||
|
assert_equal("\n".join(sorted(warnings)), "\n".join(sorted(observed_warnings)))
|
||||||
|
assert_equal(result[0]['success'], success)
|
||||||
|
if error_code is not None:
|
||||||
|
assert_equal(result[0]['error']['code'], error_code)
|
||||||
|
assert_equal(result[0]['error']['message'], error_message)
|
||||||
|
|
||||||
|
def run_test(self):
|
||||||
|
self.log.info('Setting up wallets')
|
||||||
|
self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False)
|
||||||
|
w0 = self.nodes[0].get_wallet_rpc('w0')
|
||||||
|
|
||||||
|
self.nodes[1].createwallet(wallet_name='w1', disable_private_keys=True, blank=True, descriptors=True)
|
||||||
|
w1 = self.nodes[1].get_wallet_rpc('w1')
|
||||||
|
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||||
|
|
||||||
|
self.nodes[1].createwallet(wallet_name="wpriv", disable_private_keys=False, blank=True, descriptors=True)
|
||||||
|
wpriv = self.nodes[1].get_wallet_rpc("wpriv")
|
||||||
|
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 0)
|
||||||
|
|
||||||
|
self.log.info('Mining coins')
|
||||||
|
w0.generatetoaddress(101, w0.getnewaddress())
|
||||||
|
|
||||||
|
# RPC importdescriptors -----------------------------------------------
|
||||||
|
|
||||||
|
# # Test import fails if no descriptor present
|
||||||
|
key = get_generate_key()
|
||||||
|
self.log.info("Import should fail if a descriptor is not provided")
|
||||||
|
self.test_importdesc({"timestamp": "now"},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message='Descriptor not found.')
|
||||||
|
|
||||||
|
# # Test importing of a P2PKH descriptor
|
||||||
|
key = get_generate_key()
|
||||||
|
self.log.info("Should import a p2pkh descriptor")
|
||||||
|
self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"),
|
||||||
|
"timestamp": "now",
|
||||||
|
"label": "Descriptor import test"},
|
||||||
|
success=True)
|
||||||
|
test_address(w1,
|
||||||
|
key.p2pkh_addr,
|
||||||
|
solvable=True,
|
||||||
|
ismine=True,
|
||||||
|
labels=["Descriptor import test"])
|
||||||
|
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||||
|
|
||||||
|
self.log.info("Internal addresses cannot have labels")
|
||||||
|
self.test_importdesc({"desc": descsum_create("pkh(" + key.pubkey + ")"),
|
||||||
|
"timestamp": "now",
|
||||||
|
"internal": True,
|
||||||
|
"label": "Descriptor import test"},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message="Internal addresses should not have a label")
|
||||||
|
|
||||||
|
# # Test importing of a P2SH-P2WPKH descriptor
|
||||||
|
key = get_generate_key()
|
||||||
|
self.log.info("Should not import a p2sh-p2wpkh descriptor without checksum")
|
||||||
|
self.test_importdesc({"desc": "sh(wpkh(" + key.pubkey + "))",
|
||||||
|
"timestamp": "now"
|
||||||
|
},
|
||||||
|
success=False,
|
||||||
|
error_code=-5,
|
||||||
|
error_message="Missing checksum")
|
||||||
|
|
||||||
|
self.log.info("Should not import a p2sh-p2wpkh descriptor that has range specified")
|
||||||
|
self.test_importdesc({"desc": descsum_create("sh(wpkh(" + key.pubkey + "))"),
|
||||||
|
"timestamp": "now",
|
||||||
|
"range": 1,
|
||||||
|
},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message="Range should not be specified for an un-ranged descriptor")
|
||||||
|
|
||||||
|
self.log.info("Should not import a p2sh-p2wpkh descriptor and have it set to active")
|
||||||
|
self.test_importdesc({"desc": descsum_create("sh(wpkh(" + key.pubkey + "))"),
|
||||||
|
"timestamp": "now",
|
||||||
|
"active": True,
|
||||||
|
},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message="Active descriptors must be ranged")
|
||||||
|
|
||||||
|
self.log.info("Should import a (non-active) p2sh-p2wpkh descriptor")
|
||||||
|
self.test_importdesc({"desc": descsum_create("sh(wpkh(" + key.pubkey + "))"),
|
||||||
|
"timestamp": "now",
|
||||||
|
"active": False,
|
||||||
|
},
|
||||||
|
success=True)
|
||||||
|
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||||
|
|
||||||
|
test_address(w1,
|
||||||
|
key.p2sh_p2wpkh_addr,
|
||||||
|
ismine=True,
|
||||||
|
solvable=True)
|
||||||
|
|
||||||
|
# # Test importing of a multisig descriptor
|
||||||
|
key1 = get_generate_key()
|
||||||
|
key2 = get_generate_key()
|
||||||
|
self.log.info("Should import a 1-of-2 bare multisig from descriptor")
|
||||||
|
self.test_importdesc({"desc": descsum_create("multi(1," + key1.pubkey + "," + key2.pubkey + ")"),
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True)
|
||||||
|
self.log.info("Should not treat individual keys from the imported bare multisig as watchonly")
|
||||||
|
test_address(w1,
|
||||||
|
key1.p2pkh_addr,
|
||||||
|
ismine=False)
|
||||||
|
|
||||||
|
# # Test ranged descriptors
|
||||||
|
xpriv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"
|
||||||
|
xpub = "tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H"
|
||||||
|
addresses = ["2N7yv4p8G8yEaPddJxY41kPihnWvs39qCMf", "2MsHxyb2JS3pAySeNUsJ7mNnurtpeenDzLA"] # hdkeypath=m/0'/0'/0' and 1'
|
||||||
|
addresses += ["bcrt1qrd3n235cj2czsfmsuvqqpr3lu6lg0ju7scl8gn", "bcrt1qfqeppuvj0ww98r6qghmdkj70tv8qpchehegrg8"] # wpkh subscripts corresponding to the above addresses
|
||||||
|
desc = "sh(wpkh(" + xpub + "/0/0/*" + "))"
|
||||||
|
|
||||||
|
self.log.info("Ranged descriptors cannot have labels")
|
||||||
|
self.test_importdesc({"desc":descsum_create(desc),
|
||||||
|
"timestamp": "now",
|
||||||
|
"range": [0, 100],
|
||||||
|
"label": "test"},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message='Ranged descriptors should not have a label')
|
||||||
|
|
||||||
|
self.log.info("Private keys required for private keys enabled wallet")
|
||||||
|
self.test_importdesc({"desc":descsum_create(desc),
|
||||||
|
"timestamp": "now",
|
||||||
|
"range": [0, 100]},
|
||||||
|
success=False,
|
||||||
|
error_code=-4,
|
||||||
|
error_message='Cannot import descriptor without private keys to a wallet with private keys enabled',
|
||||||
|
wallet=wpriv)
|
||||||
|
|
||||||
|
self.log.info("Ranged descriptor import should warn without a specified range")
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc),
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True,
|
||||||
|
warnings=['Range not given, using default keypool range'])
|
||||||
|
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
|
||||||
|
|
||||||
|
# # Test importing of a ranged descriptor with xpriv
|
||||||
|
self.log.info("Should not import a ranged descriptor that includes xpriv into a watch-only wallet")
|
||||||
|
desc = "sh(wpkh(" + xpriv + "/0'/0'/*'" + "))"
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc),
|
||||||
|
"timestamp": "now",
|
||||||
|
"range": 1},
|
||||||
|
success=False,
|
||||||
|
error_code=-4,
|
||||||
|
error_message='Cannot import private keys to a wallet with private keys disabled')
|
||||||
|
for address in addresses:
|
||||||
|
test_address(w1,
|
||||||
|
address,
|
||||||
|
ismine=False,
|
||||||
|
solvable=False)
|
||||||
|
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": -1},
|
||||||
|
success=False, error_code=-8, error_message='End of range is too high')
|
||||||
|
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [-1, 10]},
|
||||||
|
success=False, error_code=-8, error_message='Range should be greater or equal than 0')
|
||||||
|
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [(2 << 31 + 1) - 1000000, (2 << 31 + 1)]},
|
||||||
|
success=False, error_code=-8, error_message='End of range is too high')
|
||||||
|
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [2, 1]},
|
||||||
|
success=False, error_code=-8, error_message='Range specified as [begin,end] must not have begin after end')
|
||||||
|
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 1000001]},
|
||||||
|
success=False, error_code=-8, error_message='Range is too large')
|
||||||
|
|
||||||
|
# Make sure ranged imports import keys in order
|
||||||
|
w1 = self.nodes[1].get_wallet_rpc('w1')
|
||||||
|
self.log.info('Key ranges should be imported in order')
|
||||||
|
xpub = "tpubDAXcJ7s7ZwicqjprRaEWdPoHKrCS215qxGYxpusRLLmJuT69ZSicuGdSfyvyKpvUNYBW1s2U3NSrT6vrCYB9e6nZUEvrqnwXPF8ArTCRXMY"
|
||||||
|
addresses = [
|
||||||
|
'bcrt1qtmp74ayg7p24uslctssvjm06q5phz4yrxucgnv', # m/0'/0'/0
|
||||||
|
'bcrt1q8vprchan07gzagd5e6v9wd7azyucksq2xc76k8', # m/0'/0'/1
|
||||||
|
'bcrt1qtuqdtha7zmqgcrr26n2rqxztv5y8rafjp9lulu', # m/0'/0'/2
|
||||||
|
'bcrt1qau64272ymawq26t90md6an0ps99qkrse58m640', # m/0'/0'/3
|
||||||
|
'bcrt1qsg97266hrh6cpmutqen8s4s962aryy77jp0fg0', # m/0'/0'/4
|
||||||
|
]
|
||||||
|
|
||||||
|
self.test_importdesc({'desc': descsum_create('wpkh([80002067/0h/0h]' + xpub + '/*)'),
|
||||||
|
'active': True,
|
||||||
|
'range' : [0, 2],
|
||||||
|
'timestamp': 'now'
|
||||||
|
},
|
||||||
|
success=True)
|
||||||
|
self.test_importdesc({'desc': descsum_create('sh(wpkh([abcdef12/0h/0h]' + xpub + '/*))'),
|
||||||
|
'active': True,
|
||||||
|
'range' : [0, 2],
|
||||||
|
'timestamp': 'now'
|
||||||
|
},
|
||||||
|
success=True)
|
||||||
|
self.test_importdesc({'desc': descsum_create('pkh([12345678/0h/0h]' + xpub + '/*)'),
|
||||||
|
'active': True,
|
||||||
|
'range' : [0, 2],
|
||||||
|
'timestamp': 'now'
|
||||||
|
},
|
||||||
|
success=True)
|
||||||
|
|
||||||
|
assert_equal(w1.getwalletinfo()['keypoolsize'], 5 * 3)
|
||||||
|
for i, expected_addr in enumerate(addresses):
|
||||||
|
received_addr = w1.getnewaddress('', 'bech32')
|
||||||
|
assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress, 'bech32')
|
||||||
|
assert_equal(received_addr, expected_addr)
|
||||||
|
bech32_addr_info = w1.getaddressinfo(received_addr)
|
||||||
|
assert_equal(bech32_addr_info['desc'][:23], 'wpkh([80002067/0\'/0\'/{}]'.format(i))
|
||||||
|
|
||||||
|
shwpkh_addr = w1.getnewaddress('', 'p2sh-segwit')
|
||||||
|
shwpkh_addr_info = w1.getaddressinfo(shwpkh_addr)
|
||||||
|
assert_equal(shwpkh_addr_info['desc'][:26], 'sh(wpkh([abcdef12/0\'/0\'/{}]'.format(i))
|
||||||
|
|
||||||
|
pkh_addr = w1.getnewaddress('', 'legacy')
|
||||||
|
pkh_addr_info = w1.getaddressinfo(pkh_addr)
|
||||||
|
assert_equal(pkh_addr_info['desc'][:22], 'pkh([12345678/0\'/0\'/{}]'.format(i))
|
||||||
|
|
||||||
|
assert_equal(w1.getwalletinfo()['keypoolsize'], 4 * 3) # After retrieving a key, we don't refill the keypool again, so it's one less for each address type
|
||||||
|
w1.keypoolrefill()
|
||||||
|
assert_equal(w1.getwalletinfo()['keypoolsize'], 5 * 3)
|
||||||
|
|
||||||
|
# Check active=False default
|
||||||
|
self.log.info('Check imported descriptors are not active by default')
|
||||||
|
self.test_importdesc({'desc': descsum_create('pkh([12345678/0h/0h]' + xpub + '/*)'),
|
||||||
|
'range' : [0, 2],
|
||||||
|
'timestamp': 'now',
|
||||||
|
'internal': True
|
||||||
|
},
|
||||||
|
success=True)
|
||||||
|
assert_raises_rpc_error(-4, 'This wallet has no available keys', w1.getrawchangeaddress, 'legacy')
|
||||||
|
|
||||||
|
# # Test importing a descriptor containing a WIF private key
|
||||||
|
wif_priv = "cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh"
|
||||||
|
address = "2MuhcG52uHPknxDgmGPsV18jSHFBnnRgjPg"
|
||||||
|
desc = "sh(wpkh(" + wif_priv + "))"
|
||||||
|
self.log.info("Should import a descriptor with a WIF private key as spendable")
|
||||||
|
self.test_importdesc({"desc": descsum_create(desc),
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True,
|
||||||
|
wallet=wpriv)
|
||||||
|
test_address(wpriv,
|
||||||
|
address,
|
||||||
|
solvable=True,
|
||||||
|
ismine=True)
|
||||||
|
txid = w0.sendtoaddress(address, 49.99995540)
|
||||||
|
w0.generatetoaddress(6, w0.getnewaddress())
|
||||||
|
self.sync_blocks()
|
||||||
|
tx = wpriv.createrawtransaction([{"txid": txid, "vout": 0}], {w0.getnewaddress(): 49.999})
|
||||||
|
signed_tx = wpriv.signrawtransactionwithwallet(tx)
|
||||||
|
w1.sendrawtransaction(signed_tx['hex'])
|
||||||
|
|
||||||
|
# Make sure that we can use import and use multisig as addresses
|
||||||
|
self.log.info('Test that multisigs can be imported, signed for, and getnewaddress\'d')
|
||||||
|
self.nodes[1].createwallet(wallet_name="wmulti_priv", disable_private_keys=False, blank=True, descriptors=True)
|
||||||
|
wmulti_priv = self.nodes[1].get_wallet_rpc("wmulti_priv")
|
||||||
|
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 0)
|
||||||
|
|
||||||
|
self.test_importdesc({"desc":"wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/0h/0h/*))#m2sr93jn",
|
||||||
|
"active": True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True,
|
||||||
|
wallet=wmulti_priv)
|
||||||
|
self.test_importdesc({"desc":"wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,tprv8ZgxMBicQKsPeonDt8Ka2mrQmHa61hQ5FQCsvWBTpSNzBFgM58cV2EuXNAHF14VawVpznnme3SuTbA62sGriwWyKifJmXntfNeK7zeqMCj1/84h/1h/0h/*))#q3sztvx5",
|
||||||
|
"active": True,
|
||||||
|
"internal" : True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True,
|
||||||
|
wallet=wmulti_priv)
|
||||||
|
|
||||||
|
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1001) # Range end (1000) is inclusive, so 1001 addresses generated
|
||||||
|
addr = wmulti_priv.getnewaddress('', 'bech32')
|
||||||
|
assert_equal(addr, 'bcrt1qdt0qy5p7dzhxzmegnn4ulzhard33s2809arjqgjndx87rv5vd0fq2czhy8') # Derived at m/84'/0'/0'/0
|
||||||
|
change_addr = wmulti_priv.getrawchangeaddress('bech32')
|
||||||
|
assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e')
|
||||||
|
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000)
|
||||||
|
txid = w0.sendtoaddress(addr, 10)
|
||||||
|
self.nodes[0].generate(6)
|
||||||
|
send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8)
|
||||||
|
decoded = wmulti_priv.decoderawtransaction(wmulti_priv.gettransaction(send_txid)['hex'])
|
||||||
|
assert_equal(len(decoded['vin'][0]['txinwitness']), 4)
|
||||||
|
self.nodes[0].generate(6)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
self.nodes[1].createwallet(wallet_name="wmulti_pub", disable_private_keys=True, blank=True, descriptors=True)
|
||||||
|
wmulti_pub = self.nodes[1].get_wallet_rpc("wmulti_pub")
|
||||||
|
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 0)
|
||||||
|
|
||||||
|
self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))#tsry0s5e",
|
||||||
|
"active": True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True,
|
||||||
|
wallet=wmulti_pub)
|
||||||
|
self.test_importdesc({"desc":"wsh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))#c08a2rzv",
|
||||||
|
"active": True,
|
||||||
|
"internal" : True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True,
|
||||||
|
wallet=wmulti_pub)
|
||||||
|
|
||||||
|
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 1000) # The first one was already consumed by previous import and is detected as used
|
||||||
|
addr = wmulti_pub.getnewaddress('', 'bech32')
|
||||||
|
assert_equal(addr, 'bcrt1qp8s25ckjl7gr6x2q3dx3tn2pytwp05upkjztk6ey857tt50r5aeqn6mvr9') # Derived at m/84'/0'/0'/1
|
||||||
|
change_addr = wmulti_pub.getrawchangeaddress('bech32')
|
||||||
|
assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e')
|
||||||
|
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999)
|
||||||
|
txid = w0.sendtoaddress(addr, 10)
|
||||||
|
vout = find_vout_for_address(self.nodes[0], txid, addr)
|
||||||
|
self.nodes[0].generate(6)
|
||||||
|
self.sync_all()
|
||||||
|
assert_equal(wmulti_pub.getbalance(), wmulti_priv.getbalance())
|
||||||
|
|
||||||
|
self.log.info("Multisig with distributed keys")
|
||||||
|
self.nodes[1].createwallet(wallet_name="wmulti_priv1", descriptors=True)
|
||||||
|
wmulti_priv1 = self.nodes[1].get_wallet_rpc("wmulti_priv1")
|
||||||
|
res = wmulti_priv1.importdescriptors([
|
||||||
|
{
|
||||||
|
"desc": descsum_create("wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/0h/0h/*,[59b09cd6/84h/0h/0h]tpubDDBF2BTR6s8drwrfDei8WxtckGuSm1cyoKxYY1QaKSBFbHBYQArWhHPA6eJrzZej6nfHGLSURYSLHr7GuYch8aY5n61tGqgn8b4cXrMuoPH/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"),
|
||||||
|
"active": True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"desc": descsum_create("wsh(multi(2,tprv8ZgxMBicQKsPevADjDCWsa6DfhkVXicu8NQUzfibwX2MexVwW4tCec5mXdCW8kJwkzBRRmAay1KZya4WsehVvjTGVW6JLqiqd8DdZ4xSg52/84h/1h/0h/*,[59b09cd6/84h/1h/0h]tpubDCYfZY2ceyHzYzMMVPt9MNeiqtQ2T7Uyp9QSFwYXh8Vi9iJFYXcuphJaGXfF3jUQJi5Y3GMNXvM11gaL4txzZgNGK22BFAwMXynnzv4z2Jh/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"),
|
||||||
|
"active": True,
|
||||||
|
"internal" : True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"
|
||||||
|
}])
|
||||||
|
assert_equal(res[0]['success'], True)
|
||||||
|
assert_equal(res[0]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||||
|
assert_equal(res[1]['success'], True)
|
||||||
|
assert_equal(res[1]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||||
|
|
||||||
|
self.nodes[1].createwallet(wallet_name='wmulti_priv2', blank=True, descriptors=True)
|
||||||
|
wmulti_priv2 = self.nodes[1].get_wallet_rpc('wmulti_priv2')
|
||||||
|
res = wmulti_priv2.importdescriptors([
|
||||||
|
{
|
||||||
|
"desc": descsum_create("wsh(multi(2,[7b2d0242/84h/0h/0h]tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/0h/0h/*,[e81a0532/84h/0h/0h]tpubDCsWoW1kuQB9kG5MXewHqkbjPtqPueRnXju7uM2NK7y3JYb2ajAZ9EiuZXNNuE4661RAfriBWhL8UsnAPpk8zrKKnZw1Ug7X4oHgMdZiU4E/*))"),
|
||||||
|
"active": True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"desc": descsum_create("wsh(multi(2,[7b2d0242/84h/1h/0h]tpubDCXqdwWZcszwqYJSnZp8eARkxGJfHAk23KDxbztV4BbschfaTfYLTcSkSJ3TN64dRqwa1rnFUScsYormKkGqNbbPwkorQimVevXjxzUV9Gf/*,tprv8ZgxMBicQKsPdSNWUhDiwTScDr6JfkZuLshTRwzvZGnMSnGikV6jxpmdDkC3YRc4T3GD6Nvg9uv6hQg73RVv1EiTXDZwxVbsLugVHU8B1aq/84h/1h/0h/*,[e81a0532/84h/1h/0h]tpubDC6UGqnsQStngYuGD4MKsMy7eD1Yg9NTJfPdvjdG2JE5oZ7EsSL3WHg4Gsw2pR5K39ZwJ46M1wZayhedVdQtMGaUhq5S23PH6fnENK3V1sb/*))"),
|
||||||
|
"active": True,
|
||||||
|
"internal" : True,
|
||||||
|
"range": 1000,
|
||||||
|
"next_index": 0,
|
||||||
|
"timestamp": "now"
|
||||||
|
}])
|
||||||
|
assert_equal(res[0]['success'], True)
|
||||||
|
assert_equal(res[0]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||||
|
assert_equal(res[1]['success'], True)
|
||||||
|
assert_equal(res[1]['warnings'][0], 'Not all private keys provided. Some wallet functionality may return unexpected errors')
|
||||||
|
|
||||||
|
rawtx = self.nodes[1].createrawtransaction([{'txid': txid, 'vout': vout}], {w0.getnewaddress(): 9.999})
|
||||||
|
tx_signed_1 = wmulti_priv1.signrawtransactionwithwallet(rawtx)
|
||||||
|
assert_equal(tx_signed_1['complete'], False)
|
||||||
|
tx_signed_2 = wmulti_priv2.signrawtransactionwithwallet(tx_signed_1['hex'])
|
||||||
|
assert_equal(tx_signed_2['complete'], True)
|
||||||
|
self.nodes[1].sendrawtransaction(tx_signed_2['hex'])
|
||||||
|
|
||||||
|
self.log.info("Combo descriptors cannot be active")
|
||||||
|
self.test_importdesc({"desc": descsum_create("combo(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"),
|
||||||
|
"active": True,
|
||||||
|
"range": 1,
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=False,
|
||||||
|
error_code=-4,
|
||||||
|
error_message="Combo descriptors cannot be set to active")
|
||||||
|
|
||||||
|
self.log.info("Descriptors with no type cannot be active")
|
||||||
|
self.test_importdesc({"desc": descsum_create("pk(tpubDCJtdt5dgJpdhW4MtaVYDhG4T4tF6jcLR1PxL43q9pq1mxvXgMS9Mzw1HnXG15vxUGQJMMSqCQHMTy3F1eW5VkgVroWzchsPD5BUojrcWs8/*)"),
|
||||||
|
"active": True,
|
||||||
|
"range": 1,
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=True,
|
||||||
|
warnings=["Unknown output type, cannot set descriptor to active."])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ImportDescriptorsTest().main()
|
Loading…
Add table
Reference in a new issue