From 3fc9d9f241a44ab64774aa9ddc3ded4bb589ed5a Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Mon, 12 May 2025 16:19:31 -0700 Subject: [PATCH 1/3] wallet, rpc: Push the normalized parent descriptor Instead of providing the descriptor string as stored in the db, use the normalized descriptor as is done for getaddressinfo's parent_desc field. --- src/wallet/rpc/util.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wallet/rpc/util.cpp b/src/wallet/rpc/util.cpp index 219378cfd44..e5cbfeafb9e 100644 --- a/src/wallet/rpc/util.cpp +++ b/src/wallet/rpc/util.cpp @@ -109,7 +109,10 @@ void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, { UniValue parent_descs(UniValue::VARR); for (const auto& desc: wallet.GetWalletDescriptors(script_pubkey)) { - parent_descs.push_back(desc.descriptor->ToString()); + std::string desc_str; + FlatSigningProvider dummy_provider; + if (!CHECK_NONFATAL(desc.descriptor->ToNormalizedString(dummy_provider, desc_str, &desc.cache))) continue; + parent_descs.push_back(desc_str); } entry.pushKV("parent_descs", std::move(parent_descs)); } From 2554cee988fb2ddf65428b354a238f1a4efc1aca Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 22 May 2025 14:12:45 -0700 Subject: [PATCH 2/3] test: Enable default wallet for wallet_descriptor.py --- test/functional/wallet_descriptor.py | 38 +++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index 356b1ac47e3..d5641c8afe3 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -24,20 +24,22 @@ class WalletDescriptorTest(BitcoinTestFramework): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-keypool=100']] - self.wallet_names = [] def skip_test_if_missing_module(self): self.skip_if_no_wallet() self.skip_if_no_py_sqlite3() def run_test(self): + self.generate(self.nodes[0], COINBASE_MATURITY + 1) + # Make a descriptor wallet self.log.info("Making a descriptor wallet") self.nodes[0].createwallet(wallet_name="desc1") + wallet = self.nodes[0].get_wallet_rpc("desc1") # A descriptor wallet should have 100 addresses * 4 types = 400 keys self.log.info("Checking wallet info") - wallet_info = self.nodes[0].getwalletinfo() + wallet_info = wallet.getwalletinfo() assert_equal(wallet_info['format'], 'sqlite') assert_equal(wallet_info['keypoolsize'], 400) assert_equal(wallet_info['keypoolsize_hd_internal'], 400) @@ -45,44 +47,44 @@ class WalletDescriptorTest(BitcoinTestFramework): # Check that getnewaddress works self.log.info("Test that getnewaddress and getrawchangeaddress work") - addr = self.nodes[0].getnewaddress("", "legacy") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getnewaddress("", "legacy") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('pkh(') assert_equal(addr_info['hdkeypath'], 'm/44h/1h/0h/0/0') - addr = self.nodes[0].getnewaddress("", "p2sh-segwit") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getnewaddress("", "p2sh-segwit") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('sh(wpkh(') assert_equal(addr_info['hdkeypath'], 'm/49h/1h/0h/0/0') - addr = self.nodes[0].getnewaddress("", "bech32") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getnewaddress("", "bech32") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('wpkh(') assert_equal(addr_info['hdkeypath'], 'm/84h/1h/0h/0/0') - addr = self.nodes[0].getnewaddress("", "bech32m") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getnewaddress("", "bech32m") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('tr(') assert_equal(addr_info['hdkeypath'], 'm/86h/1h/0h/0/0') # Check that getrawchangeaddress works - addr = self.nodes[0].getrawchangeaddress("legacy") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getrawchangeaddress("legacy") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('pkh(') assert_equal(addr_info['hdkeypath'], 'm/44h/1h/0h/1/0') - addr = self.nodes[0].getrawchangeaddress("p2sh-segwit") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getrawchangeaddress("p2sh-segwit") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('sh(wpkh(') assert_equal(addr_info['hdkeypath'], 'm/49h/1h/0h/1/0') - addr = self.nodes[0].getrawchangeaddress("bech32") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getrawchangeaddress("bech32") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('wpkh(') assert_equal(addr_info['hdkeypath'], 'm/84h/1h/0h/1/0') - addr = self.nodes[0].getrawchangeaddress("bech32m") - addr_info = self.nodes[0].getaddressinfo(addr) + addr = wallet.getrawchangeaddress("bech32m") + addr_info = wallet.getaddressinfo(addr) assert addr_info['desc'].startswith('tr(') assert_equal(addr_info['hdkeypath'], 'm/86h/1h/0h/1/0') From 0def84d407facd319b52826d013cad0d5fc8dbf5 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 22 May 2025 14:13:08 -0700 Subject: [PATCH 3/3] test: Verify parent_desc in RPCs getaddressinfo, listunspent, listtransactions, listsinceblock, and gettransaction all include parent_desc(s). Make sure that these are consistent with each other, as well as being in normalized form. --- test/functional/wallet_descriptor.py | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index d5641c8afe3..4ab79b799ee 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -9,6 +9,8 @@ try: except ImportError: pass +import re + from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -29,6 +31,54 @@ class WalletDescriptorTest(BitcoinTestFramework): self.skip_if_no_wallet() self.skip_if_no_py_sqlite3() + def test_parent_descriptors(self): + self.log.info("Check that parent_descs is the same for all RPCs and is normalized") + self.nodes[0].createwallet(wallet_name="parent_descs") + wallet = self.nodes[0].get_wallet_rpc("parent_descs") + default_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + addr = wallet.getnewaddress() + parent_desc = wallet.getaddressinfo(addr)["parent_desc"] + + # Verify that the parent descriptor is normalized + # First remove the checksum + desc_verify = parent_desc.split("#")[0] + # Next extract the xpub + desc_verify = re.sub(r"tpub\w+?(?=/)", "", desc_verify) + # Extract origin info + origin_match = re.search(r'\[([\da-fh/]+)\]', desc_verify) + origin_part = origin_match.group(1) if origin_match else "" + # Split on "]" for everything after the origin info + after_origin = desc_verify.split("]", maxsplit=1)[-1] + # Look for the hardened markers “h” inside each piece + # We don't need to check for aspostrophe as normalization will not output aspostrophe + found_hardened_in_origin = "h" in origin_part + found_hardened_after_origin = "h" in after_origin + assert_equal(found_hardened_in_origin, True) + assert_equal(found_hardened_after_origin, False) + + # Send some coins so we can check listunspent, listtransactions, listunspent, and gettransaction + since_block = self.nodes[0].getbestblockhash() + txid = default_wallet.sendtoaddress(addr, 1) + self.generate(self.nodes[0], 1) + + unspent = wallet.listunspent() + assert_equal(len(unspent), 1) + assert_equal(unspent[0]["parent_descs"], [parent_desc]) + + txs = wallet.listtransactions() + assert_equal(len(txs), 1) + assert_equal(txs[0]["parent_descs"], [parent_desc]) + + txs = wallet.listsinceblock(since_block)["transactions"] + assert_equal(len(txs), 1) + assert_equal(txs[0]["parent_descs"], [parent_desc]) + + tx = wallet.gettransaction(txid=txid, verbose=True) + assert_equal(tx["details"][0]["parent_descs"], [parent_desc]) + + wallet.unloadwallet() + def run_test(self): self.generate(self.nodes[0], COINBASE_MATURITY + 1) @@ -218,6 +268,7 @@ class WalletDescriptorTest(BitcoinTestFramework): conn.close() assert_raises_rpc_error(-4, "Unexpected legacy entry in descriptor wallet found.", self.nodes[0].loadwallet, "crashme") + self.test_parent_descriptors() if __name__ == '__main__': WalletDescriptorTest(__file__).main()