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,99 @@ return ret; } +static 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 +4491,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 @@ -1263,6 +1263,11 @@ /* 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 @@ -1542,7 +1542,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); 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()