Skip to content

Commit dd61f08

Browse files
committed
Merge bitcoin/bitcoin#33031: wallet: Set descriptor cache upgraded flag for migrated wallets
88b0647 wallet: Always write last hardened cache flag in migrated wallets (Ava Chow) 8a08eef tests: Check that the last hardened cache upgrade occurs (Ava Chow) Pull request description: #32597 set the descriptor cache upgraded flag for newly created wallets, but migrated wallets still did not have the flag set when they are migrated. For consistency, and to avoid an unnecessary upgrade, we should be setting this flag for migrated wallets. The flag would end up being set anyways at the end of migration when the wallet is reloaded as it would perform the automatic upgrade at that time. However, this is unnecessary and we should just set it from the get go. This PR also adds a couple tests to verify that the flag is being set, and that the upgrade is being performed. ACKs for top commit: cedwies: re-ACK 88b0647 rkrux: lgtm ACK 88b0647 pablomartin4btc: ACK 88b0647 Tree-SHA512: 7d0850db0ae38eedd1e6a3bfaa548c6c612182291059fb1a47279a4c4984ee7914ecd02d8c7e427ef67bf9f5e67cbc57a7ae4412fad539e1bf3e05c512a60d69
2 parents 350692e + 88b0647 commit dd61f08

File tree

4 files changed

+67
-15
lines changed

4 files changed

+67
-15
lines changed

src/wallet/wallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3884,7 +3884,7 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
38843884
m_internal_spk_managers.clear();
38853885

38863886
// Setup new descriptors (only if we are migrating any key material)
3887-
SetWalletFlagWithDB(local_wallet_batch, WALLET_FLAG_DESCRIPTORS);
3887+
SetWalletFlagWithDB(local_wallet_batch, WALLET_FLAG_DESCRIPTORS | WALLET_FLAG_LAST_HARDENED_XPUB_CACHED);
38883888
if (has_spendable_material && !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
38893889
// Use the existing master key if we have it
38903890
if (data.master_key.key.IsValid()) {

test/functional/test_framework/test_framework.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,3 +1110,15 @@ def convert_to_json_for_cli(self, text):
11101110
if self.options.usecli:
11111111
return json.dumps(text)
11121112
return text
1113+
1114+
def inspect_sqlite_db(self, path, fn, *args, **kwargs):
1115+
try:
1116+
import sqlite3 # type: ignore[import]
1117+
conn = sqlite3.connect(path)
1118+
with conn:
1119+
result = fn(conn, *args, **kwargs)
1120+
conn.close()
1121+
return result
1122+
except ImportError:
1123+
self.log.warning("sqlite3 module not available, skipping tests that inspect the database")
1124+

test/functional/wallet_backwards_compatibility.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
from test_framework.blocktools import COINBASE_MATURITY
2222
from test_framework.test_framework import BitcoinTestFramework
2323
from test_framework.descriptors import descsum_create
24+
from test_framework.messages import ser_string
2425

2526
from test_framework.util import (
2627
assert_equal,
28+
assert_greater_than,
2729
assert_raises_rpc_error,
2830
)
2931

@@ -149,18 +151,13 @@ def test_v22_inactivehdchain_path(self):
149151
assert_equal(bad_deriv_wallet_master.getaddressinfo(bad_path_addr)["hdkeypath"], good_deriv_path)
150152
bad_deriv_wallet_master.unloadwallet()
151153

152-
# If we have sqlite3, verify that there are no keymeta records
153-
try:
154-
import sqlite3
155-
wallet_db = node_master.wallets_path / wallet_name / "wallet.dat"
156-
conn = sqlite3.connect(wallet_db)
157-
with conn:
158-
# Retrieve all records that have the "keymeta" prefix. The remaining key data varies for each record.
159-
keymeta_rec = conn.execute("SELECT value FROM main where key >= x'076b65796d657461' AND key < x'076b65796d657462'").fetchone()
160-
assert_equal(keymeta_rec, None)
161-
conn.close()
162-
except ImportError:
163-
self.log.warning("sqlite3 module not available, skipping lack of keymeta records check")
154+
def check_keymeta(conn):
155+
# Retrieve all records that have the "keymeta" prefix. The remaining key data varies for each record.
156+
keymeta_rec = conn.execute(f"SELECT value FROM main where key >= x'{ser_string(b'keymeta').hex()}' AND key < x'{ser_string(b'keymetb').hex()}'").fetchone()
157+
assert_equal(keymeta_rec, None)
158+
159+
wallet_db = node_master.wallets_path / wallet_name / "wallet.dat"
160+
self.inspect_sqlite_db(wallet_db, check_keymeta)
164161

165162
def test_ignore_legacy_during_startup(self, legacy_nodes, node_master):
166163
self.log.info("Test that legacy wallets are ignored during startup on v29+")
@@ -342,6 +339,13 @@ def run_test(self):
342339
# Remove the wallet from old node
343340
wallet_prev.unloadwallet()
344341

342+
# Open backup with sqlite and get flags
343+
def get_flags(conn):
344+
flags_rec = conn.execute(f"SELECT value FROM main WHERE key = x'{ser_string(b'flags').hex()}'").fetchone()
345+
return int.from_bytes(flags_rec[0], byteorder="little")
346+
347+
old_flags = self.inspect_sqlite_db(backup_path, get_flags)
348+
345349
# Restore the wallet to master
346350
load_res = node_master.restorewallet(wallet_name, backup_path)
347351

@@ -378,6 +382,21 @@ def run_test(self):
378382

379383
wallet.unloadwallet()
380384

385+
# Open the wallet with sqlite and inspect the flags and records
386+
def check_upgraded_records(conn, old_flags):
387+
flags_rec = conn.execute(f"SELECT value FROM main WHERE key = x'{ser_string(b'flags').hex()}'").fetchone()
388+
new_flags = int.from_bytes(flags_rec[0], byteorder="little")
389+
diff_flags = new_flags & ~old_flags
390+
391+
# Check for last hardened xpubs if the flag is newly set
392+
if diff_flags & (1 << 2):
393+
self.log.debug("Checking descriptor cache was upgraded")
394+
# Fetch all records with the walletdescriptorlhcache prefix
395+
lh_cache_recs = conn.execute(f"SELECT value FROM main where key >= x'{ser_string(b'walletdescriptorlhcache').hex()}' AND key < x'{ser_string(b'walletdescriptorlhcachf').hex()}'").fetchall()
396+
assert_greater_than(len(lh_cache_recs), 0)
397+
398+
self.inspect_sqlite_db(down_backup_path, check_upgraded_records, old_flags)
399+
381400
# Check that no automatic upgrade broke downgrading the wallet
382401
target_dir = node.wallets_path / down_wallet_name
383402
os.makedirs(target_dir, exist_ok=True)

test/functional/wallet_migration.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
from test_framework.descriptors import descsum_create
1919
from test_framework.key import ECPubKey
2020
from test_framework.test_framework import BitcoinTestFramework
21-
from test_framework.messages import COIN, CTransaction, CTxOut
21+
from test_framework.messages import COIN, CTransaction, CTxOut, ser_string
2222
from test_framework.script import hash160
2323
from test_framework.script_util import key_to_p2pkh_script, key_to_p2pk_script, script_to_p2sh_script, script_to_p2wsh_script
2424
from test_framework.util import (
2525
assert_equal,
26+
assert_greater_than,
2627
assert_raises_rpc_error,
2728
find_vout_for_address,
2829
sha256sum_file,
@@ -140,7 +141,8 @@ def migrate_and_get_rpc(self, wallet_name, **kwargs):
140141
# (in which case the wallet name would be suffixed by the 'watchonly' term)
141142
migrated_wallet_name = migrate_info['wallet_name']
142143
wallet = self.master_node.get_wallet_rpc(migrated_wallet_name)
143-
assert_equal(wallet.getwalletinfo()["descriptors"], True)
144+
wallet_info = wallet.getwalletinfo()
145+
assert_equal(wallet_info["descriptors"], True)
144146
self.assert_is_sqlite(migrated_wallet_name)
145147
# Always verify the backup path exist after migration
146148
assert os.path.exists(migrate_info['backup_path'])
@@ -154,6 +156,25 @@ def migrate_and_get_rpc(self, wallet_name, **kwargs):
154156
assert_equal(str(expected_backup_path), migrate_info['backup_path'])
155157
assert {"name": backup_filename} not in self.master_node.listwalletdir()["wallets"]
156158

159+
# Open the wallet with sqlite and verify that the wallet has the last hardened cache flag
160+
# set and the last hardened cache entries
161+
def check_last_hardened(conn):
162+
flags_rec = conn.execute(f"SELECT value FROM main WHERE key = x'{ser_string(b'flags').hex()}'").fetchone()
163+
flags = int.from_bytes(flags_rec[0], byteorder="little")
164+
165+
# All wallets should have the upgrade flag set
166+
assert_equal(bool(flags & (1 << 2)), True)
167+
168+
# Fetch all records with the walletdescriptorlhcache prefix
169+
# if the wallet has private keys and is not blank
170+
if wallet_info["private_keys_enabled"] and not wallet_info["blank"]:
171+
lh_cache_recs = conn.execute(f"SELECT value FROM main where key >= x'{ser_string(b'walletdescriptorlhcache').hex()}' AND key < x'{ser_string(b'walletdescriptorlhcachf').hex()}'").fetchall()
172+
assert_greater_than(len(lh_cache_recs), 0)
173+
174+
inspect_path = os.path.join(self.options.tmpdir, os.path.basename(f"{migrated_wallet_name}_inspect.dat"))
175+
wallet.backupwallet(inspect_path)
176+
self.inspect_sqlite_db(inspect_path, check_last_hardened)
177+
157178
return migrate_info, wallet
158179

159180
def test_basic(self):

0 commit comments

Comments
 (0)