Overhaul importmulti logic

This introduces various changes to the importmulti logic:
* Instead of processing input and importing things at the same time, first
  process all input data and verify it, so no changes are made in case of
  an error.
* Verify that no superfluous information is provided (no keys or scripts
  that don't contribute to solvability in particular).
* Add way more sanity checks, by means of descending into all involved
  scripts.
This commit is contained in:
Pieter Wuille 2018-10-24 18:28:17 -07:00
parent f65bce858f
commit bdacbda253
2 changed files with 280 additions and 217 deletions

View file

@ -836,9 +836,98 @@ UniValue dumpwallet(const JSONRPCRequest& request)
return reply; return reply;
} }
struct ImportData
{
// Input data
std::unique_ptr<CScript> redeemscript; //!< Provided redeemScript; will be moved to `import_scripts` if relevant.
std::unique_ptr<CScript> witnessscript; //!< Provided witnessScript; will be moved to `import_scripts` if relevant.
// Output data
std::set<CScript> import_scripts;
std::map<CKeyID, bool> used_keys; //!< Import these private keys if available (the value indicates whether if the key is required for solvability)
};
enum class ScriptContext
{
TOP, //! Top-level scriptPubKey
P2SH, //! P2SH redeemScript
WITNESS_V0, //! P2WSH witnessScript
};
// 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<std::vector<unsigned char>> 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");
if (script_ctx == ScriptContext::WITNESS_V0) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Trying to nest P2SH inside a P2WSH");
assert(script_ctx == ScriptContext::TOP);
CScriptID id = CScriptID(uint160(solverdata[0]));
auto subscript = std::move(import_data.redeemscript); // Remove redeemscript from import_data to check for superfluous script later.
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_WITNESS_V0_SCRIPTHASH: {
if (script_ctx == ScriptContext::WITNESS_V0) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Trying to nest P2WSH inside another P2WSH");
uint256 fullid(solverdata[0]);
CScriptID id;
CRIPEMD160().Write(fullid.begin(), fullid.size()).Finalize(id.begin());
auto subscript = std::move(import_data.witnessscript); // Remove redeemscript from import_data to check for superfluous script later.
if (!subscript) return "missing witnessscript";
if (CScriptID(*subscript) != id) return "witnessScript does not match the scriptPubKey or redeemScript";
if (script_ctx == ScriptContext::TOP) {
import_data.import_scripts.emplace(script); // Special rule for IsMine: native P2WSH requires the TOP script imported (see script/ismine.cpp)
}
import_data.import_scripts.emplace(*subscript);
return RecurseImportData(*subscript, import_data, ScriptContext::WITNESS_V0);
}
case TX_WITNESS_V0_KEYHASH: {
if (script_ctx == ScriptContext::WITNESS_V0) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Trying to nest P2WPKH inside P2WSH");
CKeyID id = CKeyID(uint160(solverdata[0]));
import_data.used_keys[id] = true;
if (script_ctx == ScriptContext::TOP) {
import_data.import_scripts.emplace(script); // Special rule for IsMine: native P2WPKH requires the TOP script imported (see script/ismine.cpp)
}
return "";
}
case TX_NULL_DATA:
return "unspendable script";
case TX_NONSTANDARD:
case TX_WITNESS_UNKNOWN:
default:
return "unrecognized script";
}
}
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 result(UniValue::VOBJ);
try { try {
// First ensure scriptPubKey has either a script or JSON with "address" string // First ensure scriptPubKey has either a script or JSON with "address" string
const UniValue& scriptPubKey = data["scriptPubKey"]; const UniValue& scriptPubKey = data["scriptPubKey"];
@ -860,18 +949,16 @@ static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, con
// Generate the script and destination for the scriptPubKey provided // Generate the script and destination for the scriptPubKey provided
CScript script; CScript script;
CTxDestination dest; CTxDestination dest;
if (!isScript) { if (!isScript) {
dest = DecodeDestination(output); dest = DecodeDestination(output);
if (!IsValidDestination(dest)) { if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address \"" + output + "\"");
} }
script = GetScriptForDestination(dest); script = GetScriptForDestination(dest);
} else { } else {
if (!IsHex(output)) { if (!IsHex(output)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid scriptPubKey"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid scriptPubKey \"" + output + "\"");
} }
std::vector<unsigned char> vData(ParseHex(output)); std::vector<unsigned char> vData(ParseHex(output));
script = CScript(vData.begin(), vData.end()); script = CScript(vData.begin(), vData.end());
if (!ExtractDestination(script, dest) && !internal) { if (!ExtractDestination(script, dest) && !internal) {
@ -879,9 +966,48 @@ static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, con
} }
} }
// Watchonly and private keys // Parse all arguments
if (watchOnly && keys.size()) { ImportData import_data;
throw JSONRPCError(RPC_INVALID_PARAMETER, "Watch-only addresses should not include private keys"); 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
@ -889,193 +1015,112 @@ static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, con
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label"); throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label");
} }
// Force users to provide the witness script in its field rather than redeemscript // Verify and process input data
if (!strRedeemScript.empty() && script.IsPayToWitnessScriptHash()) { bool have_solving_data = import_data.redeemscript || import_data.witnessscript || pubkey_map.size() || privkey_map.size();
throw JSONRPCError(RPC_INVALID_PARAMETER, "P2WSH addresses have an empty redeemscript. Please provide the witnessscript instead."); if (have_solving_data) {
} // Match up data in import_data with the scriptPubKey in script.
auto error = RecurseImportData(script, import_data, ScriptContext::TOP);
CScript scriptpubkey_script = script; // Verify whether the watchonly option corresponds to the availability of private keys.
CTxDestination scriptpubkey_dest = dest; 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; });
bool allow_p2wpkh = true; if (!watchOnly && !spendable) {
warnings.push_back("Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag.");
// P2SH }
if (!strRedeemScript.empty() && script.IsPayToScriptHash()) { if (watchOnly && spendable) {
// Check the redeemScript is valid warnings.push_back("All private keys are provided, outputs will be considered spendable. If this is intentional, do not specify the watchonly flag.");
if (!IsHex(strRedeemScript)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid redeem script: must be hex string");
} }
// Import redeem script. // Check that all required keys for solvability are provided.
std::vector<unsigned char> vData(ParseHex(strRedeemScript)); if (error.empty()) {
CScript redeemScript = CScript(vData.begin(), vData.end()); for (const auto& require_key : import_data.used_keys) {
CScriptID redeem_id(redeemScript); if (!require_key.second) continue; // Not a required key
if (pubkey_map.count(require_key.first) == 0 && privkey_map.count(require_key.first) == 0) {
// Check that the redeemScript and scriptPubKey match error = "some required keys are missing";
if (GetScriptForDestination(redeem_id) != script) { }
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The redeemScript does not match the scriptPubKey");
}
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");
}
// Now set script to the redeemScript so we parse the inner script as P2WSH or P2WPKH below
script = redeemScript;
ExtractDestination(script, dest);
}
// (P2SH-)P2WSH
if (!witness_script_hex.empty() && script.IsPayToWitnessScriptHash()) {
if (!IsHex(witness_script_hex)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid witness script: must be hex string");
}
// Generate the scripts
std::vector<unsigned char> witness_script_parsed(ParseHex(witness_script_hex));
CScript witness_script = CScript(witness_script_parsed.begin(), witness_script_parsed.end());
CScriptID witness_id(witness_script);
// Check that the witnessScript and scriptPubKey match
if (GetScriptForDestination(WitnessV0ScriptHash(witness_script)) != script) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The witnessScript does not match the scriptPubKey or redeemScript");
}
// Add the witness script as watch only only if it is not for P2SH-P2WSH
if (!scriptpubkey_script.IsPayToScriptHash() && !pwallet->AddWatchOnly(witness_script, timestamp)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
}
if (!pwallet->HaveCScript(witness_id) && !pwallet->AddCScript(witness_script)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding p2wsh witnessScript to wallet");
}
// Now set script to the witnessScript so we parse the inner script as P2PK or P2PKH below
script = witness_script;
ExtractDestination(script, dest);
allow_p2wpkh = false; // P2WPKH cannot be embedded in P2WSH
}
// (P2SH-)P2PK/P2PKH/P2WPKH
if (dest.type() == typeid(CKeyID) || dest.type() == typeid(WitnessV0KeyHash)) {
if (!allow_p2wpkh && dest.type() == typeid(WitnessV0KeyHash)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "P2WPKH cannot be embedded in P2WSH");
}
if (keys.size() > 1 || pubKeys.size() > 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "More than one key given for one single-key address");
}
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<unsigned char> 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");
}
pubkey = pubkey_temp;
} }
if (pubkey.size() > 0) {
if (!pubkey.IsFullyValid()) { if (!error.empty()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey is not a valid public key"); 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(); ) {
// Check the key corresponds to the destination given auto oldit = it++;
std::vector<CTxDestination> destinations = GetAllDestinationsForKey(pubkey); auto key_data_it = import_data.used_keys.find(oldit->first);
if (std::find(destinations.begin(), destinations.end(), dest) == destinations.end()) { if (key_data_it == import_data.used_keys.end() || !key_data_it->second) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Key does not match address destination"); warnings.push_back("Ignoring public key \"" + HexStr(oldit->first) + "\" as it doesn't appear inside P2PKH or P2WPKH.");
} pubkey_map.erase(oldit);
}
// 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");
}
pwallet->MarkDirty();
if (!pwallet->AddWatchOnly(scriptRawPubKey, timestamp)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
} }
} }
} }
// Import the address // Check whether we have any work to do
if (::IsMine(*pwallet, scriptpubkey_script) == ISMINE_SPENDABLE) { if (::IsMine(*pwallet, script) & ISMINE_SPENDABLE) {
throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this address or script"); throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this address or script");
} }
// All good, time to import
pwallet->MarkDirty(); pwallet->MarkDirty();
for (const auto& entry : import_data.import_scripts) {
if (!pwallet->AddWatchOnly(scriptpubkey_script, timestamp)) { if (!pwallet->HaveCScript(CScriptID(entry)) && !pwallet->AddCScript(entry)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet"); throw JSONRPCError(RPC_WALLET_ERROR, "Error adding script 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) {
assert(IsValidDestination(scriptpubkey_dest));
pwallet->SetAddressBook(scriptpubkey_dest, label, "receive");
}
// 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(); for (const auto& entry : privkey_map) {
assert(key.VerifyPubKey(pubKey)); const CKey& key = entry.second;
CPubKey pubkey = key.GetPubKey();
CKeyID vchAddress = pubKey.GetID(); const CKeyID& id = entry.first;
pwallet->MarkDirty(); assert(key.VerifyPubKey(pubkey));
pwallet->mapKeyMetadata[id].nCreateTime = timestamp;
if (pwallet->HaveKey(vchAddress)) { // If the private key is not present in the wallet, insert it.
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key"); if (!pwallet->HaveKey(id) && !pwallet->AddKeyPubKey(key, pubkey)) {
}
pwallet->mapKeyMetadata[vchAddress].nCreateTime = timestamp;
if (!pwallet->AddKeyPubKey(key, pubKey)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet"); throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
} }
pwallet->UpdateTimeFirstKey(timestamp); 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)); result.pushKV("success", UniValue(true));
return result;
} catch (const UniValue& e) { } catch (const UniValue& e) {
UniValue result = UniValue(UniValue::VOBJ);
result.pushKV("success", UniValue(false)); result.pushKV("success", UniValue(false));
result.pushKV("error", e); result.pushKV("error", e);
return result;
} catch (...) { } catch (...) {
UniValue result = UniValue(UniValue::VOBJ);
result.pushKV("success", UniValue(false)); result.pushKV("success", UniValue(false));
result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, "Missing required fields")); 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) static int64_t GetImportTimestamp(const UniValue& data, int64_t now)
@ -1122,18 +1167,18 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
}, },
{"redeemscript", RPCArg::Type::STR, /* opt */ true, /* default_val */ "omitted", "Allowed only if the scriptPubKey is a P2SH or P2SH-P2WSH address/scriptPubKey"}, {"redeemscript", RPCArg::Type::STR, /* opt */ true, /* default_val */ "omitted", "Allowed only if the scriptPubKey is a P2SH or P2SH-P2WSH address/scriptPubKey"},
{"witnessscript", RPCArg::Type::STR, /* opt */ true, /* default_val */ "omitted", "Allowed only if the scriptPubKey is a P2SH-P2WSH or P2WSH address/scriptPubKey"}, {"witnessscript", RPCArg::Type::STR, /* opt */ true, /* default_val */ "omitted", "Allowed only if the scriptPubKey is a P2SH-P2WSH or P2WSH address/scriptPubKey"},
{"pubkeys", RPCArg::Type::ARR, /* opt */ true, /* default_val */ "empty array", "Array of strings giving pubkeys that must occur in the output or redeemscript", {"pubkeys", RPCArg::Type::ARR, /* opt */ true, /* default_val */ "empty array", "Array of strings giving pubkeys to import. They must occur in P2PKH or P2WPKH 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 */ "empty array", "Array of strings giving private keys whose corresponding public keys must occur in the output or redeemscript", {"keys", RPCArg::Type::ARR, /* opt */ true, /* 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 */ "", ""}, {"key", RPCArg::Type::STR, /* opt */ false, /* default_val */ "", ""},
} }
}, },
{"internal", RPCArg::Type::BOOL, /* opt */ true, /* default_val */ "false", "Stating whether matching outputs should be treated as not incoming payments aka 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 watched even when they're not spendable, only allowed if keys are empty"}, {"watchonly", RPCArg::Type::BOOL, /* opt */ true, /* default_val */ "false", "Stating whether matching outputs should be 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 allowed with internal=false"}, {"label", RPCArg::Type::STR, /* opt */ true, /* default_val */ "''", "Label to assign to the address, only allowed with internal=false"},
}, },
}, },
@ -1154,7 +1199,7 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
HelpExampleCli("importmulti", "'[{ \"scriptPubKey\": { \"address\": \"<my address>\" }, \"timestamp\":1455191478 }]' '{ \"rescan\": false}'") + HelpExampleCli("importmulti", "'[{ \"scriptPubKey\": { \"address\": \"<my address>\" }, \"timestamp\":1455191478 }]' '{ \"rescan\": false}'") +
"\nResponse is an array with the same size as the input that has the execution result :\n" "\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"); " [{\"success\": true}, {\"success\": true, \"warnings\": [\"Ignoring irrelevant private key\"]}, {\"success\": false, \"error\": {\"code\": -1, \"message\": \"Internal Server Error\"}}, ...]\n");
RPCTypeCheck(mainRequest.params, {UniValue::VARR, UniValue::VOBJ}); RPCTypeCheck(mainRequest.params, {UniValue::VARR, UniValue::VOBJ});

View file

@ -119,9 +119,13 @@ class ImportMultiTest(BitcoinTestFramework):
CScript([OP_HASH160, witness_script, OP_EQUAL]).hex(), # p2sh-p2wsh CScript([OP_HASH160, witness_script, OP_EQUAL]).hex(), # p2sh-p2wsh
script_to_p2sh_p2wsh(script_code)) # p2sh-p2wsh addr script_to_p2sh_p2wsh(script_code)) # p2sh-p2wsh addr
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""" """Run importmulti and assert success"""
result = self.nodes[1].importmulti([req]) 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) assert_equal(result[0]['success'], success)
if error_code is not None: if error_code is not None:
assert_equal(result[0]['error']['code'], error_code) assert_equal(result[0]['error']['code'], error_code)
@ -178,7 +182,7 @@ class ImportMultiTest(BitcoinTestFramework):
"timestamp": "now"}, "timestamp": "now"},
False, False,
error_code=-5, error_code=-5,
error_message='Invalid address') error_message='Invalid address \"not valid address\"')
# ScriptPubKey + internal # ScriptPubKey + internal
self.log.info("Should import a scriptPubKey with internal flag") self.log.info("Should import a scriptPubKey with internal flag")
@ -227,7 +231,8 @@ class ImportMultiTest(BitcoinTestFramework):
"timestamp": "now", "timestamp": "now",
"pubkeys": [key.pubkey], "pubkeys": [key.pubkey],
"internal": False}, "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, self.test_address(address,
iswatchonly=True, iswatchonly=True,
ismine=False, ismine=False,
@ -241,7 +246,8 @@ class ImportMultiTest(BitcoinTestFramework):
"timestamp": "now", "timestamp": "now",
"pubkeys": [key.pubkey], "pubkeys": [key.pubkey],
"internal": True}, "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, self.test_address(address,
iswatchonly=True, iswatchonly=True,
ismine=False, ismine=False,
@ -284,20 +290,19 @@ class ImportMultiTest(BitcoinTestFramework):
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')
# Address + Private key + watchonly # Address + Private key + watchonly
self.log.info("Should not import an address with private key and with watchonly") self.log.info("Should import an address with private key and with watchonly")
key = self.get_key() key = self.get_key()
address = key.p2pkh_addr address = key.p2pkh_addr
self.test_importmulti({"scriptPubKey": {"address": address}, self.test_importmulti({"scriptPubKey": {"address": address},
"timestamp": "now", "timestamp": "now",
"keys": [key.privkey], "keys": [key.privkey],
"watchonly": True}, "watchonly": True},
False, True,
error_code=-8, warnings=["All private keys are provided, outputs will be considered spendable. If this is intentional, do not specify the watchonly flag."])
error_message='Watch-only addresses should not include private keys')
self.test_address(address, self.test_address(address,
iswatchonly=False, iswatchonly=False,
ismine=False, ismine=True,
timestamp=None) timestamp=timestamp)
# ScriptPubKey + Private key + internal # ScriptPubKey + Private key + internal
self.log.info("Should import a scriptPubKey with internal and with private key") self.log.info("Should import a scriptPubKey with internal and with private key")
@ -358,8 +363,9 @@ class ImportMultiTest(BitcoinTestFramework):
self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_addr}, self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_addr},
"timestamp": "now", "timestamp": "now",
"redeemscript": multisig.redeem_script}, "redeemscript": multisig.redeem_script},
True) True,
self.test_address(multisig.p2sh_addr, timestamp=timestamp) 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] p2shunspent = self.nodes[1].listunspent(0, 999999, [multisig.p2sh_addr])[0]
assert_equal(p2shunspent['spendable'], False) assert_equal(p2shunspent['spendable'], False)
@ -377,9 +383,13 @@ class ImportMultiTest(BitcoinTestFramework):
"timestamp": "now", "timestamp": "now",
"redeemscript": multisig.redeem_script, "redeemscript": multisig.redeem_script,
"keys": multisig.privkeys[0:2]}, "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, 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] p2shunspent = self.nodes[1].listunspent(0, 999999, [multisig.p2sh_addr])[0]
assert_equal(p2shunspent['spendable'], False) assert_equal(p2shunspent['spendable'], False)
@ -398,28 +408,31 @@ class ImportMultiTest(BitcoinTestFramework):
"redeemscript": multisig.redeem_script, "redeemscript": multisig.redeem_script,
"keys": multisig.privkeys[0:2], "keys": multisig.privkeys[0:2],
"watchonly": True}, "watchonly": True},
False, True)
error_code=-8, self.test_address(multisig.p2sh_addr,
error_message='Watch-only addresses should not include private keys') iswatchonly=True,
ismine=False,
solvable=True,
timestamp=timestamp)
# Address + Public key + !Internal + Wrong pubkey # 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() key = self.get_key()
address = key.p2pkh_addr address = key.p2pkh_addr
wrong_key = self.get_key().pubkey wrong_key = self.get_key().pubkey
self.test_importmulti({"scriptPubKey": {"address": address}, self.test_importmulti({"scriptPubKey": {"address": address},
"timestamp": "now", "timestamp": "now",
"pubkeys": [wrong_key]}, "pubkeys": [wrong_key]},
False, True,
error_code=-5, 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."])
error_message='Key does not match address destination')
self.test_address(address, self.test_address(address,
iswatchonly=False, iswatchonly=True,
ismine=False, ismine=False,
timestamp=None) solvable=False,
timestamp=timestamp)
# ScriptPubKey + Public key + internal + Wrong pubkey # ScriptPubKey + Public key + internal + Wrong pubkey
self.log.info("Should not import a scriptPubKey with internal and with a wrong public key") self.log.info("Should import a scriptPubKey with internal and with a wrong public key as non-solvable")
key = self.get_key() key = self.get_key()
address = key.p2pkh_addr address = key.p2pkh_addr
wrong_key = self.get_key().pubkey wrong_key = self.get_key().pubkey
@ -427,32 +440,32 @@ class ImportMultiTest(BitcoinTestFramework):
"timestamp": "now", "timestamp": "now",
"pubkeys": [wrong_key], "pubkeys": [wrong_key],
"internal": True}, "internal": True},
False, True,
error_code=-5, 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."])
error_message='Key does not match address destination')
self.test_address(address, self.test_address(address,
iswatchonly=False, iswatchonly=True,
ismine=False, ismine=False,
timestamp=None) solvable=False,
timestamp=timestamp)
# Address + Private key + !watchonly + Wrong private key # 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() key = self.get_key()
address = key.p2pkh_addr address = key.p2pkh_addr
wrong_privkey = self.get_key().privkey wrong_privkey = self.get_key().privkey
self.test_importmulti({"scriptPubKey": {"address": address}, self.test_importmulti({"scriptPubKey": {"address": address},
"timestamp": "now", "timestamp": "now",
"keys": [wrong_privkey]}, "keys": [wrong_privkey]},
False, True,
error_code=-5, 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."])
error_message='Key does not match address destination')
self.test_address(address, self.test_address(address,
iswatchonly=False, iswatchonly=True,
ismine=False, ismine=False,
timestamp=None) solvable=False,
timestamp=timestamp)
# ScriptPubKey + Private key + internal + Wrong private key # ScriptPubKey + Private key + internal + Wrong private key
self.log.info("Should not import a scriptPubKey with internal and with a wrong private key") self.log.info("Should import a scriptPubKey with internal and with a wrong private key as non-solvable")
key = self.get_key() key = self.get_key()
address = key.p2pkh_addr address = key.p2pkh_addr
wrong_privkey = self.get_key().privkey wrong_privkey = self.get_key().privkey
@ -460,13 +473,13 @@ class ImportMultiTest(BitcoinTestFramework):
"timestamp": "now", "timestamp": "now",
"keys": [wrong_privkey], "keys": [wrong_privkey],
"internal": True}, "internal": True},
False, True,
error_code=-5, 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."])
error_message='Key does not match address destination')
self.test_address(address, self.test_address(address,
iswatchonly=False, iswatchonly=True,
ismine=False, ismine=False,
timestamp=None) solvable=False,
timestamp=timestamp)
# Importing existing watch only address with new timestamp should replace saved timestamp. # Importing existing watch only address with new timestamp should replace saved timestamp.
assert_greater_than(timestamp, watchonly_timestamp) assert_greater_than(timestamp, watchonly_timestamp)
@ -516,7 +529,8 @@ class ImportMultiTest(BitcoinTestFramework):
self.test_importmulti({"scriptPubKey": {"address": address}, self.test_importmulti({"scriptPubKey": {"address": address},
"timestamp": "now", "timestamp": "now",
"pubkeys": [key.pubkey]}, "pubkeys": [key.pubkey]},
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, self.test_address(address,
ismine=False, ismine=False,
solvable=True) solvable=True)
@ -571,7 +585,8 @@ class ImportMultiTest(BitcoinTestFramework):
"timestamp": "now", "timestamp": "now",
"redeemscript": key.p2sh_p2wpkh_redeem_script, "redeemscript": key.p2sh_p2wpkh_redeem_script,
"pubkeys": [key.pubkey]}, "pubkeys": [key.pubkey]},
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, self.test_address(address,
solvable=True, solvable=True,
ismine=False) ismine=False)
@ -591,14 +606,17 @@ class ImportMultiTest(BitcoinTestFramework):
# P2SH-P2WSH multisig + redeemscript with no private key # P2SH-P2WSH multisig + redeemscript with no private key
multisig = self.get_multisig() multisig = self.get_multisig()
address = multisig.p2sh_p2wsh_addr
self.log.info("Should import a p2sh-p2wsh with respective redeem script but no private key") self.log.info("Should import a p2sh-p2wsh with respective redeem script but no private key")
self.test_importmulti({"scriptPubKey": {"address": multisig.p2sh_p2wsh_addr}, self.test_importmulti({"scriptPubKey": {"address": address},
"timestamp": "now", "timestamp": "now",
"redeemscript": multisig.p2wsh_script, "redeemscript": multisig.p2wsh_script,
"witnessscript": multisig.redeem_script}, "witnessscript": multisig.redeem_script},
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, self.test_address(address,
solvable=True) solvable=True,
ismine=False)
if __name__ == '__main__': if __name__ == '__main__':
ImportMultiTest().main() ImportMultiTest().main()