feat: sethdseed rpc added. Based on bitcoin#12560 and the newest related changes

The key difference between bitcoin's and dash's implementation that sethdseed
does not update existing seed for wallet. Seed can be set only once.
It behave similarly to `upgradetohd` rpc, but since v20.1 all wallets are HD and
the name `upgradetohd` is not relevant more.
This commit is contained in:
Konstantin Akimov 2024-04-17 16:49:26 +07:00
parent a8cfd70587
commit 266aefc544
No known key found for this signature in database
GPG Key ID: 2176C4A5D01EA524
4 changed files with 227 additions and 3 deletions

View File

@ -70,6 +70,7 @@ namespace {
const QStringList historyFilter = QStringList()
<< "importprivkey"
<< "importmulti"
<< "sethdseed"
<< "signmessagewithprivkey"
<< "signrawtransactionwithkey"
<< "upgradetohd"

View File

@ -45,6 +45,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendtoaddress", 7, "conf_target" },
{ "sendtoaddress", 9, "avoid_reuse" },
{ "settxfee", 0, "amount" },
{ "sethdseed", 0, "newkeypool" },
{ "getreceivedbyaddress", 1, "minconf" },
{ "getreceivedbyaddress", 2, "addlocked" },
{ "getreceivedbylabel", 1, "minconf" },

View File

@ -83,14 +83,12 @@ static bool ParseIncludeWatchonly(const UniValue& include_watchonly, const CWall
/** Checks if a CKey is in the given CWallet compressed or otherwise*/
/*
bool HaveKey(const SigningProvider& wallet, const CKey& key)
{
CKey key2;
key2.Set(key.begin(), key.end(), !key.IsCompressed());
return wallet.HaveKey(key.GetPubKey().GetID()) || wallet.HaveKey(key2.GetPubKey().GetID());
}
*/
bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& wallet_name)
{
@ -2976,7 +2974,7 @@ static RPCHelpMan createwallet()
{
{"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name for the new wallet. If this is a path, the wallet will be created at the path location."},
{"disable_private_keys", RPCArg::Type::BOOL, /* default */ "false", "Disable the possibility of private keys (only watchonlys are possible in this mode)."},
{"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using upgradetohd."},
{"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using upgradetohd (by mnemonic) or sethdseed (WIF private key)."},
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."},
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."},
{"descriptors", RPCArg::Type::BOOL, /* default */ "false", "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation"},
@ -4272,6 +4270,85 @@ static RPCHelpMan send()
};
}
static RPCHelpMan sethdseed()
{
return RPCHelpMan{"sethdseed",
"\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 can not be updated to a new HD seed.\n"
"\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the HD wallet seed." +
HELP_REQUIRING_PASSPHRASE,
{
{"newkeypool", RPCArg::Type::BOOL, /* 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 from the existing keypool will be used until it has been depleted."},
{"seed", RPCArg::Type::STR, /* default */ "random seed", "The WIF private key to use as the new HD seed.\n"
"The seed value can be retrieved using the dumpwallet command. It is the private key marked hdseed=1"},
},
RPCResult{RPCResult::Type::NONE, "", ""},
RPCExamples{
HelpExampleCli("sethdseed", "")
+ HelpExampleCli("sethdseed", "false")
+ HelpExampleCli("sethdseed", "true \"wifkey\"")
+ HelpExampleRpc("sethdseed", "true, \"wifkey\"")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
// TODO: add mnemonic feature to sethdseed or remove it in favour of upgradetohd
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true);
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");
}
LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore);
// Do not do anything to non-HD wallets
if (!pwallet->CanSupportFeature(FEATURE_HD)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set an HD seed on a non-HD wallet. Use the upgradewallet RPC in order to upgrade a non-HD wallet to HD");
}
if (pwallet->IsHDEnabled()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed. The wallet already has a seed");
}
EnsureWalletIsUnlocked(pwallet);
bool flush_key_pool = true;
if (!request.params[0].isNull()) {
flush_key_pool = request.params[0].get_bool();
}
if (request.params[1].isNull()) {
spk_man.GenerateNewHDChain("", "");
} else {
CKey key = DecodeSecret(request.params[1].get_str());
if (!key.IsValid()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
}
if (HaveKey(spk_man, key)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key (either as an HD seed or as a loose private key)");
}
CHDChain newHdChain;
if (!newHdChain.SetSeed(SecureVector(key.begin(), key.end()), true)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key: SetSeed failed");
}
if (!spk_man.SetHDChainSingle(newHdChain, false)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key: SetHDChainSingle failed");
}
// add default account
newHdChain.AddAccount();
}
if (flush_key_pool) spk_man.NewKeyPool();
return NullUniValue;
},
};
}
RPCHelpMan walletprocesspsbt()
{
return RPCHelpMan{"walletprocesspsbt",
@ -4576,6 +4653,7 @@ static const CRPCCommand commands[] =
{ "wallet", "send", &send, {"outputs","conf_target","estimate_mode","options"} },
{ "wallet", "sendmany", &sendmany, {"dummy","amounts","minconf","addlocked","comment","subtractfeefrom","use_is","use_cj","conf_target","estimate_mode"} },
{ "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","use_is","use_cj","conf_target","estimate_mode", "avoid_reuse"} },
{ "wallet", "sethdseed", &sethdseed, {"newkeypool","seed"} },
{ "wallet", "setcoinjoinrounds", &setcoinjoinrounds, {"rounds"} },
{ "wallet", "setcoinjoinamount", &setcoinjoinamount, {"amount"} },
{ "wallet", "setlabel", &setlabel, {"address","label"} },

View File

@ -11,6 +11,7 @@ from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)
class WalletHDTest(BitcoinTestFramework):
@ -137,5 +138,148 @@ class WalletHDTest(BitcoinTestFramework):
assert_equal(keypath[0:13], "m/44'/1'/0'/1")
if not self.options.descriptors:
# NOTE: sethdseed can't replace existing seed in Dash Core
# though bitcoin lets to do it. Therefore this functional test
# are not the same with bitcoin's
# Generate a new HD seed on node 1 and make sure it is set
self.nodes[1].createwallet(wallet_name='wallet_new_seed', blank=True)
wallet_new_seed = self.nodes[1].get_wallet_rpc('wallet_new_seed')
assert 'hdchainid' not in wallet_new_seed.getwalletinfo()
wallet_new_seed.sethdseed()
new_masterkeyid = wallet_new_seed.getwalletinfo()['hdchainid']
addr = wallet_new_seed.getnewaddress()
# Make sure the new address is the first from the keypool
assert_equal(wallet_new_seed.getaddressinfo(addr)['hdkeypath'], "m/44'/1'/0'/0/1")
wallet_new_seed.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())
assert_raises_rpc_error(-4, "Cannot set a HD seed. The wallet already has a seed", wallet_new_seed.sethdseed, False, new_seed)
self.nodes[1].createwallet(wallet_name='wallet_imported_seed', blank=True)
wallet_imported_seed = self.nodes[1].get_wallet_rpc('wallet_imported_seed')
wallet_imported_seed.sethdseed(False, new_seed)
new_masterkeyid = wallet_imported_seed.getwalletinfo()['hdchainid']
addr = wallet_imported_seed.getnewaddress()
assert_equal(new_masterkeyid, wallet_imported_seed.getaddressinfo(addr)['hdchainid'])
# Make sure the new address continues previous keypool
assert_equal(wallet_imported_seed.getaddressinfo(addr)['hdkeypath'], "m/44'/1'/0'/0/0")
# Check that the next address is from the new seed
wallet_imported_seed.keypoolrefill(1)
next_addr = wallet_imported_seed.getnewaddress()
assert_equal(new_masterkeyid, wallet_imported_seed.getaddressinfo(next_addr)['hdchainid'])
# Make sure the new address is not from previous keypool
assert_equal(wallet_imported_seed.getaddressinfo(next_addr)['hdkeypath'], "m/44'/1'/0'/0/1")
assert next_addr != addr
self.nodes[1].createwallet(wallet_name='wallet_no_seed', blank=True)
wallet_no_seed = self.nodes[1].get_wallet_rpc('wallet_no_seed')
wallet_no_seed.importprivkey(non_hd_key)
# 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", wallet_no_seed.sethdseed, False, "not_wif")
assert_raises_rpc_error(-1, "JSON value is not a boolean as expected", wallet_no_seed.sethdseed, "Not_bool")
assert_raises_rpc_error(-1, "JSON value is not a string as expected", wallet_no_seed.sethdseed, False, True)
assert_raises_rpc_error(-5, "Already have this key", wallet_no_seed.sethdseed, False, non_hd_key)
self.log.info('Test sethdseed restoring with keys outside of the initial keypool')
self.nodes[0].generate(10)
# Restart node 1 with keypool of 3 and a different wallet
self.nodes[1].createwallet(wallet_name='origin', blank=True)
self.restart_node(1, extra_args=['-keypool=3', '-wallet=origin'])
self.connect_nodes(0, 1)
# sethdseed restoring and seeing txs to addresses out of the keypool
origin_rpc = self.nodes[1].get_wallet_rpc('origin')
seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress())
origin_rpc.sethdseed(True, seed)
self.nodes[1].createwallet(wallet_name='restore', blank=True)
restore_rpc = self.nodes[1].get_wallet_rpc('restore')
restore_rpc.sethdseed(True, seed) # Set to be the same seed as origin_rpc
self.nodes[1].createwallet(wallet_name='restore2', blank=True)
restore2_rpc = self.nodes[1].get_wallet_rpc('restore2')
restore2_rpc.sethdseed(True, seed) # Set to be the same seed as origin_rpc
# Check persistence of inactive seed by reloading restore. restore2 is still loaded to test the case where the wallet is not reloaded
restore_rpc.unloadwallet()
self.nodes[1].loadwallet('restore')
restore_rpc = self.nodes[1].get_wallet_rpc('restore')
# Empty origin keypool and get an address that is beyond the initial keypool
origin_rpc.getnewaddress()
origin_rpc.getnewaddress()
last_addr = origin_rpc.getnewaddress() # Last address of initial keypool
addr = origin_rpc.getnewaddress() # First address beyond initial keypool
# Check that the restored seed has last_addr but does not have addr
info = restore_rpc.getaddressinfo(last_addr)
assert_equal(info['ismine'], True)
info = restore_rpc.getaddressinfo(addr)
assert_equal(info['ismine'], False)
info = restore2_rpc.getaddressinfo(last_addr)
assert_equal(info['ismine'], True)
info = restore2_rpc.getaddressinfo(addr)
assert_equal(info['ismine'], False)
# Check that the origin seed has addr
info = origin_rpc.getaddressinfo(addr)
assert_equal(info['ismine'], True)
# Send a transaction to addr, which is out of the initial keypool.
# The wallet that has set a new seed (restore_rpc) should not detect this transaction.
txid = self.nodes[0].sendtoaddress(addr, 1)
origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex'])
self.nodes[0].generate(1)
self.sync_blocks()
origin_rpc.gettransaction(txid)
assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, txid)
out_of_kp_txid = txid
# Send a transaction to last_addr, which is in the initial keypool.
# The wallet that has set a new seed (restore_rpc) should detect this transaction and generate 3 new keys from the initial seed.
# The previous transaction (out_of_kp_txid) should still not be detected as a rescan is required.
txid = self.nodes[0].sendtoaddress(last_addr, 1)
origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex'])
self.nodes[0].generate(1)
self.sync_blocks()
origin_rpc.gettransaction(txid)
restore_rpc.gettransaction(txid)
assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, out_of_kp_txid)
restore2_rpc.gettransaction(txid)
assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore2_rpc.gettransaction, out_of_kp_txid)
# After rescanning, restore_rpc should now see out_of_kp_txid and generate an additional key.
# addr should now be part of restore_rpc and be ismine
restore_rpc.rescanblockchain()
restore_rpc.gettransaction(out_of_kp_txid)
info = restore_rpc.getaddressinfo(addr)
assert_equal(info['ismine'], True)
restore2_rpc.rescanblockchain()
restore2_rpc.gettransaction(out_of_kp_txid)
info = restore2_rpc.getaddressinfo(addr)
assert_equal(info['ismine'], True)
# Check again that 3 keys were derived.
# Empty keypool and get an address that is beyond the initial keypool
origin_rpc.getnewaddress()
origin_rpc.getnewaddress()
last_addr = origin_rpc.getnewaddress()
addr = origin_rpc.getnewaddress()
# Check that the restored seed has last_addr but does not have addr
info = restore_rpc.getaddressinfo(last_addr)
assert_equal(info['ismine'], True)
info = restore_rpc.getaddressinfo(addr)
assert_equal(info['ismine'], False)
info = restore2_rpc.getaddressinfo(last_addr)
assert_equal(info['ismine'], True)
info = restore2_rpc.getaddressinfo(addr)
assert_equal(info['ismine'], False)
if __name__ == '__main__':
WalletHDTest().main ()