diff --git a/dash-spv/src/sync/legacy/filters/matching.rs b/dash-spv/src/sync/legacy/filters/matching.rs index 56161a148..6edcc5ba3 100644 --- a/dash-spv/src/sync/legacy/filters/matching.rs +++ b/dash-spv/src/sync/legacy/filters/matching.rs @@ -20,27 +20,6 @@ use crate::network::NetworkManager; use crate::storage::StorageManager; impl super::manager::FilterSyncManager { - pub async fn check_filter_for_matches< - W: key_wallet_manager::wallet_interface::WalletInterface, - >( - &self, - filter_data: &[u8], - block_hash: &BlockHash, - wallet: &mut W, - ) -> SyncResult { - // Create the BlockFilter from the raw data - let filter = dashcore::bip158::BlockFilter::new(filter_data); - - // Use wallet's check_compact_filter method - let matches = wallet.check_compact_filter(&filter, block_hash).await; - if matches { - tracing::info!("🎯 Filter match found for block {}", block_hash); - Ok(true) - } else { - Ok(false) - } - } - /// Check if filter matches any of the provided scripts using BIP158 GCS filter. #[allow(dead_code)] fn filter_matches_scripts( diff --git a/dash-spv/src/sync/legacy/message_handlers.rs b/dash-spv/src/sync/legacy/message_handlers.rs index 942f16576..b90f8d1e8 100644 --- a/dash-spv/src/sync/legacy/message_handlers.rs +++ b/dash-spv/src/sync/legacy/message_handlers.rs @@ -1,19 +1,19 @@ //! Message handlers for synchronization phases. -use std::ops::DerefMut; -use std::time::Instant; - +use dashcore::bip158::BlockFilter; use dashcore::block::Block; use dashcore::network::message::NetworkMessage; use dashcore::network::message_blockdata::Inventory; +use std::collections::HashMap; +use std::time::Instant; +use super::manager::SyncManager; +use super::phases::SyncPhase; use crate::error::{SyncError, SyncResult}; use crate::network::NetworkManager; use crate::storage::StorageManager; use key_wallet_manager::wallet_interface::WalletInterface; - -use super::manager::SyncManager; -use super::phases::SyncPhase; +use key_wallet_manager::wallet_manager::{check_compact_filters_for_addresses, FilterMatchKey}; impl SyncManager { /// Handle incoming network messages with phase filtering @@ -496,8 +496,6 @@ impl SyncManager SyncManager SyncManager)>>>; @@ -57,15 +55,6 @@ impl WalletInterface for MockWallet { processed.push(tx.txid()); } - async fn check_compact_filter( - &mut self, - _filter: &dashcore::bip158::BlockFilter, - _block_hash: &dashcore::BlockHash, - ) -> bool { - // Return true for all filters in test - true - } - async fn describe(&self) -> String { "MockWallet (test implementation)".to_string() } @@ -74,6 +63,10 @@ impl WalletInterface for MockWallet { let map = self.effects.lock().await; map.get(&tx.txid()).cloned() } + + fn monitored_addresses(&self) -> Vec
{ + Vec::new() + } } /// Mock wallet that returns false for filter checks @@ -94,13 +87,8 @@ impl WalletInterface for NonMatchingMockWallet { async fn process_mempool_transaction(&mut self, _tx: &Transaction) {} - async fn check_compact_filter( - &mut self, - _filter: &dashcore::bip158::BlockFilter, - _block_hash: &dashcore::BlockHash, - ) -> bool { - // Always return false - filter doesn't match - false + fn monitored_addresses(&self) -> Vec
{ + Vec::new() } async fn describe(&self) -> String { diff --git a/key-wallet-manager/src/wallet_interface.rs b/key-wallet-manager/src/wallet_interface.rs index 5ca24035e..e043a1095 100644 --- a/key-wallet-manager/src/wallet_interface.rs +++ b/key-wallet-manager/src/wallet_interface.rs @@ -5,7 +5,6 @@ use alloc::string::String; use alloc::vec::Vec; use async_trait::async_trait; -use dashcore::bip158::BlockFilter; use dashcore::prelude::CoreBlockHeight; use dashcore::{Address, Block, Transaction, Txid}; @@ -47,13 +46,8 @@ pub trait WalletInterface: Send + Sync + 'static { /// Called when a transaction is seen in the mempool async fn process_mempool_transaction(&mut self, tx: &Transaction); - /// Check if a compact filter matches any watched items - /// Returns true if the block should be downloaded - async fn check_compact_filter( - &mut self, - filter: &BlockFilter, - block_hash: &dashcore::BlockHash, - ) -> bool; + /// Get all addresses the wallet is monitoring for incoming transactions + fn monitored_addresses(&self) -> Vec
; /// Return the wallet's per-transaction net change and involved addresses if known. /// Returns (net_amount, addresses) where net_amount is received - sent in satoshis. diff --git a/key-wallet-manager/src/wallet_manager/matching.rs b/key-wallet-manager/src/wallet_manager/matching.rs new file mode 100644 index 000000000..3a3b008c1 --- /dev/null +++ b/key-wallet-manager/src/wallet_manager/matching.rs @@ -0,0 +1,166 @@ +use alloc::vec::Vec; +use dashcore::bip158::BlockFilter; +use dashcore::prelude::CoreBlockHeight; +use dashcore::{Address, BlockHash}; +use rayon::prelude::{IntoParallelIterator, ParallelIterator}; +use std::collections::{BTreeSet, HashMap}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct FilterMatchKey { + height: CoreBlockHeight, + hash: BlockHash, +} + +impl FilterMatchKey { + pub fn new(height: CoreBlockHeight, hash: BlockHash) -> Self { + Self { + height, + hash, + } + } + pub fn height(&self) -> CoreBlockHeight { + self.height + } + pub fn hash(&self) -> &BlockHash { + &self.hash + } +} + +/// Check compact filters for addresses and return the keys that matched. +pub fn check_compact_filters_for_addresses( + input: &HashMap, + addresses: Vec
, +) -> BTreeSet { + let script_pubkey_bytes: Vec> = + addresses.iter().map(|address| address.script_pubkey().to_bytes()).collect(); + + input + .into_par_iter() + .filter_map(|(key, filter)| { + filter + .match_any(key.hash(), script_pubkey_bytes.iter().map(|v| v.as_slice())) + .unwrap_or(false) + .then_some(key.clone()) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Network; + use dashcore::{Block, Transaction}; + + #[test] + fn test_empty_input_returns_empty() { + let result = check_compact_filters_for_addresses(&HashMap::new(), vec![]); + assert!(result.is_empty()); + } + + #[test] + fn test_empty_addresses_returns_empty() { + let address = Address::dummy(Network::Regtest, 1); + let tx = Transaction::dummy(&address, 0..0, &[1]); + let block = Block::dummy(100, vec![tx]); + let filter = BlockFilter::dummy(&block); + let key = FilterMatchKey::new(100, block.block_hash()); + + let mut input = HashMap::new(); + input.insert(key.clone(), filter); + + let output = check_compact_filters_for_addresses(&input, vec![]); + assert!(!output.contains(&key)); + } + + #[test] + fn test_matching_filter() { + let address = Address::dummy(Network::Regtest, 1); + let tx = Transaction::dummy(&address, 0..0, &[1]); + let block = Block::dummy(100, vec![tx]); + let filter = BlockFilter::dummy(&block); + let key = FilterMatchKey::new(100, block.block_hash()); + + let mut input = HashMap::new(); + input.insert(key.clone(), filter); + + let output = check_compact_filters_for_addresses(&input, vec![address]); + assert!(output.contains(&key)); + } + + #[test] + fn test_non_matching_filter() { + let address = Address::dummy(Network::Regtest, 1); + let address_other = Address::dummy(Network::Regtest, 2); + + let tx = Transaction::dummy(&address_other, 0..0, &[1]); + let block = Block::dummy(100, vec![tx]); + let filter = BlockFilter::dummy(&block); + let key = FilterMatchKey::new(100, block.block_hash()); + + let mut input = HashMap::new(); + input.insert(key.clone(), filter); + + let output = check_compact_filters_for_addresses(&input, vec![address]); + assert!(!output.contains(&key)); + } + + #[test] + fn test_batch_mixed_results() { + let unrelated_address = Address::dummy(Network::Regtest, 0); + let address_1 = Address::dummy(Network::Regtest, 1); + let address_2 = Address::dummy(Network::Regtest, 2); + + let tx_1 = Transaction::dummy(&address_1, 0..0, &[1]); + let block_1 = Block::dummy(100, vec![tx_1]); + let filter_1 = BlockFilter::dummy(&block_1); + let key_1 = FilterMatchKey::new(100, block_1.block_hash()); + + let tx_2 = Transaction::dummy(&address_2, 0..0, &[2]); + let block_2 = Block::dummy(200, vec![tx_2]); + let filter_2 = BlockFilter::dummy(&block_2); + let key_2 = FilterMatchKey::new(200, block_2.block_hash()); + + let tx_3 = Transaction::dummy(&unrelated_address, 0..0, &[10]); + let block_3 = Block::dummy(300, vec![tx_3]); + let filter_3 = BlockFilter::dummy(&block_3); + let key_3 = FilterMatchKey::new(300, block_3.block_hash()); + + let mut input = HashMap::new(); + input.insert(key_1.clone(), filter_1); + input.insert(key_2.clone(), filter_2); + input.insert(key_3.clone(), filter_3); + + let output = check_compact_filters_for_addresses(&input, vec![address_1, address_2]); + assert_eq!(output.len(), 2); + assert!(output.contains(&key_1)); + assert!(output.contains(&key_2)); + assert!(!output.contains(&key_3)); + } + + #[test] + fn test_output_sorted_by_height() { + let address = Address::dummy(Network::Regtest, 1); + + // Create blocks at different heights (inserted in non-sorted order) + let heights = [500, 100, 300, 200, 400]; + let mut input = HashMap::new(); + + for (i, &height) in heights.iter().enumerate() { + let tx = Transaction::dummy(&address, 0..0, &[i as u64]); + let block = Block::dummy(height, vec![tx]); + let filter = BlockFilter::dummy(&block); + let key = FilterMatchKey::new(height, block.block_hash()); + input.insert(key, filter); + } + + let output = check_compact_filters_for_addresses(&input, vec![address]); + + // Verify output is sorted by height (ascending) + let heights_out: Vec = output.iter().map(|k| k.height()).collect(); + let mut sorted_heights = heights_out.clone(); + sorted_heights.sort(); + + assert_eq!(heights_out, sorted_heights); + assert_eq!(heights_out, vec![100, 200, 300, 400, 500]); + } +} diff --git a/key-wallet-manager/src/wallet_manager/mod.rs b/key-wallet-manager/src/wallet_manager/mod.rs index b095b4124..ebcbd1896 100644 --- a/key-wallet-manager/src/wallet_manager/mod.rs +++ b/key-wallet-manager/src/wallet_manager/mod.rs @@ -4,9 +4,11 @@ //! each of which can have multiple accounts. This follows the architecture //! pattern where a manager oversees multiple distinct wallets. +mod matching; mod process_block; mod transaction_building; +pub use crate::wallet_manager::matching::{check_compact_filters_for_addresses, FilterMatchKey}; use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; diff --git a/key-wallet-manager/src/wallet_manager/process_block.rs b/key-wallet-manager/src/wallet_manager/process_block.rs index 180ec10a2..c281d69ec 100644 --- a/key-wallet-manager/src/wallet_manager/process_block.rs +++ b/key-wallet-manager/src/wallet_manager/process_block.rs @@ -4,9 +4,8 @@ use alloc::string::String; use alloc::vec::Vec; use async_trait::async_trait; use core::fmt::Write as _; -use dashcore::bip158::BlockFilter; use dashcore::prelude::CoreBlockHeight; -use dashcore::{Block, BlockHash, Transaction}; +use dashcore::{Address, Block, Transaction}; use key_wallet::transaction_checking::transaction_router::TransactionRouter; use key_wallet::transaction_checking::TransactionContext; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; @@ -58,30 +57,8 @@ impl WalletInterface for WalletM .await; } - async fn check_compact_filter(&mut self, filter: &BlockFilter, block_hash: &BlockHash) -> bool { - // Collect all scripts we're watching - let mut script_bytes = Vec::new(); - - // Get all wallet addresses for this network - for info in self.wallet_infos.values() { - let monitored = info.monitored_addresses(); - for address in monitored { - script_bytes.push(address.script_pubkey().as_bytes().to_vec()); - } - } - - // If we don't watch any scripts for this network, there can be no match. - // Note: BlockFilterReader::match_any returns true for an empty query set, - // so we must guard this case explicitly to avoid false positives. - let hit = if script_bytes.is_empty() { - false - } else { - filter - .match_any(block_hash, &mut script_bytes.iter().map(|s| s.as_slice())) - .unwrap_or(false) - }; - - hit + fn monitored_addresses(&self) -> Vec
{ + self.monitored_addresses() } async fn transaction_effect(&self, tx: &Transaction) -> Option<(i64, Vec)> { diff --git a/key-wallet-manager/tests/spv_integration_tests.rs b/key-wallet-manager/tests/spv_integration_tests.rs index bf9da5dbe..be32f4764 100644 --- a/key-wallet-manager/tests/spv_integration_tests.rs +++ b/key-wallet-manager/tests/spv_integration_tests.rs @@ -1,6 +1,5 @@ //! Integration tests for SPV wallet functionality -use dashcore::bip158::BlockFilter; use dashcore::blockdata::block::Block; use dashcore::blockdata::transaction::Transaction; use dashcore::constants::COINBASE_MATURITY; @@ -12,36 +11,6 @@ use key_wallet::Network; use key_wallet_manager::wallet_interface::WalletInterface; use key_wallet_manager::wallet_manager::WalletManager; -#[tokio::test] -async fn test_filter_checking() { - let mut manager = WalletManager::::new(Network::Testnet); - - // Add a test address to monitor - simplified for testing - // In reality, addresses would be generated from wallet accounts - - let _wallet_id = manager - .create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default) - .expect("Failed to create wallet"); - - // Create a test block with a transaction - let tx = Transaction::dummy(&Address::dummy(Network::Testnet, 0), 0..0, &[100000]); - let block = Block::dummy(100, vec![tx]); - let filter = BlockFilter::dummy(&block); - let block_hash = block.block_hash(); - - // Check the filter - let should_download = manager.check_compact_filter(&filter, &block_hash).await; - - // The filter matching depends on whether the wallet has any addresses - // being watched. Since we just created an empty wallet, it may or may not match. - // We'll just check that the method doesn't panic - let _ = should_download; - - // Test filter caching - calling again should use cached result - let should_download_cached = manager.check_compact_filter(&filter, &block_hash).await; - assert_eq!(should_download, should_download_cached, "Cached result should match original"); -} - #[tokio::test] async fn test_block_processing() { let mut manager = WalletManager::::new(Network::Testnet); @@ -99,40 +68,6 @@ async fn test_block_processing_result_empty() { assert!(result.new_addresses.is_empty()); } -#[tokio::test] -async fn test_filter_caching() { - let mut manager = WalletManager::::new(Network::Testnet); - - // Create a wallet with some addresses - let _wallet_id = manager - .create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default) - .expect("Failed to create wallet"); - - // Create multiple blocks with different hashes - let tx1 = Transaction::dummy(&Address::dummy(Network::Testnet, 0), 0..0, &[1000]); - let tx2 = Transaction::dummy(&Address::dummy(Network::Testnet, 0), 0..0, &[2000]); - let block1 = Block::dummy(100, vec![tx1]); - let block2 = Block::dummy(101, vec![tx2]); - - let filter1 = BlockFilter::dummy(&block1); - let filter2 = BlockFilter::dummy(&block2); - - let hash1 = block1.block_hash(); - let hash2 = block2.block_hash(); - - // Check filters for both blocks - let result1 = manager.check_compact_filter(&filter1, &hash1).await; - let result2 = manager.check_compact_filter(&filter2, &hash2).await; - - // Check again - should use cached results - let cached1 = manager.check_compact_filter(&filter1, &hash1).await; - let cached2 = manager.check_compact_filter(&filter2, &hash2).await; - - // Cached results should match originals - assert_eq!(result1, cached1, "Cached result for block1 should match"); - assert_eq!(result2, cached2, "Cached result for block2 should match"); -} - fn assert_wallet_heights(manager: &WalletManager, expected_height: u32) { assert_eq!(manager.current_height(), expected_height, "height should be {}", expected_height); for wallet_info in manager.get_all_wallet_infos().values() {