aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/rpc/client.cpp1
-rw-r--r--src/wallet/rpcdump.cpp294
-rw-r--r--src/wallet/rpcwallet.cpp2
-rw-r--r--src/wallet/scriptpubkeyman.cpp41
-rw-r--r--src/wallet/scriptpubkeyman.h7
-rw-r--r--src/wallet/wallet.cpp86
-rw-r--r--src/wallet/wallet.h6
-rwxr-xr-xtest/functional/test_framework/wallet_util.py23
-rwxr-xr-xtest/functional/test_runner.py1
-rwxr-xr-xtest/functional/wallet_importdescriptors.py444
10 files changed, 905 insertions, 0 deletions
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
index 4943a4ee88..3045a74d7a 100644
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -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" },
diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp
index 86e4e06673..128de52b58 100644
--- a/src/wallet/rpcdump.cpp
+++ b/src/wallet/rpcdump.cpp
@@ -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;
+}
diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp
index 8776cc9bfe..49c583c422 100644
--- a/src/wallet/rpcwallet.cpp
+++ b/src/wallet/rpcwallet.cpp
@@ -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)
{
@@ -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"} },
diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
index 6aeea700be..ecb95d599d 100644
--- a/src/wallet/scriptpubkeyman.cpp
+++ b/src/wallet/scriptpubkeyman.cpp
@@ -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);
@@ -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;
+}
diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
index 09edc32289..3117b13d35 100644
--- a/src/wallet/scriptpubkeyman.h
+++ b/src/wallet/scriptpubkeyman.h
@@ -580,6 +580,13 @@ public:
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
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 82901e09de..5ffeb9e44d 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -4399,6 +4399,7 @@ void CWallet::SetupDescriptorScriptPubKeyMans()
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_man = m_spk_managers.at(id).get();
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));
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;
+}
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
index 66d4a51a9c..24c78fceb3 100644
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -1250,6 +1250,12 @@ public:
//! Create new DescriptorScriptPubKeyMans and add them to the wallet
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);
};
/**
diff --git a/test/functional/test_framework/wallet_util.py b/test/functional/test_framework/wallet_util.py
index eb537015fb..1b6686ff45 100755
--- a/test/functional/test_framework/wallet_util.py
+++ b/test/functional/test_framework/wallet_util.py
@@ -13,6 +13,10 @@ from test_framework.address import (
script_to_p2sh_p2wsh,
script_to_p2wsh,
)
+from test_framework.key import (
+ bytes_to_wif,
+ ECKey,
+)
from test_framework.script import (
CScript,
OP_0,
@@ -66,6 +70,25 @@ def get_key(node):
p2sh_p2wpkh_redeem_script=CScript([OP_0, pkh]).hex(),
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):
"""Generate a fresh 2-of-3 multisig on node
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 4f902717c3..b13502292f 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -180,6 +180,7 @@ BASE_SCRIPTS = [
'mempool_expiry.py',
'wallet_import_rescan.py',
'wallet_import_with_label.py',
+ 'wallet_importdescriptors.py',
'rpc_bind.py --ipv4',
'rpc_bind.py --ipv6',
'rpc_bind.py --nonloopback',
diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
new file mode 100755
index 0000000000..55995152aa
--- /dev/null
+++ b/test/functional/wallet_importdescriptors.py
@@ -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()