Skip to content

Commit c7e2b9e

Browse files
committed
tests: Test migration cleans up bad inactive chain derivation path
A bug in 0.21.x and 22.x resulted in some wallets having invalid derivation paths that are the concatenation of two derivation paths. These appear only when inactive hd chains are topped up. Since key metadata is a legacy wallet only record, migrating legacy wallets to descriptor wallets will fix this issue as all key metadata records are deleted. The derivation path information is derived on-the-fly from the descriptor that is produced for the inactive hd chain. Thus we only need a test to verify that the derivation paths are good, and that all key metadata records are deleted from the migrated wallet.
1 parent 80e6ad9 commit c7e2b9e

File tree

1 file changed

+83
-1
lines changed

1 file changed

+83
-1
lines changed

test/functional/wallet_backwards_compatibility.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
assert_raises_rpc_error,
2727
)
2828

29+
LAST_KEYPOOL_INDEX = 9 # Index of the last derived address with the keypool size of 10
2930

3031
class BackwardsCompatibilityTest(BitcoinTestFramework):
3132
def set_test_params(self):
@@ -38,7 +39,7 @@ def set_test_params(self):
3839
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]"], # v25.0
3940
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]"], # v24.0.1
4041
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]"], # v23.0
41-
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]"], # v22.0
42+
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]", f"-keypool={LAST_KEYPOOL_INDEX + 1}"], # v22.0
4243
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]"], # v0.21.0
4344
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]"], # v0.20.1
4445
["-nowallet", "-walletrbf=1", "-addresstype=bech32", "[email protected]"], # v0.19.1
@@ -85,6 +86,85 @@ def major_version_at_least(self, node, major):
8586
node_major, _, _ = self.split_version(node)
8687
return node_major >= major
8788

89+
def test_v22_inactivehdchain_path(self):
90+
self.log.info("Testing inactive hd chain bad derivation path cleanup")
91+
# 0.21.x and 22.x would both produce bad derivation paths when topping up an inactive hd chain
92+
# Make sure that this is being automatically cleaned up by migration
93+
node_master = self.nodes[1]
94+
node_v22 = self.nodes[self.num_nodes - 5]
95+
wallet_name = "bad_deriv_path"
96+
node_v22.createwallet(wallet_name=wallet_name, descriptors=False)
97+
bad_deriv_wallet = node_v22.get_wallet_rpc(wallet_name)
98+
99+
# Make a dump of the wallet to get an unused address
100+
dump_path = node_v22.wallets_path / f"{wallet_name}.dump"
101+
bad_deriv_wallet.dumpwallet(dump_path)
102+
addr = None
103+
seed = None
104+
with open(dump_path, encoding="utf8") as f:
105+
for line in f:
106+
if f"hdkeypath=m/0'/0'/{LAST_KEYPOOL_INDEX}'" in line:
107+
addr = line.split(" ")[4].split("=")[1]
108+
elif " hdseed=1 " in line:
109+
seed = line.split(" ")[0]
110+
assert addr is not None
111+
assert seed is not None
112+
# Rotate seed and unload
113+
bad_deriv_wallet.sethdseed()
114+
bad_deriv_wallet.unloadwallet()
115+
# Receive at addr to trigger inactive chain topup on next load
116+
self.nodes[0].sendtoaddress(addr, 1)
117+
self.generate(self.nodes[0], 1, sync_fun=self.no_op)
118+
self.sync_all(nodes=[self.nodes[0], node_master, node_v22])
119+
node_v22.loadwallet(wallet_name)
120+
121+
# Dump again to find bad hd keypath
122+
bad_deriv_path = f"m/0'/0'/{LAST_KEYPOOL_INDEX}'/0'/0'/{LAST_KEYPOOL_INDEX + 1}'"
123+
good_deriv_path = f"m/0h/0h/{LAST_KEYPOOL_INDEX + 1}h"
124+
os.unlink(dump_path)
125+
bad_deriv_wallet.dumpwallet(dump_path)
126+
bad_path_addr = None
127+
with open(dump_path, encoding="utf8") as f:
128+
for line in f:
129+
if f"hdkeypath={bad_deriv_path}" in line:
130+
bad_path_addr = line.split(" ")[4].split("=")[1]
131+
assert bad_path_addr is not None
132+
assert_equal(bad_deriv_wallet.getaddressinfo(bad_path_addr)["hdkeypath"], bad_deriv_path)
133+
134+
# Verify that this bad derivation path addr is actually at m/0'/0'/10' by making a new wallet with the same seed but larger keypool
135+
node_v22.createwallet(wallet_name="path_verify", descriptors=False, blank=True)
136+
verify_wallet = node_v22.get_wallet_rpc("path_verify")
137+
verify_wallet.sethdseed(True, seed)
138+
# Bad addr is after keypool, so need to generate it by refilling
139+
verify_wallet.keypoolrefill(LAST_KEYPOOL_INDEX + 2)
140+
assert_equal(verify_wallet.getaddressinfo(bad_path_addr)["hdkeypath"], good_deriv_path.replace("h", "'"))
141+
142+
# Migrate with master
143+
# Since all keymeta records are now deleted after migration, the derivation path
144+
# should now be correct as it is derived on-the-fly from the inactive hd chain's descriptor
145+
backup_path = node_v22.wallets_path / f"{wallet_name}.bak"
146+
bad_deriv_wallet.backupwallet(backup_path)
147+
wallet_dir_master = os.path.join(node_master.wallets_path, wallet_name)
148+
os.makedirs(wallet_dir_master, exist_ok=True)
149+
shutil.copy(backup_path, os.path.join(wallet_dir_master, "wallet.dat"))
150+
node_master.migratewallet(wallet_name)
151+
bad_deriv_wallet_master = node_master.get_wallet_rpc(wallet_name)
152+
assert_equal(bad_deriv_wallet_master.getaddressinfo(bad_path_addr)["hdkeypath"], good_deriv_path)
153+
bad_deriv_wallet_master.unloadwallet()
154+
155+
# If we have sqlite3, verify that there are no keymeta records
156+
try:
157+
import sqlite3
158+
wallet_db = node_master.wallets_path / wallet_name / "wallet.dat"
159+
conn = sqlite3.connect(wallet_db)
160+
with conn:
161+
# Retrieve all records that have the "keymeta" prefix. The remaining key data varies for each record.
162+
keymeta_rec = conn.execute("SELECT value FROM main where key >= x'076b65796d657461' AND key < x'076b65796d657462'").fetchone()
163+
assert_equal(keymeta_rec, None)
164+
conn.close()
165+
except ImportError:
166+
self.log.warning("sqlite3 module not available, skipping lack of keymeta records check")
167+
88168
def run_test(self):
89169
node_miner = self.nodes[0]
90170
node_master = self.nodes[1]
@@ -308,5 +388,7 @@ def run_test(self):
308388
# Legacy wallets are no longer supported. Trying to load these should result in an error
309389
assert_raises_rpc_error(-18, "The wallet appears to be a Legacy wallet, please use the wallet migration tool (migratewallet RPC)", node_master.restorewallet, wallet_name, backup_path)
310390

391+
self.test_v22_inactivehdchain_path()
392+
311393
if __name__ == '__main__':
312394
BackwardsCompatibilityTest(__file__).main()

0 commit comments

Comments
 (0)