From 7abbc4e12fa227c3a7e6befc897b2a9dec54cf7a Mon Sep 17 00:00:00 2001 From: Jon Spock Date: Wed, 26 Feb 2020 18:57:50 -0800 Subject: [PATCH] Merge #9662: Add createwallet "disableprivatekeys" option: a sane mode for watchonly-wallets Summary: a3fa4d6a6acf19d640a1d5879a00aa1f059e2380 QA: Fix bug in -usecli logic that converts booleans to non-lowercase strings (Jonas Schnelli) 4704e5f074e57782d058404a594a7313cf170cf0 [QA] add createwallet disableprivatekey test (Jonas Schnelli) c7b8f343e99d9d53ea353ddce9a977f1886caf30 [Qt] Disable creating receive addresses when private keys are disabled (Jonas Schnelli) 2f15c2bc20d583b4c1788da78c9c635c36e03ed0 Add disable privatekeys option to createwallet (Jonas Schnelli) cebefba0855cee7fbcb9474b34e6779369e8e9ce Add option to disable private keys during internal wallet creation (Jonas Schnelli) 9995a602a639b64a749545b7c3bafbf67f97324f Add facility to store wallet flags (64 bits) (Jonas Schnelli) Pull request description: This mode ('createwallet {"disableprivatekeys": true}') is intended for a sane pure watch-only mode, ideal for a use-case where one likes to use Bitcoin-Core in conjunction with a hardware-wallet or another solutions for cold-storage. Since we have support for custom change addresses in `fundrawtransaction`, pure watch-only wallets including coin-selection are possible and do make sense for some use cases. This new mode disables all forms of private key generation and ensure that no mix between hot and cold keys are possible. Tree-SHA512: 3ebe7e8d54c4d4e5f790c348d4c292d456f573960a5b04d69ca5ef43a9217c7e7671761c6968cdc56f9a8bc235f3badd358576651af9f10855a0eb731f3fc508 Backport of Core PR9662 https://github.com/bitcoin/bitcoin/pull/9662/ Depends on D4175 Test Plan: make check test_runner.py bitcoin-qt Help -> Debug -> Console createwallet noprivkey true Select noprivkey wallet in the main window In the `Receive` menu, the `Request Payment` button should be greyed out and not clickable. {F4088184} Reviewers: deadalnix, Fabien, jasonbcox, O1 Bitcoin ABC, #bitcoin_abc Reviewed By: deadalnix, Fabien, O1 Bitcoin ABC, #bitcoin_abc Differential Revision: https://reviews.bitcoinabc.org/D4304 --- src/interfaces/wallet.cpp | 3 + src/interfaces/wallet.h | 3 + src/qt/receivecoinsdialog.cpp | 4 + src/qt/walletmodel.cpp | 4 + src/qt/walletmodel.h | 1 + src/rpc/client.cpp | 1 + src/wallet/rpcwallet.cpp | 39 ++++- src/wallet/test/wallet_tests.cpp | 12 -- src/wallet/wallet.cpp | 161 +++++++++++++++++-- src/wallet/wallet.h | 38 ++++- src/wallet/walletdb.cpp | 16 ++ src/wallet/walletdb.h | 1 + test/functional/test_framework/test_node.py | 3 +- test/functional/test_runner.py | 1 + test/functional/timing.json | 8 + test/functional/wallet_disableprivatekeys.py | 35 ++++ 16 files changed, 299 insertions(+), 31 deletions(-) create mode 100755 test/functional/wallet_disableprivatekeys.py diff --git a/src/interfaces/wallet.cpp b/src/interfaces/wallet.cpp index ed8c2e3ea..58ccd672a 100644 --- a/src/interfaces/wallet.cpp +++ b/src/interfaces/wallet.cpp @@ -423,6 +423,9 @@ namespace { 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/interfaces/wallet.h b/src/interfaces/wallet.h index 81779a0cc..8ca18fdea 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -210,6 +210,9 @@ class Wallet { // 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/qt/receivecoinsdialog.cpp b/src/qt/receivecoinsdialog.cpp index 046b83882..de28560b3 100644 --- a/src/qt/receivecoinsdialog.cpp +++ b/src/qt/receivecoinsdialog.cpp @@ -105,6 +105,10 @@ void ReceiveCoinsDialog::setModel(WalletModel *_model) { // 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.cpp b/src/qt/walletmodel.cpp index cd965cd93..7b6d51ffb 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -445,6 +445,10 @@ bool WalletModel::isWalletEnabled() { 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/qt/walletmodel.h b/src/qt/walletmodel.h index 63d9c3036..69b3d8bfa 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -183,6 +183,7 @@ class WalletModel : public QObject { 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/rpc/client.cpp b/src/rpc/client.cpp index f228c97c9..0735be70b 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -157,6 +157,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { {"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 index caf881851..07aa1d1f0 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -251,6 +251,11 @@ static UniValue getnewaddress(const Config &config, 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 @@ -349,6 +354,11 @@ static UniValue getrawchangeaddress(const Config &config, 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()) { @@ -2861,7 +2871,12 @@ static UniValue keypoolrefill(const Config &config, } */ auto locked_chain = pwallet->chain().lock(); - LOCK(pwallet->cs_wallet); + 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 // -keypool @@ -3390,6 +3405,9 @@ static UniValue getwalletinfo(const Config &config, "/kB\n" " \"hdchainid\": \"\" (string) " "the Hash160 of the HD master pubkey\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", "") + @@ -3437,6 +3455,8 @@ static UniValue getwalletinfo(const Config &config, obj.push_back(Pair("hdaccounts", accounts)); obj.pushKV("unlocked_until", pwallet->nRelockTime); obj.pushKV("paytxfee", ValueFromAmount(payTxFee.GetFeePerK())); + obj.pushKV("private_keys_enabled", + !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); return obj; } @@ -3527,14 +3547,18 @@ static UniValue loadwallet(const Config &config, 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. \"password\" (string, optional) The password for the encrypted wallet or blank if empty. " + "3. 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 " @@ -3554,6 +3578,10 @@ static UniValue createwallet(const Config &config, std::string password = request.params[1].get_str(); std::string error; std::string warning; + bool disable_privatekeys = false; + if (!request.params[1].isNull()) { + disable_privatekeys = request.params[1].get_bool(); + } WalletLocation location(wallet_name); @@ -3576,7 +3604,8 @@ static UniValue createwallet(const Config &config, std::shared_ptr const wallet = CWallet::CreateWalletFromFile( chainParams, *g_rpc_interfaces->chain, - location, passphrase, words, use_bls); + location, passphrase, words, use_bls, + (disable_privatekeys ? uint64_t(WALLET_FLAG_DISABLE_PRIVATE_KEYS) : 0)); if (!wallet) { throw JSONRPCError(RPC_WALLET_ERROR, "Wallet creation failed."); } @@ -4439,9 +4468,9 @@ static const ContextFreeRPCCommand commands[] = { { "wallet", "abandontransaction", abandontransaction, {"txid"} }, { "wallet", "addmultisigaddress", addmultisigaddress, {"nrequired","keys","label|account"} }, { "wallet", "backupwallet", backupwallet, {"destination"} }, - { "wallet", "createwallet", createwallet, {"wallet_name"} }, { "wallet", "getlabeladdress", getlabeladdress, {"label"} }, { "wallet", "getaddressesbylabels", getaddressesbylabels, {} }, + { "wallet", "createwallet", createwallet, {"wallet_name", "password", "disable_private_keys"} }, { "wallet", "getbalance", getbalance, {"account","minconf","include_watchonly"} }, { "wallet", "getnewaddress", getnewaddress, {"label|account"} }, { "wallet", "getrawchangeaddress", getrawchangeaddress, {} }, diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index a1326f94f..16407940f 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -867,15 +867,3 @@ TEST_CASE("ListCoins") { BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); } -BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) { - auto chain = interfaces::MakeChain(); - std::shared_ptr wallet = std::make_shared( - Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); - wallet->SetMinVersion(FEATURE_LATEST); - 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.cpp b/src/wallet/wallet.cpp index fd08661bc..6352c94f0 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -229,6 +229,7 @@ const CWalletTx *CWallet::GetWalletTx(const TxId &txid) const { } std::tuple CWallet::GenerateNewKey(CHDChain& hdChainDec, bool internal) { + assert(!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); // mapKeyMetadata AssertLockHeld(cs_wallet); @@ -1556,6 +1557,97 @@ Amount CWallet::GetChange(const CTransaction &tx) const { return nChange; } +#ifdef NEEDED +CPubKey CWallet::GenerateNewSeed() { + assert(!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); + CKey key; + key.MakeNewKey(true); + return DeriveNewSeed(key); +} + +CPubKey CWallet::DeriveNewSeed(const CKey &key) { + int64_t nCreationTime = GetTime(); + CKeyMetadata metadata(nCreationTime); + + // Calculate the seed + CPubKey seed = key.GetPubKey(); + assert(key.VerifyPubKey(seed)); + + // Set the hd keypath to "s" -> Seed, refers the seed to itself + metadata.hdKeypath = "s"; + metadata.hd_seed_id = seed.GetID(); + + LOCK(cs_wallet); + + // mem store the metadata + mapKeyMetadata[seed.GetID()] = metadata; + + // Write the key&metadata to the database + if (!AddKeyPubKey(key, seed)) { + throw std::runtime_error(std::string(__func__) + + ": AddKeyPubKey failed"); + } + + return seed; +} + +void CWallet::SetHDSeed(const CPubKey &seed) { + LOCK(cs_wallet); + + // Store the keyid (hash160) together with the child index counter in the + // database as a hdchain object. + CHDChain newHdChain; + newHdChain.nVersion = CanSupportFeature(FEATURE_HD_SPLIT) + ? CHDChain::VERSION_HD_CHAIN_SPLIT + : CHDChain::VERSION_HD_BASE; + newHdChain.seed_id = seed.GetID(); + SetHDChain(newHdChain, false); +} + +void CWallet::SetHDChain(const CHDChain &chain, bool memonly) { + LOCK(cs_wallet); + if (!memonly && !WalletBatch(*database).WriteHDChain(chain)) { + throw std::runtime_error(std::string(__func__) + + ": writing chain failed"); + } + + hdChain = chain; +} + +bool CWallet::IsHDEnabled() const { + return !hdChain.seed_id.IsNull(); +} +#endif + +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; @@ -3119,6 +3211,12 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock &locked_chainIn, // 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); @@ -3863,12 +3961,14 @@ DBErrors CWallet::LoadWallet(bool &fFirstRunRet) { } } - // This wallet is in its first run if all of these are empty - fFirstRunRet = mapKeys.empty() && mapScripts.empty() && - mapWatchKeys.empty() && setWatchOnly.empty() && - mapHdPubKeys.empty() && mapBLSPubKeys.empty() && - mapKeyMetadata.empty(); - + { + LOCK(cs_KeyStore); + // This wallet is in its first run if all of these are empty + fFirstRunRet = mapKeys.empty() && mapScripts.empty() && + mapWatchKeys.empty() && setWatchOnly.empty() && + mapHdPubKeys.empty() && mapBLSPubKeys.empty() && + mapKeyMetadata.empty() && !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + } if (nLoadWalletRet != DBErrors::LOAD_OK) { return nLoadWalletRet; } @@ -4008,6 +4108,9 @@ const std::string &CWallet::GetLabelName(const CScript &scriptPubKey) const { * 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); @@ -4130,6 +4233,9 @@ void CWallet::LoadKeyPool(int64_t nIndex, const CKeyPool &keypool) { } bool CWallet::TopUpKeyPool(unsigned int kpSize) { + if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + return false; + } LOCK(cs_wallet); if (IsLocked()) { @@ -4321,6 +4427,10 @@ void CWallet::ReturnKey(int64_t nIndex, bool fInternal, const CPubKey &pubkey) { } bool CWallet::GetKeyFromPool(CPubKey &result, bool internal) { + if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + return false; + } + CKeyPool keypool; LOCK(cs_wallet); int64_t nIndex; @@ -4940,7 +5050,7 @@ CWallet::LoadWalletFromFile(const CChainParams &chainParams, const WalletLocation &location) { auto ret = CreateWalletFromFile(chainParams, chain, location, SecureString(""), - std::vector(), false); + std::vector(), false, 0); return ret; } @@ -4949,8 +5059,8 @@ CWallet::CreateWalletFromFile(const CChainParams &chainParams, interfaces::Chain &chain, const WalletLocation &location, const SecureString& walletPassphrase, - const mnemonic::WordList& words, bool use_bls - ) { + const mnemonic::WordList& words, bool use_bls, + uint64_t wallet_creation_flags) { // Needed to restore wallet transaction meta data after -zapwallettxes std::vector vWtx; @@ -5040,7 +5150,12 @@ CWallet::CreateWalletFromFile(const CChainParams &chainParams, mnemonic::WordList cwords; std::vector hashWords; - // Create MasterKey and store to mapMasterKeys + if ((wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + // selective allow to set flags + walletInstance->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + } + + // Create MasterKey and store to mapMasterKeys LogPrintf("%s : Creating MasterKey for wallet\n", __func__); walletInstance->CreateMasteyKey(walletPassphrase, _vMasterKey); @@ -5055,9 +5170,12 @@ CWallet::CreateWalletFromFile(const CChainParams &chainParams, walletInstance->EncryptHDWallet(_vMasterKey, cwords, hashWords); walletInstance->Unlock(walletPassphrase); + + + LogPrintf("%s : Generating HD keys, please don't interrupt til' done\n", __func__); // 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; } @@ -5079,12 +5197,33 @@ CWallet::CreateWalletFromFile(const CChainParams &chainParams, } } + + + if (!fFirstRun) { + + 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()) { + InitWarning(strprintf(_("Warning: Private keys detected in wallet " + "{%s} with disabled private keys"), + walletFile)); + + } + } + // If Upgrading from Non-BLS wallet, Remove Old Key pool and Replace // Maybe should be part of Wallet upgrade - check LATER XXX // Currently will skip if wallet is locked.... if (walletInstance->fUpgradeBLSKeys && !walletInstance->HasBLSKeys()) { + { // Should Add BLS Account to hdChain here.... and then re save CHDChain hdChainCrypted; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 68fd2d0e2..a2d656687 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -95,6 +95,18 @@ constexpr OutputType DEFAULT_ADDRESS_TYPE{OutputType::LEGACY}; //! 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 { @@ -731,6 +743,7 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface { int64_t m_max_keypool_index = 0; std::map m_pool_key_to_index; std::map m_pool_blskey_to_index; + std::atomic m_wallet_flags{0}; int64_t nTimeFirstKey = 0; @@ -1283,8 +1296,8 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface { interfaces::Chain &chain, const WalletLocation &location, const SecureString& walletPassphrase, - const std::vector& words, bool use_bls - ); + const std::vector& words, bool use_bls, + uint64_t wallet_creation_flags = 0); /** * Wallet post-init setup @@ -1328,6 +1341,27 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface { bool OutputEligibleForSpending(const COutput &output, const int nConfMine, const int nConfTheirs, const uint64_t nMaxAncestors) const; + /** + * Same as LearnRelatedScripts, but when the OutputType is not known (and + * 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/walletdb.cpp b/src/wallet/walletdb.cpp index 1a51b28e5..06d865902 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -387,6 +387,14 @@ bool ReadKeyValue(CWallet *pwallet, CDataStream &ssKey, CDataStream &ssValue, strErr = "Error reading wallet database: LoadHDPubKey failed"; return false; } + } 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; + } } } catch (...) { return false; @@ -449,6 +457,10 @@ DBErrors WalletBatch::LoadWallet(CWallet *pwallet) { // 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 @@ -746,6 +758,10 @@ bool WalletBatch::EraseDestData(const CTxDestination &address, std::make_pair(EncodeDestination(address), key))); } +bool WalletBatch::WriteWalletFlags(const uint64_t flags) { + return WriteIC(std::string("flags"), flags); +} + bool WalletBatch::TxnBegin() { return m_batch.TxnBegin(); } diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 502f87ed0..1906b800d 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -187,6 +187,7 @@ class WalletBatch { bool WriteCryptedHDChain(const CHDChain& chain); bool WriteHDPubKey(const CHDPubKey& hdPubKey, const CKeyMetadata& keyMeta); + bool WriteWalletFlags(const uint64_t flags); //! Begin a new transaction bool TxnBegin(); //! Commit current transaction diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 874185081..f8c9b1d2a 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -388,7 +388,8 @@ def batch(self, requests): def send_cli(self, command, *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 index dcc182ddd..fea995b47 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -76,6 +76,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 index b13859c4f..a6b2994b8 100644 --- a/test/functional/timing.json +++ b/test/functional/timing.json @@ -335,6 +335,14 @@ "name": "wallet_disable.py", "time": 1 }, + { + "name": "wallet_disableprivatekeys.py", + "time": 0 + }, + { + "name": "wallet_disableprivatekeys.py --usecli", + "time": 0 + }, { "name": "wallet_dump.py", "time": 13 diff --git a/test/functional/wallet_disableprivatekeys.py b/test/functional/wallet_disableprivatekeys.py new file mode 100755 index 000000000..69b27cc07 --- /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()