dash/test/functional/wallet_upgradetohd.py
Konstantin Akimov 8dba6559f6
feat: enable HD wallets by default (#5807)
## Issue being fixed or feature implemented
HD wallets are old-existsing feature, appeared in Dash years ago, but
enabling HD wallets is not trivial task that requires multiple steps and
command line/rpc calls.
Let's have them enabled by default.

## What was done?
- HD wallets are enabled by default. Currently behavior `dashd`,
`dash-qt` are similar to run with option `-usehd=1`
- the rpc `upgradewallet` do not let to upgrade from non-HD wallet to HD
wallet to don't encourage user use non-crypted wallets (postponed till
v21)
- the initialization of ScriptPubKey is updated to be sure that encypted
HD seed is never written on disk (if passphrase is provided)
- enabled and dashified a script `wallet_upgradewallet.py` which test
compatibility between different versions of wallet


## What is not done?
- wallet tool still does not support passhprase, HD seed can appear on
disk
- there's no dialog that show user a mnemonic phrase and encourage him
to make a paper backup
 
Before removing a command line 'usehd' (backport bitcoin#11250) need to
make at least one major release for fail-over option (if someone wish to
use non-HD wallets only).


## How Has This Been Tested?
Run unit and functional tests.
Enabled new functional test `wallet_upgradewallet.py` that has been
backported long time ago but waited this PR to be enabled.

## Breaking Changes
HD wallets are created by default. 

## Checklist:
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have added or updated relevant unit/integration/functional/e2e
tests
- [ ] I have made corresponding changes to the documentation
- [x] I have assigned this pull request to a milestone

---------

Co-authored-by: UdjinM6 <UdjinM6@users.noreply.github.com>
2024-02-09 11:36:14 -06:00

212 lines
9.6 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
wallet_upgradetohd.py
Test upgrade to a Hierarchical Deterministic wallet via upgradetohd rpc
"""
import shutil
import os
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)
class WalletUpgradeToHDTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.extra_args = [['-usehd=0']]
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def setup_network(self):
self.add_nodes(self.num_nodes, self.extra_args)
self.start_nodes()
self.import_deterministic_coinbase_privkeys()
def recover_non_hd(self):
self.log.info("Recover non-HD wallet to check different upgrade paths")
node = self.nodes[0]
self.stop_node(0)
shutil.copyfile(os.path.join(node.datadir, "non_hd.bak"), os.path.join(node.datadir, self.chain, self.default_wallet_name, self.wallet_data_filename))
self.start_node(0)
assert 'hdchainid' not in node.getwalletinfo()
def run_test(self):
node = self.nodes[0]
node.backupwallet(os.path.join(node.datadir, "non_hd.bak"))
self.log.info("No mnemonic, no mnemonic passphrase, no wallet passphrase")
assert 'hdchainid' not in node.getwalletinfo()
balance_before = node.getbalance()
assert node.upgradetohd()
mnemonic = node.dumphdinfo()['mnemonic']
chainid = node.getwalletinfo()['hdchainid']
assert_equal(len(chainid), 64)
assert_equal(balance_before, node.getbalance())
self.log.info("Should be spendable and should use correct paths")
for i in range(5):
txid = node.sendtoaddress(node.getnewaddress(), 1)
outs = node.decoderawtransaction(node.gettransaction(txid)['hex'])['vout']
for out in outs:
if out['value'] == 1:
keypath = node.getaddressinfo(out['scriptPubKey']['addresses'][0])['hdkeypath']
assert_equal(keypath, "m/44'/1'/0'/0/%d" % i)
else:
keypath = node.getaddressinfo(out['scriptPubKey']['addresses'][0])['hdkeypath']
assert_equal(keypath, "m/44'/1'/0'/1/%d" % i)
self.bump_mocktime(1)
node.generate(1)
self.log.info("Should no longer be able to start it with HD disabled")
self.stop_node(0)
node.assert_start_raises_init_error(['-usehd=0'], "Error: Error loading %s: You can't disable HD on an already existing HD wallet" % self.default_wallet_name)
self.extra_args = []
self.start_node(0, [])
balance_after = node.getbalance()
self.recover_non_hd()
# We spent some coins from non-HD keys to HD ones earlier
balance_non_HD = node.getbalance()
assert balance_before != balance_non_HD
self.log.info("No mnemonic, no mnemonic passphrase, no wallet passphrase, should result in completely different keys")
assert node.upgradetohd()
assert mnemonic != node.dumphdinfo()['mnemonic']
assert chainid != node.getwalletinfo()['hdchainid']
assert_equal(balance_non_HD, node.getbalance())
node.keypoolrefill(5)
node.rescanblockchain()
# Completely different keys, no HD coins should be recovered
assert_equal(balance_non_HD, node.getbalance())
self.recover_non_hd()
self.log.info("No mnemonic, no mnemonic passphrase, no wallet passphrase, should result in completely different keys")
self.restart_node(0, extra_args=['-keypool=10'])
assert node.upgradetohd("", "", "", True)
# Completely different keys, no HD coins should be recovered
assert mnemonic != node.dumphdinfo()['mnemonic']
assert chainid != node.getwalletinfo()['hdchainid']
assert_equal(balance_non_HD, node.getbalance())
self.recover_non_hd()
self.log.info("Same mnemonic, another mnemonic passphrase, no wallet passphrase, should result in a different set of keys")
new_mnemonic_passphrase = "somewords"
assert node.upgradetohd(mnemonic, new_mnemonic_passphrase)
assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
new_chainid = node.getwalletinfo()['hdchainid']
assert chainid != new_chainid
assert_equal(balance_non_HD, node.getbalance())
node.keypoolrefill(5)
node.rescanblockchain()
# A different set of keys, no HD coins should be recovered
new_addresses = (node.getnewaddress(), node.getrawchangeaddress())
assert_equal(balance_non_HD, node.getbalance())
self.recover_non_hd()
self.log.info("Same mnemonic, another mnemonic passphrase, no wallet passphrase, should result in a different set of keys (again)")
assert node.upgradetohd(mnemonic, new_mnemonic_passphrase)
assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
assert_equal(new_chainid, node.getwalletinfo()['hdchainid'])
assert_equal(balance_non_HD, node.getbalance())
node.keypoolrefill(5)
node.rescanblockchain()
# A different set of keys, no HD coins should be recovered, keys should be the same as they were the previous time
assert_equal(new_addresses, (node.getnewaddress(), node.getrawchangeaddress()))
assert_equal(balance_non_HD, node.getbalance())
self.recover_non_hd()
self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, should recover all coins after rescan")
assert node.upgradetohd(mnemonic)
assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
node.keypoolrefill(5)
assert balance_after != node.getbalance()
node.rescanblockchain()
assert_equal(balance_after, node.getbalance())
self.recover_non_hd()
self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, large enough keepool, should recover all coins with no extra rescan")
self.restart_node(0, extra_args=['-keypool=10'])
assert node.upgradetohd(mnemonic)
assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
# All coins should be recovered
assert_equal(balance_after, node.getbalance())
self.recover_non_hd()
self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, large enough keepool, rescan is skipped initially, should recover all coins after rescanblockchain")
self.restart_node(0, extra_args=['-keypool=10'])
assert node.upgradetohd(mnemonic, "", "", False)
assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
assert balance_after != node.getbalance()
node.rescanblockchain()
# All coins should be recovered
assert_equal(balance_after, node.getbalance())
self.recover_non_hd()
self.log.info("Same mnemonic, same mnemonic passphrase, encrypt wallet on upgrade, should recover all coins after rescan")
walletpass = "111pass222"
assert node.upgradetohd(mnemonic, "", walletpass)
node.stop()
node.wait_until_stopped()
self.start_node(0, extra_args=['-rescan'])
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)
node.walletpassphrase(walletpass, 100)
assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
# Note: wallet encryption results in additional keypool topup,
# so we can't compare new balance to balance_non_HD here,
# assert_equal(balance_non_HD, node.getbalance()) # won't work
assert balance_non_HD != node.getbalance()
node.keypoolrefill(4)
node.rescanblockchain()
# All coins should be recovered
assert_equal(balance_after, node.getbalance())
self.recover_non_hd()
self.log.info("Same mnemonic, same mnemonic passphrase, encrypt wallet first, should recover all coins on upgrade after rescan")
walletpass = "111pass222"
node.encryptwallet(walletpass)
node.stop()
node.wait_until_stopped()
self.start_node(0, extra_args=['-rescan'])
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.upgradetohd, mnemonic)
assert_raises_rpc_error(-14, "The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
assert node.upgradetohd(mnemonic, "", walletpass)
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)
node.walletpassphrase(walletpass, 100)
assert_equal(mnemonic, node.dumphdinfo()['mnemonic'])
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
# Note: wallet encryption results in additional keypool topup,
# so we can't compare new balance to balance_non_HD here,
# assert_equal(balance_non_HD, node.getbalance()) # won't work
assert balance_non_HD != node.getbalance()
node.keypoolrefill(4)
node.rescanblockchain()
# All coins should be recovered
assert_equal(balance_after, node.getbalance())
if __name__ == '__main__':
WalletUpgradeToHDTest().main ()