0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-08 10:31:50 -05:00

Merge bitcoin/bitcoin#25957: wallet: fast rescan with BIP157 block filters for descriptor wallets

0582932260 test: add test for fast rescan using block filters (top-up detection) (Sebastian Falbesoner)
ca48a4694f rpc: doc: mention rescan speedup using `blockfilterindex=1` in affected wallet RPCs (Sebastian Falbesoner)
3449880b49 wallet: fast rescan: show log message for every non-skipped block (Sebastian Falbesoner)
935c6c4b23 wallet: take use of `FastWalletRescanFilter` (Sebastian Falbesoner)
70b3513904 wallet: add `FastWalletRescanFilter` class for speeding up rescans (Sebastian Falbesoner)
c051026586 wallet: add method for retrieving the end range for a ScriptPubKeyMan (Sebastian Falbesoner)
845279132b wallet: support fetching scriptPubKeys with minimum descriptor range index (Sebastian Falbesoner)
088e38d3bb add chain interface methods for using BIP 157 block filters (Sebastian Falbesoner)

Pull request description:

  ## Description

  This PR is another take of using BIP 157 block filters (enabled by `-blockfilterindex=1`) for faster wallet rescans and is a modern revival of #15845. For reviewers new to this topic I can highly recommend to read the corresponding PR review club (https://bitcoincore.reviews/15845).

  The basic idea is to skip blocks for deeper inspection (i.e. looking at every single tx for matches) if our block filter doesn't match any of the block's spent or created UTXOs are relevant for our wallet. Note that there can be false-positives (see https://bitcoincore.reviews/15845#l-199 for a PR review club discussion about false-positive rates), but no false-negatives, i.e. it is safe to skip blocks if the filter doesn't match; if the filter *does* match even though there are no wallet-relevant txs in the block, no harm is done, only a little more time is spent extra.

  In contrast to #15845, this solution only supports descriptor wallets, which are way more widespread now than back in the time >3 years ago. With that approach, we don't have to ever derive the relevant scriptPubKeys ourselves from keys before populating the filter, and can instead shift the full responsibility to that to the `DescriptorScriptPubKeyMan` which already takes care of that automatically. Compared to legacy wallets, the `IsMine` logic for descriptor wallets is as trivial as checking if a scriptPubKey is included in the ScriptPubKeyMan's set of scriptPubKeys (`m_map_script_pub_keys`): e191fac4f3/src/wallet/scriptpubkeyman.cpp (L1703-L1710)

  One of the unaddressed issues of #15845 was that [the filter was only created once outside the loop](https://github.com/bitcoin/bitcoin/pull/15845#discussion_r343265997) and as such didn't take into account possible top-ups that have happened. This is solved here by keeping a state of ranged `DescriptorScriptPubKeyMan`'s descriptor end ranges and check at each iteration whether that range has increased since last time. If yes, we update the filter with all scriptPubKeys that have been added since the last filter update with a range index equal or higher than the last end range. Note that finding new scriptPubKeys could be made more efficient than linearly iterating through the whole `m_script_pub_keys` map (e.g. by introducing a bidirectional map), but this would mean introducing additional complexity and state and it's probably not worth it at this time, considering that the performance gain is already significant.

  Output scripts from non-ranged `DescriptorScriptPubKeyMan`s (i.e. ones with a fixed set of output scripts that is never extended) are added only once when the filter is created first.

  ## Benchmark results

  Obviously, the speed-up indirectly correlates with the wallet tx frequency in the scanned range: the more blocks contain wallet-related transactions, the less blocks can be skipped due to block filter detection.

  In a [simple benchmark](https://github.com/theStack/bitcoin/blob/fast_rescan_functional_test_benchmark/test/functional/pr25957_benchmark.py), a regtest chain with 1008 blocks (corresponding to 1 week) is mined with 20000 scriptPubKeys contained (25 txs * 800 outputs) each. The blocks each have a weight of ~2500000 WUs and hence are about 62.5% full. A global constant `WALLET_TX_BLOCK_FREQUENCY` defines how often wallet-related txs are included in a block. The created descriptor wallet (default setting of `keypool=1000`, we have 8*1000 = 8000 scriptPubKeys at the start) is backuped via the `backupwallet` RPC before the mining starts and imported via `restorewallet` RPC after. The measured time for taking this import process (which involves a rescan) once with block filters (`-blockfilterindex=1`) and once without block filters (`-blockfilterindex=0`) yield the relevant result numbers for the benchmark.

  The following table lists the results, sorted from worst-case (all blocks contain wallte-relevant txs, 0% can be skipped) to best-case (no blocks contain walltet-relevant txs, 100% can be skipped) where the frequencies have been picked arbitrarily:

  wallet-related tx frequency; 1 tx per...    | ratio of irrelevant blocks  | w/o filters | with filters | speed gain
  --------------------------------------------|-----------------------------|-------------|--------------|-------------
  ~ 10 minutes (every block)                  |              0%             |   56.806s   |   63.554s    |  ~0.9x
  ~ 20 minutes (every 2nd block)              |           50% (1/2)         |   58.896s   |   36.076s    |  ~1.6x
  ~ 30 minutes (every 3rd block)              |          66.67% (2/3)       |   56.781s   |   25.430s    |  ~2.2x
  ~ 1 hour (every 6th block)                  |          83.33% (5/6)       |   58.193s   |   15.786s    |  ~3.7x
  ~ 6 hours (every 36th block)                |          97.22% (35/36)     |   57.500s   |    6.935s    |  ~8.3x
  ~ 1 day (every 144th block)                 |         99.31% (143/144)    |   68.881s   |    6.107s    | ~11.3x
    (no txs)                                  |              100%           |   58.529s   |    5.630s    | ~10.4x

  Since even the (rather unrealistic) worst-case scenario of having wallet-related txs in _every_ block of the rescan range obviously doesn't take significantly longer, I'd argue it's reasonable to always take advantage of block filters if they are available and there's no need to provide an option for the user.

  Feedback about the general approach (but also about details like naming, where I struggled a lot) would be greatly appreciated. Thanks fly out to furszy for discussing this subject and patiently answering basic question about descriptor wallets!

ACKs for top commit:
  achow101:
    ACK 0582932260
  Sjors:
    re-utACK 0582932260
  aureleoules:
    ACK 0582932260 - minor changes, documentation and updated test since last review
  w0xlt:
    re-ACK 0582932260

Tree-SHA512: 3289ba6e4572726e915d19f3e8b251d12a4cec8c96d041589956c484b5575e3708b14f6e1e121b05fe98aff1c8724de4564a5a9123f876967d33343cbef242e1
This commit is contained in:
Andrew Chow 2022-10-26 11:12:28 -04:00
commit 48af307481
No known key found for this signature in database
GPG key ID: 17565732E08E5E41
11 changed files with 265 additions and 35 deletions

View file

@ -5,6 +5,7 @@
#ifndef BITCOIN_INTERFACES_CHAIN_H
#define BITCOIN_INTERFACES_CHAIN_H
#include <blockfilter.h>
#include <primitives/transaction.h> // For CTransactionRef
#include <util/settings.h> // For util::SettingsValue
@ -143,6 +144,13 @@ public:
//! or one of its ancestors.
virtual std::optional<int> 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<bool> 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;

View file

@ -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";
}

View file

@ -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 {

View file

@ -4,10 +4,12 @@
#include <addrdb.h>
#include <banman.h>
#include <blockfilter.h>
#include <chain.h>
#include <chainparams.h>
#include <deploymentstatus.h>
#include <external_signer.h>
#include <index/blockfilterindex.h>
#include <init.h>
#include <interfaces/chain.h>
#include <interfaces/handler.h>
@ -536,6 +538,20 @@ public:
}
return std::nullopt;
}
bool hasBlockFilterIndex(BlockFilterType filter_type) override
{
return GetBlockFilterIndex(filter_type) != nullptr;
}
std::optional<bool> 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);

View file

@ -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."},

View file

@ -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."},

View file

@ -2644,17 +2644,27 @@ const WalletDescriptor DescriptorScriptPubKeyMan::GetWalletDescriptor() const
}
const std::unordered_set<CScript, SaltedSipHasher> DescriptorScriptPubKeyMan::GetScriptPubKeys() const
{
return GetScriptPubKeys(0);
}
const std::unordered_set<CScript, SaltedSipHasher> DescriptorScriptPubKeyMan::GetScriptPubKeys(int32_t minimum_index) const
{
LOCK(cs_desc_man);
std::unordered_set<CScript, SaltedSipHasher> 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;
}
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);

View file

@ -643,6 +643,8 @@ public:
const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
const std::unordered_set<CScript, SaltedSipHasher> GetScriptPubKeys() const override;
const std::unordered_set<CScript, SaltedSipHasher> GetScriptPubKeys(int32_t minimum_index) const;
int32_t GetEndRange() const;
bool GetDescriptorString(std::string& out, const bool priv) const;

View file

@ -5,6 +5,7 @@
#include <wallet/wallet.h>
#include <blockfilter.h>
#include <chain.h>
#include <consensus/amount.h>
#include <consensus/consensus.h>
@ -261,6 +262,64 @@ std::shared_ptr<CWallet> 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<DescriptorScriptPubKeyMan*>(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<DescriptorScriptPubKeyMan*>(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<bool> 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<uint256, int32_t> 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<CWallet> LoadWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings)
@ -1755,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<FastWalletRescanFilter> fast_rescan_filter;
if (!IsLegacy() && chain().hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique<FastWalletRescanFilter>(*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)
@ -1782,9 +1845,22 @@ 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()) {
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());
}
}
// Find next block separately from reading data above, because reading
// is slow and there might be a reorg while it is read.
@ -1793,35 +1869,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<int>(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<int>(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;

View file

@ -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',

View file

@ -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()