Merge #18836: wallet: upgradewallet fixes and additional tests

5f9c0b6360215636cfa62a70d3a70f1feb3977ab wallet: Remove -upgradewallet from dummywallet (MarcoFalke)
a314271f08215feba53ead27096ac7fda34acb3c test: Remove unused wallet.dat (MarcoFalke)
bf7635963c03203e7189ddaa56c6b086a0108cbf tests: Test specific upgradewallet scenarios and that upgrades work (Andrew Chow)
4b418a9decc3e855ee4b0bbf9e61121c8e9904e5 test: Add test_framework/bdb.py module for inspecting bdb files (Andrew Chow)
092fc434854f881330771a93a1280ac67b1d3549 tests: Add a sha256sum_file function to util (Andrew Chow)
0bd995aa19be65b0dd23df1df571c71428c2bc32 wallet: upgrade the CHDChain version number when upgrading to split hd (Andrew Chow)
8e32e1c41c995e832e643f605d35a7aa112837e6 wallet: remove nWalletMaxVersion (Andrew Chow)
bd7398cc6258c258e9f4411c50630ec4a552341b wallet: have ScriptPubKeyMan::Upgrade check against the new version (Andrew Chow)
5f720544f34dedf75b063b962845fa8eca604514 wallet: Add GetClosestWalletFeature function (Andrew Chow)
842ae3842df489f1b8d68e67a234788966218184 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 ACK 5f9c0b6360
  laanwj:
    Code review ACK 5f9c0b6360215636cfa62a70d3a70f1feb3977ab
  jonatack:
    ACK 5f9c0b6360215636cfa62a70d3a70f1feb3977ab, 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
This commit is contained in:
Andrew Chow 2020-04-29 14:48:43 -04:00 committed by Konstantin Akimov
parent 752e4ca048
commit 708586c77e
No known key found for this signature in database
GPG Key ID: 2176C4A5D01EA524
12 changed files with 291 additions and 65 deletions

View File

@ -37,7 +37,7 @@ public:
virtual bool IsWalletFlagSet(uint64_t) const = 0; virtual bool IsWalletFlagSet(uint64_t) const = 0;
virtual void UnsetBlankWalletFlag(WalletBatch&) = 0; virtual void UnsetBlankWalletFlag(WalletBatch&) = 0;
virtual bool CanSupportFeature(enum WalletFeature) const = 0; virtual bool CanSupportFeature(enum WalletFeature) const = 0;
virtual void SetMinVersion(enum WalletFeature, WalletBatch* = nullptr, bool = false) = 0; virtual void SetMinVersion(enum WalletFeature, WalletBatch* = nullptr) = 0;
virtual const CKeyingMaterial& GetEncryptionKey() const = 0; virtual const CKeyingMaterial& GetEncryptionKey() const = 0;
virtual bool HasEncryptionKeys() const = 0; virtual bool HasEncryptionKeys() const = 0;
virtual bool IsLocked(bool fForMixing = false) const = 0; virtual bool IsLocked(bool fForMixing = false) const = 0;

View File

@ -442,21 +442,13 @@ void CWallet::chainStateFlushed(const CBlockLocator& loc)
batch.WriteBestBlock(loc); batch.WriteBestBlock(loc);
} }
void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in, bool fExplicit) void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in)
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
if (nWalletVersion >= nVersion) if (nWalletVersion >= nVersion)
return; return;
// when doing an explicit upgrade, if we pass the max version permitted, upgrade all the way
if (fExplicit && nVersion > nWalletMaxVersion)
nVersion = FEATURE_LATEST;
nWalletVersion = nVersion; nWalletVersion = nVersion;
if (nVersion > nWalletMaxVersion)
nWalletMaxVersion = nVersion;
{ {
WalletBatch* batch = batch_in ? batch_in : new WalletBatch(GetDatabase()); WalletBatch* batch = batch_in ? batch_in : new WalletBatch(GetDatabase());
if (nWalletVersion > 40000) if (nWalletVersion > 40000)
@ -466,18 +458,6 @@ void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in,
} }
} }
bool CWallet::SetMaxVersion(int nVersion)
{
LOCK(cs_wallet);
// cannot downgrade below current version
if (nWalletVersion > nVersion)
return false;
nWalletMaxVersion = nVersion;
return true;
}
std::set<uint256> CWallet::GetConflicts(const uint256& txid) const std::set<uint256> CWallet::GetConflicts(const uint256& txid) const
{ {
std::set<uint256> result; std::set<uint256> result;
@ -665,7 +645,7 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
// Encryption was introduced in version 0.4.0 // Encryption was introduced in version 0.4.0
SetMinVersion(FEATURE_WALLETCRYPT, encrypted_batch, true); SetMinVersion(FEATURE_WALLETCRYPT, encrypted_batch);
if (!encrypted_batch->TxnCommit()) { if (!encrypted_batch->TxnCommit()) {
delete encrypted_batch; delete encrypted_batch;
@ -4583,7 +4563,7 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, interfaces::C
if (fFirstRun) if (fFirstRun)
{ {
walletInstance->SetMaxVersion(FEATURE_LATEST); walletInstance->SetMinVersion(FEATURE_LATEST);
walletInstance->AddWalletFlags(wallet_creation_flags); walletInstance->AddWalletFlags(wallet_creation_flags);
@ -4918,7 +4898,7 @@ bool CWallet::UpgradeWallet(int version, bilingual_str& error)
return false; return false;
} }
SetMaxVersion(nMaxVersion); SetMinVersion(GetClosestWalletFeature(version));
return true; return true;
} }

View File

@ -706,9 +706,6 @@ private:
//! the current wallet version: clients below this version are not able to load the wallet //! the current wallet version: clients below this version are not able to load the wallet
int nWalletVersion GUARDED_BY(cs_wallet){FEATURE_BASE}; int nWalletVersion GUARDED_BY(cs_wallet){FEATURE_BASE};
//! the maximum wallet format version: memory-only variable that specifies to what version this wallet may be upgraded
int nWalletMaxVersion GUARDED_BY(cs_wallet) = FEATURE_BASE;
int64_t nNextResend = 0; int64_t nNextResend = 0;
bool fBroadcastTransactions = false; bool fBroadcastTransactions = false;
// Local time that the tip block was received. Used to schedule wallet rebroadcasts. // Local time that the tip block was received. Used to schedule wallet rebroadcasts.
@ -911,8 +908,8 @@ public:
const CWalletTx* GetWalletTx(const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); const CWalletTx* GetWalletTx(const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool IsTrusted(const CWalletTx& wtx, std::set<uint256>& trusted_parents) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsTrusted(const CWalletTx& wtx, std::set<uint256>& trusted_parents) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! check whether we are allowed to upgrade (or already support) to the named feature //! check whether we support the named feature
bool CanSupportFeature(enum WalletFeature wf) const override EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return nWalletMaxVersion >= wf; } bool CanSupportFeature(enum WalletFeature wf) const override EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return IsFeatureSupported(nWalletVersion, wf); }
/** /**
* populate vCoins with vector of available COutputs. * populate vCoins with vector of available COutputs.
@ -981,7 +978,7 @@ public:
//! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo //! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo
void UpgradeKeyMetadata() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void UpgradeKeyMetadata() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; nWalletMaxVersion = std::max(nWalletMaxVersion, nVersion); return true; } bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; return true; }
//! Adds a destination data tuple to the store, without saving it to disk //! Adds a destination data tuple to the store, without saving it to disk
void LoadDestData(const CTxDestination& dest, const std::string& key, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void LoadDestData(const CTxDestination& dest, const std::string& key, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@ -1198,11 +1195,8 @@ public:
unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! signify that a particular wallet feature is now used. this may change nWalletVersion and nWalletMaxVersion if those are lower //! signify that a particular wallet feature is now used.
void SetMinVersion(enum WalletFeature, WalletBatch* batch_in = nullptr, bool fExplicit = false) override; void SetMinVersion(enum WalletFeature, WalletBatch* batch_in = nullptr) override;
//! change which version we're allowed to upgrade to (note that this does not immediately imply upgrading to that format)
bool SetMaxVersion(int nVersion);
//! get the current wallet format (the oldest client version guaranteed to understand this wallet) //! get the current wallet format (the oldest client version guaranteed to understand this wallet)
int GetVersion() const { LOCK(cs_wallet); return nWalletVersion; } int GetVersion() const { LOCK(cs_wallet); return nWalletVersion; }

View File

@ -28,3 +28,18 @@ fs::path GetWalletDir()
return path; return path;
} }
bool IsFeatureSupported(int wallet_version, int feature_version)
{
return wallet_version >= feature_version;
}
WalletFeature GetClosestWalletFeature(int version)
{
if (version >= FEATURE_LATEST) return FEATURE_LATEST;
if (version >= FEATURE_HD) return FEATURE_HD;
if (version >= FEATURE_COMPRPUBKEY) return FEATURE_COMPRPUBKEY;
if (version >= FEATURE_WALLETCRYPT) return FEATURE_WALLETCRYPT;
if (version >= FEATURE_BASE) return FEATURE_BASE;
return static_cast<WalletFeature>(0);
}

View File

@ -23,6 +23,8 @@ enum WalletFeature
FEATURE_LATEST = FEATURE_HD FEATURE_LATEST = FEATURE_HD
}; };
bool IsFeatureSupported(int wallet_version, int feature_version);
WalletFeature GetClosestWalletFeature(int version);
enum WalletFlags : uint64_t { enum WalletFlags : uint64_t {
// wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown // wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown

View File

@ -1,8 +0,0 @@
The wallet has been created by starting Bitcoin Core with the options
`-regtest -datadir=/tmp -nowallet -walletdir=$(pwd)/test/functional/data/wallets/`.
In the source code, `WalletFeature::FEATURE_LATEST` has been modified to be large, so that the minversion is too high
for a current build of the wallet.
The wallet has then been created with the RPC `createwallet high_minversion true true`, so that a blank wallet with
private keys disabled is created.

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
# Copyright (c) 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.
"""
Utilities for working directly with the wallet's BDB database file
This is specific to the configuration of BDB used in this project:
- pagesize: 4096 bytes
- Outer database contains single subdatabase named 'main'
- btree
- btree leaf pages
Each key-value pair is two entries in a btree leaf. The first is the key, the one that follows
is the value. And so on. Note that the entry data is itself not in the correct order. Instead
entry offsets are stored in the correct order and those offsets are needed to then retrieve
the data itself.
Page format can be found in BDB source code dbinc/db_page.h
This only implements the deserialization of btree metadata pages and normal btree pages. Overflow
pages are not implemented but may be needed in the future if dealing with wallets with large
transactions.
`db_dump -da wallet.dat` is useful to see the data in a wallet.dat BDB file
"""
import binascii
import struct
# Important constants
PAGESIZE = 4096
OUTER_META_PAGE = 0
INNER_META_PAGE = 2
# Page type values
BTREE_INTERNAL = 3
BTREE_LEAF = 5
BTREE_META = 9
# Some magic numbers for sanity checking
BTREE_MAGIC = 0x053162
DB_VERSION = 9
# Deserializes a leaf page into a dict.
# Btree internal pages have the same header, for those, return None.
# For the btree leaf pages, deserialize them and put all the data into a dict
def dump_leaf_page(data):
page_info = {}
page_header = data[0:26]
_, pgno, prev_pgno, next_pgno, entries, hf_offset, level, pg_type = struct.unpack('QIIIHHBB', page_header)
page_info['pgno'] = pgno
page_info['prev_pgno'] = prev_pgno
page_info['next_pgno'] = next_pgno
page_info['entries'] = entries
page_info['hf_offset'] = hf_offset
page_info['level'] = level
page_info['pg_type'] = pg_type
page_info['entry_offsets'] = struct.unpack('{}H'.format(entries), data[26:26 + entries * 2])
page_info['entries'] = []
if pg_type == BTREE_INTERNAL:
# Skip internal pages. These are the internal nodes of the btree and don't contain anything relevant to us
return None
assert pg_type == BTREE_LEAF, 'A non-btree leaf page has been encountered while dumping leaves'
for i in range(0, entries):
offset = page_info['entry_offsets'][i]
entry = {'offset': offset}
page_data_header = data[offset:offset + 3]
e_len, pg_type = struct.unpack('HB', page_data_header)
entry['len'] = e_len
entry['pg_type'] = pg_type
entry['data'] = data[offset + 3:offset + 3 + e_len]
page_info['entries'].append(entry)
return page_info
# Deserializes a btree metadata page into a dict.
# Does a simple sanity check on the magic value, type, and version
def dump_meta_page(page):
# metadata page
# general metadata
metadata = {}
meta_page = page[0:72]
_, pgno, magic, version, pagesize, encrypt_alg, pg_type, metaflags, _, free, last_pgno, nparts, key_count, record_count, flags, uid = struct.unpack('QIIIIBBBBIIIIII20s', meta_page)
metadata['pgno'] = pgno
metadata['magic'] = magic
metadata['version'] = version
metadata['pagesize'] = pagesize
metadata['encrypt_alg'] = encrypt_alg
metadata['pg_type'] = pg_type
metadata['metaflags'] = metaflags
metadata['free'] = free
metadata['last_pgno'] = last_pgno
metadata['nparts'] = nparts
metadata['key_count'] = key_count
metadata['record_count'] = record_count
metadata['flags'] = flags
metadata['uid'] = binascii.hexlify(uid)
assert magic == BTREE_MAGIC, 'bdb magic does not match bdb btree magic'
assert pg_type == BTREE_META, 'Metadata page is not a btree metadata page'
assert version == DB_VERSION, 'Database too new'
# btree metadata
btree_meta_page = page[72:512]
_, minkey, re_len, re_pad, root, _, crypto_magic, _, iv, chksum = struct.unpack('IIIII368sI12s16s20s', btree_meta_page)
metadata['minkey'] = minkey
metadata['re_len'] = re_len
metadata['re_pad'] = re_pad
metadata['root'] = root
metadata['crypto_magic'] = crypto_magic
metadata['iv'] = binascii.hexlify(iv)
metadata['chksum'] = binascii.hexlify(chksum)
return metadata
# Given the dict from dump_leaf_page, get the key-value pairs and put them into a dict
def extract_kv_pairs(page_data):
out = {}
last_key = None
for i, entry in enumerate(page_data['entries']):
# By virtue of these all being pairs, even number entries are keys, and odd are values
if i % 2 == 0:
out[entry['data']] = b''
last_key = entry['data']
else:
out[last_key] = entry['data']
return out
# Extract the key-value pairs of the BDB file given in filename
def dump_bdb_kv(filename):
# Read in the BDB file and start deserializing it
pages = []
with open(filename, 'rb') as f:
data = f.read(PAGESIZE)
while len(data) > 0:
pages.append(data)
data = f.read(PAGESIZE)
# Sanity check the meta pages
dump_meta_page(pages[OUTER_META_PAGE])
dump_meta_page(pages[INNER_META_PAGE])
# Fetch the kv pairs from the leaf pages
kv = {}
for i in range(3, len(pages)):
info = dump_leaf_page(pages[i])
if info is not None:
info_kv = extract_kv_pairs(info)
kv = {**kv, **info_kv}
return kv

View File

@ -9,6 +9,7 @@ from base64 import b64encode
from binascii import unhexlify from binascii import unhexlify
from decimal import Decimal, ROUND_DOWN from decimal import Decimal, ROUND_DOWN
from subprocess import CalledProcessError from subprocess import CalledProcessError
import hashlib
import inspect import inspect
import json import json
import logging import logging
@ -268,6 +269,14 @@ def wait_until_helper(predicate, *, attempts=float('inf'), timeout=float('inf'),
else: else:
return False return False
def sha256sum_file(filename):
h = hashlib.sha256()
with open(filename, 'rb') as f:
d = f.read(4096)
while len(d) > 0:
h.update(d)
d = f.read(4096)
return h.digest()
# RPC/P2P connection constants and functions # RPC/P2P connection constants and functions
############################################ ############################################

View File

@ -10,7 +10,9 @@ Only v0.15.2 and v0.16.3 are required by this test. The others are used in featu
import os import os
import shutil import shutil
import struct
from test_framework.bdb import dump_bdb_kv
from test_framework.blocktools import COINBASE_MATURITY from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
@ -18,17 +20,21 @@ from test_framework.util import (
assert_greater_than, assert_greater_than,
assert_greater_than_or_equal, assert_greater_than_or_equal,
assert_is_hex_string, assert_is_hex_string,
assert_raises_rpc_error,
sha256sum_file,
) )
UPGRADED_KEYMETA_VERSION = 12
class UpgradeWalletTest(BitcoinTestFramework): class UpgradeWalletTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.setup_clean_chain = True self.setup_clean_chain = True
self.num_nodes = 3 self.num_nodes = 3
self.extra_args = [ self.extra_args = [
[], # current wallet version ["-keypool=2"], # current wallet version
["-usehd=1"], # v18.2.2 wallet ["-usehd=1", "-keypool=2"], # v18.2.2 wallet
["-usehd=0"] # v0.16.1.1 wallet ["-usehd=0", "-keypool=2"], # v0.16.1.1 wallet
] ]
self.wallet_names = [self.default_wallet_name, None, None] self.wallet_names = [self.default_wallet_name, None, None]
@ -84,12 +90,12 @@ class UpgradeWalletTest(BitcoinTestFramework):
self.log.info("Test upgradewallet RPC...") self.log.info("Test upgradewallet RPC...")
# Prepare for copying of the older wallet # Prepare for copying of the older wallet
node_master_wallet_dir = os.path.join(node_master.datadir, "regtest/wallets") 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)
v18_2_wallet = os.path.join(v18_2_node.datadir, "regtest/wallets/wallet.dat") v18_2_wallet = os.path.join(v18_2_node.datadir, "regtest/wallets/wallet.dat")
v16_1_wallet = os.path.join(v16_1_node.datadir, "regtest/wallets/wallet.dat") v16_1_wallet = os.path.join(v16_1_node.datadir, "regtest/wallets/wallet.dat")
self.stop_nodes() self.stop_nodes()
# Copy the 0.16.3 wallet to the last Dash Core version and open it:
shutil.rmtree(node_master_wallet_dir) shutil.rmtree(node_master_wallet_dir)
os.mkdir(node_master_wallet_dir) os.mkdir(node_master_wallet_dir)
shutil.copy( shutil.copy(
@ -99,7 +105,32 @@ class UpgradeWalletTest(BitcoinTestFramework):
self.restart_node(0, ['-nowallet']) self.restart_node(0, ['-nowallet'])
node_master.loadwallet('') node_master.loadwallet('')
wallet = node_master.get_wallet_rpc('') def copy_v16():
node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet()
# Copy the 0.16.3 wallet to the last Dash Core version and open it:
shutil.rmtree(node_master_wallet_dir)
os.mkdir(node_master_wallet_dir)
shutil.copy(
v18_2_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 19.3.0 wallet to the last Dash Core version and open it:
shutil.rmtree(node_master_wallet_dir)
os.mkdir(node_master_wallet_dir)
shutil.copy(
v16_1_wallet,
node_master_wallet_dir
)
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"] old_version = wallet.getwalletinfo()["walletversion"]
# calling upgradewallet without version arguments # calling upgradewallet without version arguments
@ -111,27 +142,17 @@ class UpgradeWalletTest(BitcoinTestFramework):
# wallet should still contain the same balance # wallet should still contain the same balance
assert_equal(wallet.getbalance(), v18_2_balance) assert_equal(wallet.getbalance(), v18_2_balance)
self.stop_node(0) copy_non_hd()
# Copy the 19.3.0 wallet to the last Dash Core version and open it: wallet = node_master.get_wallet_rpc(self.default_wallet_name)
shutil.rmtree(node_master_wallet_dir)
os.mkdir(node_master_wallet_dir)
shutil.copy(
v16_1_wallet,
node_master_wallet_dir
)
self.restart_node(0, ['-nowallet'])
node_master.loadwallet('')
wallet = node_master.get_wallet_rpc('')
# should have no master key hash before conversion # should have no master key hash before conversion
assert_equal('hdseedid' in wallet.getwalletinfo(), False) assert_equal('hdchainid' in wallet.getwalletinfo(), False)
# calling upgradewallet with explicit version number # calling upgradewallet with explicit version number
# should return nothing if successful # should return nothing if successful
assert_equal(wallet.upgradewallet(169900), {}) assert_equal(wallet.upgradewallet(169900), {})
new_version = wallet.getwalletinfo()["walletversion"] new_version = wallet.getwalletinfo()["walletversion"]
# upgraded wallet would not have 120200 version until HD seed actually appeared # upgraded wallet would have 120200 but no HD seed actually appeared
assert_greater_than(120200, new_version) assert_equal(120200, new_version)
# after conversion master key hash should not be present yet # after conversion master key hash should not be present yet
assert 'hdchainid' not in wallet.getwalletinfo() assert 'hdchainid' not in wallet.getwalletinfo()
assert_equal(wallet.upgradetohd(), True) assert_equal(wallet.upgradetohd(), True)
@ -139,5 +160,66 @@ class UpgradeWalletTest(BitcoinTestFramework):
assert_equal(new_version, 120200) assert_equal(new_version, 120200)
assert_is_hex_string(wallet.getwalletinfo()['hdchainid']) assert_is_hex_string(wallet.getwalletinfo()['hdchainid'])
self.log.info('Intermediary versions don\'t effect anything')
copy_non_hd()
# Wallet starts with 61000 (legacy "latest")
assert_equal(61000, wallet.getwalletinfo()['walletversion'])
wallet.unloadwallet()
before_checksum = sha256sum_file(node_master_wallet)
node_master.loadwallet('')
# Can "upgrade" to 120199 which should have no effect on the wallet
wallet.upgradewallet(120199)
assert_equal(61000, 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
wallet.upgradewallet(120200)
assert_equal(120200, 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)
wallet.upgradetohd()
new_kvs = dump_bdb_kv(node_master_wallet)
assert b'\x07hdchain' in new_kvs
hd_chain = new_kvs[b'\x07hdchain']
# hd_chain:
# obj.nVersion int
# obj.id uint256
# obj.fCrypted bool
# obj.vchSeed SecureVector
# obj.vchMnemonic SecureVector
# obj.vchMnemonicPassphrase SecureVector
# obj.mapAccounts map<> accounts
assert_greater_than(220, len(hd_chain))
assert_greater_than(len(hd_chain), 180)
hd_chain_version, seed_id, is_crypted = struct.unpack('<i32s?', hd_chain[:37])
assert_equal(1, hd_chain_version)
seed_id = bytearray(seed_id)
seed_id.reverse()
# Next key should be HD
info = wallet.getaddressinfo(wallet.getnewaddress())
assert_equal(seed_id.hex(), info['hdchainid'])
assert_equal("m/44'/1'/0'/0/0", info['hdkeypath'])
prev_seed_id = info['hdchainid']
# Change key should be the same keypool
info = wallet.getaddressinfo(wallet.getrawchangeaddress())
assert_equal(prev_seed_id, info['hdchainid'])
assert_equal("m/44'/1'/0'/1/0", info['hdkeypath'])
assert_equal(120200, wallet.getwalletinfo()['walletversion'])
if __name__ == '__main__': if __name__ == '__main__':
UpgradeWalletTest().main() UpgradeWalletTest().main()