Merge #14491: Allow descriptor imports with importmulti
b985e9c850
Add release notes for importmulti descriptor support (MeshCollider)fbb5e935ea
Add test for importing via descriptor (MeshCollider)9f48053d8f
[wallet] Allow descriptor imports with importmulti (MeshCollider)d2b381cc91
[wallet] Refactor ProcessImport() to call ProcessImportLegacy() (John Newbery)4cac0ddd25
[wallet] Add ProcessImportLegacy() (John Newbery)a1b25e12a5
[wallet] Refactor ProcessImport() (John Newbery) Pull request description: ~~Based on #14454 #14565, last two commits only are for review.~~ Best reviewed with `?w=1` Allows a descriptor to be imported into the wallet using `importmulti` RPC. Start and end of range can be specified for ranged descriptors. The descriptor is implicitly converted to old structures on import. Also adds a simple test of a P2SH-P2WPKH address being imported as a descriptor. More tests to come, as well as release notes. Tree-SHA512: 160eb6fd574c4ae5b70e0109f7e5ccc95d9309138603408a1114ceb3c558065409c0d7afb66926bc8e1743c365a3b300c5f944ff18b2451acc0514fbeca1f2b3
This commit is contained in:
commit
9127bd7aba
3 changed files with 359 additions and 145 deletions
5
doc/release-notes-14491.md
Normal file
5
doc/release-notes-14491.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Descriptor import support
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The `importmulti` RPC now supports importing of addresses from descriptors. A "desc" parameter can be provided instead of the "scriptPubKey" in a request, as well as an optional range for ranged descriptors to specify the start and end of the range to import. More information about
|
||||||
|
descriptors can be found [here](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md).
|
|
@ -9,6 +9,7 @@
|
||||||
#include <merkleblock.h>
|
#include <merkleblock.h>
|
||||||
#include <rpc/server.h>
|
#include <rpc/server.h>
|
||||||
#include <rpc/util.h>
|
#include <rpc/util.h>
|
||||||
|
#include <script/descriptor.h>
|
||||||
#include <script/script.h>
|
#include <script/script.h>
|
||||||
#include <script/standard.h>
|
#include <script/standard.h>
|
||||||
#include <sync.h>
|
#include <sync.h>
|
||||||
|
@ -964,159 +965,273 @@ static std::string RecurseImportData(const CScript& script, ImportData& import_d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static UniValue ProcessImportLegacy(ImportData& import_data, std::map<CKeyID, CPubKey>& pubkey_map, std::map<CKeyID, CKey>& privkey_map, std::set<CScript>& script_pub_keys, bool& have_solving_data, const UniValue& data)
|
||||||
|
{
|
||||||
|
UniValue warnings(UniValue::VARR);
|
||||||
|
|
||||||
|
// First ensure scriptPubKey has either a script or JSON with "address" string
|
||||||
|
const UniValue& scriptPubKey = data["scriptPubKey"];
|
||||||
|
bool isScript = scriptPubKey.getType() == UniValue::VSTR;
|
||||||
|
if (!isScript && !(scriptPubKey.getType() == UniValue::VOBJ && scriptPubKey.exists("address"))) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "scriptPubKey must be string with script or JSON with address string");
|
||||||
|
}
|
||||||
|
const std::string& output = isScript ? scriptPubKey.get_str() : scriptPubKey["address"].get_str();
|
||||||
|
|
||||||
|
// Optional fields.
|
||||||
|
const std::string& strRedeemScript = data.exists("redeemscript") ? data["redeemscript"].get_str() : "";
|
||||||
|
const std::string& witness_script_hex = data.exists("witnessscript") ? data["witnessscript"].get_str() : "";
|
||||||
|
const UniValue& pubKeys = data.exists("pubkeys") ? data["pubkeys"].get_array() : UniValue();
|
||||||
|
const UniValue& keys = data.exists("keys") ? data["keys"].get_array() : UniValue();
|
||||||
|
const bool internal = data.exists("internal") ? data["internal"].get_bool() : false;
|
||||||
|
const bool watchOnly = data.exists("watchonly") ? data["watchonly"].get_bool() : false;
|
||||||
|
|
||||||
|
if (data.exists("range")) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for a non-descriptor import");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the script and destination for the scriptPubKey provided
|
||||||
|
CScript script;
|
||||||
|
if (!isScript) {
|
||||||
|
CTxDestination dest = DecodeDestination(output);
|
||||||
|
if (!IsValidDestination(dest)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address \"" + output + "\"");
|
||||||
|
}
|
||||||
|
script = GetScriptForDestination(dest);
|
||||||
|
} else {
|
||||||
|
if (!IsHex(output)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid scriptPubKey \"" + output + "\"");
|
||||||
|
}
|
||||||
|
std::vector<unsigned char> vData(ParseHex(output));
|
||||||
|
script = CScript(vData.begin(), vData.end());
|
||||||
|
CTxDestination dest;
|
||||||
|
if (!ExtractDestination(script, dest) && !internal) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal must be set to true for nonstandard scriptPubKey imports.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
script_pub_keys.emplace(script);
|
||||||
|
|
||||||
|
// Parse all arguments
|
||||||
|
if (strRedeemScript.size()) {
|
||||||
|
if (!IsHex(strRedeemScript)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid redeem script \"" + strRedeemScript + "\": must be hex string");
|
||||||
|
}
|
||||||
|
auto parsed_redeemscript = ParseHex(strRedeemScript);
|
||||||
|
import_data.redeemscript = MakeUnique<CScript>(parsed_redeemscript.begin(), parsed_redeemscript.end());
|
||||||
|
}
|
||||||
|
if (witness_script_hex.size()) {
|
||||||
|
if (!IsHex(witness_script_hex)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid witness script \"" + witness_script_hex + "\": must be hex string");
|
||||||
|
}
|
||||||
|
auto parsed_witnessscript = ParseHex(witness_script_hex);
|
||||||
|
import_data.witnessscript = MakeUnique<CScript>(parsed_witnessscript.begin(), parsed_witnessscript.end());
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < pubKeys.size(); ++i) {
|
||||||
|
const auto& str = pubKeys[i].get_str();
|
||||||
|
if (!IsHex(str)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" must be a hex string");
|
||||||
|
}
|
||||||
|
auto parsed_pubkey = ParseHex(str);
|
||||||
|
CPubKey pubkey(parsed_pubkey.begin(), parsed_pubkey.end());
|
||||||
|
if (!pubkey.IsFullyValid()) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" is not a valid public key");
|
||||||
|
}
|
||||||
|
pubkey_map.emplace(pubkey.GetID(), pubkey);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < keys.size(); ++i) {
|
||||||
|
const auto& str = keys[i].get_str();
|
||||||
|
CKey key = DecodeSecret(str);
|
||||||
|
if (!key.IsValid()) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding");
|
||||||
|
}
|
||||||
|
CPubKey pubkey = key.GetPubKey();
|
||||||
|
CKeyID id = pubkey.GetID();
|
||||||
|
if (pubkey_map.count(id)) {
|
||||||
|
pubkey_map.erase(id);
|
||||||
|
}
|
||||||
|
privkey_map.emplace(id, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Verify and process input data
|
||||||
|
have_solving_data = import_data.redeemscript || import_data.witnessscript || pubkey_map.size() || privkey_map.size();
|
||||||
|
if (have_solving_data) {
|
||||||
|
// Match up data in import_data with the scriptPubKey in script.
|
||||||
|
auto error = RecurseImportData(script, import_data, ScriptContext::TOP);
|
||||||
|
|
||||||
|
// Verify whether the watchonly option corresponds to the availability of private keys.
|
||||||
|
bool spendable = std::all_of(import_data.used_keys.begin(), import_data.used_keys.end(), [&](const std::pair<CKeyID, bool>& used_key){ return privkey_map.count(used_key.first) > 0; });
|
||||||
|
if (!watchOnly && !spendable) {
|
||||||
|
warnings.push_back("Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag.");
|
||||||
|
}
|
||||||
|
if (watchOnly && spendable) {
|
||||||
|
warnings.push_back("All private keys are provided, outputs will be considered spendable. If this is intentional, do not specify the watchonly flag.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all required keys for solvability are provided.
|
||||||
|
if (error.empty()) {
|
||||||
|
for (const auto& require_key : import_data.used_keys) {
|
||||||
|
if (!require_key.second) continue; // Not a required key
|
||||||
|
if (pubkey_map.count(require_key.first) == 0 && privkey_map.count(require_key.first) == 0) {
|
||||||
|
error = "some required keys are missing";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error.empty()) {
|
||||||
|
warnings.push_back("Importing as non-solvable: " + error + ". If this is intentional, don't provide any keys, pubkeys, witnessscript, or redeemscript.");
|
||||||
|
import_data = ImportData();
|
||||||
|
pubkey_map.clear();
|
||||||
|
privkey_map.clear();
|
||||||
|
have_solving_data = false;
|
||||||
|
} else {
|
||||||
|
// RecurseImportData() removes any relevant redeemscript/witnessscript from import_data, so we can use that to discover if a superfluous one was provided.
|
||||||
|
if (import_data.redeemscript) warnings.push_back("Ignoring redeemscript as this is not a P2SH script.");
|
||||||
|
if (import_data.witnessscript) warnings.push_back("Ignoring witnessscript as this is not a (P2SH-)P2WSH script.");
|
||||||
|
for (auto it = privkey_map.begin(); it != privkey_map.end(); ) {
|
||||||
|
auto oldit = it++;
|
||||||
|
if (import_data.used_keys.count(oldit->first) == 0) {
|
||||||
|
warnings.push_back("Ignoring irrelevant private key.");
|
||||||
|
privkey_map.erase(oldit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto it = pubkey_map.begin(); it != pubkey_map.end(); ) {
|
||||||
|
auto oldit = it++;
|
||||||
|
auto key_data_it = import_data.used_keys.find(oldit->first);
|
||||||
|
if (key_data_it == import_data.used_keys.end() || !key_data_it->second) {
|
||||||
|
warnings.push_back("Ignoring public key \"" + HexStr(oldit->first) + "\" as it doesn't appear inside P2PKH or P2WPKH.");
|
||||||
|
pubkey_map.erase(oldit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UniValue ProcessImportDescriptor(ImportData& import_data, std::map<CKeyID, CPubKey>& pubkey_map, std::map<CKeyID, CKey>& privkey_map, std::set<CScript>& script_pub_keys, bool& have_solving_data, const UniValue& data)
|
||||||
|
{
|
||||||
|
UniValue warnings(UniValue::VARR);
|
||||||
|
|
||||||
|
const std::string& descriptor = data["desc"].get_str();
|
||||||
|
FlatSigningProvider keys;
|
||||||
|
auto parsed_desc = Parse(descriptor, keys);
|
||||||
|
if (!parsed_desc) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
have_solving_data = parsed_desc->IsSolvable();
|
||||||
|
const bool watch_only = data.exists("watchonly") ? data["watchonly"].get_bool() : false;
|
||||||
|
|
||||||
|
int64_t range_start = 0, range_end = 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")) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor is ranged, please specify the range");
|
||||||
|
}
|
||||||
|
const UniValue& range = data["range"];
|
||||||
|
range_start = range.exists("start") ? range["start"].get_int64() : 0;
|
||||||
|
if (!range.exists("end")) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "End of range for descriptor must be specified");
|
||||||
|
}
|
||||||
|
range_end = range["end"].get_int64();
|
||||||
|
if (range_end < range_start || range_start < 0) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid descriptor range specified");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniValue& priv_keys = data.exists("keys") ? data["keys"].get_array() : UniValue();
|
||||||
|
|
||||||
|
FlatSigningProvider out_keys;
|
||||||
|
|
||||||
|
// Expand all descriptors to get public keys and scripts.
|
||||||
|
// TODO: get private keys from descriptors too
|
||||||
|
for (int i = range_start; i <= range_end; ++i) {
|
||||||
|
std::vector<CScript> scripts_temp;
|
||||||
|
parsed_desc->Expand(i, keys, scripts_temp, out_keys);
|
||||||
|
std::copy(scripts_temp.begin(), scripts_temp.end(), std::inserter(script_pub_keys, script_pub_keys.end()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& x : out_keys.scripts) {
|
||||||
|
import_data.import_scripts.emplace(x.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::copy(out_keys.pubkeys.begin(), out_keys.pubkeys.end(), std::inserter(pubkey_map, pubkey_map.end()));
|
||||||
|
|
||||||
|
for (size_t i = 0; i < priv_keys.size(); ++i) {
|
||||||
|
const auto& str = priv_keys[i].get_str();
|
||||||
|
CKey key = DecodeSecret(str);
|
||||||
|
if (!key.IsValid()) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding");
|
||||||
|
}
|
||||||
|
CPubKey pubkey = key.GetPubKey();
|
||||||
|
CKeyID id = pubkey.GetID();
|
||||||
|
|
||||||
|
// Check if this private key corresponds to a public key from the descriptor
|
||||||
|
if (!pubkey_map.count(id)) {
|
||||||
|
warnings.push_back("Ignoring irrelevant private key.");
|
||||||
|
} else {
|
||||||
|
privkey_map.emplace(id, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all the public keys have corresponding private keys in the import for spendability.
|
||||||
|
// This does not take into account threshold multisigs which could be spendable without all keys.
|
||||||
|
// Thus, threshold multisigs without all keys will be considered not spendable here, even if they are,
|
||||||
|
// perhaps triggering a false warning message. This is consistent with the current wallet IsMine check.
|
||||||
|
bool spendable = std::all_of(pubkey_map.begin(), pubkey_map.end(),
|
||||||
|
[&](const std::pair<CKeyID, CPubKey>& used_key) {
|
||||||
|
return privkey_map.count(used_key.first) > 0;
|
||||||
|
});
|
||||||
|
if (!watch_only && !spendable) {
|
||||||
|
warnings.push_back("Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag.");
|
||||||
|
}
|
||||||
|
if (watch_only && spendable) {
|
||||||
|
warnings.push_back("All private keys are provided, outputs will be considered spendable. If this is intentional, do not specify the watchonly flag.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
|
static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
|
||||||
{
|
{
|
||||||
UniValue warnings(UniValue::VARR);
|
UniValue warnings(UniValue::VARR);
|
||||||
UniValue result(UniValue::VOBJ);
|
UniValue result(UniValue::VOBJ);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First ensure scriptPubKey has either a script or JSON with "address" string
|
|
||||||
const UniValue& scriptPubKey = data["scriptPubKey"];
|
|
||||||
bool isScript = scriptPubKey.getType() == UniValue::VSTR;
|
|
||||||
if (!isScript && !(scriptPubKey.getType() == UniValue::VOBJ && scriptPubKey.exists("address"))) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "scriptPubKey must be string with script or JSON with address string");
|
|
||||||
}
|
|
||||||
const std::string& output = isScript ? scriptPubKey.get_str() : scriptPubKey["address"].get_str();
|
|
||||||
|
|
||||||
// Optional fields.
|
|
||||||
const std::string& strRedeemScript = data.exists("redeemscript") ? data["redeemscript"].get_str() : "";
|
|
||||||
const std::string& witness_script_hex = data.exists("witnessscript") ? data["witnessscript"].get_str() : "";
|
|
||||||
const UniValue& pubKeys = data.exists("pubkeys") ? data["pubkeys"].get_array() : UniValue();
|
|
||||||
const UniValue& keys = data.exists("keys") ? data["keys"].get_array() : UniValue();
|
|
||||||
const bool internal = data.exists("internal") ? data["internal"].get_bool() : false;
|
const bool internal = data.exists("internal") ? data["internal"].get_bool() : false;
|
||||||
const bool watchOnly = data.exists("watchonly") ? data["watchonly"].get_bool() : false;
|
|
||||||
const std::string& label = data.exists("label") ? data["label"].get_str() : "";
|
|
||||||
|
|
||||||
// If private keys are disabled, abort if private keys are being imported
|
|
||||||
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.isNull()) {
|
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the script and destination for the scriptPubKey provided
|
|
||||||
CScript script;
|
|
||||||
CTxDestination dest;
|
|
||||||
if (!isScript) {
|
|
||||||
dest = DecodeDestination(output);
|
|
||||||
if (!IsValidDestination(dest)) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address \"" + output + "\"");
|
|
||||||
}
|
|
||||||
script = GetScriptForDestination(dest);
|
|
||||||
} else {
|
|
||||||
if (!IsHex(output)) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid scriptPubKey \"" + output + "\"");
|
|
||||||
}
|
|
||||||
std::vector<unsigned char> vData(ParseHex(output));
|
|
||||||
script = CScript(vData.begin(), vData.end());
|
|
||||||
if (!ExtractDestination(script, dest) && !internal) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal must be set to true for nonstandard scriptPubKey imports.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse all arguments
|
|
||||||
ImportData import_data;
|
|
||||||
if (strRedeemScript.size()) {
|
|
||||||
if (!IsHex(strRedeemScript)) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid redeem script \"" + strRedeemScript + "\": must be hex string");
|
|
||||||
}
|
|
||||||
auto parsed_redeemscript = ParseHex(strRedeemScript);
|
|
||||||
import_data.redeemscript = MakeUnique<CScript>(parsed_redeemscript.begin(), parsed_redeemscript.end());
|
|
||||||
}
|
|
||||||
if (witness_script_hex.size()) {
|
|
||||||
if (!IsHex(witness_script_hex)) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid witness script \"" + witness_script_hex + "\": must be hex string");
|
|
||||||
}
|
|
||||||
auto parsed_witnessscript = ParseHex(witness_script_hex);
|
|
||||||
import_data.witnessscript = MakeUnique<CScript>(parsed_witnessscript.begin(), parsed_witnessscript.end());
|
|
||||||
}
|
|
||||||
std::map<CKeyID, CPubKey> pubkey_map;
|
|
||||||
for (size_t i = 0; i < pubKeys.size(); ++i) {
|
|
||||||
const auto& str = pubKeys[i].get_str();
|
|
||||||
if (!IsHex(str)) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" must be a hex string");
|
|
||||||
}
|
|
||||||
auto parsed_pubkey = ParseHex(str);
|
|
||||||
CPubKey pubkey(parsed_pubkey.begin(), parsed_pubkey.end());
|
|
||||||
if (!pubkey.IsFullyValid()) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" is not a valid public key");
|
|
||||||
}
|
|
||||||
pubkey_map.emplace(pubkey.GetID(), pubkey);
|
|
||||||
}
|
|
||||||
std::map<CKeyID, CKey> privkey_map;
|
|
||||||
for (size_t i = 0; i < keys.size(); ++i) {
|
|
||||||
const auto& str = keys[i].get_str();
|
|
||||||
CKey key = DecodeSecret(str);
|
|
||||||
if (!key.IsValid()) {
|
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key encoding");
|
|
||||||
}
|
|
||||||
CPubKey pubkey = key.GetPubKey();
|
|
||||||
CKeyID id = pubkey.GetID();
|
|
||||||
if (pubkey_map.count(id)) {
|
|
||||||
pubkey_map.erase(id);
|
|
||||||
}
|
|
||||||
privkey_map.emplace(id, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal addresses should not have a label
|
// Internal addresses should not have a label
|
||||||
if (internal && data.exists("label")) {
|
if (internal && data.exists("label")) {
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label");
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label");
|
||||||
}
|
}
|
||||||
|
const std::string& label = data.exists("label") ? data["label"].get_str() : "";
|
||||||
|
|
||||||
// Verify and process input data
|
ImportData import_data;
|
||||||
bool have_solving_data = import_data.redeemscript || import_data.witnessscript || pubkey_map.size() || privkey_map.size();
|
std::map<CKeyID, CPubKey> pubkey_map;
|
||||||
if (have_solving_data) {
|
std::map<CKeyID, CKey> privkey_map;
|
||||||
// Match up data in import_data with the scriptPubKey in script.
|
std::set<CScript> script_pub_keys;
|
||||||
auto error = RecurseImportData(script, import_data, ScriptContext::TOP);
|
bool have_solving_data;
|
||||||
|
|
||||||
// Verify whether the watchonly option corresponds to the availability of private keys.
|
if (data.exists("scriptPubKey") && data.exists("desc")) {
|
||||||
bool spendable = std::all_of(import_data.used_keys.begin(), import_data.used_keys.end(), [&](const std::pair<CKeyID, bool>& used_key){ return privkey_map.count(used_key.first) > 0; });
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Both a descriptor and a scriptPubKey should not be provided.");
|
||||||
if (!watchOnly && !spendable) {
|
} else if (data.exists("scriptPubKey")) {
|
||||||
warnings.push_back("Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag.");
|
warnings = ProcessImportLegacy(import_data, pubkey_map, privkey_map, script_pub_keys, have_solving_data, data);
|
||||||
}
|
} else if (data.exists("desc")) {
|
||||||
if (watchOnly && spendable) {
|
warnings = ProcessImportDescriptor(import_data, pubkey_map, privkey_map, script_pub_keys, have_solving_data, data);
|
||||||
warnings.push_back("All private keys are provided, outputs will be considered spendable. If this is intentional, do not specify the watchonly flag.");
|
} else {
|
||||||
}
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Either a descriptor or scriptPubKey must be provided.");
|
||||||
|
}
|
||||||
|
|
||||||
// Check that all required keys for solvability are provided.
|
// If private keys are disabled, abort if private keys are being imported
|
||||||
if (error.empty()) {
|
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !privkey_map.empty()) {
|
||||||
for (const auto& require_key : import_data.used_keys) {
|
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled");
|
||||||
if (!require_key.second) continue; // Not a required key
|
|
||||||
if (pubkey_map.count(require_key.first) == 0 && privkey_map.count(require_key.first) == 0) {
|
|
||||||
error = "some required keys are missing";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!error.empty()) {
|
|
||||||
warnings.push_back("Importing as non-solvable: " + error + ". If this is intentional, don't provide any keys, pubkeys, witnessscript, or redeemscript.");
|
|
||||||
import_data = ImportData();
|
|
||||||
pubkey_map.clear();
|
|
||||||
privkey_map.clear();
|
|
||||||
have_solving_data = false;
|
|
||||||
} else {
|
|
||||||
// RecurseImportData() removes any relevant redeemscript/witnessscript from import_data, so we can use that to discover if a superfluous one was provided.
|
|
||||||
if (import_data.redeemscript) warnings.push_back("Ignoring redeemscript as this is not a P2SH script.");
|
|
||||||
if (import_data.witnessscript) warnings.push_back("Ignoring witnessscript as this is not a (P2SH-)P2WSH script.");
|
|
||||||
for (auto it = privkey_map.begin(); it != privkey_map.end(); ) {
|
|
||||||
auto oldit = it++;
|
|
||||||
if (import_data.used_keys.count(oldit->first) == 0) {
|
|
||||||
warnings.push_back("Ignoring irrelevant private key.");
|
|
||||||
privkey_map.erase(oldit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (auto it = pubkey_map.begin(); it != pubkey_map.end(); ) {
|
|
||||||
auto oldit = it++;
|
|
||||||
auto key_data_it = import_data.used_keys.find(oldit->first);
|
|
||||||
if (key_data_it == import_data.used_keys.end() || !key_data_it->second) {
|
|
||||||
warnings.push_back("Ignoring public key \"" + HexStr(oldit->first) + "\" as it doesn't appear inside P2PKH or P2WPKH.");
|
|
||||||
pubkey_map.erase(oldit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we have any work to do
|
// Check whether we have any work to do
|
||||||
if (::IsMine(*pwallet, script) & ISMINE_SPENDABLE) {
|
for (const CScript& script : script_pub_keys) {
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this address or script");
|
if (::IsMine(*pwallet, script) & ISMINE_SPENDABLE) {
|
||||||
|
throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this address or script (\"" + HexStr(script.begin(), script.end()) + "\")");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All good, time to import
|
// All good, time to import
|
||||||
|
@ -1146,14 +1261,18 @@ static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, con
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
|
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!have_solving_data || !::IsMine(*pwallet, script)) { // Always call AddWatchOnly for non-solvable watch-only, so that watch timestamp gets updated
|
|
||||||
if (!pwallet->AddWatchOnly(script, timestamp)) {
|
for (const CScript& script : script_pub_keys) {
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
|
if (!have_solving_data || !::IsMine(*pwallet, script)) { // Always call AddWatchOnly for non-solvable watch-only, so that watch timestamp gets updated
|
||||||
|
if (!pwallet->AddWatchOnly(script, timestamp)) {
|
||||||
|
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CTxDestination dest;
|
||||||
|
ExtractDestination(script, dest);
|
||||||
|
if (!internal && IsValidDestination(dest)) {
|
||||||
|
pwallet->SetAddressBook(dest, label, "receive");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!internal) {
|
|
||||||
assert(IsValidDestination(dest));
|
|
||||||
pwallet->SetAddressBook(dest, label, "receive");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.pushKV("success", UniValue(true));
|
result.pushKV("success", UniValue(true));
|
||||||
|
@ -1204,7 +1323,8 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
|
||||||
{
|
{
|
||||||
{"", RPCArg::Type::OBJ, /* opt */ false, /* default_val */ "", "",
|
{"", RPCArg::Type::OBJ, /* opt */ false, /* default_val */ "", "",
|
||||||
{
|
{
|
||||||
{"scriptPubKey", RPCArg::Type::STR, /* opt */ false, /* default_val */ "", "Type of scriptPubKey (string for script, json for address)",
|
{"desc", RPCArg::Type::STR, /* opt */ true, /* default_val */ "", "Descriptor to import. If using descriptor, do not also provide address/scriptPubKey, scripts, or pubkeys"},
|
||||||
|
{"scriptPubKey", RPCArg::Type::STR, /* opt */ false, /* default_val */ "", "Type of scriptPubKey (string for script, json for address). Should not be provided if using a descriptor",
|
||||||
/* oneline_description */ "", {"\"<script>\" | { \"address\":\"<address>\" }", "string / json"}
|
/* oneline_description */ "", {"\"<script>\" | { \"address\":\"<address>\" }", "string / json"}
|
||||||
},
|
},
|
||||||
{"timestamp", RPCArg::Type::NUM, /* opt */ false, /* default_val */ "", "Creation time of the key in seconds since epoch (Jan 1 1970 GMT),\n"
|
{"timestamp", RPCArg::Type::NUM, /* opt */ false, /* default_val */ "", "Creation time of the key in seconds since epoch (Jan 1 1970 GMT),\n"
|
||||||
|
@ -1227,6 +1347,12 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
|
||||||
{"key", RPCArg::Type::STR, /* opt */ false, /* default_val */ "", ""},
|
{"key", RPCArg::Type::STR, /* opt */ false, /* default_val */ "", ""},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{"range", RPCArg::Type::OBJ, /* opt */ true, /* default_val */ "", "If a ranged descriptor is used, this specifies the start and end of the range to import",
|
||||||
|
{
|
||||||
|
{"start", RPCArg::Type::NUM, /* opt */ true, /* default_val */ "0", "Start of the range to import"},
|
||||||
|
{"end", RPCArg::Type::NUM, /* opt */ false, /* default_val */ "", "End of the range to import (inclusive)"},
|
||||||
|
}
|
||||||
|
},
|
||||||
{"internal", RPCArg::Type::BOOL, /* opt */ true, /* default_val */ "false", "Stating whether matching outputs should be treated as not incoming payments (also known as change)"},
|
{"internal", RPCArg::Type::BOOL, /* opt */ true, /* default_val */ "false", "Stating whether matching outputs should be treated as not incoming payments (also known as change)"},
|
||||||
{"watchonly", RPCArg::Type::BOOL, /* opt */ true, /* default_val */ "false", "Stating whether matching outputs should be considered watchonly."},
|
{"watchonly", RPCArg::Type::BOOL, /* opt */ true, /* default_val */ "false", "Stating whether matching outputs should be considered watchonly."},
|
||||||
{"label", RPCArg::Type::STR, /* opt */ true, /* default_val */ "''", "Label to assign to the address, only allowed with internal=false"},
|
{"label", RPCArg::Type::STR, /* opt */ true, /* default_val */ "''", "Label to assign to the address, only allowed with internal=false"},
|
||||||
|
|
|
@ -203,7 +203,7 @@ class ImportMultiTest(BitcoinTestFramework):
|
||||||
"keys": [key.privkey]},
|
"keys": [key.privkey]},
|
||||||
success=False,
|
success=False,
|
||||||
error_code=-4,
|
error_code=-4,
|
||||||
error_message='The wallet already contains the private key for this address or script')
|
error_message='The wallet already contains the private key for this address or script ("' + key.p2pkh_script + '")')
|
||||||
|
|
||||||
# Address + Private key + watchonly
|
# Address + Private key + watchonly
|
||||||
self.log.info("Should import an address with private key and with watchonly")
|
self.log.info("Should import an address with private key and with watchonly")
|
||||||
|
@ -543,5 +543,88 @@ class ImportMultiTest(BitcoinTestFramework):
|
||||||
solvable=True,
|
solvable=True,
|
||||||
ismine=False)
|
ismine=False)
|
||||||
|
|
||||||
|
# Test importing of a P2SH-P2WPKH address via descriptor + private key
|
||||||
|
key = get_key(self.nodes[0])
|
||||||
|
self.log.info("Should import a p2sh-p2wpkh address from descriptor and private key")
|
||||||
|
self.test_importmulti({"desc": "sh(wpkh(" + key.pubkey + "))",
|
||||||
|
"timestamp": "now",
|
||||||
|
"label": "Descriptor import test",
|
||||||
|
"keys": [key.privkey]},
|
||||||
|
success=True)
|
||||||
|
test_address(self.nodes[1],
|
||||||
|
key.p2sh_p2wpkh_addr,
|
||||||
|
solvable=True,
|
||||||
|
ismine=True,
|
||||||
|
label="Descriptor import test")
|
||||||
|
|
||||||
|
# Test ranged descriptor fails if range is not specified
|
||||||
|
xpriv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"
|
||||||
|
addresses = ["2N7yv4p8G8yEaPddJxY41kPihnWvs39qCMf", "2MsHxyb2JS3pAySeNUsJ7mNnurtpeenDzLA"] # hdkeypath=m/0'/0'/0' and 1'
|
||||||
|
desc = "sh(wpkh(" + xpriv + "/0'/0'/*'" + "))"
|
||||||
|
self.log.info("Ranged descriptor import should fail without a specified range")
|
||||||
|
self.test_importmulti({"desc": desc,
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message='Descriptor is ranged, please specify the range')
|
||||||
|
|
||||||
|
# Test importing of a ranged descriptor without keys
|
||||||
|
self.log.info("Should import the ranged descriptor with specified range as solvable")
|
||||||
|
self.test_importmulti({"desc": desc,
|
||||||
|
"timestamp": "now",
|
||||||
|
"range": {"end": 1}},
|
||||||
|
success=True,
|
||||||
|
warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."])
|
||||||
|
for address in addresses:
|
||||||
|
test_address(self.nodes[1],
|
||||||
|
key.p2sh_p2wpkh_addr,
|
||||||
|
solvable=True)
|
||||||
|
|
||||||
|
# Test importing of a P2PKH address via descriptor
|
||||||
|
key = get_key(self.nodes[0])
|
||||||
|
self.log.info("Should import a p2pkh address from descriptor")
|
||||||
|
self.test_importmulti({"desc": "pkh(" + key.pubkey + ")",
|
||||||
|
"timestamp": "now",
|
||||||
|
"label": "Descriptor import test"},
|
||||||
|
True,
|
||||||
|
warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."])
|
||||||
|
test_address(self.nodes[1],
|
||||||
|
key.p2pkh_addr,
|
||||||
|
solvable=True,
|
||||||
|
ismine=False,
|
||||||
|
label="Descriptor import test")
|
||||||
|
|
||||||
|
# Test import fails if both desc and scriptPubKey are provided
|
||||||
|
key = get_key(self.nodes[0])
|
||||||
|
self.log.info("Import should fail if both scriptPubKey and desc are provided")
|
||||||
|
self.test_importmulti({"desc": "pkh(" + key.pubkey + ")",
|
||||||
|
"scriptPubKey": {"address": key.p2pkh_addr},
|
||||||
|
"timestamp": "now"},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message='Both a descriptor and a scriptPubKey should not be provided.')
|
||||||
|
|
||||||
|
# Test import fails if neither desc nor scriptPubKey are present
|
||||||
|
key = get_key(self.nodes[0])
|
||||||
|
self.log.info("Import should fail if neither a descriptor nor a scriptPubKey are provided")
|
||||||
|
self.test_importmulti({"timestamp": "now"},
|
||||||
|
success=False,
|
||||||
|
error_code=-8,
|
||||||
|
error_message='Either a descriptor or scriptPubKey must be provided.')
|
||||||
|
|
||||||
|
# Test importing of a multisig via descriptor
|
||||||
|
key1 = get_key(self.nodes[0])
|
||||||
|
key2 = get_key(self.nodes[0])
|
||||||
|
self.log.info("Should import a 1-of-2 bare multisig from descriptor")
|
||||||
|
self.test_importmulti({"desc": "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(self.nodes[1],
|
||||||
|
key1.p2pkh_addr,
|
||||||
|
ismine=False,
|
||||||
|
iswatchonly=False)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
ImportMultiTest().main()
|
ImportMultiTest().main()
|
||||||
|
|
Loading…
Reference in a new issue