diff options
author | Hugo Nguyen <hugh.hn@gmail.com> | 2019-08-01 15:08:47 -0700 |
---|---|---|
committer | Andrew Chow <achow101-github@achow101.com> | 2020-04-23 13:59:48 -0400 |
commit | f193ea889ddb53d9a5c47647966681d525e38368 (patch) | |
tree | 6f552b8ff48b64dfa33b06ab756a8531b1148c18 /src | |
parent | ce24a944940019185efebcc5d85eac458ed26016 (diff) |
add importdescriptors RPC and tests for native descriptor wallets
Co-authored-by: Andrew Chow <achow101-github@achow101.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/rpc/client.cpp | 1 | ||||
-rw-r--r-- | src/wallet/rpcdump.cpp | 294 | ||||
-rw-r--r-- | src/wallet/rpcwallet.cpp | 2 | ||||
-rw-r--r-- | src/wallet/scriptpubkeyman.cpp | 41 | ||||
-rw-r--r-- | src/wallet/scriptpubkeyman.h | 7 | ||||
-rw-r--r-- | src/wallet/wallet.cpp | 86 | ||||
-rw-r--r-- | src/wallet/wallet.h | 6 |
7 files changed, 437 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); }; /** |