mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 12:02:48 +01:00
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:
parent
a8cfd70587
commit
266aefc544
@ -70,6 +70,7 @@ namespace {
|
||||
const QStringList historyFilter = QStringList()
|
||||
<< "importprivkey"
|
||||
<< "importmulti"
|
||||
<< "sethdseed"
|
||||
<< "signmessagewithprivkey"
|
||||
<< "signrawtransactionwithkey"
|
||||
<< "upgradetohd"
|
||||
|
@ -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" },
|
||||
|
@ -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"} },
|
||||
|
@ -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 ()
|
||||
|
Loading…
Reference in New Issue
Block a user