From 088e38d3bbea9694b319bc34e0d2e70d210c38b4 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Tue, 23 Aug 2022 16:10:59 +0200 Subject: [PATCH 1/8] add chain interface methods for using BIP 157 block filters This is useful for speeding up wallet rescans and is based on an earlier version from PR #15845 ("wallet: Fast rescan with BIP157 block filters"), which was never merged. Co-authored-by: MacroFake --- src/interfaces/chain.h | 8 ++++++++ src/node/interfaces.cpp | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 5fc0e540a94..7a3d88b18f5 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_INTERFACES_CHAIN_H #define BITCOIN_INTERFACES_CHAIN_H +#include #include // For CTransactionRef #include // For util::SettingsValue @@ -143,6 +144,13 @@ public: //! or one of its ancestors. virtual std::optional findLocatorFork(const CBlockLocator& locator) = 0; + //! Returns whether a block filter index is available. + virtual bool hasBlockFilterIndex(BlockFilterType filter_type) = 0; + + //! Returns whether any of the elements match the block via a BIP 157 block filter + //! or std::nullopt if the block filter for this block couldn't be found. + virtual std::optional blockFilterMatchesAny(BlockFilterType filter_type, const uint256& block_hash, const GCSFilter::ElementSet& filter_set) = 0; + //! Return whether node has the block and optionally return block metadata //! or contents. virtual bool findBlock(const uint256& hash, const FoundBlock& block={}) = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 8a0011a629e..979c6254633 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -4,10 +4,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -536,6 +538,20 @@ public: } return std::nullopt; } + bool hasBlockFilterIndex(BlockFilterType filter_type) override + { + return GetBlockFilterIndex(filter_type) != nullptr; + } + std::optional blockFilterMatchesAny(BlockFilterType filter_type, const uint256& block_hash, const GCSFilter::ElementSet& filter_set) override + { + const BlockFilterIndex* block_filter_index{GetBlockFilterIndex(filter_type)}; + if (!block_filter_index) return std::nullopt; + + BlockFilter filter; + const CBlockIndex* index{WITH_LOCK(::cs_main, return chainman().m_blockman.LookupBlockIndex(block_hash))}; + if (index == nullptr || !block_filter_index->LookupFilter(index, filter)) return std::nullopt; + return filter.GetFilter().MatchAny(filter_set); + } bool findBlock(const uint256& hash, const FoundBlock& block) override { WAIT_LOCK(cs_main, lock); From 845279132b494f03b84d689c666fdcfad37f5a42 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Fri, 26 Aug 2022 02:34:55 +0200 Subject: [PATCH 2/8] wallet: support fetching scriptPubKeys with minimum descriptor range index This extra method will be needed for updating the filter set for faster wallet rescans; after an internal top-up has happened, we only want to add the newly created scriptPubKeys. --- src/wallet/scriptpubkeyman.cpp | 9 +++++++-- src/wallet/scriptpubkeyman.h | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 39afb79600f..c432bfc221c 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -2644,13 +2644,18 @@ const WalletDescriptor DescriptorScriptPubKeyMan::GetWalletDescriptor() const } const std::unordered_set DescriptorScriptPubKeyMan::GetScriptPubKeys() const +{ + return GetScriptPubKeys(0); +} + +const std::unordered_set DescriptorScriptPubKeyMan::GetScriptPubKeys(int32_t minimum_index) const { LOCK(cs_desc_man); std::unordered_set script_pub_keys; script_pub_keys.reserve(m_map_script_pub_keys.size()); - for (auto const& script_pub_key: m_map_script_pub_keys) { - script_pub_keys.insert(script_pub_key.first); + for (auto const& [script_pub_key, index] : m_map_script_pub_keys) { + if (index >= minimum_index) script_pub_keys.insert(script_pub_key); } return script_pub_keys; } diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 3ab489c3744..4ff7fdb1547 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -643,6 +643,7 @@ public: const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); const std::unordered_set GetScriptPubKeys() const override; + const std::unordered_set GetScriptPubKeys(int32_t minimum_index) const; bool GetDescriptorString(std::string& out, const bool priv) const; From c051026586fb269584bcba41de8a4a90280f5a7e Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Wed, 12 Oct 2022 19:25:22 +0200 Subject: [PATCH 3/8] wallet: add method for retrieving the end range for a ScriptPubKeyMan --- src/wallet/scriptpubkeyman.cpp | 5 +++++ src/wallet/scriptpubkeyman.h | 1 + 2 files changed, 6 insertions(+) diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index c432bfc221c..4c534d64ec9 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -2660,6 +2660,11 @@ const std::unordered_set DescriptorScriptPubKeyMan::Ge return script_pub_keys; } +int32_t DescriptorScriptPubKeyMan::GetEndRange() const +{ + return m_max_cached_index + 1; +} + bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out, const bool priv) const { LOCK(cs_desc_man); diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 4ff7fdb1547..eb77015956a 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -644,6 +644,7 @@ public: const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); const std::unordered_set GetScriptPubKeys() const override; const std::unordered_set GetScriptPubKeys(int32_t minimum_index) const; + int32_t GetEndRange() const; bool GetDescriptorString(std::string& out, const bool priv) const; From 70b35139040a2351c845a1cec1dafd2fbcd16e93 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Fri, 26 Aug 2022 01:41:01 +0200 Subject: [PATCH 4/8] wallet: add `FastWalletRescanFilter` class for speeding up rescans This only supports wallet descriptors right now. --- src/wallet/wallet.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 671c432b101..e77648f7ffd 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -5,6 +5,7 @@ #include +#include #include #include #include @@ -261,6 +262,64 @@ std::shared_ptr LoadWalletInternal(WalletContext& context, const std::s return nullptr; } } + +class FastWalletRescanFilter +{ +public: + FastWalletRescanFilter(const CWallet& wallet) : m_wallet(wallet) + { + // fast rescanning via block filters is only supported by descriptor wallets right now + assert(!m_wallet.IsLegacy()); + + // create initial filter with scripts from all ScriptPubKeyMans + for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) { + auto desc_spkm{dynamic_cast(spkm)}; + assert(desc_spkm != nullptr); + AddScriptPubKeys(desc_spkm); + // save each range descriptor's end for possible future filter updates + if (desc_spkm->IsHDEnabled()) { + m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange()); + } + } + } + + void UpdateIfNeeded() + { + // repopulate filter with new scripts if top-up has happened since last iteration + for (const auto& [desc_spkm_id, last_range_end] : m_last_range_ends) { + auto desc_spkm{dynamic_cast(m_wallet.GetScriptPubKeyMan(desc_spkm_id))}; + assert(desc_spkm != nullptr); + int32_t current_range_end{desc_spkm->GetEndRange()}; + if (current_range_end > last_range_end) { + AddScriptPubKeys(desc_spkm, last_range_end); + m_last_range_ends.at(desc_spkm->GetID()) = current_range_end; + } + } + } + + std::optional MatchesBlock(const uint256& block_hash) const + { + return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set); + } + +private: + const CWallet& m_wallet; + /** Map for keeping track of each range descriptor's last seen end range. + * This information is used to detect whether new addresses were derived + * (that is, if the current end range is larger than the saved end range) + * after processing a block and hence a filter set update is needed to + * take possible keypool top-ups into account. + */ + std::map m_last_range_ends; + GCSFilter::ElementSet m_filter_set; + + void AddScriptPubKeys(const DescriptorScriptPubKeyMan* desc_spkm, int32_t last_range_end = 0) + { + for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) { + m_filter_set.emplace(script_pub_key.begin(), script_pub_key.end()); + } + } +}; } // namespace std::shared_ptr LoadWallet(WalletContext& context, const std::string& name, std::optional load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector& warnings) From 935c6c4b234bbb0565cda6f58ee298048856acae Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Fri, 26 Aug 2022 02:57:41 +0200 Subject: [PATCH 5/8] wallet: take use of `FastWalletRescanFilter` Can be reviewed with `--ignore-all-space`. --- src/wallet/wallet.cpp | 77 ++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e77648f7ffd..912f995ca2c 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1814,7 +1814,11 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc uint256 block_hash = start_block; ScanResult result; - WalletLogPrintf("Rescan started from block %s...\n", start_block.ToString()); + std::unique_ptr fast_rescan_filter; + if (!IsLegacy() && chain().hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique(*this); + + WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(), + fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks"); fAbortRescan = false; ShowProgress(strprintf("%s " + _("Rescanning…").translated, GetDisplayName()), 0); // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption) @@ -1841,9 +1845,16 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current); } - // Read block data - CBlock block; - chain().findBlock(block_hash, FoundBlock().data(block)); + bool fetch_block{true}; + if (fast_rescan_filter) { + fast_rescan_filter->UpdateIfNeeded(); + auto matches_block{fast_rescan_filter->MatchesBlock(block_hash)}; + if (matches_block.has_value() && !*matches_block) { + result.last_scanned_block = block_hash; + result.last_scanned_height = block_height; + fetch_block = false; + } + } // Find next block separately from reading data above, because reading // is slow and there might be a reorg while it is read. @@ -1852,35 +1863,41 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc uint256 next_block_hash; chain().findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(next_block).hash(next_block_hash))); - if (!block.IsNull()) { - LOCK(cs_wallet); - if (!block_still_active) { - // Abort scan if current block is no longer active, to prevent - // marking transactions as coming from the wrong block. + if (fetch_block) { + // Read block data + CBlock block; + chain().findBlock(block_hash, FoundBlock().data(block)); + + if (!block.IsNull()) { + LOCK(cs_wallet); + if (!block_still_active) { + // Abort scan if current block is no longer active, to prevent + // marking transactions as coming from the wrong block. + result.last_failed_block = block_hash; + result.status = ScanResult::FAILURE; + break; + } + for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { + SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast(posInBlock)}, fUpdate, /*rescanning_old_block=*/true); + } + // scan succeeded, record block as most recent successfully scanned + result.last_scanned_block = block_hash; + result.last_scanned_height = block_height; + + if (save_progress && next_interval) { + CBlockLocator loc = m_chain->getActiveChainLocator(block_hash); + + if (!loc.IsNull()) { + WalletLogPrintf("Saving scan progress %d.\n", block_height); + WalletBatch batch(GetDatabase()); + batch.WriteBestBlock(loc); + } + } + } else { + // could not scan block, keep scanning but record this block as the most recent failure result.last_failed_block = block_hash; result.status = ScanResult::FAILURE; - break; } - for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { - SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast(posInBlock)}, fUpdate, /*rescanning_old_block=*/true); - } - // scan succeeded, record block as most recent successfully scanned - result.last_scanned_block = block_hash; - result.last_scanned_height = block_height; - - if (save_progress && next_interval) { - CBlockLocator loc = m_chain->getActiveChainLocator(block_hash); - - if (!loc.IsNull()) { - WalletLogPrintf("Saving scan progress %d.\n", block_height); - WalletBatch batch(GetDatabase()); - batch.WriteBestBlock(loc); - } - } - } else { - // could not scan block, keep scanning but record this block as the most recent failure - result.last_failed_block = block_hash; - result.status = ScanResult::FAILURE; } if (max_height && block_height >= *max_height) { break; From 3449880b499d54bfbcf6caeed52851ce55259ed7 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Sat, 1 Oct 2022 12:23:59 +0200 Subject: [PATCH 6/8] wallet: fast rescan: show log message for every non-skipped block For that purpose, a new logging category BCLog::SCAN is introduced. --- src/logging.cpp | 3 +++ src/logging.h | 1 + src/wallet/wallet.cpp | 14 ++++++++++---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/logging.cpp b/src/logging.cpp index c95c0b7e377..ed0c2a56a5c 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -181,6 +181,7 @@ const CLogCategoryDesc LogCategories[] = {BCLog::UTIL, "util"}, {BCLog::BLOCKSTORE, "blockstorage"}, {BCLog::TXRECONCILIATION, "txreconciliation"}, + {BCLog::SCAN, "scan"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, }; @@ -283,6 +284,8 @@ std::string LogCategoryToStr(BCLog::LogFlags category) return "blockstorage"; case BCLog::LogFlags::TXRECONCILIATION: return "txreconciliation"; + case BCLog::LogFlags::SCAN: + return "scan"; case BCLog::LogFlags::ALL: return "all"; } diff --git a/src/logging.h b/src/logging.h index 5ee6665c763..14a0f08f8dc 100644 --- a/src/logging.h +++ b/src/logging.h @@ -67,6 +67,7 @@ namespace BCLog { UTIL = (1 << 25), BLOCKSTORE = (1 << 26), TXRECONCILIATION = (1 << 27), + SCAN = (1 << 28), ALL = ~(uint32_t)0, }; enum class Level { diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 912f995ca2c..560058af030 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1849,10 +1849,16 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc if (fast_rescan_filter) { fast_rescan_filter->UpdateIfNeeded(); auto matches_block{fast_rescan_filter->MatchesBlock(block_hash)}; - if (matches_block.has_value() && !*matches_block) { - result.last_scanned_block = block_hash; - result.last_scanned_height = block_height; - fetch_block = false; + if (matches_block.has_value()) { + if (*matches_block) { + LogPrint(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString()); + } else { + result.last_scanned_block = block_hash; + result.last_scanned_height = block_height; + fetch_block = false; + } + } else { + LogPrint(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString()); } } From ca48a4694f73e5be8f971ae482ebc2cce4caef44 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Fri, 2 Sep 2022 18:02:53 +0200 Subject: [PATCH 7/8] rpc: doc: mention rescan speedup using `blockfilterindex=1` in affected wallet RPCs --- src/wallet/rpc/backup.cpp | 7 +++++-- src/wallet/rpc/transactions.cpp | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 1206a428fce..1d2d7d2a10d 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1589,7 +1589,8 @@ RPCHelpMan importdescriptors() return RPCHelpMan{"importdescriptors", "\nImport descriptors. This will trigger a rescan of the blockchain based on the earliest timestamp of all descriptors being imported. Requires a new wallet backup.\n" "\nNote: This call can take over an hour to complete if using an early timestamp; during that time, other rpc calls\n" - "may report that the imported keys, addresses or scripts exist but related transactions are still missing.\n", + "may report that the imported keys, addresses or scripts exist but related transactions are still missing.\n" + "The rescan is significantly faster if block filters are available (using startup option \"-blockfilterindex=1\").\n", { {"requests", RPCArg::Type::ARR, RPCArg::Optional::NO, "Data to be imported", { @@ -1887,7 +1888,9 @@ RPCHelpMan restorewallet() { return RPCHelpMan{ "restorewallet", - "\nRestore and loads a wallet from backup.\n", + "\nRestore and loads a wallet from backup.\n" + "\nThe rescan is significantly faster if a descriptor wallet is restored" + "\nand block filters are available (using startup option \"-blockfilterindex=1\").\n", { {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name that will be applied to the restored wallet"}, {"backup_file", RPCArg::Type::STR, RPCArg::Optional::NO, "The backup file that will be used to restore the wallet."}, diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp index 0e13e4756bc..0fe4ed703dd 100644 --- a/src/wallet/rpc/transactions.cpp +++ b/src/wallet/rpc/transactions.cpp @@ -839,7 +839,9 @@ RPCHelpMan rescanblockchain() { return RPCHelpMan{"rescanblockchain", "\nRescan the local blockchain for wallet related transactions.\n" - "Note: Use \"getwalletinfo\" to query the scanning progress.\n", + "Note: Use \"getwalletinfo\" to query the scanning progress.\n" + "The rescan is significantly faster when used on a descriptor wallet\n" + "and block filters are available (using startup option \"-blockfilterindex=1\").\n", { {"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "block height where the rescan should start"}, {"stop_height", RPCArg::Type::NUM, RPCArg::Optional::OMITTED_NAMED_ARG, "the last block height that should be scanned. If none is provided it will rescan up to the tip at return time of this call."}, From 0582932260e7de4e8aba01d63e7c8a9ddb9c3685 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Thu, 8 Sep 2022 18:27:54 +0200 Subject: [PATCH 8/8] test: add test for fast rescan using block filters (top-up detection) --- test/functional/test_runner.py | 1 + test/functional/wallet_fast_rescan.py | 102 ++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100755 test/functional/wallet_fast_rescan.py diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index e20de8ea8ee..e2c13a67056 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -136,6 +136,7 @@ BASE_SCRIPTS = [ # vv Tests less than 30s vv 'wallet_keypool_topup.py --legacy-wallet', 'wallet_keypool_topup.py --descriptors', + 'wallet_fast_rescan.py --descriptors', 'feature_fee_estimation.py', 'interface_zmq.py', 'rpc_invalid_address_message.py', diff --git a/test/functional/wallet_fast_rescan.py b/test/functional/wallet_fast_rescan.py new file mode 100755 index 00000000000..3b8ae8eb925 --- /dev/null +++ b/test/functional/wallet_fast_rescan.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test that fast rescan using block filters for descriptor wallets detects + top-ups correctly and finds the same transactions than the slow variant.""" +import os +from typing import List + +from test_framework.descriptors import descsum_create +from test_framework.test_framework import BitcoinTestFramework +from test_framework.test_node import TestNode +from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet +from test_framework.wallet_util import get_generate_key + + +KEYPOOL_SIZE = 100 # smaller than default size to speed-up test +NUM_DESCRIPTORS = 9 # number of descriptors (8 default ranged ones + 1 fixed non-ranged one) +NUM_BLOCKS = 6 # number of blocks to mine + + +class WalletFastRescanTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [[f'-keypool={KEYPOOL_SIZE}', '-blockfilterindex=1']] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + self.skip_if_no_sqlite() + + def get_wallet_txids(self, node: TestNode, wallet_name: str) -> List[str]: + w = node.get_wallet_rpc(wallet_name) + txs = w.listtransactions('*', 1000000) + return [tx['txid'] for tx in txs] + + def run_test(self): + node = self.nodes[0] + wallet = MiniWallet(node) + wallet.rescan_utxos() + + self.log.info("Create descriptor wallet with backup") + WALLET_BACKUP_FILENAME = os.path.join(node.datadir, 'wallet.bak') + node.createwallet(wallet_name='topup_test', descriptors=True) + w = node.get_wallet_rpc('topup_test') + fixed_key = get_generate_key() + print(w.importdescriptors([{"desc": descsum_create(f"wpkh({fixed_key.privkey})"), "timestamp": "now"}])) + descriptors = w.listdescriptors()['descriptors'] + assert_equal(len(descriptors), NUM_DESCRIPTORS) + w.backupwallet(WALLET_BACKUP_FILENAME) + + self.log.info(f"Create txs sending to end range address of each descriptor, triggering top-ups") + for i in range(NUM_BLOCKS): + self.log.info(f"Block {i+1}/{NUM_BLOCKS}") + for desc_info in w.listdescriptors()['descriptors']: + if 'range' in desc_info: + start_range, end_range = desc_info['range'] + addr = w.deriveaddresses(desc_info['desc'], [end_range, end_range])[0] + spk = bytes.fromhex(w.getaddressinfo(addr)['scriptPubKey']) + self.log.info(f"-> range [{start_range},{end_range}], last address {addr}") + else: + spk = bytes.fromhex(fixed_key.p2wpkh_script) + self.log.info(f"-> fixed non-range descriptor address {fixed_key.p2wpkh_addr}") + wallet.send_to(from_node=node, scriptPubKey=spk, amount=10000) + self.generate(node, 1) + + self.log.info("Import wallet backup with block filter index") + with node.assert_debug_log(['fast variant using block filters']): + node.restorewallet('rescan_fast', WALLET_BACKUP_FILENAME) + txids_fast = self.get_wallet_txids(node, 'rescan_fast') + + self.log.info("Import non-active descriptors with block filter index") + node.createwallet(wallet_name='rescan_fast_nonactive', descriptors=True, disable_private_keys=True, blank=True) + with node.assert_debug_log(['fast variant using block filters']): + w = node.get_wallet_rpc('rescan_fast_nonactive') + w.importdescriptors([{"desc": descriptor['desc'], "timestamp": 0} for descriptor in descriptors]) + txids_fast_nonactive = self.get_wallet_txids(node, 'rescan_fast_nonactive') + + self.restart_node(0, [f'-keypool={KEYPOOL_SIZE}', '-blockfilterindex=0']) + self.log.info("Import wallet backup w/o block filter index") + with node.assert_debug_log(['slow variant inspecting all blocks']): + node.restorewallet("rescan_slow", WALLET_BACKUP_FILENAME) + txids_slow = self.get_wallet_txids(node, 'rescan_slow') + + self.log.info("Import non-active descriptors w/o block filter index") + node.createwallet(wallet_name='rescan_slow_nonactive', descriptors=True, disable_private_keys=True, blank=True) + with node.assert_debug_log(['slow variant inspecting all blocks']): + w = node.get_wallet_rpc('rescan_slow_nonactive') + w.importdescriptors([{"desc": descriptor['desc'], "timestamp": 0} for descriptor in descriptors]) + txids_slow_nonactive = self.get_wallet_txids(node, 'rescan_slow_nonactive') + + self.log.info("Verify that all rescans found the same txs in slow and fast variants") + assert_equal(len(txids_slow), NUM_DESCRIPTORS * NUM_BLOCKS) + assert_equal(len(txids_fast), NUM_DESCRIPTORS * NUM_BLOCKS) + assert_equal(len(txids_slow_nonactive), NUM_DESCRIPTORS * NUM_BLOCKS) + assert_equal(len(txids_fast_nonactive), NUM_DESCRIPTORS * NUM_BLOCKS) + assert_equal(sorted(txids_slow), sorted(txids_fast)) + assert_equal(sorted(txids_slow_nonactive), sorted(txids_fast_nonactive)) + + +if __name__ == '__main__': + WalletFastRescanTest().main()