From 7482a84fa0a7dd81478664c3b6ecd276eb8bbbfe Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 11 Aug 2026 15:06:22 +0700 Subject: [PATCH 1/2] perf(dash-spv): cache the filter-scan query keyed by wallet monitor revision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scan_batch re-collected every behind wallet's scan scripts and re-grouped them into FilterQuerys on every batch, even though the query only changes when the wallet's monitored set does. The pruning pass added for #948 made that per-batch assembly O(total addresses) per wallet (~165us at 6k historical CoinJoin addresses), repeated across the ~1,160 batches of a full mainnet scan while nothing changed. Maintain the query as persistent state instead: FiltersManager keeps a per-wallet CachedWalletQuery (scripts, bare elements, pre-grouped FilterQuery) keyed by the new WalletInterface::wallet_monitor_revision — per-wallet account revisions plus account_generation, which move exactly when an address is derived, an account is added, or a UTXO is created or spent. The union query over the behind set is cached the same way, keyed by the sorted (wallet, revision) pairs it was assembled from. During a quiet catch-up the revision never moves, so consecutive batches share one assembled query and the wallet read lock is held only for the revision check. check_compact_filters_for_query lets callers match a pre-built FilterQuery; check_compact_filters_for_elements builds one and delegates. Co-Authored-By: Claude Fable 5 --- dash-spv/src/sync/filters/manager.rs | 244 ++++++++++++++---- key-wallet-manager/benches/filter_scan.rs | 19 +- key-wallet-manager/src/lib.rs | 4 +- key-wallet-manager/src/matching.rs | 22 +- key-wallet-manager/src/process_block.rs | 56 ++++ .../src/test_utils/mock_wallet.rs | 18 ++ key-wallet-manager/src/wallet_interface.rs | 16 ++ 7 files changed, 330 insertions(+), 49 deletions(-) diff --git a/dash-spv/src/sync/filters/manager.rs b/dash-spv/src/sync/filters/manager.rs index fcc907b09..e533499e1 100644 --- a/dash-spv/src/sync/filters/manager.rs +++ b/dash-spv/src/sync/filters/manager.rs @@ -23,7 +23,9 @@ use crate::validation::{FilterValidationInput, FilterValidator, Validator}; use crate::sync::progress::ProgressPercentage; use dashcore::hash_types::FilterHeader; use key_wallet_manager::WalletInterface; -use key_wallet_manager::{check_compact_filters_for_elements, FilterMatchKey, WalletId}; +use key_wallet_manager::{ + check_compact_filters_for_elements, check_compact_filters_for_query, FilterMatchKey, WalletId, +}; use tokio::sync::RwLock; /// Batch size for processing filters. @@ -35,11 +37,26 @@ struct WalletScanState { id: WalletId, /// The wallet's committed sync checkpoint; heights at or below it are skipped. synced: u32, - /// Monitored scriptPubKeys. + /// The wallet's cached scan query (see [`CachedWalletQuery`]). + cached: Arc, +} + +/// One wallet's forward-scan query, built once and reused across every batch +/// until the wallet's monitor revision moves (an address derived, an account +/// added, or a UTXO created or spent — the only events that can change the +/// scan set). Rebuilding per batch would re-collect and re-group the same +/// scripts hundreds of times over a long catch-up while nothing changed. +struct CachedWalletQuery { + /// The `wallet_monitor_revision` this entry was built at. + revision: u64, + /// Pruned scan scriptPubKeys, kept for assembling the union query. scripts: Vec, - /// Bare `hash160` filter elements (owner/voting key hashes) a compact - /// filter carries beyond the scriptPubKeys. + /// Bare filter elements (owner/voting key hashes, watched outpoints), + /// kept for assembling the union query. elements: Vec>, + /// Pre-grouped query over `scripts` + `elements`, used to attribute + /// matched blocks to this wallet. + query: FilterQuery, } /// Maximum number of batches to scan ahead while waiting for blocks. @@ -84,6 +101,28 @@ pub struct FiltersManager< /// `BlockProcessed` and the per-wallet record of which wallets already /// have a given processed block applied. pub(super) tracker: BlockMatchTracker, + + // === Scan-query caches === + /// Per-wallet scan queries keyed by the wallet's monitor revision; + /// entries are rebuilt lazily in `scan_batch` when the revision moves. + /// Entries for removed wallets linger harmlessly (never matched again); + /// the map is bounded by the number of wallets ever managed. + scan_query_cache: HashMap>, + /// The union query over the behind set, keyed by the exact + /// `(wallet, revision)` pairs it was assembled from. Reused as long as + /// the behind set and every member's revision are unchanged. + union_query_cache: Option, +} + +/// The assembled union scan query and the per-wallet revisions it reflects. +struct CachedUnionQuery { + /// Sorted `(wallet, revision)` pairs the union was built from; any + /// difference — a wallet entering or leaving the behind set, or a + /// revision moving — invalidates the entry. + key: Vec<(WalletId, u64)>, + /// The pre-grouped union query over every member wallet's scripts and + /// bare elements. + query: Arc, } impl @@ -128,6 +167,8 @@ impl = Vec::new(); for wallet_id in &behind { let synced = wallet.wallet_synced_height(wallet_id); - // The scan query, not the full monitored set: spent single-use - // (CoinJoin) addresses are pruned so the per-filter match cost - // stays bounded by active UTXOs + gap lookahead instead of - // growing with every historical mixing round - // (dashpay/rust-dashcore#948). - let scripts = wallet.scan_script_pubkeys_for(wallet_id); - // Bare owner/voting key hashes a compact filter carries beyond the - // wallet's scriptPubKeys. - let elements = wallet.monitored_filter_elements_for(wallet_id); - if !scripts.is_empty() || !elements.is_empty() { + // Reuse the wallet's cached scan query unless its monitor + // revision moved (an address derived, an account added, or a + // UTXO created/spent — the only events that can change the scan + // set). During a quiet catch-up the revision never moves, so + // hundreds of consecutive batches share one query. + let revision = wallet.wallet_monitor_revision(wallet_id); + let cached = match self.scan_query_cache.get(wallet_id) { + Some(entry) if entry.revision == revision => Arc::clone(entry), + _ => { + // The scan query, not the full monitored set: spent + // single-use (CoinJoin) addresses are pruned so the + // per-filter match cost stays bounded by active UTXOs + + // gap lookahead instead of growing with every historical + // mixing round (dashpay/rust-dashcore#948). + let scripts = wallet.scan_script_pubkeys_for(wallet_id); + // Bare owner/voting key hashes a compact filter carries + // beyond the wallet's scriptPubKeys. + let elements = wallet.monitored_filter_elements_for(wallet_id); + let mut query: FilterQuery = scripts.iter().map(|s| s.as_bytes()).collect(); + for element in &elements { + query.push(element); + } + let entry = Arc::new(CachedWalletQuery { + revision, + scripts, + elements, + query, + }); + self.scan_query_cache.insert(*wallet_id, Arc::clone(&entry)); + entry + } + }; + if !cached.scripts.is_empty() || !cached.elements.is_empty() { wallet_states.push(WalletScanState { id: *wallet_id, synced, - scripts, - elements, + cached, }); } } @@ -909,28 +972,36 @@ impl = - wallet_states.iter().flat_map(|s| s.scripts.iter().cloned()).collect(); - let union_elements: Vec> = - wallet_states.iter().flat_map(|s| s.elements.iter().cloned()).collect(); + // Single-pass union-then-attribute: match the union query over all + // behind wallets once, then for each matched block re-test the + // per-wallet cached queries to attribute the match correctly. + // + // The union query is itself cached: it only changes when the behind + // set changes or a member wallet's revision moves, so consecutive + // batches over a stable wallet set reuse the assembled query as-is. let min_synced = wallet_states.iter().map(|s| s.synced).min().unwrap_or(0); - - // Pre-group each wallet's scripts and bare elements by length once; - // reused across every matched filter. - let wallet_queries: Vec<(WalletId, u32, FilterQuery)> = wallet_states - .iter() - .map(|s| { - let mut query: FilterQuery = s.scripts.iter().map(|sp| sp.as_bytes()).collect(); - for element in &s.elements { - query.push(element); + let union_key: Vec<(WalletId, u64)> = + wallet_states.iter().map(|s| (s.id, s.cached.revision)).collect(); + let union_query = match &self.union_query_cache { + Some(cached) if cached.key == union_key => Arc::clone(&cached.query), + _ => { + let mut query = FilterQuery::new(); + for state in &wallet_states { + for script in &state.cached.scripts { + query.push(script.as_bytes()); + } + for element in &state.cached.elements { + query.push(element); + } } - (s.id, s.synced, query) - }) - .collect(); + let query = Arc::new(query); + self.union_query_cache = Some(CachedUnionQuery { + key: union_key, + query: Arc::clone(&query), + }); + query + } + }; let block_to_wallets = { let Some(batch) = self.active_batches.get(&batch_start) else { @@ -938,12 +1009,7 @@ impl> = BTreeMap::new(); for key in matches { @@ -955,11 +1021,11 @@ impl matched, Err(e) => { tracing::warn!( @@ -971,7 +1037,7 @@ impl (FilterMatchKey, FilterMatchKey) { + let mut filters: HashMap = HashMap::new(); + let (key_a, f_a) = filter_for_address(height_a, address_a); + let (key_b, f_b) = filter_for_address(height_b, address_b); + filters.insert(key_a.clone(), f_a); + filters.insert(key_b.clone(), f_b); + let mut batch = FiltersBatch::new(start, start + 99, filters); + batch.mark_verified(); + manager.active_batches.insert(start, batch); + (key_a, key_b) + } + + let needed = |events: &[SyncEvent]| -> BTreeMap> { + events + .iter() + .find_map(|e| match e { + SyncEvent::BlocksNeeded { + blocks, + } => Some(blocks.clone()), + _ => None, + }) + .unwrap_or_default() + }; + + manager.progress.update_stored_height(299); + + // Batch 1: the scan query is [A]; only A's block is needed. + let (key_a, key_b) = seed_batch(&mut manager, &address_a, &address_b, 0, 30, 60); + let blocks = needed(&manager.scan_batch(0).await.unwrap()); + assert!(blocks.contains_key(&key_a)); + assert!(!blocks.contains_key(&key_b)); + + // Batch 2: scripts change to [B] but the revision does not move — + // the cached [A] query keeps being served. + multi_handle.write().await.set_scan_addresses(wallet_id, vec![address_b.clone()]); + let (key_a, key_b) = seed_batch(&mut manager, &address_a, &address_b, 100, 130, 160); + let blocks = needed(&manager.scan_batch(100).await.unwrap()); + assert!(blocks.contains_key(&key_a), "unchanged revision must reuse the cached query"); + assert!(!blocks.contains_key(&key_b)); + + // Batch 3: the revision moves — the cache is rebuilt and the [B] + // query takes effect. + multi_handle.write().await.set_wallet_revision(wallet_id, 1); + let (key_a, key_b) = seed_batch(&mut manager, &address_a, &address_b, 200, 230, 260); + let blocks = needed(&manager.scan_batch(200).await.unwrap()); + assert!(blocks.contains_key(&key_b), "revision bump must rebuild the cached query"); + assert!(!blocks.contains_key(&key_a)); + } + /// `rescan_batch` with multiple wallets in `scripts_by_wallet`: /// each wallet's new scripts are matched independently and the /// attribution is correct in the emitted `BlocksNeeded`. diff --git a/key-wallet-manager/benches/filter_scan.rs b/key-wallet-manager/benches/filter_scan.rs index e9eed83d2..fc26c12cd 100644 --- a/key-wallet-manager/benches/filter_scan.rs +++ b/key-wallet-manager/benches/filter_scan.rs @@ -23,7 +23,7 @@ use std::collections::HashMap; use std::hint::black_box; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; -use dashcore::bip158::BlockFilter; +use dashcore::bip158::{BlockFilter, FilterQuery}; use dashcore::hashes::Hash; use dashcore::{Address, Block, OutPoint, Transaction, TxOut, Txid}; use key_wallet::account::ManagedAccountTrait; @@ -168,6 +168,23 @@ fn bench_filter_scan(c: &mut Criterion) { } group.finish(); + + // What the revision-keyed query cache saves per batch: re-collecting the + // wallet's scan scripts and re-grouping them into a `FilterQuery`. With + // the cache, this cost is paid once per wallet change instead of once + // per batch. + let mut assembly = c.benchmark_group("query_assembly"); + for used in USED_ADDRESSES { + let (manager, wallet_id) = wallet_with_mixing_history(used); + assembly.bench_with_input(BenchmarkId::new("pruned", used), &(), |b, _| { + b.iter(|| { + let scripts = manager.scan_script_pubkeys_for(black_box(&wallet_id)); + let query: FilterQuery = scripts.iter().map(|s| s.as_bytes()).collect(); + black_box(query) + }) + }); + } + assembly.finish(); } criterion_group!(benches, bench_filter_scan); diff --git a/key-wallet-manager/src/lib.rs b/key-wallet-manager/src/lib.rs index 89002d0ac..53677ea29 100644 --- a/key-wallet-manager/src/lib.rs +++ b/key-wallet-manager/src/lib.rs @@ -21,7 +21,9 @@ mod wallet_interface; pub use error::WalletError; pub use events::{DerivedAddress, WalletEvent}; -pub use matching::{check_compact_filters_for_elements, FilterMatchKey}; +pub use matching::{ + check_compact_filters_for_elements, check_compact_filters_for_query, FilterMatchKey, +}; pub use wallet_interface::{BlockProcessingResult, MempoolTransactionResult, WalletInterface}; use dashcore::blockdata::transaction::Transaction; diff --git a/key-wallet-manager/src/matching.rs b/key-wallet-manager/src/matching.rs index 68502ad0e..97125dd6b 100644 --- a/key-wallet-manager/src/matching.rs +++ b/key-wallet-manager/src/matching.rs @@ -48,11 +48,31 @@ pub fn check_compact_filters_for_elements( for element in extra_elements { query.push(element); } + check_compact_filters_for_query(input, &query, min_height) +} + +/// Check compact filters against a pre-built [`FilterQuery`], returning the +/// keys that matched. +/// +/// Same semantics as [`check_compact_filters_for_elements`], which builds the +/// query from scripts and bare elements and delegates here. Callers that +/// match the same query set across many batches (e.g. `dash-spv`'s filter +/// scan, whose query only changes when the wallet's monitored set does) can +/// build the query once, cache it, and skip the per-call collect-and-group +/// step entirely. +/// +/// Entries with `key.height() <= min_height` are skipped. Pass `0` to test +/// every filter in the input. +pub fn check_compact_filters_for_query( + input: &HashMap, + query: &FilterQuery, + min_height: CoreBlockHeight, +) -> BTreeSet { let match_filter = |(key, filter): (&FilterMatchKey, &BlockFilter)| { if key.height() <= min_height { return None; } - match filter.match_any(key.hash(), &query) { + match filter.match_any(key.hash(), query) { Ok(true) => Some(key.clone()), Ok(false) => None, Err(e) => { diff --git a/key-wallet-manager/src/process_block.rs b/key-wallet-manager/src/process_block.rs index 28e0f986f..40748c01f 100644 --- a/key-wallet-manager/src/process_block.rs +++ b/key-wallet-manager/src/process_block.rs @@ -249,6 +249,16 @@ impl WalletInterface for WalletM self.monitor_revision() } + fn wallet_monitor_revision(&self, wallet_id: &WalletId) -> u64 { + // Account-level revisions cover address derivations and UTXO + // changes; `account_generation` covers a freshly added account whose + // own revision is still zero. + self.wallet_infos + .get(wallet_id) + .map(|info| info.monitor_revision() + info.account_generation()) + .unwrap_or(0) + } + async fn earliest_required_height(&self) -> CoreBlockHeight { self.wallet_infos.values().map(|info| info.birth_height()).min().unwrap_or(0) } @@ -779,6 +789,52 @@ mod tests { assert!(manager.scan_script_pubkeys_for(&[0xff; 32]).is_empty()); } + #[tokio::test] + async fn test_wallet_monitor_revision_tracks_scan_set_changes() { + let (mut manager, wallet_id, addr) = setup_manager_with_wallet(); + + let initial = manager.wallet_monitor_revision(&wallet_id); + + // A UTXO change (mempool tx paying us) must move the revision — it + // can retire or fund addresses in the scan set. + let tx = create_tx_paying_to(&addr, 0xe0); + manager.process_mempool_transaction(&tx, None).await; + let after_utxo = manager.wallet_monitor_revision(&wallet_id); + assert!(after_utxo > initial, "UTXO change must advance the wallet revision"); + + // Adding a managed account must move it even before that account has + // any activity of its own (its account-level revision starts at + // zero): `add_managed_account` bumps the wallet's + // `account_generation`, which the per-wallet revision folds in. + manager + .create_account( + &wallet_id, + AccountType::Standard { + index: 7, + standard_account_type: StandardAccountType::BIP44Account, + }, + None, + ) + .unwrap(); + { + use key_wallet::wallet::managed_wallet_info::managed_account_operations::ManagedAccountOperations; + let (wallet, info) = manager.get_wallet_and_info_mut(&wallet_id).expect("wallet"); + info.add_managed_account( + wallet, + AccountType::Standard { + index: 7, + standard_account_type: StandardAccountType::BIP44Account, + }, + ) + .expect("add managed account"); + } + let after_account = manager.wallet_monitor_revision(&wallet_id); + assert!(after_account > after_utxo, "account add must advance the wallet revision"); + + // Unknown wallets report zero. + assert_eq!(manager.wallet_monitor_revision(&[0xff; 32]), 0); + } + #[tokio::test] async fn test_monitor_revision_bumps_and_stability() { let mut manager: WalletManager = WalletManager::new(Network::Testnet); diff --git a/key-wallet-manager/src/test_utils/mock_wallet.rs b/key-wallet-manager/src/test_utils/mock_wallet.rs index a559f520c..40d78c326 100644 --- a/key-wallet-manager/src/test_utils/mock_wallet.rs +++ b/key-wallet-manager/src/test_utils/mock_wallet.rs @@ -394,6 +394,11 @@ pub struct MultiMockWallet { /// hand the filter scan a pruned query while the monitored set stays full /// (dashpay/rust-dashcore#948). scan_addresses: std::collections::BTreeMap>, + /// Per-wallet override for `wallet_monitor_revision`. Wallets absent here + /// report revision `0`. Lets tests drive the filter scan's revision-keyed + /// query cache: mutate the scripts without bumping the revision to prove + /// the cache is reused, bump it to prove the cache is rebuilt. + wallet_revisions: std::collections::BTreeMap, event_sender: broadcast::Sender, /// Track every block processed for assertions. processed: Arc>>, @@ -411,6 +416,7 @@ impl MultiMockWallet { Self { wallets: std::collections::BTreeMap::new(), scan_addresses: std::collections::BTreeMap::new(), + wallet_revisions: std::collections::BTreeMap::new(), event_sender, processed: Arc::new(Mutex::new(Vec::new())), } @@ -427,6 +433,14 @@ impl MultiMockWallet { self.scan_addresses.insert(wallet_id, addresses); } + /// Set one wallet's `wallet_monitor_revision`. A consumer caching scan + /// queries by revision must rebuild its cache for this wallet after the + /// value changes, and may keep serving the cached query while it does + /// not. + pub fn set_wallet_revision(&mut self, wallet_id: WalletId, revision: u64) { + self.wallet_revisions.insert(wallet_id, revision); + } + /// Mutable access to a wallet's state, panicking if absent. pub fn wallet_mut(&mut self, wallet_id: &WalletId) -> &mut MockWalletState { self.wallets.get_mut(wallet_id).expect("wallet present") @@ -485,6 +499,10 @@ impl WalletInterface for MultiMockWallet { } } + fn wallet_monitor_revision(&self, wallet_id: &WalletId) -> u64 { + self.wallet_revisions.get(wallet_id).copied().unwrap_or(0) + } + fn watched_outpoints(&self) -> Vec { Vec::new() } diff --git a/key-wallet-manager/src/wallet_interface.rs b/key-wallet-manager/src/wallet_interface.rs index 484fb2c84..d88898c37 100644 --- a/key-wallet-manager/src/wallet_interface.rs +++ b/key-wallet-manager/src/wallet_interface.rs @@ -182,6 +182,22 @@ pub trait WalletInterface: Send + Sync + 'static { 0 } + /// Return a revision counter for one wallet that increments whenever that + /// wallet's monitored set can have changed — an address derived, an + /// account added, or a UTXO created or spent (spends matter because they + /// can retire a single-use CoinJoin address from the scan query, see + /// [`Self::scan_script_pubkeys_for`]). Filter sync caches each wallet's + /// scan query keyed by this value and rebuilds it only when the value + /// moves, instead of re-collecting the scripts every batch. + /// + /// Any monotonically advancing value that never misses a relevant change + /// is valid; over-reporting (bumping on unrelated changes) only costs a + /// rebuild. The default delegates to the global [`Self::monitor_revision`], + /// which is exactly such a conservative over-approximation. + fn wallet_monitor_revision(&self, _wallet_id: &WalletId) -> u64 { + self.monitor_revision() + } + /// Reclaim expired receive-address reservations across every managed /// wallet, returning the total number reclaimed. /// From 1354236f114c24fb0d4a7afd26146d485177af11 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 11 Aug 2026 15:15:40 +0700 Subject: [PATCH 2/2] test(key-wallet-manager): benchmark per-batch cost with and without the cached scan query scan_batch_query mirrors dash-spv's scan_batch for one behind wallet: "rebuilt" re-collects the scan scripts and re-groups the queries every batch (pre-cache shape), "cached" does a revision check and matches the pre-assembled query. 512 filters, pruned query (555 scripts): used=500 rebuilt 5.54ms | cached 5.43ms (~2% saved) used=2000 rebuilt 5.42ms | cached 5.28ms (~2.5% saved) used=6000 rebuilt 5.71ms | cached 5.27ms (~7.6% saved) The cached path is flat regardless of wallet history; the rebuilt path grows with total historical addresses because the pruning walk is O(all pool addresses) per batch. Co-Authored-By: Claude Fable 5 --- key-wallet-manager/benches/filter_scan.rs | 55 ++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/key-wallet-manager/benches/filter_scan.rs b/key-wallet-manager/benches/filter_scan.rs index fc26c12cd..a8618d5d6 100644 --- a/key-wallet-manager/benches/filter_scan.rs +++ b/key-wallet-manager/benches/filter_scan.rs @@ -31,7 +31,8 @@ use key_wallet::wallet::initialization::WalletAccountCreationOptions; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::{KeySource, ManagedAccountType, Network, Utxo}; use key_wallet_manager::{ - check_compact_filters_for_elements, FilterMatchKey, WalletInterface, WalletManager, + check_compact_filters_for_elements, check_compact_filters_for_query, FilterMatchKey, + WalletInterface, WalletManager, }; /// Denominated coins still unspent in the CoinJoin account — the wallet's @@ -185,6 +186,58 @@ fn bench_filter_scan(c: &mut Criterion) { }); } assembly.finish(); + + // Full per-batch cost with and without the revision-keyed cache, + // mirroring dash-spv's `scan_batch` for a single behind wallet. + // + // "rebuilt" is the pre-cache shape: collect the scan scripts and bare + // elements from the wallet, clone them into the union set, group the + // per-wallet attribution query, and match (the union query is grouped + // inside the matcher). "cached" is the post-cache shape: a revision + // check, then matching with the pre-assembled query. The difference is + // the assembly work the cache moves from once-per-batch to + // once-per-wallet-change. + let mut per_batch = c.benchmark_group("scan_batch_query"); + per_batch.sample_size(10); + per_batch.throughput(Throughput::Elements(u64::from(FILTERS))); + for used in USED_ADDRESSES { + let (manager, wallet_id) = wallet_with_mixing_history(used); + + per_batch.bench_with_input(BenchmarkId::new("rebuilt", used), &(), |b, _| { + b.iter(|| { + let scripts = manager.scan_script_pubkeys_for(black_box(&wallet_id)); + let elements = manager.monitored_filter_elements_for(&wallet_id); + let union_scripts = scripts.clone(); + let mut attribution_query: FilterQuery = + scripts.iter().map(|s| s.as_bytes()).collect(); + for element in &elements { + attribution_query.push(element); + } + black_box(&attribution_query); + check_compact_filters_for_elements( + black_box(&filters), + &union_scripts, + &elements, + 0, + ) + }) + }); + + let scripts = manager.scan_script_pubkeys_for(&wallet_id); + let elements = manager.monitored_filter_elements_for(&wallet_id); + let mut cached_query: FilterQuery = scripts.iter().map(|s| s.as_bytes()).collect(); + for element in &elements { + cached_query.push(element); + } + per_batch.bench_with_input(BenchmarkId::new("cached", used), &cached_query, |b, query| { + b.iter(|| { + let revision = manager.wallet_monitor_revision(black_box(&wallet_id)); + black_box(revision); + check_compact_filters_for_query(black_box(&filters), black_box(query), 0) + }) + }); + } + per_batch.finish(); } criterion_group!(benches, bench_filter_scan);