|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2022 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | +"""Test that fast rescan using block filters for descriptor wallets detects |
| 6 | + top-ups correctly and finds the same transactions than the slow variant.""" |
| 7 | +import os |
| 8 | +from typing import List |
| 9 | + |
| 10 | +from test_framework.descriptors import descsum_create |
| 11 | +from test_framework.test_framework import BitcoinTestFramework |
| 12 | +from test_framework.test_node import TestNode |
| 13 | +from test_framework.util import assert_equal |
| 14 | +from test_framework.wallet import MiniWallet |
| 15 | +from test_framework.wallet_util import get_generate_key |
| 16 | + |
| 17 | + |
| 18 | +KEYPOOL_SIZE = 100 # smaller than default size to speed-up test |
| 19 | +NUM_DESCRIPTORS = 9 # number of descriptors (8 default ranged ones + 1 fixed non-ranged one) |
| 20 | +NUM_BLOCKS = 6 # number of blocks to mine |
| 21 | + |
| 22 | + |
| 23 | +class WalletFastRescanTest(BitcoinTestFramework): |
| 24 | + def set_test_params(self): |
| 25 | + self.num_nodes = 1 |
| 26 | + self.extra_args = [[f'-keypool={KEYPOOL_SIZE}', '-blockfilterindex=1']] |
| 27 | + |
| 28 | + def skip_test_if_missing_module(self): |
| 29 | + self.skip_if_no_wallet() |
| 30 | + self.skip_if_no_sqlite() |
| 31 | + |
| 32 | + def get_wallet_txids(self, node: TestNode, wallet_name: str) -> List[str]: |
| 33 | + w = node.get_wallet_rpc(wallet_name) |
| 34 | + txs = w.listtransactions('*', 1000000) |
| 35 | + return [tx['txid'] for tx in txs] |
| 36 | + |
| 37 | + def run_test(self): |
| 38 | + node = self.nodes[0] |
| 39 | + wallet = MiniWallet(node) |
| 40 | + wallet.rescan_utxos() |
| 41 | + |
| 42 | + self.log.info("Create descriptor wallet with backup") |
| 43 | + WALLET_BACKUP_FILENAME = os.path.join(node.datadir, 'wallet.bak') |
| 44 | + node.createwallet(wallet_name='topup_test', descriptors=True) |
| 45 | + w = node.get_wallet_rpc('topup_test') |
| 46 | + fixed_key = get_generate_key() |
| 47 | + print(w.importdescriptors([{"desc": descsum_create(f"wpkh({fixed_key.privkey})"), "timestamp": "now"}])) |
| 48 | + descriptors = w.listdescriptors()['descriptors'] |
| 49 | + assert_equal(len(descriptors), NUM_DESCRIPTORS) |
| 50 | + w.backupwallet(WALLET_BACKUP_FILENAME) |
| 51 | + |
| 52 | + self.log.info(f"Create txs sending to end range address of each descriptor, triggering top-ups") |
| 53 | + for i in range(NUM_BLOCKS): |
| 54 | + self.log.info(f"Block {i+1}/{NUM_BLOCKS}") |
| 55 | + for desc_info in w.listdescriptors()['descriptors']: |
| 56 | + if 'range' in desc_info: |
| 57 | + start_range, end_range = desc_info['range'] |
| 58 | + addr = w.deriveaddresses(desc_info['desc'], [end_range, end_range])[0] |
| 59 | + spk = bytes.fromhex(w.getaddressinfo(addr)['scriptPubKey']) |
| 60 | + self.log.info(f"-> range [{start_range},{end_range}], last address {addr}") |
| 61 | + else: |
| 62 | + spk = bytes.fromhex(fixed_key.p2wpkh_script) |
| 63 | + self.log.info(f"-> fixed non-range descriptor address {fixed_key.p2wpkh_addr}") |
| 64 | + wallet.send_to(from_node=node, scriptPubKey=spk, amount=10000) |
| 65 | + self.generate(node, 1) |
| 66 | + |
| 67 | + self.log.info("Import wallet backup with block filter index") |
| 68 | + with node.assert_debug_log(['fast variant using block filters']): |
| 69 | + node.restorewallet('rescan_fast', WALLET_BACKUP_FILENAME) |
| 70 | + txids_fast = self.get_wallet_txids(node, 'rescan_fast') |
| 71 | + |
| 72 | + self.log.info("Import non-active descriptors with block filter index") |
| 73 | + node.createwallet(wallet_name='rescan_fast_nonactive', descriptors=True, disable_private_keys=True, blank=True) |
| 74 | + with node.assert_debug_log(['fast variant using block filters']): |
| 75 | + w = node.get_wallet_rpc('rescan_fast_nonactive') |
| 76 | + w.importdescriptors([{"desc": descriptor['desc'], "timestamp": 0} for descriptor in descriptors]) |
| 77 | + txids_fast_nonactive = self.get_wallet_txids(node, 'rescan_fast_nonactive') |
| 78 | + |
| 79 | + self.restart_node(0, [f'-keypool={KEYPOOL_SIZE}', '-blockfilterindex=0']) |
| 80 | + self.log.info("Import wallet backup w/o block filter index") |
| 81 | + with node.assert_debug_log(['slow variant inspecting all blocks']): |
| 82 | + node.restorewallet("rescan_slow", WALLET_BACKUP_FILENAME) |
| 83 | + txids_slow = self.get_wallet_txids(node, 'rescan_slow') |
| 84 | + |
| 85 | + self.log.info("Import non-active descriptors w/o block filter index") |
| 86 | + node.createwallet(wallet_name='rescan_slow_nonactive', descriptors=True, disable_private_keys=True, blank=True) |
| 87 | + with node.assert_debug_log(['slow variant inspecting all blocks']): |
| 88 | + w = node.get_wallet_rpc('rescan_slow_nonactive') |
| 89 | + w.importdescriptors([{"desc": descriptor['desc'], "timestamp": 0} for descriptor in descriptors]) |
| 90 | + txids_slow_nonactive = self.get_wallet_txids(node, 'rescan_slow_nonactive') |
| 91 | + |
| 92 | + self.log.info("Verify that all rescans found the same txs in slow and fast variants") |
| 93 | + assert_equal(len(txids_slow), NUM_DESCRIPTORS * NUM_BLOCKS) |
| 94 | + assert_equal(len(txids_fast), NUM_DESCRIPTORS * NUM_BLOCKS) |
| 95 | + assert_equal(len(txids_slow_nonactive), NUM_DESCRIPTORS * NUM_BLOCKS) |
| 96 | + assert_equal(len(txids_fast_nonactive), NUM_DESCRIPTORS * NUM_BLOCKS) |
| 97 | + assert_equal(sorted(txids_slow), sorted(txids_fast)) |
| 98 | + assert_equal(sorted(txids_slow_nonactive), sorted(txids_fast_nonactive)) |
| 99 | + |
| 100 | + |
| 101 | +if __name__ == '__main__': |
| 102 | + WalletFastRescanTest().main() |
0 commit comments