diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -222,6 +222,9 @@ // Return whether HD enabled. virtual bool hdEnabled() = 0; + // Check if a certain wallet flag is set. + virtual bool IsWalletFlagSet(uint64_t flag) = 0; + // Get default address type. virtual OutputType getDefaultAddressType() = 0; diff --git a/src/interfaces/wallet.cpp b/src/interfaces/wallet.cpp --- a/src/interfaces/wallet.cpp +++ b/src/interfaces/wallet.cpp @@ -392,6 +392,9 @@ OutputType getDefaultAddressType() override { return m_wallet.m_default_address_type; } + bool IsWalletFlagSet(uint64_t flag) override { + return m_wallet.IsWalletFlagSet(flag); + } OutputType getDefaultChangeType() override { return m_wallet.m_default_change_type; } diff --git a/src/qt/receivecoinsdialog.cpp b/src/qt/receivecoinsdialog.cpp --- a/src/qt/receivecoinsdialog.cpp +++ b/src/qt/receivecoinsdialog.cpp @@ -107,6 +107,10 @@ // geometry is ready. columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer( tableView, AMOUNT_MINIMUM_COLUMN_WIDTH, DATE_COLUMN_WIDTH, this); + + // Eventually disable the main receive button if private key operations + // are disabled. + ui->receiveButton->setEnabled(!model->privateKeysDisabled()); } } diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -208,6 +208,7 @@ const std::string &sRequest); static bool isWalletEnabled(); + bool privateKeysDisabled() const; interfaces::Node &node() const { return m_node; } interfaces::Wallet &wallet() const { return *m_wallet; } diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -475,6 +475,10 @@ return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET); } +bool WalletModel::privateKeysDisabled() const { + return m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); +} + QString WalletModel::getWalletName() const { return QString::fromStdString(m_wallet->getWalletName()); } diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -157,6 +157,7 @@ {"echojson", 9, "arg9"}, {"rescanblockchain", 0, "start_height"}, {"rescanblockchain", 1, "stop_height"}, + {"createwallet", 1, "disable_private_keys"}, }; class CRPCConvertTable { diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -180,6 +180,11 @@ HelpExampleRpc("getnewaddress", "")); } + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, + "Error: Private keys are disabled for this wallet"); + } + LOCK2(cs_main, pwallet->cs_wallet); // Parse the label first so we don't generate a key if there's an error @@ -327,6 +332,11 @@ HelpExampleRpc("getrawchangeaddress", "")); } + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, + "Error: Private keys are disabled for this wallet"); + } + LOCK2(cs_main, pwallet->cs_wallet); if (!pwallet->IsLocked()) { @@ -2992,6 +3002,11 @@ HelpExampleRpc("keypoolrefill", "")); } + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, + "Error: Private keys are disabled for this wallet"); + } + LOCK2(cs_main, pwallet->cs_wallet); // 0 is interpreted by TopUpKeyPool() as the default keypool size given by @@ -3608,6 +3623,9 @@ " \"hdmasterkeyid\": \"\" (string, optional) alias " "for hdseedid retained for backwards-compatibility. Will be " "removed in V0.21.\n" + " \"private_keys_enabled\": true|false (boolean) false if " + "privatekeys are disabled for this wallet (enforced watch-only " + "wallet)\n" "}\n" "\nExamples:\n" + HelpExampleCli("getwalletinfo", "") + @@ -3646,6 +3664,8 @@ obj.pushKV("hdseedid", seed_id.GetHex()); obj.pushKV("hdmasterkeyid", seed_id.GetHex()); } + obj.pushKV("private_keys_enabled", + !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); return obj; } @@ -3750,14 +3770,18 @@ static UniValue createwallet(const Config &config, const JSONRPCRequest &request) { - if (request.fHelp || request.params.size() != 1) { + if (request.fHelp || request.params.size() < 1 || + request.params.size() > 2) { throw std::runtime_error( - "createwallet \"wallet_name\"\n" + "createwallet \"wallet_name\" ( disable_private_keys )\n" "\nCreates and loads a new wallet.\n" "\nArguments:\n" "1. \"wallet_name\" (string, required) The name for the new " "wallet. If this is a path, the wallet will be created at the path " "location.\n" + "2. disable_private_keys (boolean, optional, default: false) " + "Disable the possibility of private keys (only watchonlys are " + "possible in this mode).\n" "\nResult:\n" "{\n" " \"name\" : , (string) The wallet name if " @@ -3777,6 +3801,11 @@ std::string error; std::string warning; + bool disable_privatekeys = false; + if (!request.params[1].isNull()) { + disable_privatekeys = request.params[1].get_bool(); + } + fs::path wallet_path = fs::absolute(wallet_name, GetWalletDir()); if (fs::symlink_status(wallet_path).type() != fs::file_not_found) { throw JSONRPCError(RPC_WALLET_ERROR, @@ -3791,7 +3820,8 @@ } std::shared_ptr const wallet = CWallet::CreateWalletFromFile( - chainParams, wallet_name, fs::absolute(wallet_name, GetWalletDir())); + chainParams, wallet_name, fs::absolute(wallet_name, GetWalletDir()), + (disable_privatekeys ? uint64_t(WALLET_FLAG_DISABLE_PRIVATE_KEYS) : 0)); if (!wallet) { throw JSONRPCError(RPC_WALLET_ERROR, "Wallet creation failed."); } @@ -5452,7 +5482,7 @@ { "wallet", "abandontransaction", abandontransaction, {"txid"} }, { "wallet", "addmultisigaddress", addmultisigaddress, {"nrequired","keys","label|account"} }, { "wallet", "backupwallet", backupwallet, {"destination"} }, - { "wallet", "createwallet", createwallet, {"wallet_name"} }, + { "wallet", "createwallet", createwallet, {"wallet_name", "disable_private_keys"} }, { "wallet", "encryptwallet", encryptwallet, {"passphrase"} }, { "wallet", "getaddressinfo", getaddressinfo, {"address"} }, { "wallet", "getbalance", getbalance, {"account","minconf","include_watchonly"} }, diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -397,4 +397,13 @@ BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); } +BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) { + std::shared_ptr wallet = std::make_shared( + Params(), "dummy", WalletDatabase::CreateDummy()); + wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + BOOST_CHECK(!wallet->TopUpKeyPool(1000)); + CPubKey pubkey; + BOOST_CHECK(!wallet->GetKeyFromPool(pubkey, false)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -98,6 +98,19 @@ //! Default for -changetype constexpr OutputType DEFAULT_CHANGE_TYPE{OutputType::CHANGE_AUTO}; +enum WalletFlags : uint64_t { + // Wallet flags in the upper section (> 1 << 31) will lead to not opening + // the wallet if flag is unknown. + // Unknown wallet flags in the lower section <= (1 << 31) will be tolerated. + + // Will enforce the rule that the wallet can't contain any private keys + // (only watch-only/pubkeys). + WALLET_FLAG_DISABLE_PRIVATE_KEYS = (1ULL << 32), +}; + +static constexpr uint64_t g_known_wallet_flags = + WALLET_FLAG_DISABLE_PRIVATE_KEYS; + /** A key pool entry */ class CKeyPool { public: @@ -751,6 +764,7 @@ std::set set_pre_split_keypool; int64_t m_max_keypool_index = 0; std::map m_pool_key_to_index; + std::atomic m_wallet_flags{0}; int64_t nTimeFirstKey = 0; @@ -1290,7 +1304,8 @@ */ static std::shared_ptr CreateWalletFromFile(const CChainParams &chainParams, - const std::string &name, const fs::path &path); + const std::string &name, const fs::path &path, + uint64_t wallet_creation_flags = 0); /** * Wallet post-init setup @@ -1345,6 +1360,22 @@ * could be anything). */ void LearnAllRelatedScripts(const CPubKey &key); + + /** + * Set a single wallet flag. + */ + void SetWalletFlag(uint64_t flags); + + /** + * Check if a certain wallet flag is set. + */ + bool IsWalletFlagSet(uint64_t flag); + + /** + * Overwrite all flags by the given uint64_t. + * Returns false if unknown, non-tolerable flags are present. + */ + bool SetWalletFlags(uint64_t overwriteFlags, bool memOnly); }; /** A key allocated from the key pool. */ diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -161,6 +161,7 @@ } CPubKey CWallet::GenerateNewKey(WalletBatch &batch, bool internal) { + assert(!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); // mapKeyMetadata AssertLockHeld(cs_wallet); // default to compressed public keys if we want 0.6.0 wallets @@ -1548,6 +1549,7 @@ } CPubKey CWallet::GenerateNewSeed() { + assert(!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); CKey key; key.MakeNewKey(true); return DeriveNewSeed(key); @@ -1606,6 +1608,35 @@ return !hdChain.seed_id.IsNull(); } +void CWallet::SetWalletFlag(uint64_t flags) { + LOCK(cs_wallet); + m_wallet_flags |= flags; + if (!WalletBatch(*database).WriteWalletFlags(m_wallet_flags)) { + throw std::runtime_error(std::string(__func__) + + ": writing wallet flags failed"); + } +} + +bool CWallet::IsWalletFlagSet(uint64_t flag) { + return (m_wallet_flags & flag); +} + +bool CWallet::SetWalletFlags(uint64_t overwriteFlags, bool memonly) { + LOCK(cs_wallet); + m_wallet_flags = overwriteFlags; + if (((overwriteFlags & g_known_wallet_flags) >> 32) ^ + (overwriteFlags >> 32)) { + // contains unknown non-tolerable wallet flags + return false; + } + if (!memonly && !WalletBatch(*database).WriteWalletFlags(m_wallet_flags)) { + throw std::runtime_error(std::string(__func__) + + ": writing wallet flags failed"); + } + + return true; +} + int64_t CWalletTx::GetTxTime() const { int64_t n = nTimeSmart; return n ? n : nTimeReceived; @@ -3000,6 +3031,12 @@ // with keys of ours to recover post-backup change. // Reserve a new key pair from key pool + if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + strFailReason = + _("Can't generate a change-address key. Private keys " + "are disabled for this wallet."); + return false; + } CPubKey vchPubKey; bool ret; ret = reservekey.GetReservedKey(vchPubKey, true); @@ -3431,7 +3468,8 @@ // This wallet is in its first run if all of these are empty fFirstRunRet = mapKeys.empty() && mapCryptedKeys.empty() && mapWatchKeys.empty() && setWatchOnly.empty() && - mapScripts.empty(); + mapScripts.empty() && + !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); } if (nLoadWalletRet != DBErrors::LOAD_OK) { @@ -3560,6 +3598,9 @@ * Mark old keypool keys as used, and generate all new keys. */ bool CWallet::NewKeyPool() { + if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + return false; + } LOCK(cs_wallet); WalletBatch batch(*database); @@ -3616,6 +3657,9 @@ } bool CWallet::TopUpKeyPool(unsigned int kpSize) { + if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + return false; + } LOCK(cs_wallet); if (IsLocked()) { @@ -3757,6 +3801,10 @@ } bool CWallet::GetKeyFromPool(CPubKey &result, bool internal) { + if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + return false; + } + CKeyPool keypool; LOCK(cs_wallet); int64_t nIndex; @@ -4366,7 +4414,8 @@ std::shared_ptr CWallet::CreateWalletFromFile(const CChainParams &chainParams, - const std::string &name, const fs::path &path) { + const std::string &name, const fs::path &path, + uint64_t wallet_creation_flags) { const std::string &walletFile = name; // Needed to restore wallet transaction meta data after -zapwallettxes @@ -4504,17 +4553,39 @@ } walletInstance->SetMinVersion(FEATURE_LATEST); - // Generate a new seed - CPubKey seed = walletInstance->GenerateNewSeed(); - walletInstance->SetHDSeed(seed); + if ((wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + // selective allow to set flags + walletInstance->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + } else { + // generate a new seed + CPubKey seed = walletInstance->GenerateNewSeed(); + walletInstance->SetHDSeed(seed); + } // Top up the keypool - if (!walletInstance->TopUpKeyPool()) { + if (!walletInstance->IsWalletFlagSet( + WALLET_FLAG_DISABLE_PRIVATE_KEYS) && + !walletInstance->TopUpKeyPool()) { InitError(_("Unable to generate initial keys") += "\n"); return nullptr; } walletInstance->ChainStateFlushed(chainActive.GetLocator()); + } else if (wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS) { + // Make it impossible to disable private keys after creation + InitError(strprintf(_("Error loading %s: Private keys can only be " + "disabled during creation"), + walletFile)); + return nullptr; + } else if (walletInstance->IsWalletFlagSet( + WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + LOCK(walletInstance->cs_KeyStore); + if (!walletInstance->mapKeys.empty() || + !walletInstance->mapCryptedKeys.empty()) { + InitWarning(strprintf(_("Warning: Private keys detected in wallet " + "{%s} with disabled private keys"), + walletFile)); + } } else if (gArgs.IsArgSet("-usehd")) { bool useHD = gArgs.GetBoolArg("-usehd", true); if (walletInstance->IsHDEnabled() && !useHD) { diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -249,6 +249,7 @@ //! write the hdchain model (external chain child index counter) bool WriteHDChain(const CHDChain &chain); + bool WriteWalletFlags(const uint64_t flags); //! Begin a new transaction bool TxnBegin(); //! Commit current transaction diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -505,6 +505,14 @@ CHDChain chain; ssValue >> chain; pwallet->SetHDChain(chain, true); + } else if (strType == "flags") { + uint64_t flags; + ssValue >> flags; + if (!pwallet->SetWalletFlags(flags, true)) { + strErr = "Error reading wallet database: Unknown non-tolerable " + "wallet flags found"; + return false; + } } else if (strType != "bestblock" && strType != "bestblock_nomerkle") { wss.m_unknown_records++; } @@ -562,6 +570,10 @@ // we assume the user can live with: if (IsKeyType(strType) || strType == "defaultkey") { result = DBErrors::CORRUPT; + } else if (strType == "flags") { + // Reading the wallet flags can only fail if unknown flags + // are present. + result = DBErrors::TOO_NEW; } else { // Leave other errors alone, if we try to fix them we might // make things worse. But do warn the user there is @@ -864,6 +876,10 @@ return WriteIC(std::string("hdchain"), chain); } +bool WalletBatch::WriteWalletFlags(const uint64_t flags) { + return WriteIC(std::string("flags"), flags); +} + bool WalletBatch::TxnBegin() { return m_batch.TxnBegin(); } diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -452,7 +452,8 @@ def send_cli(self, command=None, *args, **kwargs): """Run bitcoin-cli command. Deserializes returned string as python object.""" - pos_args = [str(arg) for arg in args] + 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()] assert not ( diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -77,6 +77,7 @@ "wallet_txn_doublespend.py": [["--mineblock"]], "wallet_txn_clone.py": [["--mineblock"]], "wallet_multiwallet.py": [["--usecli"]], + "wallet_disableprivatekeys.py": [["--usecli"]], } # Used to limit the number of tests, when list of tests is not provided on command line diff --git a/test/functional/timing.json b/test/functional/timing.json --- a/test/functional/timing.json +++ b/test/functional/timing.json @@ -355,6 +355,14 @@ "name": "wallet_disable.py", "time": 0 }, + { + "name": "wallet_disableprivatekeys.py", + "time": 0 + }, + { + "name": "wallet_disableprivatekeys.py --usecli", + "time": 0 + }, { "name": "wallet_dump.py", "time": 4 diff --git a/test/functional/wallet_disableprivatekeys.py b/test/functional/wallet_disableprivatekeys.py new file mode 100755 --- /dev/null +++ b/test/functional/wallet_disableprivatekeys.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test disable-privatekeys mode. +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_raises_rpc_error, +) + + +class DisablePrivateKeysTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 1 + self.supports_cli = True + + def run_test(self): + node = self.nodes[0] + self.log.info("Test disableprivatekeys creation.") + self.nodes[0].createwallet('w1', True) + self.nodes[0].createwallet('w2') + w1 = node.get_wallet_rpc('w1') + w2 = node.get_wallet_rpc('w2') + assert_raises_rpc_error( + -4, "Error: Private keys are disabled for this wallet", w1.getnewaddress) + assert_raises_rpc_error( + -4, "Error: Private keys are disabled for this wallet", w1.getrawchangeaddress) + w1.importpubkey(w2.getaddressinfo(w2.getnewaddress())['pubkey']) + + +if __name__ == '__main__': + DisablePrivateKeysTest().main()