mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-24 10:17:45 -03:00
c48e788246
5f9c0b6360
wallet: Remove -upgradewallet from dummywallet (MarcoFalke)a314271f08
test: Remove unused wallet.dat (MarcoFalke)bf7635963c
tests: Test specific upgradewallet scenarios and that upgrades work (Andrew Chow)4b418a9dec
test: Add test_framework/bdb.py module for inspecting bdb files (Andrew Chow)092fc43485
tests: Add a sha256sum_file function to util (Andrew Chow)0bd995aa19
wallet: upgrade the CHDChain version number when upgrading to split hd (Andrew Chow)8e32e1c41c
wallet: remove nWalletMaxVersion (Andrew Chow)bd7398cc62
wallet: have ScriptPubKeyMan::Upgrade check against the new version (Andrew Chow)5f720544f3
wallet: Add GetClosestWalletFeature function (Andrew Chow)842ae3842d
wallet: Add utility method for CanSupportFeature (Andrew Chow) Pull request description: This PR cleans up the wallet upgrade mechanism a bit, fixes some probably bugs, and adds more test cases. The `nWalletMaxVersion` member variable has been removed as it made `CanSupportFeature` unintuitive and was causing a couple of bugs. The reason this was introduced originally was to allow a wallet upgrade to only occur when the new feature is first used. While this makes sense for the old `-upgradewallet` option, for an RPC, this does not quite make sense. It's more intuitive for an upgrade to occur if possible if the `upgradewallet` RPC is used as that's an explicit request to upgrade a particular wallet to a newer version. `nWalletMaxVersion` was only relevant for upgrades to `FEATURE_WALLETCRYPT` and `FEATURE_COMPRPUBKEY` both of which are incredibly old features. So for such wallets, the behavior of `upgradewallet` will be that the feature is enabled immediately without the wallet needing to be encrypted at that time (note that `FEATURE_WALLETCRYPT` indicates support for encryption, not that the wallet is encrypted) or for a new key to be generated. `CanSupportFeature` would previously indicate whether we could upgrade to `nWalletMaxVersion` not just whether the current wallet version supported a feature. While this property was being used to determine whether we should upgrade to HD and HD chain split, it was also causing a few bugs. Determining whether we should upgrade to HD or HD chain split is resolved by passing into `ScriptPubKeyMan::Upgrade` the version we are upgrading to and checking against that. By removing `nWalletMaxVersion` we also fix a bug where you could upgrade to HD chain split without the pre-split keypool. `nWalletMaxVersion` was also the version that was being reported by `getwalletinfo` which meant that the version reported was not always consistent across restarts as it depended on whether `upgradewallet` was used. Additionally to make the wallet versions consistent with actually supported versions, instead of just setting the wallet version to whatever is given to `upgradewallet`, we normalize the version number to the closest supported version number. For example, if given 150000, we would store and report 139900. Another bug where CHDChain was not being upgraded to the version supporting HD chain split is also fixed by this PR. Lastly several more tests have been added. Some refactoring to the test was made to make these tests easier. These tests check specific upgrading scenarios, such as from non-HD (version 60000) to HD to pre-split keypool. Although not specifically related to `upgradewallet`, `UpgradeKeyMetadata` is now being tested too. Part of the new tests is checking that the wallet files are identical before and after failed upgrades. To facilitate this, a utility function `sha256sum_file` has been added. Another part of the tests is to examine the wallet file itself to ensure that the records in the wallet.dat file have been correctly modified. So a new `bdb.py` module has been added to deserialize the BDB db of the wallet.dat file. This format isn't explicitly documented anywhere, but the code and comments in BDB's source code in file `dbinc/db_page.h` describe it. This module just dumps all of the fields into a dict. ACKs for top commit: MarcoFalke: approach ACK5f9c0b6360
laanwj: Code review ACK5f9c0b6360
jonatack: ACK5f9c0b6360
, approach seems fine, code review, only skimmed the test changes but they look well done, rebased on current master, debug built and verified the `wallet_upgradewallet.py` test runs green both before and after running `test/get_previous_releases.py -b v0.19.1 v0.18.1 v0.17.2 v0.16.3 v0.15.2` Tree-SHA512: 7c4ebf420850d596a586cb6dd7f2ef39c6477847d12d105fcd362abb07f2a8aa4f7afc5bfd36cbc8b8c72fcdd1de8d2d3f16ad8e8ba736b6f4f31f133fe5feba
346 lines
15 KiB
Python
Executable file
346 lines
15 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2018-2020 The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""upgradewallet RPC functional test
|
|
|
|
Test upgradewallet RPC. Download node binaries:
|
|
|
|
test/get_previous_releases.py -b v0.19.1 v0.18.1 v0.17.2 v0.16.3 v0.15.2
|
|
|
|
Only v0.15.2 and v0.16.3 are required by this test. The others are used in feature_backwards_compatibility.py
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import struct
|
|
|
|
from io import BytesIO
|
|
|
|
from test_framework.bdb import dump_bdb_kv
|
|
from test_framework.messages import deser_compact_size, deser_string
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import (
|
|
assert_equal,
|
|
assert_greater_than,
|
|
assert_is_hex_string,
|
|
assert_raises_rpc_error,
|
|
sha256sum_file,
|
|
)
|
|
|
|
|
|
UPGRADED_KEYMETA_VERSION = 12
|
|
|
|
def deser_keymeta(f):
|
|
ver, create_time = struct.unpack('<Iq', f.read(12))
|
|
kp_str = deser_string(f)
|
|
seed_id = f.read(20)
|
|
fpr = f.read(4)
|
|
path_len = 0
|
|
path = []
|
|
has_key_orig = False
|
|
if ver == UPGRADED_KEYMETA_VERSION:
|
|
path_len = deser_compact_size(f)
|
|
for i in range(0, path_len):
|
|
path.append(struct.unpack('<I', f.read(4))[0])
|
|
has_key_orig = bool(f.read(1))
|
|
return ver, create_time, kp_str, seed_id, fpr, path_len, path, has_key_orig
|
|
|
|
class UpgradeWalletTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.setup_clean_chain = True
|
|
self.num_nodes = 3
|
|
self.extra_args = [
|
|
["-addresstype=bech32", "-keypool=2"], # current wallet version
|
|
["-usehd=1", "-keypool=2"], # v0.16.3 wallet
|
|
["-usehd=0", "-keypool=2"] # v0.15.2 wallet
|
|
]
|
|
self.wallet_names = [self.default_wallet_name, None, None]
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
self.skip_if_no_previous_releases()
|
|
|
|
def setup_network(self):
|
|
self.setup_nodes()
|
|
|
|
def setup_nodes(self):
|
|
self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[
|
|
None,
|
|
160300,
|
|
150200,
|
|
])
|
|
self.start_nodes()
|
|
self.import_deterministic_coinbase_privkeys()
|
|
|
|
def dumb_sync_blocks(self):
|
|
"""
|
|
Little helper to sync older wallets.
|
|
Notice that v0.15.2's regtest is hardforked, so there is
|
|
no sync for it.
|
|
v0.15.2 is only being used to test for version upgrade
|
|
and master hash key presence.
|
|
v0.16.3 is being used to test for version upgrade and balances.
|
|
Further info: https://github.com/bitcoin/bitcoin/pull/18774#discussion_r416967844
|
|
"""
|
|
node_from = self.nodes[0]
|
|
v16_3_node = self.nodes[1]
|
|
to_height = node_from.getblockcount()
|
|
height = self.nodes[1].getblockcount()
|
|
for i in range(height, to_height+1):
|
|
b = node_from.getblock(blockhash=node_from.getblockhash(i), verbose=0)
|
|
v16_3_node.submitblock(b)
|
|
assert_equal(v16_3_node.getblockcount(), to_height)
|
|
|
|
def run_test(self):
|
|
self.nodes[0].generatetoaddress(101, self.nodes[0].getnewaddress())
|
|
self.dumb_sync_blocks()
|
|
# # Sanity check the test framework:
|
|
res = self.nodes[0].getblockchaininfo()
|
|
assert_equal(res['blocks'], 101)
|
|
node_master = self.nodes[0]
|
|
v16_3_node = self.nodes[1]
|
|
v15_2_node = self.nodes[2]
|
|
|
|
# Send coins to old wallets for later conversion checks.
|
|
v16_3_wallet = v16_3_node.get_wallet_rpc('wallet.dat')
|
|
v16_3_address = v16_3_wallet.getnewaddress()
|
|
node_master.generatetoaddress(101, v16_3_address)
|
|
self.dumb_sync_blocks()
|
|
v16_3_balance = v16_3_wallet.getbalance()
|
|
|
|
self.log.info("Test upgradewallet RPC...")
|
|
# Prepare for copying of the older wallet
|
|
node_master_wallet_dir = os.path.join(node_master.datadir, "regtest/wallets", self.default_wallet_name)
|
|
node_master_wallet = os.path.join(node_master_wallet_dir, self.default_wallet_name, self.wallet_data_filename)
|
|
v16_3_wallet = os.path.join(v16_3_node.datadir, "regtest/wallets/wallet.dat")
|
|
v15_2_wallet = os.path.join(v15_2_node.datadir, "regtest/wallet.dat")
|
|
split_hd_wallet = os.path.join(v15_2_node.datadir, "regtest/splithd")
|
|
self.stop_nodes()
|
|
|
|
# Make split hd wallet
|
|
self.start_node(2, ['-usehd=1', '-keypool=2', '-wallet=splithd'])
|
|
self.stop_node(2)
|
|
|
|
def copy_v16():
|
|
node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet()
|
|
# Copy the 0.16.3 wallet to the last Bitcoin Core version and open it:
|
|
shutil.rmtree(node_master_wallet_dir)
|
|
os.mkdir(node_master_wallet_dir)
|
|
shutil.copy(
|
|
v16_3_wallet,
|
|
node_master_wallet_dir
|
|
)
|
|
node_master.loadwallet(self.default_wallet_name)
|
|
|
|
def copy_non_hd():
|
|
node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet()
|
|
# Copy the 0.15.2 non hd wallet to the last Bitcoin Core version and open it:
|
|
shutil.rmtree(node_master_wallet_dir)
|
|
os.mkdir(node_master_wallet_dir)
|
|
shutil.copy(
|
|
v15_2_wallet,
|
|
node_master_wallet_dir
|
|
)
|
|
node_master.loadwallet(self.default_wallet_name)
|
|
|
|
def copy_split_hd():
|
|
node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet()
|
|
# Copy the 0.15.2 split hd wallet to the last Bitcoin Core version and open it:
|
|
shutil.rmtree(node_master_wallet_dir)
|
|
os.mkdir(node_master_wallet_dir)
|
|
shutil.copy(
|
|
split_hd_wallet,
|
|
os.path.join(node_master_wallet_dir, 'wallet.dat')
|
|
)
|
|
node_master.loadwallet(self.default_wallet_name)
|
|
|
|
self.restart_node(0)
|
|
copy_v16()
|
|
wallet = node_master.get_wallet_rpc(self.default_wallet_name)
|
|
old_version = wallet.getwalletinfo()["walletversion"]
|
|
|
|
# calling upgradewallet without version arguments
|
|
# should return nothing if successful
|
|
assert_equal(wallet.upgradewallet(), {})
|
|
new_version = wallet.getwalletinfo()["walletversion"]
|
|
# upgraded wallet version should be greater than older one
|
|
assert_greater_than(new_version, old_version)
|
|
# wallet should still contain the same balance
|
|
assert_equal(wallet.getbalance(), v16_3_balance)
|
|
|
|
copy_non_hd()
|
|
wallet = node_master.get_wallet_rpc(self.default_wallet_name)
|
|
# should have no master key hash before conversion
|
|
assert_equal('hdseedid' in wallet.getwalletinfo(), False)
|
|
# calling upgradewallet with explicit version number
|
|
# should return nothing if successful
|
|
assert_equal(wallet.upgradewallet(169900), {})
|
|
new_version = wallet.getwalletinfo()["walletversion"]
|
|
# upgraded wallet should have version 169900
|
|
assert_equal(new_version, 169900)
|
|
# after conversion master key hash should be present
|
|
assert_is_hex_string(wallet.getwalletinfo()['hdseedid'])
|
|
|
|
self.log.info('Intermediary versions don\'t effect anything')
|
|
copy_non_hd()
|
|
# Wallet starts with 60000
|
|
assert_equal(60000, wallet.getwalletinfo()['walletversion'])
|
|
wallet.unloadwallet()
|
|
before_checksum = sha256sum_file(node_master_wallet)
|
|
node_master.loadwallet('')
|
|
# Can "upgrade" to 129999 which should have no effect on the wallet
|
|
wallet.upgradewallet(129999)
|
|
assert_equal(60000, wallet.getwalletinfo()['walletversion'])
|
|
wallet.unloadwallet()
|
|
assert_equal(before_checksum, sha256sum_file(node_master_wallet))
|
|
node_master.loadwallet('')
|
|
|
|
self.log.info('Wallets cannot be downgraded')
|
|
copy_non_hd()
|
|
assert_raises_rpc_error(-4, 'Cannot downgrade wallet', wallet.upgradewallet, 40000)
|
|
wallet.unloadwallet()
|
|
assert_equal(before_checksum, sha256sum_file(node_master_wallet))
|
|
node_master.loadwallet('')
|
|
|
|
self.log.info('Can upgrade to HD')
|
|
# Inspect the old wallet and make sure there is no hdchain
|
|
orig_kvs = dump_bdb_kv(node_master_wallet)
|
|
assert b'\x07hdchain' not in orig_kvs
|
|
# Upgrade to HD, no split
|
|
wallet.upgradewallet(130000)
|
|
assert_equal(130000, wallet.getwalletinfo()['walletversion'])
|
|
# Check that there is now a hd chain and it is version 1, no internal chain counter
|
|
new_kvs = dump_bdb_kv(node_master_wallet)
|
|
assert b'\x07hdchain' in new_kvs
|
|
hd_chain = new_kvs[b'\x07hdchain']
|
|
assert_equal(28, len(hd_chain))
|
|
hd_chain_version, external_counter, seed_id = struct.unpack('<iI20s', hd_chain)
|
|
assert_equal(1, hd_chain_version)
|
|
seed_id = bytearray(seed_id)
|
|
seed_id.reverse()
|
|
old_kvs = new_kvs
|
|
# First 2 keys should still be non-HD
|
|
for i in range(0, 2):
|
|
info = wallet.getaddressinfo(wallet.getnewaddress())
|
|
assert 'hdkeypath' not in info
|
|
assert 'hdseedid' not in info
|
|
# Next key should be HD
|
|
info = wallet.getaddressinfo(wallet.getnewaddress())
|
|
assert_equal(seed_id.hex(), info['hdseedid'])
|
|
assert_equal('m/0\'/0\'/0\'', info['hdkeypath'])
|
|
prev_seed_id = info['hdseedid']
|
|
# Change key should be the same keypool
|
|
info = wallet.getaddressinfo(wallet.getrawchangeaddress())
|
|
assert_equal(prev_seed_id, info['hdseedid'])
|
|
assert_equal('m/0\'/0\'/1\'', info['hdkeypath'])
|
|
|
|
self.log.info('Cannot upgrade to HD Split, needs Pre Split Keypool')
|
|
assert_raises_rpc_error(-4, 'Cannot upgrade a non HD split wallet without upgrading to support pre split keypool', wallet.upgradewallet, 139900)
|
|
assert_equal(130000, wallet.getwalletinfo()['walletversion'])
|
|
assert_raises_rpc_error(-4, 'Cannot upgrade a non HD split wallet without upgrading to support pre split keypool', wallet.upgradewallet, 159900)
|
|
assert_equal(130000, wallet.getwalletinfo()['walletversion'])
|
|
assert_raises_rpc_error(-4, 'Cannot upgrade a non HD split wallet without upgrading to support pre split keypool', wallet.upgradewallet, 169899)
|
|
assert_equal(130000, wallet.getwalletinfo()['walletversion'])
|
|
|
|
self.log.info('Upgrade HD to HD chain split')
|
|
wallet.upgradewallet(169900)
|
|
assert_equal(169900, wallet.getwalletinfo()['walletversion'])
|
|
# Check that the hdchain updated correctly
|
|
new_kvs = dump_bdb_kv(node_master_wallet)
|
|
hd_chain = new_kvs[b'\x07hdchain']
|
|
assert_equal(32, len(hd_chain))
|
|
hd_chain_version, external_counter, seed_id, internal_counter = struct.unpack('<iI20sI', hd_chain)
|
|
assert_equal(2, hd_chain_version)
|
|
assert_equal(0, internal_counter)
|
|
seed_id = bytearray(seed_id)
|
|
seed_id.reverse()
|
|
assert_equal(seed_id.hex(), prev_seed_id)
|
|
# Next change address is the same keypool
|
|
info = wallet.getaddressinfo(wallet.getrawchangeaddress())
|
|
assert_equal(prev_seed_id, info['hdseedid'])
|
|
assert_equal('m/0\'/0\'/2\'', info['hdkeypath'])
|
|
# Next change address is the new keypool
|
|
info = wallet.getaddressinfo(wallet.getrawchangeaddress())
|
|
assert_equal(prev_seed_id, info['hdseedid'])
|
|
assert_equal('m/0\'/1\'/0\'', info['hdkeypath'])
|
|
# External addresses use the same keypool
|
|
info = wallet.getaddressinfo(wallet.getnewaddress())
|
|
assert_equal(prev_seed_id, info['hdseedid'])
|
|
assert_equal('m/0\'/0\'/3\'', info['hdkeypath'])
|
|
|
|
self.log.info('Upgrade non-HD to HD chain split')
|
|
copy_non_hd()
|
|
wallet.upgradewallet(169900)
|
|
assert_equal(169900, wallet.getwalletinfo()['walletversion'])
|
|
# Check that the hdchain updated correctly
|
|
new_kvs = dump_bdb_kv(node_master_wallet)
|
|
hd_chain = new_kvs[b'\x07hdchain']
|
|
assert_equal(32, len(hd_chain))
|
|
hd_chain_version, external_counter, seed_id, internal_counter = struct.unpack('<iI20sI', hd_chain)
|
|
assert_equal(2, hd_chain_version)
|
|
assert_equal(2, internal_counter)
|
|
# Drain the keypool by fetching one external key and one change key. Should still be the same keypool
|
|
info = wallet.getaddressinfo(wallet.getnewaddress())
|
|
assert 'hdseedid' not in info
|
|
assert 'hdkeypath' not in info
|
|
info = wallet.getaddressinfo(wallet.getrawchangeaddress())
|
|
assert 'hdseedid' not in info
|
|
assert 'hdkeypath' not in info
|
|
# The next addresses are HD and should be on different HD chains
|
|
info = wallet.getaddressinfo(wallet.getnewaddress())
|
|
ext_id = info['hdseedid']
|
|
assert_equal('m/0\'/0\'/0\'', info['hdkeypath'])
|
|
info = wallet.getaddressinfo(wallet.getrawchangeaddress())
|
|
assert_equal(ext_id, info['hdseedid'])
|
|
assert_equal('m/0\'/1\'/0\'', info['hdkeypath'])
|
|
|
|
self.log.info('KeyMetadata should upgrade when loading into master')
|
|
copy_v16()
|
|
old_kvs = dump_bdb_kv(v16_3_wallet)
|
|
new_kvs = dump_bdb_kv(node_master_wallet)
|
|
for k, old_v in old_kvs.items():
|
|
if k.startswith(b'\x07keymeta'):
|
|
new_ver, new_create_time, new_kp_str, new_seed_id, new_fpr, new_path_len, new_path, new_has_key_orig = deser_keymeta(BytesIO(new_kvs[k]))
|
|
old_ver, old_create_time, old_kp_str, old_seed_id, old_fpr, old_path_len, old_path, old_has_key_orig = deser_keymeta(BytesIO(old_v))
|
|
assert_equal(10, old_ver)
|
|
if old_kp_str == b"": # imported things that don't have keymeta (i.e. imported coinbase privkeys) won't be upgraded
|
|
assert_equal(new_kvs[k], old_v)
|
|
continue
|
|
assert_equal(12, new_ver)
|
|
assert_equal(new_create_time, old_create_time)
|
|
assert_equal(new_kp_str, old_kp_str)
|
|
assert_equal(new_seed_id, old_seed_id)
|
|
assert_equal(0, old_path_len)
|
|
assert_equal(new_path_len, len(new_path))
|
|
assert_equal([], old_path)
|
|
assert_equal(False, old_has_key_orig)
|
|
assert_equal(True, new_has_key_orig)
|
|
|
|
# Check that the path is right
|
|
built_path = []
|
|
for s in new_kp_str.decode().split('/')[1:]:
|
|
h = 0
|
|
if s[-1] == '\'':
|
|
s = s[:-1]
|
|
h = 0x80000000
|
|
p = int(s) | h
|
|
built_path.append(p)
|
|
assert_equal(new_path, built_path)
|
|
|
|
self.log.info('Upgrading to NO_DEFAULT_KEY should not remove the defaultkey')
|
|
copy_split_hd()
|
|
# Check the wallet has a default key initially
|
|
old_kvs = dump_bdb_kv(node_master_wallet)
|
|
defaultkey = old_kvs[b'\x0adefaultkey']
|
|
# Upgrade the wallet. Should still have the same default key
|
|
wallet.upgradewallet(159900)
|
|
new_kvs = dump_bdb_kv(node_master_wallet)
|
|
up_defaultkey = new_kvs[b'\x0adefaultkey']
|
|
assert_equal(defaultkey, up_defaultkey)
|
|
# 0.16.3 doesn't have a default key
|
|
v16_3_kvs = dump_bdb_kv(v16_3_wallet)
|
|
assert b'\x0adefaultkey' not in v16_3_kvs
|
|
|
|
if __name__ == '__main__':
|
|
UpgradeWalletTest().main()
|