dash/test/functional/wallet_upgradetohd.py
fanquake 5a6b8b6b1f
partial Merge bitcoin/bitcoin#27053: wallet: reuse change dest when re-creating TX with avoidpartialspends
14b4921a91920df25b19ff420bfe2bff8c56f71e wallet: reuse change dest when recreating TX with avoidpartialspends (Matthew Zipkin)

Pull request description:

  Closes https://github.com/bitcoin/bitcoin/issues/27051

  When the wallet creates a transaction internally, it will also create an alternative that spends using destination groups and see if the fee difference is negligible. If it costs the user the same to send the grouped version, we send it (even if the user has `avoidpartialspends` set to `false` which is default). This patch ensures that the second transaction creation attempt re-uses the change destination selected by the first attempt. Otherwise, the first change address remains reserved, will not be used in the second attempt, and then will never be used by the wallet, leaving gaps in the BIP44 chain.

  If the user had `avoidpartialspends` set to true, there is no second version of the created transaction and the change addresses are not affected.

  I believe this behavior was introduced in https://github.com/bitcoin/bitcoin/pull/14582

ACKs for top commit:
  achow101:
    ACK 14b4921a91920df25b19ff420bfe2bff8c56f71e

Tree-SHA512: a3d56f251ff4b333fc11325f30d05513e34ab0a2eb703fadd0ad98d167ae074493df1a24068298336c6ed2da6b31aa2befa490bc790bbc260ed357c8f2397659
2024-03-18 16:30:45 +07: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 ()