diff --git a/src/keystore.h b/src/keystore.h --- a/src/keystore.h +++ b/src/keystore.h @@ -88,4 +88,7 @@ */ CKeyID GetKeyForDestination(const CKeyStore &store, const CTxDestination &dest); +/** Checks if a CKey is in the given CKeyStore compressed or otherwise*/ +bool HaveKey(const CKeyStore &store, const CKey &key); + #endif // BITCOIN_KEYSTORE_H diff --git a/src/keystore.cpp b/src/keystore.cpp --- a/src/keystore.cpp +++ b/src/keystore.cpp @@ -169,3 +169,10 @@ } return CKeyID(); } + +bool HaveKey(const CKeyStore &store, const CKey &key) { + CKey key2; + key2.Set(key.begin(), key.end(), !key.IsCompressed()); + return store.HaveKey(key.GetPubKey().GetID()) || + store.HaveKey(key2.GetPubKey().GetID()); +} diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -66,6 +66,7 @@ // don't add private key handling cmd's to the history const QStringList historyFilter = QStringList() << "importprivkey" << "importmulti" + << "sethdseed" << "signmessagewithprivkey" << "signrawtransactionwithkey" << "walletpassphrase" diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -36,6 +36,7 @@ {"sendtoaddress", 1, "amount"}, {"sendtoaddress", 4, "subtractfeefromamount"}, {"settxfee", 0, "amount"}, + {"sethdseed", 0, "newkeypool"}, {"getreceivedbyaddress", 1, "minconf"}, {"getreceivedbyaccount", 1, "minconf"}, {"getreceivedbylabel", 1, "minconf"}, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4359,6 +4359,97 @@ return ret; } +UniValue sethdseed(const Config &config, const JSONRPCRequest &request) { + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + CWallet *const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() > 2) { + throw std::runtime_error( + "sethdseed ( \"newkeypool\" \"seed\" )\n" + "\nSet or generate a new HD wallet seed. Non-HD wallets will not " + "be upgraded to being a HD wallet. Wallets that are already\n" + "HD will have a new HD seed set so that new keys added to the " + "keypool will be derived from this new seed.\n" + "\nNote that you will need to MAKE A NEW BACKUP of your wallet " + "after setting the HD wallet seed.\n" + + HelpRequiringPassphrase(pwallet) + + "\nArguments:\n" + "1. \"newkeypool\" (boolean, optional, default=true) " + "Whether to flush old unused addresses, including change " + "addresses, from the keypool and regenerate it.\n" + " If true, the next address from " + "getnewaddress and change address from getrawchangeaddress will be " + "from this new seed.\n" + " If false, addresses (including " + "change addresses if the wallet already had HD Chain Split " + "enabled) from the existing\n" + " keypool will be used until it has " + "been depleted.\n" + "2. \"seed\" (string, optional) The WIF private key " + "to use as the new HD seed; if not provided a random seed will be " + "used.\n" + " The seed value can be retrieved " + "using the dumpwallet command. It is the private key marked " + "hdmaster=1\n" + "\nExamples:\n" + + HelpExampleCli("sethdseed", "") + + HelpExampleCli("sethdseed", "false") + + HelpExampleCli("sethdseed", "true \"wifkey\"") + + HelpExampleRpc("sethdseed", "true, \"wifkey\"")); + } + + if (IsInitialBlockDownload()) { + throw JSONRPCError( + RPC_CLIENT_IN_INITIAL_DOWNLOAD, + "Cannot set a new HD seed while still in Initial Block Download"); + } + + LOCK2(cs_main, pwallet->cs_wallet); + + // Do not do anything to non-HD wallets + if (!pwallet->IsHDEnabled()) { + throw JSONRPCError( + RPC_WALLET_ERROR, + "Cannot set a HD seed on a non-HD wallet. Start with " + "-upgradewallet in order to upgrade a non-HD wallet to HD"); + } + + EnsureWalletIsUnlocked(pwallet); + + bool flush_key_pool = true; + if (!request.params[0].isNull()) { + flush_key_pool = request.params[0].get_bool(); + } + + CPubKey master_pub_key; + if (request.params[1].isNull()) { + master_pub_key = pwallet->GenerateNewHDMasterKey(); + } else { + CKey key = DecodeSecret(request.params[1].get_str()); + if (!key.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + "Invalid private key"); + } + + if (HaveKey(*pwallet, key)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + "Already have this key (either as an HD seed or " + "as a loose private key)"); + } + + master_pub_key = pwallet->DeriveNewMasterHDKey(key); + } + + pwallet->SetHDMasterKey(master_pub_key); + if (flush_key_pool) pwallet->NewKeyPool(); + + return NullUniValue; +} + // clang-format off static const ContextFreeRPCCommand commands[] = { // category name actor (function) argNames @@ -4398,6 +4489,7 @@ { "wallet", "lockunspent", lockunspent, {"unlock","transactions"} }, { "wallet", "move", movecmd, {"fromaccount","toaccount","amount","minconf","comment"} }, { "wallet", "rescanblockchain", rescanblockchain, {"start_height", "stop_height"} }, + { "wallet", "sethdseed", sethdseed, {"newkeypool","seed"} }, { "wallet", "sendfrom", sendfrom, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, { "wallet", "sendmany", sendmany, {"fromaccount","amounts","minconf","comment","subtractfeefrom"} }, { "wallet", "sendtoaddress", sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount"} }, diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -85,8 +85,10 @@ // Wallet without a default key written FEATURE_NO_DEFAULT_KEY = 190700, - // HD is optional, use FEATURE_COMPRPUBKEY as latest version - FEATURE_LATEST = FEATURE_COMPRPUBKEY, + // Upgraded to HD SPLIT and can have a pre-split keypool + FEATURE_PRE_SPLIT_KEYPOOL = 169900, + + FEATURE_LATEST = FEATURE_PRE_SPLIT_KEYPOOL, }; enum class OutputType { @@ -102,7 +104,10 @@ public: int64_t nTime; CPubKey vchPubKey; - bool fInternal; // for change outputs + // for change outputs + bool fInternal; + // For keys generated before keypool split upgrade + bool m_pre_split; CKeyPool(); CKeyPool(const CPubKey &vchPubKeyIn, bool internalIn); @@ -129,8 +134,19 @@ */ fInternal = false; } + try { + READWRITE(m_pre_split); + } catch (std::ios_base::failure &) { + /** + * flag as postsplit address if we can't read the m_pre_split + * boolean (this will be the case for any wallet that upgrades + * to HD chain split) + **/ + m_pre_split = false; + } } else { READWRITE(fInternal); + READWRITE(m_pre_split); } } }; @@ -722,6 +738,7 @@ std::set setInternalKeyPool; std::set setExternalKeyPool; + std::set set_pre_split_keypool; int64_t m_max_keypool_index = 0; std::map m_pool_key_to_index; @@ -794,6 +811,7 @@ void LoadKeyPool(int64_t nIndex, const CKeyPool &keypool) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void MarkPreSplitKeys(); // Map from Key ID to key metadata. std::map mapKeyMetadata; @@ -1262,6 +1280,9 @@ /* Generates a new HD master key (will not be activated) */ CPubKey GenerateNewHDMasterKey(); + /* Derives a new HD master key (will not be activated) */ + CPubKey DeriveNewMasterHDKey(const CKey &key); + /** * Set the current HD master key (will reset the chain child index counters) * Sets the master key's version based on the current wallet version (so the diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1550,7 +1550,10 @@ CPubKey CWallet::GenerateNewHDMasterKey() { CKey key; key.MakeNewKey(true); + return DeriveNewMasterHDKey(key); +} +CPubKey CWallet::DeriveNewMasterHDKey(const CKey &key) { int64_t nCreationTime = GetTime(); CKeyMetadata metadata(nCreationTime); @@ -3570,6 +3573,11 @@ } setExternalKeyPool.clear(); + for (int64_t nIndex : set_pre_split_keypool) { + batch.ErasePool(nIndex); + } + set_pre_split_keypool.clear(); + m_pool_key_to_index.clear(); if (!TopUpKeyPool()) { @@ -3583,12 +3591,14 @@ size_t CWallet::KeypoolCountExternalKeys() { // setExternalKeyPool AssertLockHeld(cs_wallet); - return setExternalKeyPool.size(); + return setExternalKeyPool.size() + set_pre_split_keypool.size(); } void CWallet::LoadKeyPool(int64_t nIndex, const CKeyPool &keypool) { AssertLockHeld(cs_wallet); - if (keypool.fInternal) { + if (keypool.m_pre_split) { + set_pre_split_keypool.insert(nIndex); + } else if (keypool.fInternal) { setInternalKeyPool.insert(nIndex); } else { setExternalKeyPool.insert(nIndex); @@ -3661,7 +3671,8 @@ LogPrintf( "keypool added %d keys (%d internal), size=%u (%u internal)\n", missingInternal + missingExternal, missingInternal, - setInternalKeyPool.size() + setExternalKeyPool.size(), + setInternalKeyPool.size() + setExternalKeyPool.size() + + set_pre_split_keypool.size(), setInternalKeyPool.size()); } @@ -3683,7 +3694,9 @@ CanSupportFeature(FEATURE_HD_SPLIT) && fRequestedInternal; std::set &setKeyPool = - fReturningInternal ? setInternalKeyPool : setExternalKeyPool; + set_pre_split_keypool.empty() + ? (fReturningInternal ? setInternalKeyPool : setExternalKeyPool) + : set_pre_split_keypool; // Get the oldest key if (setKeyPool.empty()) { @@ -3702,7 +3715,9 @@ throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); } - if (keypool.fInternal != fReturningInternal) { + // If the key was pre-split keypool, we don't care about what type it is + if (set_pre_split_keypool.size() == 0 && + keypool.fInternal != fReturningInternal) { throw std::runtime_error(std::string(__func__) + ": keypool entry misclassified"); } @@ -3725,6 +3740,8 @@ LOCK(cs_wallet); if (fInternal) { setInternalKeyPool.insert(nIndex); + } else if (!set_pre_split_keypool.empty()) { + set_pre_split_keypool.insert(nIndex); } else { setExternalKeyPool.insert(nIndex); } @@ -3781,6 +3798,11 @@ if (IsHDEnabled() && CanSupportFeature(FEATURE_HD_SPLIT)) { oldestKey = std::max(GetOldestKeyTimeInPool(setInternalKeyPool, batch), oldestKey); + if (!set_pre_split_keypool.empty()) { + oldestKey = + std::max(GetOldestKeyTimeInPool(set_pre_split_keypool, batch), + oldestKey); + } } return oldestKey; @@ -3989,11 +4011,14 @@ AssertLockHeld(cs_wallet); bool internal = setInternalKeyPool.count(keypool_id); if (!internal) { - assert(setExternalKeyPool.count(keypool_id)); + assert(setExternalKeyPool.count(keypool_id) || + set_pre_split_keypool.count(keypool_id)); } std::set *setKeyPool = - internal ? &setInternalKeyPool : &setExternalKeyPool; + internal ? &setInternalKeyPool + : (set_pre_split_keypool.empty() ? &setExternalKeyPool + : &set_pre_split_keypool); auto it = setKeyPool->begin(); WalletBatch batch(*database); @@ -4312,6 +4337,26 @@ error_string); } +void CWallet::MarkPreSplitKeys() { + WalletBatch batch(*database); + for (auto it = setExternalKeyPool.begin(); + it != setExternalKeyPool.end();) { + int64_t index = *it; + CKeyPool keypool; + if (!batch.ReadPool(index, keypool)) { + throw std::runtime_error(std::string(__func__) + + ": read keypool entry failed"); + } + keypool.m_pre_split = true; + if (!batch.WritePool(index, keypool)) { + throw std::runtime_error(std::string(__func__) + + ": writing modified keypool entry failed"); + } + set_pre_split_keypool.insert(index); + it = setExternalKeyPool.erase(it); + } +} + std::shared_ptr CWallet::CreateWalletFromFile(const CChainParams &chainParams, const std::string &name, const fs::path &path) { @@ -4371,6 +4416,7 @@ uiInterface.LoadWallet(walletInstance); + int prev_version = walletInstance->nWalletVersion; if (gArgs.GetBoolArg("-upgradewallet", fFirstRun)) { int nMaxVersion = gArgs.GetArg("-upgradewallet", 0); // The -upgradewallet without argument case @@ -4391,6 +4437,57 @@ walletInstance->SetMaxVersion(nMaxVersion); } + // Upgrade to HD if explicit upgrade + if (gArgs.GetBoolArg("-upgradewallet", false)) { + LOCK(walletInstance->cs_wallet); + + // Do not upgrade versions to any version between HD_SPLIT and + // FEATURE_PRE_SPLIT_KEYPOOL unless already supporting HD_SPLIT + int max_version = walletInstance->nWalletVersion; + if (!walletInstance->CanSupportFeature(FEATURE_HD_SPLIT) && + max_version >= FEATURE_HD_SPLIT && + max_version < FEATURE_PRE_SPLIT_KEYPOOL) { + InitError( + _("Cannot upgrade a non HD split wallet without upgrading to " + "support pre split keypool. Please use -upgradewallet=169900 " + "or -upgradewallet with no version specified.")); + return nullptr; + } + + bool hd_upgrade = false; + bool split_upgrade = false; + if (walletInstance->CanSupportFeature(FEATURE_HD) && + !walletInstance->IsHDEnabled()) { + LogPrintf("Upgrading wallet to HD\n"); + walletInstance->SetMinVersion(FEATURE_HD); + + // generate a new master key + CPubKey masterPubKey = walletInstance->GenerateNewHDMasterKey(); + if (!walletInstance->SetHDMasterKey(masterPubKey)) { + throw std::runtime_error(std::string(__func__) + + ": Storing master key failed"); + } + hd_upgrade = true; + } + // Upgrade to HD chain split if necessary + if (walletInstance->CanSupportFeature(FEATURE_HD_SPLIT)) { + LogPrintf("Upgrading wallet to use HD chain split\n"); + walletInstance->SetMinVersion(FEATURE_PRE_SPLIT_KEYPOOL); + split_upgrade = FEATURE_HD_SPLIT > prev_version; + } + // Mark all keys currently in the keypool as pre-split + if (split_upgrade) { + walletInstance->MarkPreSplitKeys(); + } + // Regenerate the keypool if upgraded to HD + if (hd_upgrade) { + if (!walletInstance->TopUpKeyPool()) { + InitError(_("Unable to generate keys") += "\n"); + return nullptr; + } + } + } + if (fFirstRun) { // Ensure this wallet.dat can only be opened by clients supporting // HD with chain split and expects no default key. @@ -4400,7 +4497,7 @@ walletFile)); return nullptr; } - walletInstance->SetMinVersion(FEATURE_NO_DEFAULT_KEY); + walletInstance->SetMinVersion(FEATURE_LATEST); // Generate a new master key. CPubKey masterPubKey = walletInstance->GenerateNewHDMasterKey(); @@ -4613,12 +4710,14 @@ CKeyPool::CKeyPool() { nTime = GetTime(); fInternal = false; + m_pre_split = false; } CKeyPool::CKeyPool(const CPubKey &vchPubKeyIn, bool internalIn) { nTime = GetTime(); vchPubKey = vchPubKeyIn; fInternal = internalIn; + m_pre_split = false; } CWalletKey::CWalletKey(int64_t nExpires) { diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -8,7 +8,11 @@ import shutil from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, connect_nodes_bi +from test_framework.util import ( + assert_equal, + connect_nodes_bi, + assert_raises_rpc_error +) class WalletHDTest(BitcoinTestFramework): @@ -133,6 +137,54 @@ assert_equal(keypath[0:7], "m/0'/1'") + # Generate a new HD seed on node 1 and make sure it is set + orig_masterkeyid = self.nodes[1].getwalletinfo()['hdmasterkeyid'] + self.nodes[1].sethdseed() + new_masterkeyid = self.nodes[1].getwalletinfo()['hdmasterkeyid'] + assert orig_masterkeyid != new_masterkeyid + addr = self.nodes[1].getnewaddress() + # Make sure the new address is the first from the keypool + assert_equal(self.nodes[1].getaddressinfo( + addr)['hdkeypath'], 'm/0\'/0\'/0\'') + self.nodes[1].keypoolrefill(1) # Fill keypool with 1 key + + # Set a new HD seed on node 1 without flushing the keypool + new_seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress()) + orig_masterkeyid = new_masterkeyid + self.nodes[1].sethdseed(False, new_seed) + new_masterkeyid = self.nodes[1].getwalletinfo()['hdmasterkeyid'] + assert orig_masterkeyid != new_masterkeyid + addr = self.nodes[1].getnewaddress() + assert_equal(orig_masterkeyid, self.nodes[1].getaddressinfo( + addr)['hdmasterkeyid']) + # Make sure the new address continues previous keypool + assert_equal(self.nodes[1].getaddressinfo( + addr)['hdkeypath'], 'm/0\'/0\'/1\'') + + # Check that the next address is from the new seed + self.nodes[1].keypoolrefill(1) + next_addr = self.nodes[1].getnewaddress() + assert_equal(new_masterkeyid, self.nodes[1].getaddressinfo( + next_addr)['hdmasterkeyid']) + # Make sure the new address is not from previous keypool + assert_equal(self.nodes[1].getaddressinfo( + next_addr)['hdkeypath'], 'm/0\'/0\'/0\'') + assert next_addr != addr + + # Sethdseed parameter validity + assert_raises_rpc_error(-1, 'sethdseed', + self.nodes[0].sethdseed, False, new_seed, 0) + assert_raises_rpc_error(-5, "Invalid private key", + self.nodes[1].sethdseed, False, "not_wif") + assert_raises_rpc_error(-1, "JSON value is not a boolean as expected", + self.nodes[1].sethdseed, "Not_bool") + assert_raises_rpc_error(-1, "JSON value is not a string as expected", + self.nodes[1].sethdseed, False, True) + assert_raises_rpc_error(-5, "Already have this key", + self.nodes[1].sethdseed, False, new_seed) + assert_raises_rpc_error(-5, "Already have this key", + self.nodes[1].sethdseed, False, self.nodes[1].dumpprivkey(self.nodes[1].getnewaddress())) + if __name__ == '__main__': WalletHDTest().main()