diff --git a/dash-spv/src/bloom/builder.rs b/dash-spv/src/bloom/builder.rs deleted file mode 100644 index 9aaa11947..000000000 --- a/dash-spv/src/bloom/builder.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Bloom filter construction utilities - -use super::utils::{extract_pubkey_hash, outpoint_to_bytes}; -use crate::error::SpvError; -use dashcore::address::Address; -use dashcore::bloom::{BloomFilter, BloomFlags}; -use dashcore::OutPoint; - -/// Builder for constructing bloom filters from wallet state -pub struct BloomFilterBuilder { - /// Expected number of elements - elements: u32, - /// Desired false positive rate - false_positive_rate: f64, - /// Random tweak value - tweak: u32, - /// Update flags - flags: BloomFlags, - /// Addresses to include - addresses: Vec
, - /// Outpoints to include - outpoints: Vec, - /// Raw data elements to include - data_elements: Vec>, -} - -impl BloomFilterBuilder { - /// Create a new bloom filter builder - pub fn new() -> Self { - Self { - elements: 100, - false_positive_rate: 0.001, - tweak: rand::random::(), - flags: BloomFlags::All, - addresses: Vec::new(), - outpoints: Vec::new(), - data_elements: Vec::new(), - } - } - - /// Set the expected number of elements - pub fn elements(mut self, elements: u32) -> Self { - self.elements = elements; - self - } - - /// Set the false positive rate - pub fn false_positive_rate(mut self, rate: f64) -> Self { - self.false_positive_rate = rate; - self - } - - /// Set the tweak value - pub fn tweak(mut self, tweak: u32) -> Self { - self.tweak = tweak; - self - } - - /// Set the update flags - pub fn flags(mut self, flags: BloomFlags) -> Self { - self.flags = flags; - self - } - - /// Add an address to the filter - pub fn add_address(mut self, address: Address) -> Self { - self.addresses.push(address); - self - } - - /// Add multiple addresses - pub fn add_addresses(mut self, addresses: impl IntoIterator) -> Self { - self.addresses.extend(addresses); - self - } - - /// Add an outpoint to the filter - pub fn add_outpoint(mut self, outpoint: OutPoint) -> Self { - self.outpoints.push(outpoint); - self - } - - /// Add multiple outpoints - pub fn add_outpoints(mut self, outpoints: impl IntoIterator) -> Self { - self.outpoints.extend(outpoints); - self - } - - /// Add raw data to the filter - pub fn add_data(mut self, data: Vec) -> Self { - self.data_elements.push(data); - self - } - - // Removed: from_wallet - wallet functionality is now handled externally - // The wallet interface doesn't expose addresses and UTXOs directly - - /// Build the bloom filter - pub fn build(self) -> Result { - // Calculate actual elements - let actual_elements = - self.addresses.len() + self.outpoints.len() + self.data_elements.len(); - let elements = std::cmp::max(self.elements, actual_elements as u32); - - // Create filter - let mut filter = - BloomFilter::new(elements, self.false_positive_rate, self.tweak, self.flags).map_err( - |e| SpvError::General(format!("Failed to create bloom filter: {:?}", e)), - )?; - - // Add addresses - for address in self.addresses { - let script = address.script_pubkey(); - filter.insert(script.as_bytes()); - - // For P2PKH, also add the pubkey hash - if let Some(hash) = extract_pubkey_hash(&script) { - filter.insert(&hash); - } - } - - // Add outpoints - for outpoint in self.outpoints { - filter.insert(&outpoint_to_bytes(&outpoint)); - } - - // Add raw data - for data in self.data_elements { - filter.insert(&data); - } - - Ok(filter) - } -} - -impl Default for BloomFilterBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/dash-spv/src/bloom/manager.rs b/dash-spv/src/bloom/manager.rs deleted file mode 100644 index 145b4dc65..000000000 --- a/dash-spv/src/bloom/manager.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! Bloom filter lifecycle management for SPV clients - -use super::utils::{extract_pubkey_hash, outpoint_to_bytes}; -use crate::error::SpvError; -use dashcore::address::Address; -use dashcore::bloom::{BloomFilter, BloomFlags}; -use dashcore::network::message_bloom::{FilterAdd, FilterLoad}; -use dashcore::transaction::Transaction; -use dashcore::OutPoint; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// Configuration for bloom filter behavior -#[derive(Debug, Clone)] -pub struct BloomFilterConfig { - /// Expected number of elements - pub elements: u32, - /// Desired false positive rate (0.0 to 1.0) - pub false_positive_rate: f64, - /// Random value added to hash seeds - pub tweak: u32, - /// Update behavior flags - pub flags: BloomFlags, - /// Auto-recreate filter when false positive rate exceeds this threshold - pub max_false_positive_rate: f64, - /// Track performance statistics - pub enable_stats: bool, -} - -impl Default for BloomFilterConfig { - fn default() -> Self { - Self { - elements: 100, - false_positive_rate: 0.001, - tweak: rand::random::(), - flags: BloomFlags::All, - max_false_positive_rate: 0.05, - enable_stats: true, - } - } -} - -/// Statistics for bloom filter performance -#[derive(Debug, Clone, Default)] -pub struct BloomFilterStats { - /// Number of items added to the filter - pub items_added: u64, - /// Number of positive matches - pub matches: u64, - /// Number of queries performed - pub queries: u64, - /// Number of times filter was recreated - pub recreations: u64, - /// Current estimated false positive rate - pub current_false_positive_rate: f64, -} - -/// Manages bloom filter lifecycle for SPV client -pub struct BloomFilterManager { - /// Current bloom filter - filter: Arc>>, - /// Configuration - config: BloomFilterConfig, - /// Performance statistics - stats: Arc>, - /// Addresses being watched - addresses: Arc>>, - /// Outpoints being watched - outpoints: Arc>>, - /// Data elements being watched - data_elements: Arc>>>, -} - -impl BloomFilterManager { - /// Create a new bloom filter manager - pub fn new(config: BloomFilterConfig) -> Self { - Self { - filter: Arc::new(RwLock::new(None)), - config, - stats: Arc::new(RwLock::new(BloomFilterStats::default())), - addresses: Arc::new(RwLock::new(Vec::new())), - outpoints: Arc::new(RwLock::new(Vec::new())), - data_elements: Arc::new(RwLock::new(Vec::new())), - } - } - - /// Initialize or recreate the bloom filter - pub async fn create_filter(&self) -> Result { - let addresses = self.addresses.read().await; - let outpoints = self.outpoints.read().await; - let data_elements = self.data_elements.read().await; - - // Calculate total elements - let total_elements = - addresses.len() as u32 + outpoints.len() as u32 + data_elements.len() as u32; - - let elements = std::cmp::max(self.config.elements, total_elements); - - // Create new filter - let mut new_filter = BloomFilter::new( - elements, - self.config.false_positive_rate, - self.config.tweak, - self.config.flags, - ) - .map_err(|e| SpvError::General(format!("Failed to create bloom filter: {:?}", e)))?; - - // Add all watched elements - for address in addresses.iter() { - self.add_address_to_filter(&mut new_filter, address)?; - } - - for outpoint in outpoints.iter() { - new_filter.insert(&outpoint_to_bytes(outpoint)); - } - - for data in data_elements.iter() { - new_filter.insert(data); - } - - // Update stats - if self.config.enable_stats { - let mut stats = self.stats.write().await; - stats.recreations += 1; - stats.items_added = total_elements as u64; - stats.current_false_positive_rate = - new_filter.estimate_false_positive_rate(total_elements); - } - - // Store the new filter - let filter_load = FilterLoad::from_bloom_filter(&new_filter); - *self.filter.write().await = Some(new_filter); - - Ok(filter_load) - } - - /// Add an address to the filter - pub async fn add_address(&self, address: &Address) -> Result, SpvError> { - // Add to tracked addresses - { - let mut addresses = self.addresses.write().await; - addresses.push(address.clone()); - } // Explicitly drop the lock here - - // Update filter if it exists - if let Some(ref mut filter) = *self.filter.write().await { - let mut data = Vec::new(); - self.add_address_to_filter(filter, address)?; - - // Get the script pubkey bytes - let script = address.script_pubkey(); - data.extend_from_slice(script.as_bytes()); - - if self.config.enable_stats { - let mut stats = self.stats.write().await; - stats.items_added += 1; - } - - return Ok(Some(FilterAdd { - data, - })); - } - - Ok(None) - } - - /// Add an outpoint to the filter - pub async fn add_outpoint(&self, outpoint: &OutPoint) -> Result, SpvError> { - // Add to tracked outpoints - { - let mut outpoints = self.outpoints.write().await; - outpoints.push(*outpoint); - } // Explicitly drop the lock here - - // Update filter if it exists - if let Some(ref mut filter) = *self.filter.write().await { - let data = outpoint_to_bytes(outpoint); - filter.insert(&data); - - if self.config.enable_stats { - let mut stats = self.stats.write().await; - stats.items_added += 1; - } - - return Ok(Some(FilterAdd { - data, - })); - } - - Ok(None) - } - - /// Add arbitrary data to the filter - pub async fn add_data(&self, data: Vec) -> Result, SpvError> { - // Add to tracked data - { - let mut data_elements = self.data_elements.write().await; - data_elements.push(data.clone()); - } // Explicitly drop the lock here - - // Update filter if it exists - if let Some(ref mut filter) = *self.filter.write().await { - filter.insert(&data); - - if self.config.enable_stats { - let mut stats = self.stats.write().await; - stats.items_added += 1; - } - - return Ok(Some(FilterAdd { - data, - })); - } - - Ok(None) - } - - /// Check if data matches the filter - pub async fn contains(&self, data: &[u8]) -> bool { - if let Some(ref filter) = *self.filter.read().await { - let result = filter.contains(data); - - if self.config.enable_stats { - let mut stats = self.stats.write().await; - stats.queries += 1; - if result { - stats.matches += 1; - } - } - - result - } else { - // No filter means match everything - true - } - } - - /// Process a transaction to check for matches - pub async fn process_transaction(&self, tx: &Transaction) -> bool { - if self.filter.read().await.is_none() { - return true; // No filter means match everything - } - - // Check if any output matches our addresses - for output in &tx.output { - if self.contains(output.script_pubkey.as_bytes()).await { - return true; - } - } - - // Check if any input matches our outpoints - for input in &tx.input { - if self.contains(&outpoint_to_bytes(&input.previous_output)).await { - return true; - } - } - - false - } - - /// Check if filter needs recreation based on false positive rate - pub async fn needs_recreation(&self) -> bool { - if self.config.enable_stats { - let stats = self.stats.read().await; - stats.current_false_positive_rate > self.config.max_false_positive_rate - } else { - false - } - } - - /// Get current statistics - pub async fn get_stats(&self) -> BloomFilterStats { - self.stats.read().await.clone() - } - - /// Clear the filter - pub async fn clear(&self) { - { - let mut filter = self.filter.write().await; - *filter = None; - } - { - let mut addresses = self.addresses.write().await; - addresses.clear(); - } - { - let mut outpoints = self.outpoints.write().await; - outpoints.clear(); - } - { - let mut data_elements = self.data_elements.write().await; - data_elements.clear(); - } - { - let mut stats = self.stats.write().await; - *stats = BloomFilterStats::default(); - } - } - - /// Helper to add address to filter - fn add_address_to_filter( - &self, - filter: &mut BloomFilter, - address: &Address, - ) -> Result<(), SpvError> { - // Add the script pubkey - let script = address.script_pubkey(); - filter.insert(script.as_bytes()); - - // For P2PKH addresses, also add the public key hash - if let Some(pubkey_hash) = extract_pubkey_hash(&script) { - filter.insert(&pubkey_hash); - } - - Ok(()) - } -} diff --git a/dash-spv/src/bloom/mod.rs b/dash-spv/src/bloom/mod.rs deleted file mode 100644 index 82116f573..000000000 --- a/dash-spv/src/bloom/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Bloom filter support for SPV clients - -pub mod builder; -pub mod manager; -pub mod stats; -pub mod utils; - -#[cfg(test)] -mod tests; - -pub use builder::BloomFilterBuilder; -pub use manager::{BloomFilterConfig, BloomFilterManager}; -pub use stats::{BloomFilterStats, BloomStatsTracker, DetailedBloomStats}; diff --git a/dash-spv/src/bloom/stats.rs b/dash-spv/src/bloom/stats.rs deleted file mode 100644 index ded1feff8..000000000 --- a/dash-spv/src/bloom/stats.rs +++ /dev/null @@ -1,242 +0,0 @@ -//! Bloom filter performance statistics and monitoring - -use std::collections::VecDeque; -use std::time::{Duration, Instant}; - -/// Detailed statistics for bloom filter performance -#[derive(Debug, Clone)] -pub struct DetailedBloomStats { - /// Basic statistics - pub basic: BloomFilterStats, - /// Query performance metrics - pub query_performance: QueryPerformance, - /// Filter health metrics - pub filter_health: FilterHealth, - /// Network impact metrics - pub network_impact: NetworkImpact, -} - -/// Basic bloom filter statistics -#[derive(Debug, Clone, Default)] -pub struct BloomFilterStats { - /// Number of items added to the filter - pub items_added: u64, - /// Number of positive matches - pub matches: u64, - /// Number of queries performed - pub queries: u64, - /// Number of times filter was recreated - pub recreations: u64, - /// Current estimated false positive rate - pub current_false_positive_rate: f64, -} - -/// Query performance metrics -#[derive(Debug, Clone, Default)] -pub struct QueryPerformance { - /// Average query time in microseconds - pub avg_query_time_us: f64, - /// Maximum query time in microseconds - pub max_query_time_us: u64, - /// Minimum query time in microseconds - pub min_query_time_us: u64, - /// Total query time in microseconds - pub total_query_time_us: u64, -} - -/// Filter health metrics -#[derive(Debug, Clone, Default)] -pub struct FilterHealth { - /// Current filter size in bytes - pub filter_size_bytes: usize, - /// Number of bits set in the filter - pub bits_set: usize, - /// Total bits in the filter - pub total_bits: usize, - /// Filter saturation percentage (0-100) - pub saturation_percent: f64, - /// Time since last recreation - pub time_since_recreation: Option, -} - -/// Network impact metrics -#[derive(Debug, Clone, Default)] -pub struct NetworkImpact { - /// Number of transactions received due to filter - pub transactions_received: u64, - /// Number of false positive transactions - pub false_positive_transactions: u64, - /// Estimated bandwidth saved (in bytes) - pub bandwidth_saved_bytes: u64, - /// Number of filter update messages sent - pub filter_updates_sent: u64, -} - -/// Tracks bloom filter performance over time -pub struct BloomStatsTracker { - /// Current statistics - stats: DetailedBloomStats, - /// Last filter recreation time - last_recreation: Option, - /// Query timing accumulator - query_times: VecDeque, -} - -impl BloomStatsTracker { - /// Create a new stats tracker - pub fn new() -> Self { - Self { - stats: DetailedBloomStats { - basic: BloomFilterStats::default(), - query_performance: QueryPerformance::default(), - filter_health: FilterHealth::default(), - network_impact: NetworkImpact::default(), - }, - last_recreation: None, - query_times: VecDeque::with_capacity(1000), - } - } - - /// Record a query operation - pub fn record_query(&mut self, duration: Duration, matched: bool) { - self.stats.basic.queries += 1; - if matched { - self.stats.basic.matches += 1; - } - - // Update query performance - let micros = duration.as_micros() as u64; - self.stats.query_performance.total_query_time_us += micros; - - if self.stats.query_performance.min_query_time_us == 0 - || micros < self.stats.query_performance.min_query_time_us - { - self.stats.query_performance.min_query_time_us = micros; - } - - if micros > self.stats.query_performance.max_query_time_us { - self.stats.query_performance.max_query_time_us = micros; - } - - // Keep last 1000 query times for moving average - if self.query_times.len() >= 1000 { - self.query_times.pop_front(); - } - self.query_times.push_back(duration); - - // Update average - let total_micros: u64 = self.query_times.iter().map(|d| d.as_micros() as u64).sum(); - self.stats.query_performance.avg_query_time_us = - total_micros as f64 / self.query_times.len() as f64; - } - - /// Record an item addition - pub fn record_addition(&mut self) { - self.stats.basic.items_added += 1; - } - - /// Record a filter recreation - pub fn record_recreation(&mut self, filter_size: usize, bits_set: usize, total_bits: usize) { - self.stats.basic.recreations += 1; - self.last_recreation = Some(Instant::now()); - - // Update filter health - self.stats.filter_health.filter_size_bytes = filter_size; - self.stats.filter_health.bits_set = bits_set; - self.stats.filter_health.total_bits = total_bits; - self.stats.filter_health.saturation_percent = (bits_set as f64 / total_bits as f64) * 100.0; - } - - /// Record a transaction received - pub fn record_transaction(&mut self, is_false_positive: bool, tx_size: usize) { - self.stats.network_impact.transactions_received += 1; - if is_false_positive { - self.stats.network_impact.false_positive_transactions += 1; - } else { - // Estimate bandwidth saved by not downloading unrelated transactions - // Assume average transaction size if this was a true positive - self.stats.network_impact.bandwidth_saved_bytes += (tx_size * 10) as u64; - // Rough estimate - } - } - - /// Record a filter update sent - pub fn record_filter_update(&mut self) { - self.stats.network_impact.filter_updates_sent += 1; - } - - /// Update false positive rate estimate - pub fn update_false_positive_rate(&mut self, rate: f64) { - self.stats.basic.current_false_positive_rate = rate; - } - - /// Get current statistics - pub fn get_stats(&mut self) -> DetailedBloomStats { - // Update time since recreation - if let Some(last) = self.last_recreation { - self.stats.filter_health.time_since_recreation = Some(last.elapsed()); - } - - self.stats.clone() - } - - /// Reset statistics - pub fn reset(&mut self) { - *self = Self::new(); - } - - /// Get a summary report - pub fn summary_report(&self) -> String { - let stats = &self.stats; - format!( - "Bloom Filter Statistics:\n\ - Items Added: {}\n\ - Queries: {} (Matches: {}, Rate: {:.2}%)\n\ - Current FP Rate: {:.4}%\n\ - Filter Recreations: {}\n\ - \n\ - Query Performance:\n\ - Avg: {:.2}μs, Min: {}μs, Max: {}μs\n\ - \n\ - Filter Health:\n\ - Size: {} bytes, Saturation: {:.1}%\n\ - \n\ - Network Impact:\n\ - Transactions: {} (FP: {}, Rate: {:.2}%)\n\ - Bandwidth Saved: ~{:.2} MB\n\ - Filter Updates: {}", - stats.basic.items_added, - stats.basic.queries, - stats.basic.matches, - if stats.basic.queries > 0 { - (stats.basic.matches as f64 / stats.basic.queries as f64) * 100.0 - } else { - 0.0 - }, - stats.basic.current_false_positive_rate * 100.0, - stats.basic.recreations, - stats.query_performance.avg_query_time_us, - stats.query_performance.min_query_time_us, - stats.query_performance.max_query_time_us, - stats.filter_health.filter_size_bytes, - stats.filter_health.saturation_percent, - stats.network_impact.transactions_received, - stats.network_impact.false_positive_transactions, - if stats.network_impact.transactions_received > 0 { - (stats.network_impact.false_positive_transactions as f64 - / stats.network_impact.transactions_received as f64) - * 100.0 - } else { - 0.0 - }, - stats.network_impact.bandwidth_saved_bytes as f64 / 1_048_576.0, - stats.network_impact.filter_updates_sent - ) - } -} - -impl Default for BloomStatsTracker { - fn default() -> Self { - Self::new() - } -} diff --git a/dash-spv/src/bloom/tests.rs b/dash-spv/src/bloom/tests.rs deleted file mode 100644 index 6c15c5aeb..000000000 --- a/dash-spv/src/bloom/tests.rs +++ /dev/null @@ -1,799 +0,0 @@ -//! Comprehensive unit tests for bloom filter module - -#[cfg(test)] -#[allow(clippy::module_inception)] -mod tests { - use crate::bloom::{ - builder::BloomFilterBuilder, - manager::{BloomFilterConfig, BloomFilterManager}, - stats::BloomStatsTracker, - utils, - }; - - use dashcore::{ - address::{Address, Payload}, - blockdata::script::ScriptBuf, - bloom::BloomFlags, - hash_types::PubkeyHash, - OutPoint, Txid, - }; - - use std::sync::Arc; - - // Test data helpers - fn test_address() -> Address { - // Create a simple test address from a pubkey hash - let pubkey_hash = PubkeyHash::from([0u8; 20]); - Address::new(dashcore::Network::Dash, Payload::PubkeyHash(pubkey_hash)) - } - - fn test_outpoint() -> OutPoint { - OutPoint { - txid: Txid::from_hex( - "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", - ) - .unwrap(), - vout: 0, - } - } - - // === BloomFilterBuilder Tests === - - #[test] - fn test_builder_default() { - let builder = BloomFilterBuilder::new(); - // Since fields are private, we can't directly access them - // Instead, test the behavior through public interface - let filter = builder.build().unwrap(); - assert!(filter.is_empty()); - } - - #[test] - fn test_builder_configuration() { - let builder = BloomFilterBuilder::new() - .elements(1000) - .false_positive_rate(0.01) - .tweak(12345) - .flags(BloomFlags::None); - - // Build and verify it doesn't error - let filter = builder.build().unwrap(); - assert!(filter.is_empty()); - } - - #[test] - fn test_builder_add_single_address() { - let address = test_address(); - let builder = BloomFilterBuilder::new().add_address(address.clone()); - - let filter = builder.build().unwrap(); - - // Verify filter contains the address - let script = address.script_pubkey(); - assert!(filter.contains(script.as_bytes())); - } - - #[test] - fn test_builder_add_multiple_addresses() { - let addresses = vec![ - test_address(), - Address::new(dashcore::Network::Dash, Payload::PubkeyHash(PubkeyHash::from([1u8; 20]))), - ]; - let builder = BloomFilterBuilder::new().add_addresses(addresses.clone()); - - let filter = builder.build().unwrap(); - - // Verify filter contains all addresses - for address in addresses { - let script = address.script_pubkey(); - assert!(filter.contains(script.as_bytes())); - } - } - - #[test] - fn test_builder_add_outpoints() { - let outpoint1 = test_outpoint(); - let outpoint2 = OutPoint { - txid: Txid::from_hex( - "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd", - ) - .unwrap(), - vout: 1, - }; - - let builder = - BloomFilterBuilder::new().add_outpoint(outpoint1).add_outpoints(vec![outpoint2]); - - let filter = builder.build().unwrap(); - - // Verify filter contains outpoints - let outpoint1_bytes = utils::outpoint_to_bytes(&outpoint1); - let outpoint2_bytes = utils::outpoint_to_bytes(&outpoint2); - assert!(filter.contains(&outpoint1_bytes)); - assert!(filter.contains(&outpoint2_bytes)); - } - - #[test] - fn test_builder_add_data() { - let data1 = vec![1, 2, 3, 4]; - let data2 = vec![5, 6, 7, 8]; - - let builder = BloomFilterBuilder::new().add_data(data1.clone()).add_data(data2.clone()); - - let filter = builder.build().unwrap(); - - // Verify filter contains data - assert!(filter.contains(&data1)); - assert!(filter.contains(&data2)); - } - - #[test] - fn test_builder_build_empty() { - let builder = BloomFilterBuilder::new(); - let filter = builder.build().unwrap(); - - // Empty filter should still be created with default parameters - assert!(filter.is_empty()); - } - - #[test] - fn test_builder_build_with_elements() { - let address = test_address(); - let outpoint = test_outpoint(); - let data = vec![0xDE, 0xAD, 0xBE, 0xEF]; - - let builder = BloomFilterBuilder::new() - .elements(50) - .false_positive_rate(0.001) - .add_address(address.clone()) - .add_outpoint(outpoint) - .add_data(data.clone()); - - let filter = builder.build().unwrap(); - - // Verify filter contains added elements - let script = address.script_pubkey(); - assert!(filter.contains(script.as_bytes())); - - let outpoint_bytes = utils::outpoint_to_bytes(&outpoint); - assert!(filter.contains(&outpoint_bytes)); - - assert!(filter.contains(&data)); - } - - #[test] - fn test_builder_auto_adjusts_elements() { - // Add more elements than configured - let mut builder = BloomFilterBuilder::new().elements(1); - - for i in 0..10 { - builder = builder.add_data(vec![i]); - } - - // Should build successfully with adjusted element count - let filter = builder.build().unwrap(); - assert!(!filter.is_empty()); - } - - // === BloomFilterManager Tests === - - #[tokio::test] - async fn test_manager_creation() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config.clone()); - - // Check initial state through public interface - let stats = manager.get_stats().await; - assert_eq!(stats.items_added, 0); - assert_eq!(stats.queries, 0); - assert_eq!(stats.matches, 0); - assert_eq!(stats.recreations, 0); - } - - #[tokio::test] - async fn test_manager_create_filter_empty() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - let _filter_load = manager.create_filter().await.unwrap(); - - let stats = manager.get_stats().await; - assert_eq!(stats.recreations, 1); - assert_eq!(stats.items_added, 0); - } - - #[tokio::test] - async fn test_manager_add_address() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Create filter first - manager.create_filter().await.unwrap(); - - // Add address - let address = test_address(); - let filter_add = manager.add_address(&address).await.unwrap(); - - assert!(filter_add.is_some()); - - let stats = manager.get_stats().await; - assert_eq!(stats.items_added, 1); - } - - #[tokio::test] - async fn test_manager_add_address_no_filter() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Add address without creating filter - let address = test_address(); - let filter_add = manager.add_address(&address).await.unwrap(); - - assert!(filter_add.is_none()); - } - - #[tokio::test] - async fn test_manager_add_outpoint() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Create filter first - manager.create_filter().await.unwrap(); - - // Add outpoint - let outpoint = test_outpoint(); - let filter_add = manager.add_outpoint(&outpoint).await.unwrap(); - - assert!(filter_add.is_some()); - - let stats = manager.get_stats().await; - assert_eq!(stats.items_added, 1); - } - - #[tokio::test] - async fn test_manager_add_data() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Create filter first - manager.create_filter().await.unwrap(); - - // Add data - let data = vec![0x01, 0x02, 0x03]; - let filter_add = manager.add_data(data.clone()).await.unwrap(); - - assert!(filter_add.is_some()); - - let stats = manager.get_stats().await; - assert_eq!(stats.items_added, 1); - } - - #[tokio::test] - async fn test_manager_contains() { - let config = BloomFilterConfig { - enable_stats: true, - ..Default::default() - }; - let manager = BloomFilterManager::new(config); - - // No filter - should return true - assert!(manager.contains(&[1, 2, 3]).await); - - // Create filter and add data - manager.create_filter().await.unwrap(); - let data = vec![0xAB, 0xCD]; - manager.add_data(data.clone()).await.unwrap(); - - // Test contains - assert!(manager.contains(&data).await); - assert!(!manager.contains(&[0xFF, 0xFF]).await); // Should not contain random data - - let stats = manager.get_stats().await; - assert_eq!(stats.queries, 2); - assert_eq!(stats.matches, 1); - } - - #[tokio::test] - async fn test_manager_clear() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Add elements and create filter - manager.add_address(&test_address()).await.unwrap(); - manager.add_outpoint(&test_outpoint()).await.unwrap(); - manager.create_filter().await.unwrap(); - - // Clear - manager.clear().await; - - // Verify everything is cleared through stats - let stats = manager.get_stats().await; - assert_eq!(stats.items_added, 0); - assert_eq!(stats.queries, 0); - assert_eq!(stats.matches, 0); - assert_eq!(stats.recreations, 0); - } - - #[tokio::test] - async fn test_manager_needs_recreation() { - let config = BloomFilterConfig { - enable_stats: true, - max_false_positive_rate: 0.05, - ..Default::default() - }; - let manager = BloomFilterManager::new(config); - - // Initially should not need recreation - assert!(!manager.needs_recreation().await); - - // We can't directly set the false positive rate, but we can test the method - // returns false when stats are disabled - let config_no_stats = BloomFilterConfig { - enable_stats: false, - max_false_positive_rate: 0.05, - ..Default::default() - }; - let manager_no_stats = BloomFilterManager::new(config_no_stats); - assert!(!manager_no_stats.needs_recreation().await); - } - - #[tokio::test] - async fn test_manager_thread_safety() { - let config = BloomFilterConfig::default(); - let manager = Arc::new(BloomFilterManager::new(config)); - - // Create filter - manager.create_filter().await.unwrap(); - - // Spawn multiple tasks to add elements concurrently - let mut handles = vec![]; - - for i in 0..10 { - let manager_clone = Arc::clone(&manager); - let handle = tokio::spawn(async move { - let data = vec![i as u8; 4]; - manager_clone.add_data(data).await.unwrap(); - }); - handles.push(handle); - } - - // Wait for all tasks - for handle in handles { - handle.await.unwrap(); - } - - // Verify all elements were added - let stats = manager.get_stats().await; - assert_eq!(stats.items_added, 10); - } - - // === BloomFilterStats Tests === - - #[test] - fn test_stats_tracker_creation() { - let mut tracker = BloomStatsTracker::new(); - let stats = tracker.get_stats(); - - assert_eq!(stats.basic.items_added, 0); - assert_eq!(stats.basic.queries, 0); - assert_eq!(stats.basic.matches, 0); - assert_eq!(stats.basic.recreations, 0); - assert_eq!(stats.query_performance.avg_query_time_us, 0.0); - assert_eq!(stats.filter_health.filter_size_bytes, 0); - assert_eq!(stats.network_impact.transactions_received, 0); - } - - #[test] - fn test_stats_tracker_record_query() { - let mut tracker = BloomStatsTracker::new(); - - // Record successful query - tracker.record_query(std::time::Duration::from_micros(100), true); - let stats = tracker.get_stats(); - assert_eq!(stats.basic.queries, 1); - assert_eq!(stats.basic.matches, 1); - assert_eq!(stats.query_performance.total_query_time_us, 100); - assert_eq!(stats.query_performance.min_query_time_us, 100); - assert_eq!(stats.query_performance.max_query_time_us, 100); - - // Record failed query - tracker.record_query(std::time::Duration::from_micros(50), false); - let stats = tracker.get_stats(); - assert_eq!(stats.basic.queries, 2); - assert_eq!(stats.basic.matches, 1); - assert_eq!(stats.query_performance.min_query_time_us, 50); - assert_eq!(stats.query_performance.max_query_time_us, 100); - } - - #[test] - fn test_stats_tracker_record_addition() { - let mut tracker = BloomStatsTracker::new(); - - tracker.record_addition(); - let stats = tracker.get_stats(); - assert_eq!(stats.basic.items_added, 1); - - tracker.record_addition(); - let stats = tracker.get_stats(); - assert_eq!(stats.basic.items_added, 2); - } - - #[test] - fn test_stats_tracker_record_recreation() { - let mut tracker = BloomStatsTracker::new(); - - tracker.record_recreation(1024, 512, 8192); - let stats = tracker.get_stats(); - assert_eq!(stats.basic.recreations, 1); - assert_eq!(stats.filter_health.filter_size_bytes, 1024); - assert_eq!(stats.filter_health.bits_set, 512); - assert_eq!(stats.filter_health.total_bits, 8192); - assert_eq!(stats.filter_health.saturation_percent, 6.25); - assert!(stats.filter_health.time_since_recreation.is_some()); - } - - #[test] - fn test_stats_tracker_record_transaction() { - let mut tracker = BloomStatsTracker::new(); - - // Record true positive - tracker.record_transaction(false, 250); - let stats = tracker.get_stats(); - assert_eq!(stats.network_impact.transactions_received, 1); - assert_eq!(stats.network_impact.false_positive_transactions, 0); - assert!(stats.network_impact.bandwidth_saved_bytes > 0); - - // Record false positive - tracker.record_transaction(true, 250); - let stats = tracker.get_stats(); - assert_eq!(stats.network_impact.transactions_received, 2); - assert_eq!(stats.network_impact.false_positive_transactions, 1); - } - - #[test] - fn test_stats_tracker_update_false_positive_rate() { - let mut tracker = BloomStatsTracker::new(); - - tracker.update_false_positive_rate(0.025); - let stats = tracker.get_stats(); - assert_eq!(stats.basic.current_false_positive_rate, 0.025); - } - - #[test] - fn test_stats_tracker_reset() { - let mut tracker = BloomStatsTracker::new(); - - // Add some data - tracker.record_query(std::time::Duration::from_micros(100), true); - tracker.record_addition(); - tracker.record_recreation(1024, 512, 8192); - - // Reset - tracker.reset(); - - // Verify all stats are reset - let stats = tracker.get_stats(); - assert_eq!(stats.basic.items_added, 0); - assert_eq!(stats.basic.queries, 0); - assert_eq!(stats.basic.matches, 0); - assert_eq!(stats.basic.recreations, 0); - assert!(stats.filter_health.time_since_recreation.is_none()); - } - - #[test] - fn test_stats_tracker_summary_report() { - let mut tracker = BloomStatsTracker::new(); - - // Add some data - tracker.record_query(std::time::Duration::from_micros(100), true); - tracker.record_query(std::time::Duration::from_micros(200), false); - tracker.record_addition(); - tracker.record_recreation(1024, 512, 8192); - tracker.record_transaction(false, 500); - tracker.record_filter_update(); - tracker.update_false_positive_rate(0.01); - - let report = tracker.summary_report(); - - // Verify report contains expected information - assert!(report.contains("Bloom Filter Statistics")); - assert!(report.contains("Items Added: 1")); - assert!(report.contains("Queries: 2")); - assert!(report.contains("Current FP Rate: 1.0000%")); - assert!(report.contains("Filter Recreations: 1")); - assert!(report.contains("Size: 1024 bytes")); - assert!(report.contains("Saturation: 6.2%")); - } - - // === Utility Function Tests === - - #[test] - fn test_extract_pubkey_hash_valid_p2pkh() { - // Valid P2PKH script - let script_bytes = vec![ - 0x76, // OP_DUP - 0xa9, // OP_HASH160 - 0x14, // Push 20 bytes - // 20 bytes of pubkey hash - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, - 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x88, // OP_EQUALVERIFY - 0xac, // OP_CHECKSIG - ]; - let script = ScriptBuf::from(script_bytes); - - let hash = utils::extract_pubkey_hash(&script); - assert!(hash.is_some()); - - let extracted = hash.unwrap(); - assert_eq!(extracted.len(), 20); - assert_eq!(extracted[0], 0x01); - assert_eq!(extracted[19], 0x14); - } - - #[test] - fn test_extract_pubkey_hash_invalid_scripts() { - // Too short - let script1 = ScriptBuf::from(vec![0x76, 0xa9]); - assert!(utils::extract_pubkey_hash(&script1).is_none()); - - // Wrong length - let script2 = ScriptBuf::from(vec![0x76; 30]); - assert!(utils::extract_pubkey_hash(&script2).is_none()); - - // Wrong opcodes - let script3 = ScriptBuf::from(vec![ - 0x00, // Wrong opcode - 0xa9, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0xac, - ]); - assert!(utils::extract_pubkey_hash(&script3).is_none()); - - // Empty script - let script4 = ScriptBuf::from(vec![]); - assert!(utils::extract_pubkey_hash(&script4).is_none()); - } - - #[test] - fn test_outpoint_to_bytes() { - let outpoint = test_outpoint(); - let bytes = utils::outpoint_to_bytes(&outpoint); - - // Should be 32 bytes txid + 4 bytes vout - assert_eq!(bytes.len(), 36); - - // Verify txid is included - assert_eq!(&bytes[0..32], &outpoint.txid[..]); - - // Verify vout is included (little-endian) - let vout_bytes = outpoint.vout.to_le_bytes(); - assert_eq!(&bytes[32..36], &vout_bytes); - } - - #[test] - fn test_outpoint_to_bytes_different_vouts() { - let txid = - Txid::from_hex("abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234") - .unwrap(); - - let outpoint1 = OutPoint { - txid, - vout: 0, - }; - let outpoint2 = OutPoint { - txid, - vout: 1, - }; - let outpoint3 = OutPoint { - txid, - vout: u32::MAX, - }; - - let bytes1 = utils::outpoint_to_bytes(&outpoint1); - let bytes2 = utils::outpoint_to_bytes(&outpoint2); - let bytes3 = utils::outpoint_to_bytes(&outpoint3); - - // Same txid part - assert_eq!(&bytes1[0..32], &bytes2[0..32]); - assert_eq!(&bytes1[0..32], &bytes3[0..32]); - - // Different vout parts - assert_ne!(&bytes1[32..36], &bytes2[32..36]); - assert_ne!(&bytes1[32..36], &bytes3[32..36]); - assert_ne!(&bytes2[32..36], &bytes3[32..36]); - - // Verify specific vout values - assert_eq!(&bytes1[32..36], &[0, 0, 0, 0]); - assert_eq!(&bytes2[32..36], &[1, 0, 0, 0]); - assert_eq!(&bytes3[32..36], &[0xFF, 0xFF, 0xFF, 0xFF]); - } - - // === Edge Cases and Error Handling === - - #[test] - fn test_builder_zero_false_positive_rate() { - let builder = BloomFilterBuilder::new().false_positive_rate(0.0); - - // Should handle edge case gracefully - let result = builder.build(); - // Zero false positive rate might cause an error in the underlying library - // But our code should handle it gracefully - assert!(result.is_ok() || result.is_err()); - } - - #[test] - fn test_builder_very_high_false_positive_rate() { - let builder = BloomFilterBuilder::new().false_positive_rate(0.99).add_data(vec![1, 2, 3]); - - let filter = builder.build().unwrap(); - // Filter should still be created, though not very useful - assert!(!filter.is_empty()); - } - - #[tokio::test] - async fn test_manager_concurrent_operations() { - let config = BloomFilterConfig::default(); - let manager = Arc::new(BloomFilterManager::new(config)); - - // Create filter - manager.create_filter().await.unwrap(); - - // Perform concurrent operations - let m1 = Arc::clone(&manager); - let m2 = Arc::clone(&manager); - let m3 = Arc::clone(&manager); - - let (_r1, _r2, _r3) = tokio::join!( - async move { - for i in 0..10 { - m1.add_data(vec![i]).await.unwrap(); - } - }, - async move { - for _ in 0..10 { - m2.contains(&[0xFF]).await; - } - }, - async move { - for _ in 0..5 { - m3.get_stats().await; - } - } - ); - - // All operations should complete without deadlock - let final_stats = manager.get_stats().await; - assert_eq!(final_stats.items_added, 10); - assert_eq!(final_stats.queries, 10); - } - - #[test] - fn test_config_validation() { - // Construct with desired custom fields instead of reassigning after Default - let config = BloomFilterConfig { - false_positive_rate: 0.0001, - elements: 1, - max_false_positive_rate: 0.1, - ..Default::default() - }; - - assert!(config.false_positive_rate > 0.0 && config.false_positive_rate < 1.0); - assert!(config.elements > 0); - assert!(config.max_false_positive_rate > config.false_positive_rate); - } - - #[test] - fn test_stats_query_time_average() { - let mut tracker = BloomStatsTracker::new(); - - // Add many queries to test average calculation - for i in 1..=100 { - tracker.record_query(std::time::Duration::from_micros(i as u64), i % 2 == 0); - } - - let stats = tracker.get_stats(); - assert_eq!(stats.basic.queries, 100); - assert_eq!(stats.basic.matches, 50); - - // Average should be around 50.5 microseconds for last 100 queries - assert!((stats.query_performance.avg_query_time_us - 50.5).abs() < 1.0); - } - - #[test] - fn test_stats_query_time_overflow_protection() { - let mut tracker = BloomStatsTracker::new(); - - // Add more than 1000 queries to test queue overflow protection - for i in 1..=2000 { - tracker.record_query(std::time::Duration::from_micros(i as u64), true); - } - - // Should only keep last 1000 queries in the internal buffer - let stats = tracker.get_stats(); - assert_eq!(stats.basic.queries, 2000); - - // The average should be calculated from the recent queries - // For queries 1001-2000, the average should be 1500.5 - assert!((stats.query_performance.avg_query_time_us - 1500.5).abs() < 1.0); - } - - // === Transaction Processing Tests === - - #[tokio::test] - async fn test_manager_process_transaction() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Create filter and add an address - manager.create_filter().await.unwrap(); - let address = test_address(); - manager.add_address(&address).await.unwrap(); - - // Create a transaction that pays to our address - let mut tx = dashcore::Transaction { - version: 1, - lock_time: 0, - input: vec![], - output: vec![dashcore::TxOut { - value: 5000, - script_pubkey: address.script_pubkey(), - }], - special_transaction_payload: None, - }; - - // Should match because output is to our address - assert!(manager.process_transaction(&tx).await); - - // Create a transaction that doesn't involve us - tx.output[0].script_pubkey = - Address::new(dashcore::Network::Dash, Payload::PubkeyHash(PubkeyHash::from([2u8; 20]))) - .script_pubkey(); - - // Should not match - assert!(!manager.process_transaction(&tx).await); - } - - #[tokio::test] - async fn test_manager_process_transaction_with_inputs() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Create filter and add an outpoint - manager.create_filter().await.unwrap(); - let outpoint = test_outpoint(); - manager.add_outpoint(&outpoint).await.unwrap(); - - // Create a transaction that spends our outpoint - let tx = dashcore::Transaction { - version: 1, - lock_time: 0, - input: vec![dashcore::TxIn { - previous_output: outpoint, - script_sig: ScriptBuf::new(), - sequence: 0xFFFFFFFF, - witness: dashcore::blockdata::witness::Witness::default(), - }], - output: vec![], - special_transaction_payload: None, - }; - - // Should match because input spends our outpoint - assert!(manager.process_transaction(&tx).await); - } - - #[tokio::test] - async fn test_manager_process_transaction_no_filter() { - let config = BloomFilterConfig::default(); - let manager = BloomFilterManager::new(config); - - // Without a filter, all transactions should match - let tx = dashcore::Transaction { - version: 1, - lock_time: 0, - input: vec![], - output: vec![], - special_transaction_payload: None, - }; - - assert!(manager.process_transaction(&tx).await); - } -} diff --git a/dash-spv/src/bloom/utils.rs b/dash-spv/src/bloom/utils.rs deleted file mode 100644 index aaad662ec..000000000 --- a/dash-spv/src/bloom/utils.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Shared utility functions for bloom filter operations - -use dashcore::OutPoint; -use dashcore::Script; - -/// Extract pubkey hash from P2PKH script -pub fn extract_pubkey_hash(script: &Script) -> Option> { - let bytes = script.as_bytes(); - // P2PKH: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG - if bytes.len() == 25 - && bytes[0] == 0x76 // OP_DUP - && bytes[1] == 0xa9 // OP_HASH160 - && bytes[2] == 0x14 // Push 20 bytes - && bytes[23] == 0x88 // OP_EQUALVERIFY - && bytes[24] == 0xac - // OP_CHECKSIG - { - Some(bytes[3..23].to_vec()) - } else { - None - } -} - -/// Convert outpoint to bytes for bloom filter -pub fn outpoint_to_bytes(outpoint: &OutPoint) -> Vec { - let mut bytes = Vec::with_capacity(36); - bytes.extend_from_slice(&outpoint.txid[..]); - bytes.extend_from_slice(&outpoint.vout.to_le_bytes()); - bytes -} diff --git a/dash-spv/src/lib.rs b/dash-spv/src/lib.rs index 2e93b57b6..ce5994be0 100644 --- a/dash-spv/src/lib.rs +++ b/dash-spv/src/lib.rs @@ -58,7 +58,6 @@ //! - **Persistent storage**: Save and restore state between runs //! - **Extensive logging**: Built-in tracing support for debugging -pub mod bloom; pub mod chain; pub mod client; pub mod error;