From 1f77f6754ce724493b0cb084ae0b35107d58605f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Wed, 23 Jan 2019 15:51:35 -0500 Subject: [PATCH 1/3] tests: unify RPC argument to cli argument conversion and handle dicts and lists When running tests with --usecli, unify the conversion from argument objects to strings using a new function arg_to_cli(). This fixes boolean arguments when using named arguments. Also use json.dumps() to get the string values for arguments that are dicts and lists so that bitcoind's JSON parser does not become confused. --- test/functional/test_framework/test_node.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 031a8824b..674540996 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -402,6 +402,14 @@ class TestNodeCLIAttr: def get_request(self, *args, **kwargs): return lambda: self(*args, **kwargs) +def arg_to_cli(arg): + if isinstance(arg, bool): + return str(arg).lower() + elif isinstance(arg, dict) or isinstance(arg, list): + return json.dumps(arg) + else: + return str(arg) + class TestNodeCLI(): """Interface to bitcoin-cli for an individual node""" @@ -433,8 +441,8 @@ class TestNodeCLI(): def send_cli(self, command=None, *args, **kwargs): """Run bitcoin-cli command. Deserializes returned string as python object.""" - pos_args = [str(arg).lower() if type(arg) is bool else str(arg) for arg in args] - named_args = [str(key) + "=" + str(value) for (key, value) in kwargs.items()] + pos_args = [arg_to_cli(arg) for arg in args] + named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()] assert not (pos_args and named_args), "Cannot use positional arguments and named arguments in the same bitcoin-cli call" p_args = [self.binary, "-datadir=" + self.datadir] + self.options if named_args: From b5c5021b644731d14a6ef04961320a99466f035a Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 25 Jan 2019 14:37:00 -0500 Subject: [PATCH 2/3] Refactor importwallet to extract data from the file and then import Instead of importing keys and scripts as each line in the file is read, first extract the data then import them. --- src/wallet/rpcdump.cpp | 89 ++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 295dfcc63..c65db8ac8 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -578,8 +578,10 @@ UniValue importwallet(const JSONRPCRequest& request) // Use uiInterface.ShowProgress instead of pwallet.ShowProgress because pwallet.ShowProgress has a cancel button tied to AbortRescan which // we don't want for this progress bar showing the import progress. uiInterface.ShowProgress does not have a cancel button. uiInterface.ShowProgress(strprintf("%s " + _("Importing..."), pwallet->GetDisplayName()), 0, false); // show progress dialog in GUI + std::vector> keys; + std::vector> scripts; while (file.good()) { - uiInterface.ShowProgress("", std::max(1, std::min(99, (int)(((double)file.tellg() / (double)nFilesize) * 100))), false); + uiInterface.ShowProgress("", std::max(1, std::min(50, (int)(((double)file.tellg() / (double)nFilesize) * 100))), false); std::string line; std::getline(file, line); if (line.empty() || line[0] == '#') @@ -591,13 +593,6 @@ UniValue importwallet(const JSONRPCRequest& request) continue; CKey key = DecodeSecret(vstr[0]); if (key.IsValid()) { - CPubKey pubkey = key.GetPubKey(); - assert(key.VerifyPubKey(pubkey)); - CKeyID keyid = pubkey.GetID(); - if (pwallet->HaveKey(keyid)) { - pwallet->WalletLogPrintf("Skipping import of %s (key already present)\n", EncodeDestination(keyid)); - continue; - } int64_t nTime = DecodeDumpTime(vstr[1]); std::string strLabel; bool fLabel = true; @@ -613,36 +608,62 @@ UniValue importwallet(const JSONRPCRequest& request) fLabel = true; } } - pwallet->WalletLogPrintf("Importing %s...\n", EncodeDestination(keyid)); - if (!pwallet->AddKeyPubKey(key, pubkey)) { - fGood = false; - continue; - } - pwallet->mapKeyMetadata[keyid].nCreateTime = nTime; - if (fLabel) - pwallet->SetAddressBook(keyid, strLabel, "receive"); - nTimeBegin = std::min(nTimeBegin, nTime); + keys.push_back(std::make_tuple(key, nTime, fLabel, strLabel)); } else if(IsHex(vstr[0])) { - std::vector vData(ParseHex(vstr[0])); - CScript script = CScript(vData.begin(), vData.end()); - CScriptID id(script); - if (pwallet->HaveCScript(id)) { - pwallet->WalletLogPrintf("Skipping import of %s (script already present)\n", vstr[0]); - continue; - } - if(!pwallet->AddCScript(script)) { - pwallet->WalletLogPrintf("Error importing script %s\n", vstr[0]); - fGood = false; - continue; - } - int64_t birth_time = DecodeDumpTime(vstr[1]); - if (birth_time > 0) { - pwallet->m_script_metadata[id].nCreateTime = birth_time; - nTimeBegin = std::min(nTimeBegin, birth_time); - } + std::vector vData(ParseHex(vstr[0])); + CScript script = CScript(vData.begin(), vData.end()); + int64_t birth_time = DecodeDumpTime(vstr[1]); + scripts.push_back(std::pair(script, birth_time)); } } file.close(); + double total = (double)(keys.size() + scripts.size()); + double progress = 0; + for (const auto& key_tuple : keys) { + uiInterface.ShowProgress("", std::max(50, std::min(75, (int)((progress / total) * 100) + 50)), false); + const CKey& key = std::get<0>(key_tuple); + int64_t time = std::get<1>(key_tuple); + bool has_label = std::get<2>(key_tuple); + std::string label = std::get<3>(key_tuple); + + CPubKey pubkey = key.GetPubKey(); + assert(key.VerifyPubKey(pubkey)); + CKeyID keyid = pubkey.GetID(); + if (pwallet->HaveKey(keyid)) { + pwallet->WalletLogPrintf("Skipping import of %s (key already present)\n", EncodeDestination(keyid)); + continue; + } + pwallet->WalletLogPrintf("Importing %s...\n", EncodeDestination(keyid)); + if (!pwallet->AddKeyPubKey(key, pubkey)) { + fGood = false; + continue; + } + pwallet->mapKeyMetadata[keyid].nCreateTime = time; + if (has_label) + pwallet->SetAddressBook(keyid, label, "receive"); + nTimeBegin = std::min(nTimeBegin, time); + progress++; + } + for (const auto& script_pair : scripts) { + uiInterface.ShowProgress("", std::max(50, std::min(75, (int)((progress / total) * 100) + 50)), false); + const CScript& script = script_pair.first; + int64_t time = script_pair.second; + CScriptID id(script); + if (pwallet->HaveCScript(id)) { + pwallet->WalletLogPrintf("Skipping import of %s (script already present)\n", HexStr(script)); + continue; + } + if(!pwallet->AddCScript(script)) { + pwallet->WalletLogPrintf("Error importing script %s\n", HexStr(script)); + fGood = false; + continue; + } + if (time > 0) { + pwallet->m_script_metadata[id].nCreateTime = time; + nTimeBegin = std::min(nTimeBegin, time); + } + progress++; + } uiInterface.ShowProgress("", 100, false); // hide progress dialog in GUI pwallet->UpdateTimeFirstKey(nTimeBegin); } From e6c58d3b014ab8ef5cca4be68764af4b79685fcb Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 25 Jan 2019 14:38:34 -0500 Subject: [PATCH 3/3] Do not import private keys to wallets with private keys disabled --- src/wallet/rpcdump.cpp | 13 +++++++++++++ src/wallet/rpcwallet.cpp | 4 ++++ src/wallet/wallet.cpp | 3 +++ test/functional/wallet_disableprivatekeys.py | 11 +++++++++++ 4 files changed, 31 insertions(+) diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index c65db8ac8..02cd0584d 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -133,6 +133,9 @@ UniValue importprivkey(const JSONRPCRequest& request) + HelpExampleRpc("importprivkey", "\"mykey\", \"testing\", false") ); + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled"); + } WalletRescanReserver reserver(pwallet); bool fRescan = true; @@ -617,6 +620,11 @@ UniValue importwallet(const JSONRPCRequest& request) } } file.close(); + // We now know whether we are importing private keys, so we can error if private keys are disabled + if (keys.size() > 0 && pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + uiInterface.ShowProgress("", 100, false); // hide progress dialog in GUI + throw JSONRPCError(RPC_WALLET_ERROR, "Importing wallets is disabled when private keys are disabled"); + } double total = (double)(keys.size() + scripts.size()); double progress = 0; for (const auto& key_tuple : keys) { @@ -967,6 +975,11 @@ static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, con 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; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 5e036eb5d..859681d82 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3830,6 +3830,10 @@ UniValue sethdseed(const JSONRPCRequest& request) throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot set a new HD seed while still in Initial Block Download"); } + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed to a wallet with private keys disabled"); + } + auto locked_chain = pwallet->chain().lock(); LOCK(pwallet->cs_wallet); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 74deb2ddd..2b10f5a85 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -251,6 +251,9 @@ bool CWallet::AddKeyPubKeyWithDB(WalletBatch &batch, const CKey& secret, const C { AssertLockHeld(cs_wallet); // mapKeyMetadata + // Make sure we aren't adding private keys to private key disabled wallets + assert(!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); + // CCryptoKeyStore has no concept of wallet databases, but calls AddCryptedKey // which is overridden below. To avoid flushes, the database handle is // tunneled through to it. diff --git a/test/functional/wallet_disableprivatekeys.py b/test/functional/wallet_disableprivatekeys.py index 34ff52525..e55bb82e7 100755 --- a/test/functional/wallet_disableprivatekeys.py +++ b/test/functional/wallet_disableprivatekeys.py @@ -7,6 +7,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( + assert_equal, assert_raises_rpc_error, ) @@ -31,5 +32,15 @@ class DisablePrivateKeysTest(BitcoinTestFramework): assert_raises_rpc_error(-4,"Error: Private keys are disabled for this wallet", w1.getrawchangeaddress) w1.importpubkey(w2.getaddressinfo(w2.getnewaddress())['pubkey']) + self.log.info('Test that private keys cannot be imported') + addr = w2.getnewaddress('', 'legacy') + privkey = w2.dumpprivkey(addr) + assert_raises_rpc_error(-4, 'Cannot import private keys to a wallet with private keys disabled', w1.importprivkey, privkey) + result = w1.importmulti([{'scriptPubKey': {'address': addr}, 'timestamp': 'now', 'keys': [privkey]}]) + assert(not result[0]['success']) + assert('warning' not in result[0]) + assert_equal(result[0]['error']['code'], -4) + assert_equal(result[0]['error']['message'], 'Cannot import private keys to a wallet with private keys disabled') + if __name__ == '__main__': DisablePrivateKeysTest().main()