diff --git a/doc/release-notes-14565.md b/doc/release-notes-14565.md new file mode 100644 --- /dev/null +++ b/doc/release-notes-14565.md @@ -0,0 +1,5 @@ +Low-level RPC changes +--------------------- + +The `importmulti` RPC will now contain a new per-request `warnings` field with strings +that explain when fields are being ignored or inconsistent, if any. diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -1058,9 +1058,85 @@ return reply; } +struct ImportData { + // Input data + //! Provided redeemScript; will be moved to `import_scripts` if relevant. + std::unique_ptr redeemscript; + + // Output data + std::set import_scripts; + //! Import these private keys if available (the value indicates whether if + //! the key is required for solvability) + std::map used_keys; +}; + +enum class ScriptContext { + TOP, //! Top-level scriptPubKey + P2SH, //! P2SH redeemScript +}; + +// Analyse the provided scriptPubKey, determining which keys and which redeem +// scripts from the ImportData struct are needed to spend it, and mark them as +// used. Returns an error string, or the empty string for success. +static std::string RecurseImportData(const CScript &script, + ImportData &import_data, + const ScriptContext script_ctx) { + // Use Solver to obtain script type and parsed pubkeys or hashes: + std::vector> solverdata; + txnouttype script_type = Solver(script, solverdata); + + switch (script_type) { + case TX_PUBKEY: { + CPubKey pubkey(solverdata[0].begin(), solverdata[0].end()); + import_data.used_keys.emplace(pubkey.GetID(), false); + return ""; + } + case TX_PUBKEYHASH: { + CKeyID id = CKeyID(uint160(solverdata[0])); + import_data.used_keys[id] = true; + return ""; + } + case TX_SCRIPTHASH: { + if (script_ctx == ScriptContext::P2SH) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + "Trying to nest P2SH inside another P2SH"); + } + assert(script_ctx == ScriptContext::TOP); + CScriptID id = CScriptID(uint160(solverdata[0])); + // Remove redeemscript from import_data to check for superfluous + // script later. + auto subscript = std::move(import_data.redeemscript); + if (!subscript) { + return "missing redeemscript"; + } + if (CScriptID(*subscript) != id) { + return "redeemScript does not match the scriptPubKey"; + } + import_data.import_scripts.emplace(*subscript); + return RecurseImportData(*subscript, import_data, + ScriptContext::P2SH); + } + case TX_MULTISIG: { + for (size_t i = 1; i + 1 < solverdata.size(); ++i) { + CPubKey pubkey(solverdata[i].begin(), solverdata[i].end()); + import_data.used_keys.emplace(pubkey.GetID(), false); + } + return ""; + } + case TX_NULL_DATA: + return "unspendable script"; + case TX_NONSTANDARD: + default: + return "unrecognized script"; + } +} + static UniValue ProcessImport(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 { // First ensure scriptPubKey has either a script or JSON with "address" // string @@ -1102,20 +1178,18 @@ // Generate the script and destination for the scriptPubKey provided CScript script; CTxDestination dest; - if (!isScript) { dest = DecodeDestination(output, pwallet->chainParams); if (!IsValidDestination(dest)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Invalid address"); + "Invalid address \"" + output + "\""); } script = GetScriptForDestination(dest); } else { if (!IsHex(output)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Invalid scriptPubKey"); + "Invalid scriptPubKey \"" + output + "\""); } - std::vector vData(ParseHex(output)); script = CScript(vData.begin(), vData.end()); if (!ExtractDestination(script, dest) && !internal) { @@ -1125,11 +1199,50 @@ } } - // Watchonly and private keys - if (watchOnly && keys.size()) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - "Watch-only addresses should not include private keys"); + // 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 = std::make_unique( + parsed_redeemscript.begin(), parsed_redeemscript.end()); + } + std::map 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 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 @@ -1138,175 +1251,153 @@ "Internal addresses should not have a label"); } - CScript scriptpubkey_script = script; - CTxDestination scriptpubkey_dest = dest; - - // P2SH - if (!strRedeemScript.empty() && script.IsPayToScriptHash()) { - // Check the redeemScript is valid - if (!IsHex(strRedeemScript)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Invalid redeem script: must be hex string"); + // Verify and process input data + bool have_solving_data = + import_data.redeemscript || 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 &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."); } - - // Import redeem script. - std::vector vData(ParseHex(strRedeemScript)); - CScript redeemScript = CScript(vData.begin(), vData.end()); - CScriptID redeem_id(redeemScript); - - // Check that the redeemScript and scriptPubKey match - if (GetScriptForDestination(redeem_id) != script) { - throw JSONRPCError( - RPC_INVALID_ADDRESS_OR_KEY, - "The redeemScript does not match the scriptPubKey"); + 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."); } - pwallet->MarkDirty(); - - if (!pwallet->AddWatchOnly(redeemScript, timestamp)) { - throw JSONRPCError(RPC_WALLET_ERROR, - "Error adding address to wallet"); - } - - if (!pwallet->HaveCScript(redeem_id) && - !pwallet->AddCScript(redeemScript)) { - throw JSONRPCError(RPC_WALLET_ERROR, - "Error adding p2sh redeemScript to wallet"); - } - } - - // (P2SH-)P2PK/P2PKH - if (dest.type() == typeid(CKeyID)) { - CPubKey pubkey; - if (keys.size()) { - pubkey = DecodeSecret(keys[0].get_str()).GetPubKey(); - } - if (pubKeys.size()) { - const std::string &strPubKey = pubKeys[0].get_str(); - if (!IsHex(strPubKey)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Pubkey must be a hex string"); - } - std::vector vData(ParseHex(pubKeys[0].get_str())); - CPubKey pubkey_temp(vData.begin(), vData.end()); - if (pubkey.size() && pubkey_temp != pubkey) { - throw JSONRPCError( - RPC_INVALID_ADDRESS_OR_KEY, - "Private key does not match public key for address"); + // 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) { + // Not a required key + continue; + } + if (pubkey_map.count(require_key.first) == 0 && + privkey_map.count(require_key.first) == 0) { + error = "some required keys are missing"; + } } - pubkey = pubkey_temp; } - if (pubkey.size() > 0) { - if (!pubkey.IsFullyValid()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Pubkey is not a valid public key"); - } - // Check the key corresponds to the destination given - std::vector destinations = - GetAllDestinationsForKey(pubkey); - if (std::find(destinations.begin(), destinations.end(), dest) == - destinations.end()) { - throw JSONRPCError( - RPC_INVALID_ADDRESS_OR_KEY, - "Key does not match address destination"); + 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."); } - - // This is necessary to force the wallet to import the pubKey - CScript scriptRawPubKey = GetScriptForRawPubKey(pubkey); - - if (::IsMine(*pwallet, scriptRawPubKey) == ISMINE_SPENDABLE) { - throw JSONRPCError( - RPC_WALLET_ERROR, - "The wallet already contains the private key for this " - "address or 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); + } } - - pwallet->MarkDirty(); - - if (!pwallet->AddWatchOnly(scriptRawPubKey, timestamp)) { - throw JSONRPCError(RPC_WALLET_ERROR, - "Error adding address to wallet"); + 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); + } } } } - // Import the address - if (::IsMine(*pwallet, scriptpubkey_script) == ISMINE_SPENDABLE) { + // Check whether we have any work to do + if (::IsMine(*pwallet, script) & ISMINE_SPENDABLE) { throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key " "for this address or script"); } + // All good, time to import pwallet->MarkDirty(); - if (!pwallet->AddWatchOnly(scriptpubkey_script, timestamp)) { - throw JSONRPCError(RPC_WALLET_ERROR, - "Error adding address to wallet"); - } - - if (!watchOnly && - !pwallet->HaveCScript(CScriptID(scriptpubkey_script)) && - !pwallet->AddCScript(scriptpubkey_script)) { - throw JSONRPCError(RPC_WALLET_ERROR, - "Error adding scriptPubKey script to wallet"); - } - - // if not internal add to address book or update label - if (!internal) { - if (IsValidDestination(scriptpubkey_dest)) { - pwallet->SetAddressBook(scriptpubkey_dest, label, "receive"); + for (const auto &entry : import_data.import_scripts) { + if (!pwallet->HaveCScript(CScriptID(entry)) && + !pwallet->AddCScript(entry)) { + throw JSONRPCError(RPC_WALLET_ERROR, + "Error adding script to wallet"); } } - - // Import private keys. - for (size_t i = 0; i < keys.size(); i++) { - const std::string &strPrivkey = keys[i].get_str(); - - // Checks. - CKey key = DecodeSecret(strPrivkey); - - if (!key.IsValid()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Invalid private key encoding"); - } - - CPubKey pubKey = key.GetPubKey(); - assert(key.VerifyPubKey(pubKey)); - - CKeyID vchAddress = pubKey.GetID(); - pwallet->MarkDirty(); - - if (pwallet->HaveKey(vchAddress)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Already have this key"); - } - - pwallet->mapKeyMetadata[vchAddress].nCreateTime = timestamp; - - if (!pwallet->AddKeyPubKey(key, pubKey)) { + for (const auto &entry : privkey_map) { + const CKey &key = entry.second; + CPubKey pubkey = key.GetPubKey(); + const CKeyID &id = entry.first; + assert(key.VerifyPubKey(pubkey)); + pwallet->mapKeyMetadata[id].nCreateTime = timestamp; + // If the private key is not present in the wallet, insert it. + if (!pwallet->HaveKey(id) && !pwallet->AddKeyPubKey(key, pubkey)) { throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet"); } - pwallet->UpdateTimeFirstKey(timestamp); } + for (const auto &entry : pubkey_map) { + const CPubKey &pubkey = entry.second; + const CKeyID &id = entry.first; + CPubKey temp; + if (!pwallet->GetPubKey(id, temp) && + !pwallet->AddWatchOnly(GetScriptForRawPubKey(pubkey), + timestamp)) { + 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"); + } + } + if (!internal) { + assert(IsValidDestination(dest)); + pwallet->SetAddressBook(dest, label, "receive"); + } - UniValue result = UniValue(UniValue::VOBJ); result.pushKV("success", UniValue(true)); - return result; } catch (const UniValue &e) { - UniValue result = UniValue(UniValue::VOBJ); result.pushKV("success", UniValue(false)); result.pushKV("error", e); - return result; } catch (...) { - UniValue result = UniValue(UniValue::VOBJ); result.pushKV("success", UniValue(false)); result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, "Missing required fields")); - return result; } + + if (warnings.size()) { + result.pushKV("warnings", warnings); + } + return result; } static int64_t GetImportTimestamp(const UniValue &data, int64_t now) { @@ -1338,14 +1429,9 @@ mainRequest.params.size() > 2) { throw std::runtime_error(RPCHelpMan{ "importmulti", - "\nImport addresses/scripts (with private or public keys, " - "redeem script (P2SH)), rescanning all addresses in " - "one-shot-only (rescan can be disabled via options). Requires " - "a new wallet backup.\n" - "\nNote: This call can take minutes to complete if rescan is true, " - "during that time, other rpc calls\n" - "may report that the imported keys, addresses or scripts exists " - "but related transactions are still missing.\n", + "\nImport addresses/scripts (with private or public keys, redeem " + "script (P2SH)), rescanning all addresses in one-shot-only (rescan " + "can be disabled via options). Requires a new wallet backup.\n", { {"requests", RPCArg::Type::ARR, @@ -1399,26 +1485,28 @@ /* oneline_description */ "", {"timestamp | \"now\"", "integer / string"}}, {"redeemscript", RPCArg::Type::STR, - /* opt */ true, /* default_val */ "", + /* opt */ true, /* default_val */ "omitted", "Allowed only if the scriptPubKey is a P2SH " "address/scriptPubKey"}, {"pubkeys", RPCArg::Type::ARR, /* opt */ true, - /* default_val */ "", - "Array of strings giving pubkeys that must " - "occur in the output or redeemscript", + /* default_val */ "empty array", + "Array of strings giving pubkeys to import. They " + "must occur in P2PKH scripts. They are not " + "required when the private key is also provided " + "(see the \"keys\" argument).", { - {"pubKey", RPCArg::Type::STR, - /* opt */ false, /* default_val */ "", ""}, + {"pubKey", RPCArg::Type::STR, /* opt */ false, + /* default_val */ "", ""}, }}, {"keys", RPCArg::Type::ARR, /* opt */ true, - /* default_val */ "", - "Array of strings giving private keys whose " - "corresponding public keys must occur in the " - "output or redeemscript", + /* default_val */ "empty array", + "Array of strings giving private keys to import. " + "The corresponding public keys must occur in the " + "output or redeemscript.", { {"key", RPCArg::Type::STR, /* opt */ false, /* default_val */ "", ""}, @@ -1426,13 +1514,13 @@ {"internal", RPCArg::Type::BOOL, /* opt */ true, /* default_val */ "false", "Stating whether matching outputs should be " - "treated as not incoming payments aka " - "change"}, + "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 watched even when they're not " - "spendable, only allowed if keys are empty"}, + "considered watched even when not all private " + "keys are provided."}, {"label", RPCArg::Type::STR, /* opt */ true, /* default_val */ "''", "Label to assign to the address, only " @@ -1444,7 +1532,7 @@ {"options", RPCArg::Type::OBJ, /* opt */ true, - /* default_val */ "", + /* default_val */ "null", "", { {"rescan", RPCArg::Type::BOOL, /* opt */ true, @@ -1454,12 +1542,12 @@ }, "\"options\""}, }, - RPCResult{ - "\nResponse is an array with the same size as the input that " - "has the execution result :\n" - " [{ \"success\": true } , { \"success\": false, \"error\": { " - "\"code\": -1, \"message\": \"Internal Server Error\"} }, ... " - "]\n"}, + RPCResult{"\nResponse is an array with the same size as the input " + "that has the execution result :\n" + " [{\"success\": true}, {\"success\": true, " + "\"warnings\": [\"Ignoring irrelevant private key\"]}, " + "{\"success\": false, \"error\": {\"code\": -1, " + "\"message\": \"Internal Server Error\"}}, ...]\n"}, RPCExamples{ HelpExampleCli( "importmulti", diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -101,10 +101,17 @@ # redeem script script_code.hex()) - def test_importmulti(self, req, success, - error_code=None, error_message=None): + def test_importmulti(self, req, success, error_code=None, + error_message=None, warnings=[]): """Run importmulti and assert success""" result = self.nodes[1].importmulti([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) @@ -166,7 +173,7 @@ "timestamp": "now"}, False, error_code=-5, - error_message='Invalid address') + error_message='Invalid address \"not valid address\"') # ScriptPubKey + internal self.log.info("Should import a scriptPubKey with internal flag") @@ -217,7 +224,8 @@ "timestamp": "now", "pubkeys": [key.pubkey], "internal": False}, - True) + True, + warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) self.test_address(address, iswatchonly=True, ismine=False, @@ -232,7 +240,8 @@ "timestamp": "now", "pubkeys": [key.pubkey], "internal": True}, - True) + True, + warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) self.test_address(address, iswatchonly=True, ismine=False, @@ -278,20 +287,19 @@ # Address + Private key + watchonly self.log.info( - "Should not import an address with private key and with watchonly") + "Should import an address with private key and with watchonly") key = self.get_key() address = key.p2pkh_addr self.test_importmulti({"scriptPubKey": {"address": address}, "timestamp": "now", "keys": [key.privkey], "watchonly": True}, - False, - error_code=-8, - error_message='Watch-only addresses should not include private keys') + True, + warnings=["All private keys are provided, outputs will be considered spendable. If this is intentional, do not specify the watchonly flag."]) self.test_address(address, iswatchonly=False, - ismine=False, - timestamp=None) + ismine=True, + timestamp=timestamp) # ScriptPubKey + Private key + internal self.log.info( @@ -357,8 +365,14 @@ self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_addr}, "timestamp": "now", "redeemscript": multisig.redeem_script}, - True) - self.test_address(multisig.p2sh_addr, timestamp=timestamp) + True, + warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) + self.test_address( + multisig.p2sh_addr, + timestamp=timestamp, + iswatchonly=True, + ismine=False, + solvable=True) p2shunspent = self.nodes[1].listunspent( 0, 999999, [multisig.p2sh_addr])[0] @@ -379,9 +393,13 @@ "timestamp": "now", "redeemscript": multisig.redeem_script, "keys": multisig.privkeys[0:2]}, - True) + True, + warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) self.test_address(multisig.p2sh_addr, - timestamp=timestamp) + timestamp=timestamp, + ismine=False, + iswatchonly=True, + solvable=True) p2shunspent = self.nodes[1].listunspent( 0, 999999, [multisig.p2sh_addr])[0] @@ -403,29 +421,33 @@ "redeemscript": multisig.redeem_script, "keys": multisig.privkeys[0:2], "watchonly": True}, - False, - error_code=-8, - error_message='Watch-only addresses should not include private keys') + True) + self.test_address(multisig.p2sh_addr, + iswatchonly=True, + ismine=False, + solvable=True, + timestamp=timestamp) # Address + Public key + !Internal + Wrong pubkey - self.log.info("Should not import an address with a wrong public key") + self.log.info( + "Should not import an address with the wrong public key as non-solvable") key = self.get_key() address = key.p2pkh_addr wrong_key = self.get_key().pubkey self.test_importmulti({"scriptPubKey": {"address": address}, "timestamp": "now", "pubkeys": [wrong_key]}, - False, - error_code=-5, - error_message='Key does not match address destination') + True, + warnings=["Importing as non-solvable: some required keys are missing. If this is intentional, don't provide any keys, pubkeys, witnessscript, or redeemscript.", "Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) self.test_address(address, - iswatchonly=False, + iswatchonly=True, ismine=False, - timestamp=None) + solvable=False, + timestamp=timestamp) # ScriptPubKey + Public key + internal + Wrong pubkey self.log.info( - "Should not import a scriptPubKey with internal and with a wrong public key") + "Should import a scriptPubKey with internal and with a wrong public key as non-solvable") key = self.get_key() address = key.p2pkh_addr wrong_key = self.get_key().pubkey @@ -433,33 +455,34 @@ "timestamp": "now", "pubkeys": [wrong_key], "internal": True}, - False, - error_code=-5, - error_message='Key does not match address destination') + True, + warnings=["Importing as non-solvable: some required keys are missing. If this is intentional, don't provide any keys, pubkeys, witnessscript, or redeemscript.", "Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) self.test_address(address, - iswatchonly=False, + iswatchonly=True, ismine=False, - timestamp=None) + solvable=False, + timestamp=timestamp) # Address + Private key + !watchonly + Wrong private key - self.log.info("Should not import an address with a wrong private key") + self.log.info( + "Should import an address with a wrong private key as non-solvable") key = self.get_key() address = key.p2pkh_addr wrong_privkey = self.get_key().privkey self.test_importmulti({"scriptPubKey": {"address": address}, "timestamp": "now", "keys": [wrong_privkey]}, - False, - error_code=-5, - error_message='Key does not match address destination') + True, + warnings=["Importing as non-solvable: some required keys are missing. If this is intentional, don't provide any keys, pubkeys, witnessscript, or redeemscript.", "Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) self.test_address(address, - iswatchonly=False, + iswatchonly=True, ismine=False, - timestamp=None) + solvable=False, + timestamp=timestamp) # ScriptPubKey + Private key + internal + Wrong private key self.log.info( - "Should not import a scriptPubKey with internal and with a wrong private key") + "Should import a scriptPubKey with internal and with a wrong private key as non-solvable") key = self.get_key() address = key.p2pkh_addr wrong_privkey = self.get_key().privkey @@ -467,13 +490,13 @@ "timestamp": "now", "keys": [wrong_privkey], "internal": True}, - False, - error_code=-5, - error_message='Key does not match address destination') + True, + warnings=["Importing as non-solvable: some required keys are missing. If this is intentional, don't provide any keys, pubkeys, witnessscript, or redeemscript.", "Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) self.test_address(address, - iswatchonly=False, + iswatchonly=True, ismine=False, - timestamp=None) + solvable=False, + timestamp=timestamp) # Importing existing watch only address with new timestamp should # replace saved timestamp.