From aef4a0be9b8c164de7c7a6d14a5ebf4e0773158f Mon Sep 17 00:00:00 2001 From: xdustinface Date: Tue, 25 Nov 2025 19:27:19 +1000 Subject: [PATCH] chore: Drop unused sync code --- dash-spv/src/client/mod.rs | 1 - dash-spv/src/sync/chainlock_validation.rs | 372 ----- dash-spv/src/sync/discovery.rs | 40 - dash-spv/src/sync/headers.rs | 705 ---------- dash-spv/src/sync/mod.rs | 21 - dash-spv/src/sync/sequential/lifecycle.rs | 22 - dash-spv/src/sync/sequential/manager.rs | 30 - dash-spv/src/sync/sequential/mod.rs | 5 - dash-spv/src/sync/sequential/phases.rs | 173 +-- dash-spv/src/sync/sequential/progress.rs | 369 ----- dash-spv/src/sync/sequential/recovery.rs | 559 -------- .../src/sync/sequential/request_control.rs | 410 ------ dash-spv/src/sync/sequential/transitions.rs | 1 - dash-spv/src/sync/state.rs | 85 -- dash-spv/src/sync/validation.rs | 559 -------- dash-spv/src/sync/validation_state.rs | 488 ------- dash-spv/src/sync/validation_test.rs | 248 ---- dash-spv/tests/error_handling_test.rs | 1200 ----------------- .../tests/error_recovery_integration_test.rs | 776 ----------- dash-spv/tests/header_sync_test.rs | 68 - 20 files changed, 1 insertion(+), 6131 deletions(-) delete mode 100644 dash-spv/src/sync/chainlock_validation.rs delete mode 100644 dash-spv/src/sync/discovery.rs delete mode 100644 dash-spv/src/sync/headers.rs delete mode 100644 dash-spv/src/sync/sequential/progress.rs delete mode 100644 dash-spv/src/sync/sequential/recovery.rs delete mode 100644 dash-spv/src/sync/sequential/request_control.rs delete mode 100644 dash-spv/src/sync/state.rs delete mode 100644 dash-spv/src/sync/validation.rs delete mode 100644 dash-spv/src/sync/validation_state.rs delete mode 100644 dash-spv/src/sync/validation_test.rs delete mode 100644 dash-spv/tests/error_handling_test.rs delete mode 100644 dash-spv/tests/error_recovery_integration_test.rs diff --git a/dash-spv/src/client/mod.rs b/dash-spv/src/client/mod.rs index 1e8c2c0a9..7fd224386 100644 --- a/dash-spv/src/client/mod.rs +++ b/dash-spv/src/client/mod.rs @@ -7,7 +7,6 @@ //! - `core.rs` - Core DashSpvClient struct definition and simple accessors //! - `lifecycle.rs` - Client lifecycle (new, start, stop, shutdown) //! - `events.rs` - Event emission and progress tracking receivers -//! - `progress.rs` - Sync progress calculation and reporting //! - `mempool.rs` - Mempool tracking and coordination //! - `queries.rs` - Peer, masternode, and balance queries //! - `transactions.rs` - Transaction operations (e.g., broadcast) diff --git a/dash-spv/src/sync/chainlock_validation.rs b/dash-spv/src/sync/chainlock_validation.rs deleted file mode 100644 index 8627539b5..000000000 --- a/dash-spv/src/sync/chainlock_validation.rs +++ /dev/null @@ -1,372 +0,0 @@ -//! Chain lock validation for enhanced security. -//! -//! This module provides comprehensive validation of chain locks, including -//! historical verification and caching for performance optimization. - -use crate::error::{SyncError, SyncResult}; -use crate::storage::StorageManager; -use dashcore::{ - sml::{llmq_type::LLMQType, masternode_list_engine::MasternodeListEngine}, - BlockHash, ChainLock, -}; -use std::collections::{HashMap, VecDeque}; -use std::time::{Duration, Instant}; -use tracing; - -/// Configuration for chain lock validation -#[derive(Debug, Clone)] -pub struct ChainLockValidationConfig { - /// Enable chain lock validation - pub enabled: bool, - /// Maximum number of chain locks to cache - pub cache_size: usize, - /// TTL for cached validation results - pub cache_ttl: Duration, - /// Validate historical chain locks - pub validate_historical: bool, - /// Maximum depth for historical validation - pub max_historical_depth: u32, - /// Required LLMQ type for chain lock validation - pub required_llmq_type: LLMQType, -} - -impl Default for ChainLockValidationConfig { - fn default() -> Self { - Self { - enabled: true, - cache_size: 1000, - cache_ttl: Duration::from_secs(3600), // 1 hour - validate_historical: true, - max_historical_depth: 1000, - required_llmq_type: LLMQType::Llmqtype400_60, // ChainLocks quorum type - } - } -} - -/// Result of chain lock validation -#[derive(Debug, Clone)] -pub struct ChainLockValidationResult { - /// Whether the chain lock is valid - pub is_valid: bool, - /// Height of the chain lock - pub height: u32, - /// Quorum hash used for validation - pub quorum_hash: Option, - /// Validation error if any - pub error: Option, - /// Time taken for validation - pub validation_time: Duration, -} - -/// Chain lock validator with caching and historical verification -pub struct ChainLockValidator { - /// Configuration - config: ChainLockValidationConfig, - /// Cache of validated chain locks - validation_cache: HashMap, - /// LRU queue for cache eviction - cache_lru: VecDeque, - /// Statistics - stats: ChainLockStats, -} - -/// Cached chain lock validation result -#[derive(Debug, Clone)] -struct CachedChainLockResult { - is_valid: bool, - height: u32, - timestamp: Instant, -} - -/// Chain lock validation statistics -#[derive(Debug, Default)] -pub struct ChainLockStats { - total_validations: usize, - successful_validations: usize, - failed_validations: usize, - cache_hits: usize, - cache_misses: usize, - historical_validations: usize, -} - -impl ChainLockValidator { - /// Create a new chain lock validator - pub fn new(config: ChainLockValidationConfig) -> Self { - Self { - config, - validation_cache: HashMap::new(), - cache_lru: VecDeque::new(), - stats: ChainLockStats::default(), - } - } - - /// Validate a chain lock - pub async fn validate_chain_lock( - &mut self, - chain_lock: &ChainLock, - engine: &MasternodeListEngine, - storage: &dyn StorageManager, - ) -> SyncResult { - if !self.config.enabled { - return Ok(ChainLockValidationResult { - is_valid: true, - height: chain_lock.block_height, - quorum_hash: None, - error: None, - validation_time: Duration::from_secs(0), - }); - } - - let start = Instant::now(); - let block_hash = chain_lock.block_hash; - - // Check cache first - let cached_result = self.get_cached_result(&block_hash).cloned(); - if let Some(cached) = cached_result { - self.stats.cache_hits += 1; - return Ok(ChainLockValidationResult { - is_valid: cached.is_valid, - height: cached.height, - quorum_hash: None, - error: if cached.is_valid { - None - } else { - Some("Cached validation failure".to_string()) - }, - validation_time: start.elapsed(), - }); - } - - self.stats.cache_misses += 1; - - // Perform validation - let result = self.perform_chain_lock_validation(chain_lock, engine, storage).await?; - - // Cache the result - self.cache_result(block_hash, result.is_valid, result.height); - - // Update statistics - self.stats.total_validations += 1; - if result.is_valid { - self.stats.successful_validations += 1; - } else { - self.stats.failed_validations += 1; - } - - Ok(result) - } - - /// Perform actual chain lock validation - async fn perform_chain_lock_validation( - &mut self, - chain_lock: &ChainLock, - engine: &MasternodeListEngine, - storage: &dyn StorageManager, - ) -> SyncResult { - let start = Instant::now(); - - // Get the block header to verify height - let header = storage - .get_header(chain_lock.block_height) - .await - .map_err(|e| { - SyncError::Storage(format!( - "Failed to get header at height {}: {}", - chain_lock.block_height, e - )) - })? - .ok_or_else(|| { - SyncError::Validation(format!( - "Header not found at height {}", - chain_lock.block_height - )) - })?; - - // Verify block hash matches - if header.block_hash() != chain_lock.block_hash { - return Ok(ChainLockValidationResult { - is_valid: false, - height: chain_lock.block_height, - quorum_hash: None, - error: Some(format!("Block hash mismatch at height {}", chain_lock.block_height)), - validation_time: start.elapsed(), - }); - } - - // Use the engine's built-in chain lock verification - let is_valid = self.verify_chain_lock_with_engine(chain_lock, engine)?; - - Ok(ChainLockValidationResult { - is_valid, - height: chain_lock.block_height, - quorum_hash: None, // Engine doesn't expose which quorum was used - error: if is_valid { - None - } else { - Some("Chain lock verification failed".to_string()) - }, - validation_time: start.elapsed(), - }) - } - - /// Verify chain lock signature using the engine's built-in verification - fn verify_chain_lock_with_engine( - &self, - chain_lock: &ChainLock, - engine: &MasternodeListEngine, - ) -> SyncResult { - // Use the engine's built-in chain lock verification - match engine.verify_chain_lock(chain_lock) { - Ok(()) => { - tracing::debug!( - "Chain lock verified successfully for block {:x} at height {}", - chain_lock.block_hash, - chain_lock.block_height - ); - Ok(true) - } - Err(e) => { - tracing::warn!( - "Chain lock verification failed for block {:x} at height {}: {}", - chain_lock.block_hash, - chain_lock.block_height, - e - ); - Ok(false) - } - } - } - - /// Validate historical chain locks - pub async fn validate_historical_chain_locks( - &mut self, - start_height: u32, - end_height: u32, - engine: &MasternodeListEngine, - storage: &dyn StorageManager, - ) -> SyncResult> { - if !self.config.validate_historical { - return Ok(Vec::new()); - } - - let depth = end_height.saturating_sub(start_height); - if depth > self.config.max_historical_depth { - return Err(SyncError::Validation(format!( - "Historical validation depth {} exceeds maximum {}", - depth, self.config.max_historical_depth - ))); - } - - self.stats.historical_validations += 1; - let mut results = Vec::new(); - - tracing::info!( - "Starting historical chain lock validation from height {} to {}", - start_height, - end_height - ); - - for height in start_height..=end_height { - // Get chain lock at height from storage - if let Ok(Some(chain_lock)) = storage.load_chain_lock(height).await { - let result = self.validate_chain_lock(&chain_lock, engine, storage).await?; - results.push(result); - } - } - - tracing::info!("Completed historical validation of {} chain locks", results.len()); - - Ok(results) - } - - /// Get cached validation result - fn get_cached_result(&self, block_hash: &BlockHash) -> Option<&CachedChainLockResult> { - self.validation_cache - .get(block_hash) - .filter(|cached| cached.timestamp.elapsed() < self.config.cache_ttl) - } - - /// Cache a validation result - fn cache_result(&mut self, block_hash: BlockHash, is_valid: bool, height: u32) { - // Remove oldest entry if cache is full - if self.validation_cache.len() >= self.config.cache_size { - if let Some(oldest) = self.cache_lru.pop_front() { - self.validation_cache.remove(&oldest); - } - } - - self.validation_cache.insert( - block_hash, - CachedChainLockResult { - is_valid, - height, - timestamp: Instant::now(), - }, - ); - - self.cache_lru.push_back(block_hash); - } - - /// Clear the validation cache - pub fn clear_cache(&mut self) { - self.validation_cache.clear(); - self.cache_lru.clear(); - } - - /// Get validation statistics - pub fn stats(&self) -> &ChainLockStats { - &self.stats - } - - /// Get cache hit rate - pub fn cache_hit_rate(&self) -> f64 { - let total_lookups = self.stats.cache_hits + self.stats.cache_misses; - if total_lookups > 0 { - self.stats.cache_hits as f64 / total_lookups as f64 - } else { - 0.0 - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_chain_lock_validator_creation() { - let config = ChainLockValidationConfig::default(); - let validator = ChainLockValidator::new(config); - - assert_eq!(validator.stats().total_validations, 0); - assert_eq!(validator.cache_hit_rate(), 0.0); - } - - #[test] - fn test_cache_eviction() { - let config = ChainLockValidationConfig { - cache_size: 2, - ..Default::default() - }; - - let mut validator = ChainLockValidator::new(config); - - // Add entries to fill cache - let hash1 = BlockHash::from([0u8; 32]); - let hash2 = BlockHash::from([1; 32]); - let hash3 = BlockHash::from([2; 32]); - - validator.cache_result(hash1, true, 100); - validator.cache_result(hash2, true, 101); - - assert_eq!(validator.validation_cache.len(), 2); - - // Adding third entry should evict first - validator.cache_result(hash3, true, 102); - - assert_eq!(validator.validation_cache.len(), 2); - assert!(!validator.validation_cache.contains_key(&hash1)); - assert!(validator.validation_cache.contains_key(&hash2)); - assert!(validator.validation_cache.contains_key(&hash3)); - } -} diff --git a/dash-spv/src/sync/discovery.rs b/dash-spv/src/sync/discovery.rs deleted file mode 100644 index 5b674344b..000000000 --- a/dash-spv/src/sync/discovery.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Simplified discovery for masternode data following dash-evo-tool approach. -//! -//! Since we use the direct dash-evo-tool pattern with single QRInfo requests, -//! complex discovery logic is not needed. - -use dashcore::BlockHash; - -/// Simplified request for QRInfo data (kept for compatibility) -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct QRInfoRequest { - /// Base block height for the request - pub base_height: u32, - /// Tip block height for the request - pub tip_height: u32, - /// Base block hash - pub base_hash: BlockHash, - /// Tip block hash - pub tip_hash: BlockHash, - /// Whether to request extra validation data - pub extra_share: bool, -} - -impl QRInfoRequest { - /// Create a new QRInfo request - pub fn new( - base_height: u32, - tip_height: u32, - base_hash: BlockHash, - tip_hash: BlockHash, - extra_share: bool, - ) -> Self { - Self { - base_height, - tip_height, - base_hash, - tip_hash, - extra_share, - } - } -} diff --git a/dash-spv/src/sync/headers.rs b/dash-spv/src/sync/headers.rs deleted file mode 100644 index e1e60c49f..000000000 --- a/dash-spv/src/sync/headers.rs +++ /dev/null @@ -1,705 +0,0 @@ -//! Header synchronization functionality. - -use dashcore::{ - block::Header as BlockHeader, network::constants::NetworkExt, network::message::NetworkMessage, - network::message_blockdata::GetHeadersMessage, network::message_headers2::Headers2Message, - BlockHash, -}; -use dashcore_hashes::Hash; - -use crate::client::ClientConfig; -use crate::error::{SyncError, SyncResult}; -use crate::network::NetworkManager; -use crate::storage::StorageManager; -use crate::sync::headers2_state::Headers2StateManager; -use crate::types::CachedHeader; -use crate::validation::ValidationManager; - -/// Manages header synchronization. -pub struct HeaderSyncManager { - config: ClientConfig, - validation: ValidationManager, - headers2_state: Headers2StateManager, - total_headers_synced: u32, - last_progress_log: Option, - /// Whether header sync is currently in progress - syncing_headers: bool, - /// Last time sync progress was made (for timeout detection) - last_sync_progress: std::time::Instant, -} - -impl HeaderSyncManager { - /// Create a new header sync manager. - pub fn new(config: &ClientConfig) -> Self { - Self { - config: config.clone(), - validation: ValidationManager::new(config.validation_mode), - headers2_state: Headers2StateManager::new(), - total_headers_synced: 0, - last_progress_log: None, - syncing_headers: false, - last_sync_progress: std::time::Instant::now(), - } - } - - /// Handle a Headers message during header synchronization or for new blocks received post-sync. - /// Returns true if the message was processed and sync should continue, false if sync is complete. - pub async fn handle_headers_message( - &mut self, - headers: Vec, - storage: &mut dyn StorageManager, - network: &mut dyn NetworkManager, - ) -> SyncResult { - tracing::info!( - "🔍 Handle headers message called with {} headers, syncing_headers: {}", - headers.len(), - self.syncing_headers - ); - - if headers.is_empty() { - if self.syncing_headers { - // No more headers available during sync - tracing::info!("Received empty headers response, sync complete"); - self.syncing_headers = false; - return Ok(false); - } else { - // Empty headers outside of sync - just ignore - tracing::debug!("Received empty headers response outside of sync"); - return Ok(true); - } - } - - // Log the first and last header received - tracing::info!( - "📥 Processing headers: first={} last={}", - headers[0].block_hash(), - headers[headers.len() - 1].block_hash() - ); - - // Get the current tip before processing - let tip_before = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))?; - tracing::info!("📊 Current tip height before processing: {:?}", tip_before); - - if self.syncing_headers { - self.last_sync_progress = std::time::Instant::now(); - } - - // Update progress tracking - self.total_headers_synced += headers.len() as u32; - - // Log progress periodically (every 10,000 headers or every 30 seconds) - let should_log = match self.last_progress_log { - None => true, - Some(last_time) => { - last_time.elapsed() >= std::time::Duration::from_secs(30) - || self.total_headers_synced.is_multiple_of(10000) - } - }; - - if should_log { - let current_tip_height = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))? - .unwrap_or(0); - - tracing::info!( - "📊 Header sync progress: {} headers synced (current tip: height {})", - self.total_headers_synced, - current_tip_height + headers.len() as u32 - ); - tracing::debug!( - "Latest batch: {} headers, range {} → {}", - headers.len(), - headers[0].block_hash(), - headers.last().map(|h| h.block_hash()).unwrap_or_else(|| headers[0].block_hash()) - ); - self.last_progress_log = Some(std::time::Instant::now()); - } else { - // Just a brief debug message for each batch - tracing::debug!( - "Received {} headers (total synced: {})", - headers.len(), - self.total_headers_synced - ); - } - - // Validate headers - let validated_headers = self.validate_headers(&headers, storage).await?; - - // Store headers - storage - .store_headers(&validated_headers) - .await - .map_err(|e| SyncError::Storage(format!("Failed to store headers: {}", e)))?; - - // Get the current tip after processing - let tip_after = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))?; - tracing::info!("📊 Current tip height after processing: {:?}", tip_after); - - // Log if headers were actually stored - if tip_before != tip_after { - tracing::info!( - "✅ Successfully stored {} headers, tip advanced from {:?} to {:?}", - validated_headers.len(), - tip_before, - tip_after - ); - } else { - tracing::warn!("⚠️ Headers validated but tip height unchanged! Validated {} headers but tip remains at {:?}", - validated_headers.len(), tip_before); - } - - if self.syncing_headers { - // During sync mode - request next batch - if let Some(last_header) = headers.last() { - self.request_headers(network, Some(last_header.block_hash())).await?; - } else { - return Err(SyncError::InvalidState( - "Headers array empty when expected".to_string(), - )); - } - } else { - // Post-sync mode - new blocks received dynamically - tracing::info!("📋 Processed {} new headers post-sync", headers.len()); - - // For post-sync headers, we return true to indicate successful processing - // The caller can then request filter headers and filters for these new blocks - } - - Ok(true) - } - - /// Handle a Headers2 message with compressed headers. - /// Returns true if the message was processed and sync should continue, false if sync is complete. - pub async fn handle_headers2_message( - &mut self, - headers2: Headers2Message, - peer_id: crate::types::PeerId, - storage: &mut dyn StorageManager, - network: &mut dyn NetworkManager, - ) -> SyncResult { - tracing::info!( - "🔍 Handle headers2 message called with {} compressed headers from peer {}", - headers2.headers.len(), - peer_id - ); - - // Decompress headers using the peer's compression state - let headers = self - .headers2_state - .process_headers(peer_id, headers2.headers) - .map_err(|e| SyncError::Validation(format!("Failed to decompress headers: {}", e)))?; - - // Log compression statistics - let stats = self.headers2_state.get_stats(); - tracing::info!( - "📊 Headers2 compression stats: {:.1}% bandwidth saved, {:.1}% compression ratio", - stats.bandwidth_savings, - stats.compression_ratio * 100.0 - ); - - // Process decompressed headers through the normal flow - self.handle_headers_message(headers, storage, network).await - } - - /// Check if a sync timeout has occurred and handle recovery. - pub async fn check_sync_timeout( - &mut self, - storage: &mut dyn StorageManager, - network: &mut dyn NetworkManager, - ) -> SyncResult { - if !self.syncing_headers { - return Ok(false); - } - - let timeout_duration = if network.peer_count() == 0 { - // More aggressive timeout when no peers - std::time::Duration::from_secs(10) - } else { - std::time::Duration::from_secs(5) - }; - - if self.last_sync_progress.elapsed() > timeout_duration { - if network.peer_count() == 0 { - tracing::warn!("📊 Header sync stalled - no connected peers"); - self.syncing_headers = false; // Reset state to allow restart - return Err(SyncError::Network("No connected peers for header sync".to_string())); - } - - tracing::warn!( - "📊 No header sync progress for {}+ seconds, re-sending header request", - timeout_duration.as_secs() - ); - - // Get current tip for recovery - let current_tip_height = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))?; - - let recovery_base_hash = match current_tip_height { - None => None, // Genesis - Some(height) => { - // Get the current tip hash - storage - .get_header(height) - .await - .map_err(|e| { - SyncError::Storage(format!( - "Failed to get tip header for recovery: {}", - e - )) - })? - .map(|h| h.block_hash()) - } - }; - - self.request_headers(network, recovery_base_hash).await?; - self.last_sync_progress = std::time::Instant::now(); - - return Ok(true); - } - - Ok(false) - } - - /// Prepare sync state without sending network requests. - /// This allows monitoring to be set up before requests are sent. - pub async fn prepare_sync( - &mut self, - storage: &mut dyn StorageManager, - ) -> SyncResult> { - if self.syncing_headers { - return Err(SyncError::SyncInProgress); - } - - tracing::info!("Preparing header synchronization"); - - // Get current tip from storage - let current_tip_height = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))?; - - let base_hash = match current_tip_height { - None => { - tracing::info!("No tip height found, will start from genesis"); - None // Start from genesis - } - Some(height) => { - tracing::info!("Current tip height: {}", height); - // Get the current tip hash - let tip_header = storage - .get_header(height) - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip header: {}", e)))?; - let hash = tip_header.map(|h| h.block_hash()); - tracing::info!("Current tip hash: {:?}", hash); - hash - } - }; - - // Set sync state but don't send requests yet - self.syncing_headers = true; - self.last_sync_progress = std::time::Instant::now(); - tracing::info!( - "✅ Prepared header sync state, ready to request headers from {:?}", - base_hash - ); - - Ok(base_hash) - } - - /// Start synchronizing headers (initialize the sync state). - /// This replaces the old sync method but doesn't loop for messages. - pub async fn start_sync( - &mut self, - network: &mut dyn NetworkManager, - storage: &mut dyn StorageManager, - ) -> SyncResult { - if self.syncing_headers { - return Err(SyncError::SyncInProgress); - } - - tracing::info!("Starting header synchronization"); - - // Get current tip from storage - let current_tip_height = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))?; - - let base_hash = match current_tip_height { - None => None, // Start from genesis - Some(height) => { - // Get the current tip hash - let tip_header = storage - .get_header(height) - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip header: {}", e)))?; - tip_header.map(|h| h.block_hash()) - } - }; - - // Set sync state - self.syncing_headers = true; - self.last_sync_progress = std::time::Instant::now(); - tracing::info!("✅ Set syncing_headers = true, requesting headers from {:?}", base_hash); - - // Request headers starting from our current tip - self.request_headers(network, base_hash).await?; - - Ok(true) // Sync started - } - - /// Request headers from the network. - pub async fn request_headers( - &mut self, - network: &mut dyn NetworkManager, - base_hash: Option, - ) -> SyncResult<()> { - // Note: Removed broken in-flight check that was preventing subsequent requests - // The loop in sync() already handles request pacing properly - - // Build block locator - use slices where possible to reduce allocations - let block_locator = match base_hash { - Some(hash) => { - log::info!("📍 Requesting headers starting from hash: {}", hash); - vec![hash] // Need vec here for GetHeadersMessage - } - None => { - // Empty locator for initial sync - some peers expect this - log::info!("📍 Requesting headers from genesis with empty locator"); - Vec::new() - } - }; - - // No specific stop hash (all zeros means sync to tip) - let stop_hash = BlockHash::from_byte_array([0; 32]); - - // Create GetHeaders message - let getheaders_msg = GetHeadersMessage::new(block_locator.clone(), stop_hash); - - // Check if we have a peer that supports headers2 - let use_headers2 = network.has_headers2_peer().await; - - if use_headers2 { - tracing::info!("📤 Sending GetHeaders2 message (compressed headers)"); - // Send GetHeaders2 message for compressed headers - network - .send_message(NetworkMessage::GetHeaders2(getheaders_msg)) - .await - .map_err(|e| SyncError::Network(format!("Failed to send GetHeaders2: {}", e)))?; - } else { - tracing::info!("📤 Sending GetHeaders message (uncompressed headers)"); - // Send regular GetHeaders message - network - .send_message(NetworkMessage::GetHeaders(getheaders_msg)) - .await - .map_err(|e| SyncError::Network(format!("Failed to send GetHeaders: {}", e)))?; - } - - // Headers request sent successfully - - if self.total_headers_synced.is_multiple_of(10000) { - tracing::debug!("Requested headers starting from {:?}", base_hash); - } - - Ok(()) - } - - /// Validate a batch of headers. - pub async fn validate_headers( - &self, - headers: &[BlockHeader], - storage: &dyn StorageManager, - ) -> SyncResult> { - if headers.is_empty() { - return Ok(Vec::new()); - } - - // Wrap headers in CachedHeader to avoid redundant X11 hashing - // Each header's hash will be computed at most once instead of 4-6 times - let cached_headers: Vec = - headers.iter().map(|h| CachedHeader::new(*h)).collect(); - - let mut validated = Vec::with_capacity(headers.len()); - - for (i, cached_header) in cached_headers.iter().enumerate() { - let header = cached_header.header(); - // Get the previous header for validation - let prev_header = if i == 0 { - // First header in batch - get from storage - let current_tip_height = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))?; - - if let Some(height) = current_tip_height { - storage.get_header(height).await.map_err(|e| { - SyncError::Storage(format!("Failed to get previous header: {}", e)) - })? - } else { - None - } - } else { - Some(headers[i - 1]) - }; - - // Check if this header already exists in storage - // Use cached hash to avoid redundant X11 computation - let header_hash = cached_header.block_hash(); - let already_exists = storage - .get_header_height_by_hash(&header_hash) - .await - .map_err(|e| { - SyncError::Storage(format!("Failed to check header existence: {}", e)) - })? - .is_some(); - - if already_exists { - tracing::info!( - "⚠️ Header {} already exists in storage, skipping validation", - header_hash - ); - // Add the existing header to validated vector so subsequent headers - // can reference it correctly - validated.push(*header); - continue; - } - - // Validate the header - tracing::info!("Validating new header {} at index {}", header_hash, i); - if let Some(prev) = prev_header.as_ref() { - tracing::debug!("Previous header: {}", prev.block_hash()); - } - - self.validation.validate_header(header, prev_header.as_ref()).map_err(|e| { - SyncError::Validation(format!( - "Header validation failed for block {}: {}", - header_hash, e - )) - })?; - - validated.push(*header); - } - - Ok(validated) - } - - /// Download and validate a single header for a specific block hash. - pub async fn download_single_header( - &mut self, - block_hash: BlockHash, - network: &mut dyn NetworkManager, - storage: &mut dyn StorageManager, - ) -> SyncResult<()> { - // Check if we already have this header using the efficient reverse index - if let Some(height) = storage - .get_header_height_by_hash(&block_hash) - .await - .map_err(|e| SyncError::Storage(format!("Failed to check header existence: {}", e)))? - { - tracing::debug!("Header for block {} already exists at height {}", block_hash, height); - return Ok(()); - } - - tracing::info!("📥 Requesting header for block {}", block_hash); - - // Get current tip hash to use as locator - let current_tip = if let Some(tip_height) = storage - .get_tip_height() - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip height: {}", e)))? - { - storage - .get_header(tip_height) - .await - .map_err(|e| SyncError::Storage(format!("Failed to get tip header: {}", e)))? - .map(|h| h.block_hash()) - .unwrap_or_else(|| { - self.config - .network - .known_genesis_block_hash() - .ok_or_else(|| { - SyncError::InvalidState( - "Unable to get genesis block hash for network".to_string(), - ) - }) - .unwrap_or_else(|e| { - tracing::error!("Failed to get genesis block hash: {}", e); - dashcore::BlockHash::from([0u8; 32]) - }) - }) - } else { - self.config - .network - .known_genesis_block_hash() - .ok_or_else(|| { - SyncError::InvalidState( - "Unable to get genesis block hash for network".to_string(), - ) - }) - .unwrap_or_else(|e| { - tracing::error!("Failed to get genesis block hash: {}", e); - dashcore::BlockHash::from([0u8; 32]) - }) - }; - - tracing::info!( - "📍 Using tip at height {:?} as locator: {}", - storage.get_tip_height().await.ok().flatten(), - current_tip - ); - - // Create GetHeaders message requesting headers up to and including the specific block - // The peer will send headers starting after our current tip up to the requested block - let getheaders_msg = GetHeadersMessage { - version: 70214, // Dash protocol version - locator_hashes: vec![current_tip], - stop_hash: block_hash, // Request headers up to this specific block - }; - - tracing::info!("📤 Requesting headers from {} up to block {}", current_tip, block_hash); - - // Send the message - network - .send_message(NetworkMessage::GetHeaders(getheaders_msg)) - .await - .map_err(|e| SyncError::Network(format!("Failed to send GetHeaders: {}", e)))?; - - tracing::debug!("Sent getheaders request for block {}", block_hash); - - // Note: The header will be processed when we receive the headers response - // in the normal message handling flow in sync/mod.rs - - Ok(()) - } - - /// Reset sync state. - pub fn reset(&mut self) { - self.total_headers_synced = 0; - self.last_progress_log = None; - } - - /// Check if header sync is currently in progress. - pub fn is_syncing(&self) -> bool { - self.syncing_headers - } - - /// Reset any pending requests after restart. - pub fn reset_pending_requests(&mut self) { - // Headers sync doesn't track individual pending requests - // Just reset the sync state - self.syncing_headers = false; - self.last_sync_progress = std::time::Instant::now(); - tracing::debug!("Reset header sync pending requests"); - } - - /// Get headers2 compression statistics. - pub fn headers2_stats(&self) -> crate::sync::headers2_state::Headers2Stats { - self.headers2_state.get_stats() - } - - /// Reset headers2 state for a peer (e.g., on disconnect). - pub fn reset_headers2_peer(&mut self, peer_id: crate::types::PeerId) { - self.headers2_state.reset_peer(peer_id); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{client::ClientConfig, storage::MemoryStorageManager, types::ValidationMode}; - use dashcore::{block::Header as BlockHeader, block::Version, Network}; - use dashcore_hashes::Hash; - - fn create_test_header(height: u32, prev_hash: BlockHash) -> BlockHeader { - BlockHeader { - version: Version::from_consensus(1), - prev_blockhash: prev_hash, - merkle_root: dashcore::TxMerkleNode::from_byte_array([height as u8; 32]), - time: 1234567890 + height, - bits: dashcore::CompactTarget::from_consensus(0x1d00ffff), - nonce: height, - } - } - - #[tokio::test] - async fn test_validate_headers_includes_existing_headers() { - // Create storage with some existing headers - let mut storage = MemoryStorageManager::new().await.unwrap(); - - // Store the genesis header - let genesis = create_test_header(0, BlockHash::from([0u8; 32])); - storage.store_headers(&[genesis]).await.unwrap(); - - // Store header at height 1 - let header1 = create_test_header(1, genesis.block_hash()); - storage.store_headers(&[header1]).await.unwrap(); - - // Create a config and sync manager - let config = ClientConfig::new(Network::Dash).with_validation_mode(ValidationMode::Basic); - let sync_manager = HeaderSyncManager::new(&config); - - // Create a batch of headers where the first two already exist - let headers = vec![ - genesis, // Already exists - header1, // Already exists - create_test_header(2, header1.block_hash()), // New - create_test_header(3, create_test_header(2, header1.block_hash()).block_hash()), // New - ]; - - // Validate headers - let validated = sync_manager.validate_headers(&headers, &storage).await.unwrap(); - - // All headers should be in the validated vector, including existing ones - assert_eq!(validated.len(), 4, "All headers should be included in validated vector"); - - // Verify the headers are in correct order - assert_eq!(validated[0].block_hash(), genesis.block_hash()); - assert_eq!(validated[1].block_hash(), header1.block_hash()); - assert_eq!(validated[2].prev_blockhash, header1.block_hash()); - assert_eq!(validated[3].prev_blockhash, validated[2].block_hash()); - } - - #[tokio::test] - async fn test_validate_headers_with_gaps() { - // Create storage with a header at height 0 - let mut storage = MemoryStorageManager::new().await.unwrap(); - let genesis = create_test_header(0, BlockHash::from([0u8; 32])); - storage.store_headers(&[genesis]).await.unwrap(); - - // Create config and sync manager - let config = ClientConfig::new(Network::Dash).with_validation_mode(ValidationMode::Basic); - let sync_manager = HeaderSyncManager::new(&config); - - // Create headers with a gap - header at height 2 is missing from storage - let header1 = create_test_header(1, genesis.block_hash()); - let header2 = create_test_header(2, header1.block_hash()); - let header3 = create_test_header(3, header2.block_hash()); - - // Store only header1, skip header2 - storage.store_headers(&[header1]).await.unwrap(); - - // Try to validate a batch that includes the existing header1, new header2, and new header3 - let headers = vec![header1, header2, header3]; - - let validated = sync_manager.validate_headers(&headers, &storage).await.unwrap(); - - // All headers should be validated successfully - assert_eq!(validated.len(), 3, "All headers should be validated"); - - // The existing header1 should be included so header2 can reference it - assert_eq!(validated[0].block_hash(), header1.block_hash()); - assert_eq!(validated[1].prev_blockhash, header1.block_hash()); - assert_eq!(validated[2].prev_blockhash, header2.block_hash()); - } -} diff --git a/dash-spv/src/sync/mod.rs b/dash-spv/src/sync/mod.rs index 8c6e14eb7..dda91bbec 100644 --- a/dash-spv/src/sync/mod.rs +++ b/dash-spv/src/sync/mod.rs @@ -3,33 +3,12 @@ //! This module provides sequential sync strategy: //! Headers first, then filter headers, then filters on-demand -pub mod chainlock_validation; -pub mod discovery; pub mod embedded_data; pub mod filters; -pub mod headers; pub mod headers2_state; pub mod headers_with_reorg; pub mod masternodes; pub mod sequential; -pub mod state; -pub mod validation; -pub mod validation_state; - -#[cfg(test)] -mod validation_test; - pub use filters::FilterSyncManager; -pub use headers::HeaderSyncManager; pub use headers_with_reorg::{HeaderSyncManagerWithReorg, ReorgConfig}; pub use masternodes::MasternodeSyncManager; -pub use state::SyncState; - -/// Sync component types. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SyncComponent { - Headers, - FilterHeaders, - Filters, - Masternodes, -} diff --git a/dash-spv/src/sync/sequential/lifecycle.rs b/dash-spv/src/sync/sequential/lifecycle.rs index 327a6b787..b22e26c1b 100644 --- a/dash-spv/src/sync/sequential/lifecycle.rs +++ b/dash-spv/src/sync/sequential/lifecycle.rs @@ -18,7 +18,6 @@ use tokio::sync::RwLock; use super::manager::SequentialSyncManager; use super::phases::SyncPhase; -use super::request_control::RequestController; use super::transitions::TransitionManager; impl< @@ -41,7 +40,6 @@ impl< Ok(Self { current_phase: SyncPhase::Idle, transition_manager: TransitionManager::new(config), - request_controller: RequestController::new(config), header_sync: HeaderSyncManagerWithReorg::new(config, reorg_config, chain_state) .map_err(|e| { SyncError::InvalidState(format!("Failed to create header sync manager: {}", e)) @@ -181,29 +179,9 @@ impl< // Reset phase tracking self.current_phase_retries = 0; - // Clear request controller state - self.request_controller.clear_pending_requests(); - tracing::debug!("Reset sequential sync manager pending requests"); } - /// Fully reset the sync manager state to idle, used when sync initialization fails - pub fn reset_to_idle(&mut self) { - // First reset all pending requests - self.reset_pending_requests(); - - // Reset phase to idle - self.current_phase = SyncPhase::Idle; - - // Clear sync start time - self.sync_start_time = None; - - // Clear phase history - self.phase_history.clear(); - - tracing::info!("Reset sequential sync manager to idle state"); - } - /// Helper method to get base hash from storage pub(super) async fn get_base_hash_from_storage( &self, diff --git a/dash-spv/src/sync/sequential/manager.rs b/dash-spv/src/sync/sequential/manager.rs index 01247b55f..9d8f9e56d 100644 --- a/dash-spv/src/sync/sequential/manager.rs +++ b/dash-spv/src/sync/sequential/manager.rs @@ -11,7 +11,6 @@ use crate::types::SyncProgress; use key_wallet_manager::wallet_interface::WalletInterface; use super::phases::{PhaseTransition, SyncPhase}; -use super::request_control::RequestController; use super::transitions::TransitionManager; /// Number of blocks back from a ChainLock's block height where we need the masternode list @@ -68,9 +67,6 @@ pub struct SequentialSyncManager, pub(super) filter_sync: FilterSyncManager, @@ -187,11 +183,6 @@ impl< matches!(self.current_phase, SyncPhase::DownloadingBlocks { .. }) } - /// Get phase history - pub fn phase_history(&self) -> &[PhaseTransition] { - &self.phase_history - } - /// Get current phase pub fn current_phase(&self) -> &SyncPhase { &self.current_phase @@ -227,15 +218,6 @@ impl< self.masternode_sync.engine() } - /// Get a quorum manager populated from the masternode engine. - /// Returns an empty quorum manager if masternodes are disabled or not initialized. - pub fn get_quorum_manager(&self) -> crate::validation::quorum::QuorumManager { - // TODO: Populate quorum manager from masternode engine - // For now, return an empty manager - // This will be enhanced when quorum extraction from masternode list is implemented - crate::validation::quorum::QuorumManager::new() - } - /// Get a reference to the filter sync manager. pub fn filter_sync(&self) -> &FilterSyncManager { &self.filter_sync @@ -267,16 +249,4 @@ impl< Ok(storage_height) } } - - /// Set the current phase (for testing) - #[cfg(test)] - pub fn set_phase(&mut self, phase: SyncPhase) { - self.current_phase = phase; - } - - /// Get mutable reference to masternode sync manager (for testing) - #[cfg(test)] - pub fn masternode_sync_mut(&mut self) -> &mut MasternodeSyncManager { - &mut self.masternode_sync - } } diff --git a/dash-spv/src/sync/sequential/mod.rs b/dash-spv/src/sync/sequential/mod.rs index 046ec0252..6478a3871 100644 --- a/dash-spv/src/sync/sequential/mod.rs +++ b/dash-spv/src/sync/sequential/mod.rs @@ -28,7 +28,6 @@ //! - `phases` - SyncPhase enum and phase-related types //! - `progress` - Progress tracking utilities //! - `recovery` - Recovery and error handling logic -//! - `request_control` - Request flow control //! - `transitions` - Phase transition management // Sub-modules (focused implementations) @@ -40,13 +39,9 @@ pub mod post_sync; // Existing sub-modules pub mod phases; -pub mod progress; -pub mod recovery; -pub mod request_control; pub mod transitions; // Re-exports pub use manager::SequentialSyncManager; pub use phases::{PhaseTransition, SyncPhase}; -pub use request_control::RequestController; pub use transitions::TransitionManager; diff --git a/dash-spv/src/sync/sequential/phases.rs b/dash-spv/src/sync/sequential/phases.rs index 558372c98..d6a2d553a 100644 --- a/dash-spv/src/sync/sequential/phases.rs +++ b/dash-spv/src/sync/sequential/phases.rs @@ -5,18 +5,6 @@ use std::time::{Duration, Instant}; use dashcore::BlockHash; -/// Hybrid sync strategy tracking -#[derive(Debug, Clone, PartialEq)] -pub enum HybridSyncStrategy { - /// Engine-driven discovery with both QRInfo and MnListDiff - EngineDiscovery { - qr_info_requests: u32, - mn_diff_requests: u32, - qr_info_completed: u32, - mn_diff_completed: u32, - }, -} - /// Represents the current synchronization phase #[derive(Debug, Clone, PartialEq)] pub enum SyncPhase { @@ -57,8 +45,6 @@ pub enum SyncPhase { last_progress: Instant, /// Number of masternode list diffs processed diffs_processed: u32, - /// Hybrid sync strategy tracking - sync_strategy: Option, /// Total requests (QRInfo + MnListDiff) requests_total: u32, /// Completed requests @@ -139,14 +125,8 @@ impl SyncPhase { .. } => "Downloading Headers", SyncPhase::DownloadingMnList { - sync_strategy, .. - } => match sync_strategy { - Some(HybridSyncStrategy::EngineDiscovery { - .. - }) => "Downloading Masternode Lists (Hybrid)", - None => "Downloading Masternode Lists", - }, + } => "Downloading Masternode Lists", SyncPhase::DownloadingCFHeaders { .. } => "Downloading Filter Headers", @@ -221,37 +201,6 @@ impl SyncPhase { _ => {} } } - - /// Get phase elapsed time - pub fn elapsed_time(&self) -> Option { - match self { - SyncPhase::DownloadingHeaders { - start_time, - .. - } => Some(start_time.elapsed()), - SyncPhase::DownloadingMnList { - start_time, - .. - } => Some(start_time.elapsed()), - SyncPhase::DownloadingCFHeaders { - start_time, - .. - } => Some(start_time.elapsed()), - SyncPhase::DownloadingFilters { - start_time, - .. - } => Some(start_time.elapsed()), - SyncPhase::DownloadingBlocks { - start_time, - .. - } => Some(start_time.elapsed()), - SyncPhase::FullySynced { - total_sync_time, - .. - } => Some(*total_sync_time), - SyncPhase::Idle => None, - } - } } /// Progress information for a sync phase @@ -318,7 +267,6 @@ impl SyncPhase { } SyncPhase::DownloadingMnList { - sync_strategy, requests_completed, requests_total, start_time, @@ -327,44 +275,6 @@ impl SyncPhase { target_height, .. } => { - let (_method_description, _efficiency_note) = match sync_strategy { - Some(HybridSyncStrategy::EngineDiscovery { - qr_info_requests, - mn_diff_requests, - qr_info_completed, - mn_diff_completed, - }) => { - let total_requests = qr_info_requests + mn_diff_requests; - let qr_info_ratio = if total_requests > 0 { - (*qr_info_requests as f64 / total_requests as f64) * 100.0 - } else { - 0.0 - }; - - let efficiency = if qr_info_ratio > 70.0 { - "High Efficiency" - } else if qr_info_ratio > 30.0 { - "Standard Efficiency" - } else { - "Targeted Sync" - }; - - ( - format!( - "Hybrid ({:.0}% QRInfo, {:.0}% MnListDiff) - {} of {} QRInfo, {} of {} MnListDiff", - qr_info_ratio, - 100.0 - qr_info_ratio, - qr_info_completed, - qr_info_requests, - mn_diff_completed, - mn_diff_requests - ), - efficiency.to_string() - ) - } - None => ("Standard".to_string(), "Legacy Mode".to_string()), - }; - let percentage = if *requests_total > 0 { (*requests_completed as f64 / *requests_total as f64) * 100.0 } else if *target_height > *start_height { @@ -522,87 +432,6 @@ impl SyncPhase { }, } } - - /// Update hybrid sync strategy for masternode phase - pub fn set_masternode_sync_plan(&mut self, qr_info_count: u32, mn_diff_count: u32) { - if let SyncPhase::DownloadingMnList { - sync_strategy, - requests_total, - .. - } = self - { - *sync_strategy = Some(HybridSyncStrategy::EngineDiscovery { - qr_info_requests: qr_info_count, - mn_diff_requests: mn_diff_count, - qr_info_completed: 0, - mn_diff_completed: 0, - }); - *requests_total = qr_info_count + mn_diff_count; - } - } - - /// Mark a QRInfo request as completed - pub fn complete_qr_info_request(&mut self) { - if let SyncPhase::DownloadingMnList { - sync_strategy: - Some(HybridSyncStrategy::EngineDiscovery { - qr_info_requests, - qr_info_completed, - .. - }), - requests_completed, - requests_total, - last_progress, - .. - } = self - { - // Only increment if we haven't reached the planned total - if *qr_info_completed < *qr_info_requests { - *qr_info_completed += 1; - *requests_completed = (*requests_completed + 1).min(*requests_total); - *last_progress = Instant::now(); - } - } - } - - /// Mark a MnListDiff request as completed - pub fn complete_mn_diff_request(&mut self) { - if let SyncPhase::DownloadingMnList { - sync_strategy: - Some(HybridSyncStrategy::EngineDiscovery { - mn_diff_requests, - mn_diff_completed, - .. - }), - requests_completed, - requests_total, - diffs_processed, - last_progress, - .. - } = self - { - // Only increment if we haven't reached the planned total - if *mn_diff_completed < *mn_diff_requests { - *mn_diff_completed += 1; - *requests_completed = (*requests_completed + 1).min(*requests_total); - *diffs_processed += 1; // Backward compatibility - *last_progress = Instant::now(); - } - } - } - - /// Update masternode sync height - pub fn update_masternode_height(&mut self, height: u32) { - if let SyncPhase::DownloadingMnList { - current_height, - last_progress, - .. - } = self - { - *current_height = height; - *last_progress = Instant::now(); - } - } } /// Represents a phase transition in the sync process diff --git a/dash-spv/src/sync/sequential/progress.rs b/dash-spv/src/sync/sequential/progress.rs deleted file mode 100644 index d991efd0d..000000000 --- a/dash-spv/src/sync/sequential/progress.rs +++ /dev/null @@ -1,369 +0,0 @@ -//! Progress tracking for sequential sync - -use std::time::Duration; - -use super::phases::{PhaseProgress, PhaseTransition, SyncPhase}; -use super::request_control::{ - PHASE_DOWNLOADING_BLOCKS, PHASE_DOWNLOADING_CFHEADERS, PHASE_DOWNLOADING_FILTERS, - PHASE_DOWNLOADING_HEADERS, PHASE_DOWNLOADING_MNLIST, -}; - -/// Overall sync progress across all phases -#[derive(Debug, Clone)] -pub struct OverallSyncProgress { - /// Current phase name - pub current_phase: String, - - /// Progress within current phase - pub phase_progress: PhaseProgress, - - /// List of completed phases - pub phases_completed: Vec, - - /// List of remaining phases - pub phases_remaining: Vec, - - /// Total elapsed time since sync started - pub total_elapsed: Duration, - - /// Estimated total time for complete sync - pub estimated_total_time: Option, - - /// Overall completion percentage (0-100) - pub overall_percentage: f64, - - /// Human-readable status message - pub status_message: String, -} - -/// Tracks and calculates sync progress -pub struct ProgressTracker { - /// Start time of sync - sync_start: Option, - - /// Phase weights for overall percentage calculation - phase_weights: std::collections::HashMap, -} - -impl ProgressTracker { - /// Create a new progress tracker - pub fn new() -> Self { - let mut phase_weights = std::collections::HashMap::new(); - - // Assign weights based on typical time/importance - phase_weights.insert(PHASE_DOWNLOADING_HEADERS.to_string(), 0.4); - phase_weights.insert(PHASE_DOWNLOADING_MNLIST.to_string(), 0.1); - phase_weights.insert(PHASE_DOWNLOADING_CFHEADERS.to_string(), 0.2); - phase_weights.insert(PHASE_DOWNLOADING_FILTERS.to_string(), 0.2); - phase_weights.insert(PHASE_DOWNLOADING_BLOCKS.to_string(), 0.1); - - Self { - sync_start: None, - phase_weights, - } - } - - /// Mark sync as started - pub fn start_sync(&mut self) { - self.sync_start = Some(std::time::Instant::now()); - } - - /// Calculate overall sync progress - pub fn calculate_overall_progress( - &self, - current_phase: &SyncPhase, - phase_history: &[PhaseTransition], - enabled_features: EnabledFeatures, - ) -> OverallSyncProgress { - let phase_progress = current_phase.progress(); - let phases_completed = self.get_completed_phases(phase_history); - let phases_remaining = self.get_remaining_phases(current_phase, &enabled_features); - - let total_elapsed = self.sync_start.map(|start| start.elapsed()).unwrap_or_default(); - - let overall_percentage = self.calculate_overall_percentage( - current_phase, - &phases_completed, - &phases_remaining, - &phase_progress, - ); - - let estimated_total_time = self.estimate_total_time( - current_phase, - &phase_progress, - &phases_completed, - &phases_remaining, - total_elapsed, - ); - - let status_message = - self.generate_status_message(current_phase, &phase_progress, overall_percentage); - - OverallSyncProgress { - current_phase: current_phase.name().to_string(), - phase_progress, - phases_completed, - phases_remaining, - total_elapsed, - estimated_total_time, - overall_percentage, - status_message, - } - } - - /// Get list of completed phases from history - fn get_completed_phases(&self, history: &[PhaseTransition]) -> Vec { - history.iter().map(|t| t.from_phase.clone()).filter(|phase| phase != "Idle").collect() - } - - /// Get list of remaining phases - fn get_remaining_phases( - &self, - current_phase: &SyncPhase, - features: &EnabledFeatures, - ) -> Vec { - let mut remaining = Vec::new(); - - match current_phase { - SyncPhase::Idle => { - remaining.push(PHASE_DOWNLOADING_HEADERS.to_string()); - if features.masternodes { - remaining.push(PHASE_DOWNLOADING_MNLIST.to_string()); - } - if features.filters { - remaining.push(PHASE_DOWNLOADING_CFHEADERS.to_string()); - remaining.push(PHASE_DOWNLOADING_FILTERS.to_string()); - } - // Blocks phase is dynamic based on filter matches - } - - SyncPhase::DownloadingHeaders { - .. - } => { - if features.masternodes { - remaining.push(PHASE_DOWNLOADING_MNLIST.to_string()); - } - if features.filters { - remaining.push(PHASE_DOWNLOADING_CFHEADERS.to_string()); - remaining.push(PHASE_DOWNLOADING_FILTERS.to_string()); - } - } - - SyncPhase::DownloadingMnList { - .. - } => { - if features.filters { - remaining.push(PHASE_DOWNLOADING_CFHEADERS.to_string()); - remaining.push(PHASE_DOWNLOADING_FILTERS.to_string()); - } - } - - SyncPhase::DownloadingCFHeaders { - .. - } => { - remaining.push(PHASE_DOWNLOADING_FILTERS.to_string()); - } - - SyncPhase::DownloadingFilters { - .. - } => { - // Blocks phase is dynamic - } - - _ => {} - } - - remaining - } - - /// Calculate overall completion percentage - fn calculate_overall_percentage( - &self, - current_phase: &SyncPhase, - completed: &[String], - remaining: &[String], - phase_progress: &PhaseProgress, - ) -> f64 { - // Calculate total weight - let mut total_weight = 0.0; - let mut completed_weight = 0.0; - - // Add completed phases - for phase in completed { - if let Some(weight) = self.phase_weights.get(phase) { - total_weight += weight; - completed_weight += weight; - } - } - - // Add current phase - let current_phase_name = current_phase.name(); - if let Some(weight) = self.phase_weights.get(current_phase_name) { - total_weight += weight; - completed_weight += weight * (phase_progress.percentage / 100.0); - } - - // Add remaining phases - for phase in remaining { - if let Some(weight) = self.phase_weights.get(phase) { - total_weight += weight; - } - } - - if total_weight > 0.0 { - (completed_weight / total_weight) * 100.0 - } else { - 0.0 - } - } - - /// Estimate total sync time - fn estimate_total_time( - &self, - current_phase: &SyncPhase, - current_progress: &PhaseProgress, - completed: &[String], - remaining: &[String], - elapsed: Duration, - ) -> Option { - // Return None for zero or sub-second durations - if elapsed.as_secs_f64() < 1.0 { - return None; - } - - let current_phase_name = current_phase.name(); - - // Calculate total weight and completed weight - let mut total_weight = 0.0; - let mut completed_weight = 0.0; - - // Add completed phases weight - for phase in completed { - if let Some(weight) = self.phase_weights.get(phase) { - total_weight += weight; - completed_weight += weight; - } - } - - // Add current phase weight (partially completed) - if let Some(current_weight) = self.phase_weights.get(current_phase_name) { - total_weight += current_weight; - completed_weight += current_weight * (current_progress.percentage / 100.0); - } - - // Add remaining phases weight - for phase in remaining { - if let Some(weight) = self.phase_weights.get(phase) { - total_weight += weight; - } - } - - // Calculate estimated total time based on weights - if completed_weight > 0.0 && total_weight > 0.0 { - let estimated_total_secs = (elapsed.as_secs_f64() / completed_weight) * total_weight; - Some(Duration::from_secs_f64(estimated_total_secs)) - } else { - None - } - } - - /// Generate human-readable status message - fn generate_status_message( - &self, - phase: &SyncPhase, - progress: &PhaseProgress, - overall_percentage: f64, - ) -> String { - match phase { - SyncPhase::Idle => "Preparing to sync".to_string(), - - SyncPhase::DownloadingHeaders { - .. - } => { - format!( - "Downloading headers: {} at {:.1} headers/sec", - progress.items_completed, progress.rate - ) - } - - SyncPhase::DownloadingMnList { - .. - } => { - format!("Syncing masternode lists: {} processed", progress.items_completed) - } - - SyncPhase::DownloadingCFHeaders { - .. - } => { - format!( - "Downloading filter headers: {:.1}% at {:.1} headers/sec", - progress.percentage, progress.rate - ) - } - - SyncPhase::DownloadingFilters { - .. - } => { - format!( - "Downloading filters: {} of {}", - progress.items_completed, - progress.items_total.unwrap_or(0) - ) - } - - SyncPhase::DownloadingBlocks { - .. - } => { - format!( - "Downloading blocks: {} of {} ({:.1}%)", - progress.items_completed, - progress.items_total.unwrap_or(0), - progress.percentage - ) - } - - SyncPhase::FullySynced { - .. - } => { - format!("Fully synchronized ({:.1}% complete)", overall_percentage) - } - } - } -} - -/// Features enabled for sync -#[derive(Debug, Clone)] -pub struct EnabledFeatures { - pub masternodes: bool, - pub filters: bool, -} - -impl Default for ProgressTracker { - fn default() -> Self { - Self::new() - } -} - -/// Format duration in human-readable format -pub fn format_duration(duration: Duration) -> String { - let total_secs = duration.as_secs(); - let hours = total_secs / 3600; - let minutes = (total_secs % 3600) / 60; - let seconds = total_secs % 60; - - if hours > 0 { - format!("{}h {}m {}s", hours, minutes, seconds) - } else if minutes > 0 { - format!("{}m {}s", minutes, seconds) - } else { - format!("{}s", seconds) - } -} - -/// Format ETA in human-readable format -pub fn format_eta(eta: Option) -> String { - match eta { - Some(duration) => format!("ETA: {}", format_duration(duration)), - None => "ETA: calculating...".to_string(), - } -} diff --git a/dash-spv/src/sync/sequential/recovery.rs b/dash-spv/src/sync/sequential/recovery.rs deleted file mode 100644 index 2acfc44aa..000000000 --- a/dash-spv/src/sync/sequential/recovery.rs +++ /dev/null @@ -1,559 +0,0 @@ -//! Error recovery for sequential sync - -use std::time::Duration; - -use crate::error::{SyncError, SyncResult}; -use crate::network::NetworkManager; -use crate::storage::StorageManager; - -use super::phases::SyncPhase; - -/// Recovery strategies for different error types -#[derive(Debug, Clone)] -pub enum RecoveryStrategy { - /// Retry the current operation - Retry { - delay: Duration, - }, - - /// Restart the current phase from a checkpoint - RestartPhase { - checkpoint: PhaseCheckpoint, - }, - - /// Skip to the next phase (if safe) - SkipPhase { - reason: String, - }, - - /// Abort sync with error - Abort { - error: String, - }, - - /// Switch to a different peer - SwitchPeer, - - /// Wait for network connectivity - WaitForNetwork { - timeout: Duration, - }, -} - -/// Checkpoint within a phase for recovery -#[derive(Debug, Clone)] -pub struct PhaseCheckpoint { - /// Height to restart from (for height-based phases) - pub restart_height: Option, - - /// Progress to preserve - pub preserved_progress: PreservedProgress, -} - -/// Progress that can be preserved during recovery -#[derive(Debug, Clone)] -pub enum PreservedProgress { - Headers { - validated_up_to: u32, - }, - FilterHeaders { - validated_up_to: u32, - }, - Filters { - completed_heights: Vec, - }, - Blocks { - downloaded_hashes: Vec, - }, - None, -} - -/// Manages error recovery for sequential sync -pub struct RecoveryManager { - /// Maximum retries per error type - max_retries: std::collections::HashMap, - - /// Current retry counts - retry_counts: std::collections::HashMap, - - /// Recovery history - recovery_history: Vec, -} - -#[derive(Debug, Clone)] -struct RecoveryEvent { - #[allow(dead_code)] - timestamp: std::time::Instant, - phase: String, - #[allow(dead_code)] - error: String, - #[allow(dead_code)] - strategy: RecoveryStrategy, - success: bool, -} - -impl RecoveryManager { - /// Create a new recovery manager - pub fn new() -> Self { - let mut max_retries = std::collections::HashMap::new(); - max_retries.insert("timeout".to_string(), 5); - max_retries.insert("network".to_string(), 10); - max_retries.insert("validation".to_string(), 3); - max_retries.insert("storage".to_string(), 3); - max_retries.insert("peer".to_string(), 5); - - Self { - max_retries, - retry_counts: std::collections::HashMap::new(), - recovery_history: Vec::new(), - } - } - - /// Determine recovery strategy for an error - pub fn determine_strategy(&mut self, phase: &SyncPhase, error: &SyncError) -> RecoveryStrategy { - let error_type = self.classify_error(error); - let retry_count = self.get_retry_count(&error_type); - let max_retries = self.max_retries.get(&error_type).copied().unwrap_or(3); - - // Check if we've exceeded retries - if retry_count >= max_retries { - return RecoveryStrategy::Abort { - error: format!( - "Maximum retries ({}) exceeded for {} error in phase {}", - max_retries, - error_type, - phase.name() - ), - }; - } - - // Increment retry count - self.increment_retry_count(&error_type); - - // Determine strategy based on error type and phase - match (phase, error_type.as_str()) { - // Timeout errors - generally retry with backoff - (_, "timeout") => RecoveryStrategy::Retry { - delay: self.calculate_backoff_delay(retry_count), - }, - - // Network errors - may need peer switch - (_, "network") if retry_count >= 3 => RecoveryStrategy::SwitchPeer, - (_, "network") => RecoveryStrategy::Retry { - delay: Duration::from_secs(1), - }, - - // Validation errors in headers - need to restart from known good point - ( - SyncPhase::DownloadingHeaders { - current_height, - .. - }, - "validation", - ) => RecoveryStrategy::RestartPhase { - checkpoint: PhaseCheckpoint { - restart_height: Some(current_height.saturating_sub(100)), - preserved_progress: PreservedProgress::Headers { - validated_up_to: current_height.saturating_sub(100), - }, - }, - }, - - // Storage errors - usually fatal - (_, "storage") => RecoveryStrategy::Abort { - error: format!("Storage error: {}", error), - }, - - // Default - retry with delay - _ => RecoveryStrategy::Retry { - delay: Duration::from_secs(2), - }, - } - } - - /// Execute a recovery strategy - /// - /// # Example - /// ```ignore - /// let error = SyncError::Timeout("Connection timed out".to_string()); - /// let strategy = recovery_manager.determine_strategy(&phase, &error); - /// recovery_manager.execute_recovery(phase, strategy, &error, network, storage).await; - /// ``` - pub async fn execute_recovery( - &mut self, - phase: &mut SyncPhase, - strategy: RecoveryStrategy, - error: &SyncError, - network: &mut dyn NetworkManager, - storage: &mut dyn StorageManager, - ) -> SyncResult<()> { - let phase_name = phase.name().to_string(); - - tracing::info!("🔧 Executing recovery strategy {:?} for phase {}", strategy, phase_name); - - // Clone strategy for history before consuming it - let strategy_clone = match &strategy { - RecoveryStrategy::Retry { - delay, - } => RecoveryStrategy::Retry { - delay: *delay, - }, - RecoveryStrategy::RestartPhase { - checkpoint, - } => RecoveryStrategy::RestartPhase { - checkpoint: checkpoint.clone(), - }, - RecoveryStrategy::SkipPhase { - reason, - } => RecoveryStrategy::SkipPhase { - reason: reason.clone(), - }, - RecoveryStrategy::Abort { - error, - } => RecoveryStrategy::Abort { - error: error.clone(), - }, - RecoveryStrategy::SwitchPeer => RecoveryStrategy::SwitchPeer, - RecoveryStrategy::WaitForNetwork { - timeout, - } => RecoveryStrategy::WaitForNetwork { - timeout: *timeout, - }, - }; - - let result = match strategy { - RecoveryStrategy::Retry { - delay, - } => { - tracing::info!("⏳ Waiting {:?} before retry", delay); - tokio::time::sleep(delay).await; - Ok(()) - } - - RecoveryStrategy::RestartPhase { - checkpoint, - } => self.restart_phase_from_checkpoint(phase, checkpoint, storage).await, - - RecoveryStrategy::SkipPhase { - reason, - } => { - tracing::warn!("⏭️ Skipping phase {}: {}", phase_name, reason); - Ok(()) - } - - RecoveryStrategy::Abort { - error, - } => { - tracing::error!("❌ Aborting sync: {}", error); - Err(SyncError::Network(error)) - } - - RecoveryStrategy::SwitchPeer => { - tracing::info!("🔄 Switching to different peer"); - // Network manager would handle peer switching - Ok(()) - } - - RecoveryStrategy::WaitForNetwork { - timeout, - } => { - tracing::info!("🌐 Waiting for network connectivity (timeout: {:?})", timeout); - self.wait_for_network(network, timeout).await - } - }; - - self.recovery_history.push(RecoveryEvent { - timestamp: std::time::Instant::now(), - phase: phase_name, - error: error.to_string(), - strategy: strategy_clone, - success: result.is_ok(), - }); - - result - } - - /// Restart a phase from a checkpoint - async fn restart_phase_from_checkpoint( - &self, - phase: &mut SyncPhase, - checkpoint: PhaseCheckpoint, - _storage: &dyn StorageManager, - ) -> SyncResult<()> { - match phase { - SyncPhase::DownloadingHeaders { - current_height, - headers_downloaded, - .. - } => { - if let Some(restart_height) = checkpoint.restart_height { - tracing::info!( - "📍 Restarting headers from height {} (was at {})", - restart_height, - current_height - ); - *current_height = restart_height; - *headers_downloaded = restart_height; - phase.update_progress(); - } - } - - SyncPhase::DownloadingCFHeaders { - current_height, - .. - } => { - if let Some(restart_height) = checkpoint.restart_height { - tracing::info!( - "📍 Restarting filter headers from height {} (was at {})", - restart_height, - current_height - ); - *current_height = restart_height; - phase.update_progress(); - } - } - - SyncPhase::DownloadingMnList { - current_height, - diffs_processed, - .. - } => { - if let Some(restart_height) = checkpoint.restart_height { - tracing::info!( - "📍 Restarting masternode lists from height {} (was at {})", - restart_height, - current_height - ); - *current_height = restart_height; - *diffs_processed = 0; // Reset diffs processed counter - phase.update_progress(); - } - } - - SyncPhase::DownloadingFilters { - requested_ranges, - completed_heights, - batches_processed, - .. - } => { - // For filters, we can preserve completed heights from the checkpoint - if let PreservedProgress::Filters { - completed_heights: preserved, - } = checkpoint.preserved_progress - { - tracing::info!( - "📍 Restarting filters phase, preserving {} completed heights", - preserved.len() - ); - requested_ranges.clear(); // Clear pending requests - completed_heights.clear(); - completed_heights.extend(preserved); // Restore completed heights - *batches_processed = 0; // Reset batch counter - phase.update_progress(); - } else if let Some(restart_height) = checkpoint.restart_height { - // Fallback: clear all progress up to restart height - tracing::info!( - "📍 Restarting filters from height {}, clearing {} completed heights", - restart_height, - completed_heights.len() - ); - requested_ranges.clear(); - completed_heights.retain(|&h| h < restart_height); - *batches_processed = 0; - phase.update_progress(); - } - } - - SyncPhase::DownloadingBlocks { - pending_blocks, - downloading, - completed, - .. - } => { - // For blocks, we can preserve completed downloads from the checkpoint - if let PreservedProgress::Blocks { - downloaded_hashes, - } = checkpoint.preserved_progress - { - tracing::info!( - "📍 Restarting blocks phase, preserving {} completed downloads", - downloaded_hashes.len() - ); - downloading.clear(); // Clear in-progress downloads - completed.clear(); - completed.extend(downloaded_hashes); // Restore completed blocks - // Remove completed blocks from pending - pending_blocks.retain(|(hash, _)| !completed.contains(hash)); - phase.update_progress(); - } else if let Some(restart_height) = checkpoint.restart_height { - // Fallback: clear downloads above restart height - tracing::info!( - "📍 Restarting blocks from height {}, clearing downloads", - restart_height - ); - downloading.clear(); - pending_blocks.retain(|(_, height)| *height >= restart_height); - completed.clear(); - phase.update_progress(); - } - } - - _ => { - // Idle and FullySynced phases don't need checkpoint restart - tracing::debug!("Phase {} does not require checkpoint restart", phase.name()); - } - } - - Ok(()) - } - - /// Wait for network connectivity - async fn wait_for_network( - &self, - network: &mut dyn NetworkManager, - timeout: Duration, - ) -> SyncResult<()> { - let start = std::time::Instant::now(); - - loop { - if network.peer_count() > 0 { - tracing::info!("✅ Network connectivity restored"); - return Ok(()); - } - - if start.elapsed() > timeout { - return Err(SyncError::Timeout("Network timeout".to_string())); - } - - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - - /// Classify error type for recovery strategy - fn classify_error(&self, error: &SyncError) -> String { - error.category().to_string() - } - - /// Get retry count for error type - fn get_retry_count(&self, error_type: &str) -> u32 { - self.retry_counts.get(error_type).copied().unwrap_or(0) - } - - /// Increment retry count for error type - fn increment_retry_count(&mut self, error_type: &str) { - let count = self.retry_counts.entry(error_type.to_string()).or_insert(0); - *count += 1; - } - - /// Calculate exponential backoff delay - fn calculate_backoff_delay(&self, retry_count: u32) -> Duration { - let base_delay_ms = 1000; // 1 second base - let max_delay_ms = 30000; // 30 seconds max - - let delay_ms = (base_delay_ms * 2u64.pow(retry_count)).min(max_delay_ms); - Duration::from_millis(delay_ms) - } - - /// Reset retry counts (call on successful phase completion) - pub fn reset_retry_counts(&mut self) { - self.retry_counts.clear(); - } - - /// Get recovery statistics - pub fn get_stats(&self) -> RecoveryStats { - let total_recoveries = self.recovery_history.len(); - let successful_recoveries = self.recovery_history.iter().filter(|e| e.success).count(); - - let mut recoveries_by_phase = std::collections::HashMap::new(); - for event in &self.recovery_history { - *recoveries_by_phase.entry(event.phase.clone()).or_insert(0) += 1; - } - - RecoveryStats { - total_recoveries, - successful_recoveries, - failed_recoveries: total_recoveries - successful_recoveries, - recoveries_by_phase, - current_retry_counts: self.retry_counts.clone(), - } - } -} - -/// Recovery statistics -#[derive(Debug, Clone)] -pub struct RecoveryStats { - pub total_recoveries: usize, - pub successful_recoveries: usize, - pub failed_recoveries: usize, - pub recoveries_by_phase: std::collections::HashMap, - pub current_retry_counts: std::collections::HashMap, -} - -impl Default for RecoveryManager { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::error::SyncError; - use crate::sync::sequential::phases::SyncPhase; - - #[tokio::test] - async fn test_execute_recovery_preserves_error_details() { - // Create a recovery manager - let mut recovery_manager = RecoveryManager::new(); - - // Create a test phase - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 50, - current_height: 100, - target_height: None, - headers_downloaded: 50, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - // Create a test error with specific details - let error = SyncError::Timeout( - "Connection to peer 192.168.1.100:9999 timed out after 30s".to_string(), - ); - - // Determine recovery strategy - let _strategy = recovery_manager.determine_strategy(&phase, &error); - - // Create mock network and storage (would need proper mocks in real tests) - // For this test, we're mainly interested in the error being preserved - - // Check that recovery history is initially empty - assert_eq!(recovery_manager.recovery_history.len(), 0); - - // The actual execute_recovery call would require proper mocks for network and storage - // But we've demonstrated that the error parameter is now properly passed and used - - // Verify the method signature accepts the error parameter - // The actual execution would happen in integration tests with proper mocks - } - - #[test] - fn test_recovery_event_contains_error_details() { - let event = RecoveryEvent { - timestamp: std::time::Instant::now(), - phase: "DownloadingHeaders".to_string(), - error: "Connection to peer 192.168.1.100:9999 timed out after 30s".to_string(), - strategy: RecoveryStrategy::Retry { - delay: Duration::from_secs(5), - }, - success: false, - }; - - // Verify error field is not empty - assert!(!event.error.is_empty()); - assert!(event.error.contains("192.168.1.100:9999")); - assert!(event.error.contains("timed out")); - } -} diff --git a/dash-spv/src/sync/sequential/request_control.rs b/dash-spv/src/sync/sequential/request_control.rs deleted file mode 100644 index f1f5ecc60..000000000 --- a/dash-spv/src/sync/sequential/request_control.rs +++ /dev/null @@ -1,410 +0,0 @@ -//! Request control and phase validation for sequential sync - -use std::collections::{HashMap, VecDeque}; -use std::time::Instant; - -use dashcore::network::constants::NetworkExt; -use dashcore::network::message::NetworkMessage; -use dashcore::BlockHash; - -use crate::client::ClientConfig; -use crate::error::{SyncError, SyncResult}; -use crate::network::NetworkManager; -use crate::storage::StorageManager; - -use super::phases::SyncPhase; - -// Phase name constants - must match the phase names from SyncPhase::name() -pub const PHASE_DOWNLOADING_HEADERS: &str = "Downloading Headers"; -pub const PHASE_DOWNLOADING_MNLIST: &str = "Downloading Masternode Lists"; -pub const PHASE_DOWNLOADING_CFHEADERS: &str = "Downloading Filter Headers"; -pub const PHASE_DOWNLOADING_FILTERS: &str = "Downloading Filters"; -pub const PHASE_DOWNLOADING_BLOCKS: &str = "Downloading Blocks"; - -/// Types of sync requests -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RequestType { - GetHeaders(Option), - GetMnListDiff(u32), - GetCFHeaders(u32, BlockHash), - GetCFilters(u32, BlockHash), - GetBlock(BlockHash), -} - -/// A network request with metadata -#[derive(Debug, Clone)] -pub struct NetworkRequest { - pub request_type: RequestType, - pub queued_at: Instant, - pub retry_count: u32, -} - -/// Active request tracking -#[derive(Debug)] -pub struct ActiveRequest { - pub request: NetworkRequest, - pub sent_at: Instant, -} - -/// Controls request sending based on current phase -pub struct RequestController { - /// Configuration - config: ClientConfig, - - /// Queue of pending requests - pending_requests: VecDeque, - - /// Currently active requests - active_requests: HashMap, - - /// Maximum concurrent requests per phase - max_concurrent_requests: HashMap, - - /// Request rate limits (requests per second) - rate_limits: HashMap, - - /// Last request times for rate limiting - last_request_times: HashMap, -} - -impl RequestController { - /// Create a new request controller - pub fn new(config: &ClientConfig) -> Self { - let mut max_concurrent_requests = HashMap::new(); - max_concurrent_requests.insert( - PHASE_DOWNLOADING_HEADERS.to_string(), - config.max_concurrent_headers_requests.unwrap_or(1), - ); - max_concurrent_requests.insert( - PHASE_DOWNLOADING_MNLIST.to_string(), - config.max_concurrent_mnlist_requests.unwrap_or(1), - ); - max_concurrent_requests.insert( - PHASE_DOWNLOADING_CFHEADERS.to_string(), - config.max_concurrent_cfheaders_requests.unwrap_or(1), - ); - max_concurrent_requests - .insert(PHASE_DOWNLOADING_FILTERS.to_string(), config.max_concurrent_filter_requests); - max_concurrent_requests.insert( - PHASE_DOWNLOADING_BLOCKS.to_string(), - config.max_concurrent_block_requests.unwrap_or(5), - ); - - let mut rate_limits = HashMap::new(); - rate_limits.insert( - PHASE_DOWNLOADING_HEADERS.to_string(), - config.headers_request_rate_limit.unwrap_or(10.0), - ); - rate_limits.insert( - PHASE_DOWNLOADING_MNLIST.to_string(), - config.mnlist_request_rate_limit.unwrap_or(5.0), - ); - rate_limits.insert( - PHASE_DOWNLOADING_CFHEADERS.to_string(), - config.cfheaders_request_rate_limit.unwrap_or(10.0), - ); - rate_limits.insert( - PHASE_DOWNLOADING_FILTERS.to_string(), - config.filters_request_rate_limit.unwrap_or(50.0), - ); - rate_limits.insert( - PHASE_DOWNLOADING_BLOCKS.to_string(), - config.blocks_request_rate_limit.unwrap_or(10.0), - ); - - Self { - config: config.clone(), - pending_requests: VecDeque::new(), - active_requests: HashMap::new(), - max_concurrent_requests, - rate_limits, - last_request_times: HashMap::new(), - } - } - - /// Check if a request type is allowed in the current phase - pub fn is_request_allowed(&self, phase: &SyncPhase, request_type: &RequestType) -> bool { - matches!( - (phase, request_type), - (SyncPhase::DownloadingHeaders { .. }, RequestType::GetHeaders(_)) - | (SyncPhase::DownloadingMnList { .. }, RequestType::GetMnListDiff(_)) - | (SyncPhase::DownloadingCFHeaders { .. }, RequestType::GetCFHeaders(_, _)) - | (SyncPhase::DownloadingFilters { .. }, RequestType::GetCFilters(_, _)) - | (SyncPhase::DownloadingBlocks { .. }, RequestType::GetBlock(_)) - ) - } - - /// Queue a request for sending - pub fn queue_request( - &mut self, - phase: &SyncPhase, - request_type: RequestType, - ) -> SyncResult<()> { - if !self.is_request_allowed(phase, &request_type) { - return Err(SyncError::Validation(format!( - "Request type {:?} not allowed in phase {}", - request_type, - phase.name() - ))); - } - - self.pending_requests.push_back(NetworkRequest { - request_type, - queued_at: Instant::now(), - retry_count: 0, - }); - - Ok(()) - } - - /// Process pending requests based on rate limits and concurrency - pub async fn process_pending_requests( - &mut self, - phase: &SyncPhase, - network: &mut dyn NetworkManager, - storage: &dyn StorageManager, - ) -> SyncResult<()> { - let phase_name = phase.name().to_string(); - let max_concurrent = self.max_concurrent_requests.get(&phase_name).copied().unwrap_or(1); - - // Count active requests for this phase - let active_count = self - .active_requests - .values() - .filter(|ar| self.request_phase(&ar.request.request_type) == phase_name) - .count(); - - // Process pending requests up to the limit - while active_count < max_concurrent && !self.pending_requests.is_empty() { - // Check rate limit - if !self.check_rate_limit(&phase_name) { - break; - } - - // Get next request - if let Some(request) = self.pending_requests.pop_front() { - // Validate it's still allowed - if !self.is_request_allowed(phase, &request.request_type) { - continue; - } - - // Send the request - self.send_request(request, network, storage).await?; - } - } - - Ok(()) - } - - /// Send a request to the network - async fn send_request( - &mut self, - request: NetworkRequest, - network: &mut dyn NetworkManager, - storage: &dyn StorageManager, - ) -> SyncResult<()> { - let message = match &request.request_type { - RequestType::GetHeaders(locator) => { - let getheaders = dashcore::network::message_blockdata::GetHeadersMessage { - version: 70214, - locator_hashes: locator.map(|h| vec![h]).unwrap_or_default(), - stop_hash: BlockHash::from([0; 32]), - }; - NetworkMessage::GetHeaders(getheaders) - } - - RequestType::GetMnListDiff(height) => { - // Get the base block hash - either genesis or from a terminal block - let base_block_hash = if *height == 0 { - // Genesis block - self.config.network.known_genesis_block_hash().ok_or_else(|| { - SyncError::Network("No genesis hash for network".to_string()) - })? - } else { - // For non-genesis, we need to determine the base height - // This logic should match what the masternode sync manager does - let base_height = 0; // For now, always use genesis as base - if base_height == 0 { - self.config.network.known_genesis_block_hash().ok_or_else(|| { - SyncError::Network("No genesis hash for network".to_string()) - })? - } else { - storage - .get_header(base_height) - .await - .map_err(|e| { - SyncError::Storage(format!("Failed to get base header: {}", e)) - })? - .ok_or_else(|| SyncError::Storage("Base header not found".to_string()))? - .block_hash() - } - }; - - // Get the target block hash at the requested height - let block_hash = storage - .get_header(*height) - .await - .map_err(|e| { - SyncError::Storage(format!( - "Failed to get header at height {}: {}", - height, e - )) - })? - .ok_or_else(|| { - SyncError::Storage(format!("Header not found at height {}", height)) - })? - .block_hash(); - - let getmnlistdiff = dashcore::network::message_sml::GetMnListDiff { - base_block_hash, - block_hash, - }; - NetworkMessage::GetMnListD(getmnlistdiff) - } - - RequestType::GetCFHeaders(start_height, stop_hash) => { - let getcfheaders = dashcore::network::message_filter::GetCFHeaders { - filter_type: 0, // Basic filter - start_height: *start_height, - stop_hash: *stop_hash, - }; - NetworkMessage::GetCFHeaders(getcfheaders) - } - - RequestType::GetCFilters(start_height, stop_hash) => { - let getcfilters = dashcore::network::message_filter::GetCFilters { - filter_type: 0, // Basic filter - start_height: *start_height, - stop_hash: *stop_hash, - }; - NetworkMessage::GetCFilters(getcfilters) - } - - RequestType::GetBlock(hash) => { - let inv = dashcore::network::message_blockdata::Inventory::Block(*hash); - dashcore::network::message::NetworkMessage::GetData(vec![inv]) - } - }; - - // Send to network - network - .send_message(message) - .await - .map_err(|e| SyncError::Network(format!("Failed to send request: {}", e)))?; - - // Track as active - let request_type = request.request_type.clone(); - self.active_requests.insert( - request_type.clone(), - ActiveRequest { - request, - sent_at: Instant::now(), - }, - ); - - // Update rate limit tracking - let phase_name = self.request_phase(&request_type); - self.last_request_times.insert(phase_name.to_string(), Instant::now()); - - Ok(()) - } - - /// Check if we can send a request based on rate limits - fn check_rate_limit(&self, phase_name: &str) -> bool { - if let Some(rate_limit) = self.rate_limits.get(phase_name) { - if let Some(last_time) = self.last_request_times.get(phase_name) { - let elapsed = last_time.elapsed().as_secs_f64(); - let min_interval = 1.0 / rate_limit; - return elapsed >= min_interval; - } - } - true - } - - /// Get the phase name for a request type - fn request_phase(&self, request_type: &RequestType) -> &'static str { - match request_type { - RequestType::GetHeaders(_) => PHASE_DOWNLOADING_HEADERS, - RequestType::GetMnListDiff(_) => PHASE_DOWNLOADING_MNLIST, - RequestType::GetCFHeaders(_, _) => PHASE_DOWNLOADING_CFHEADERS, - RequestType::GetCFilters(_, _) => PHASE_DOWNLOADING_FILTERS, - RequestType::GetBlock(_) => PHASE_DOWNLOADING_BLOCKS, - } - } - - /// Mark a request as completed - pub fn complete_request(&mut self, request_type: &RequestType) { - self.active_requests.remove(request_type); - } - - /// Get statistics about pending and active requests - pub fn get_stats(&self) -> RequestStats { - let mut stats = RequestStats { - pending_count: self.pending_requests.len(), - active_count: self.active_requests.len(), - ..Default::default() - }; - - // Count by type - for request in &self.pending_requests { - match &request.request_type { - RequestType::GetHeaders(_) => stats.pending_headers += 1, - RequestType::GetMnListDiff(_) => stats.pending_mnlist += 1, - RequestType::GetCFHeaders(_, _) => stats.pending_cfheaders += 1, - RequestType::GetCFilters(_, _) => stats.pending_filters += 1, - RequestType::GetBlock(_) => stats.pending_blocks += 1, - } - } - - for active in self.active_requests.values() { - match &active.request.request_type { - RequestType::GetHeaders(_) => stats.active_headers += 1, - RequestType::GetMnListDiff(_) => stats.active_mnlist += 1, - RequestType::GetCFHeaders(_, _) => stats.active_cfheaders += 1, - RequestType::GetCFilters(_, _) => stats.active_filters += 1, - RequestType::GetBlock(_) => stats.active_blocks += 1, - } - } - - stats - } - - /// Clear all pending requests (used on phase transition) - pub fn clear_pending_requests(&mut self) { - self.pending_requests.clear(); - } - - /// Check for timed out requests - pub fn check_timeouts(&mut self, timeout_duration: std::time::Duration) -> Vec { - let mut timed_out = Vec::new(); - let now = Instant::now(); - - self.active_requests.retain(|request_type, active| { - if now.duration_since(active.sent_at) > timeout_duration { - timed_out.push(request_type.clone()); - false - } else { - true - } - }); - - timed_out - } -} - -/// Statistics about request queues -#[derive(Debug, Default)] -pub struct RequestStats { - pub pending_count: usize, - pub active_count: usize, - pub pending_headers: usize, - pub pending_mnlist: usize, - pub pending_cfheaders: usize, - pub pending_filters: usize, - pub pending_blocks: usize, - pub active_headers: usize, - pub active_mnlist: usize, - pub active_cfheaders: usize, - pub active_filters: usize, - pub active_blocks: usize, -} diff --git a/dash-spv/src/sync/sequential/transitions.rs b/dash-spv/src/sync/sequential/transitions.rs index 16aa7c188..7ecae778a 100644 --- a/dash-spv/src/sync/sequential/transitions.rs +++ b/dash-spv/src/sync/sequential/transitions.rs @@ -219,7 +219,6 @@ impl TransitionManager { target_height: header_tip, last_progress: Instant::now(), diffs_processed: 0, - sync_strategy: None, requests_total: 0, requests_completed: 0, })) diff --git a/dash-spv/src/sync/state.rs b/dash-spv/src/sync/state.rs deleted file mode 100644 index b973ee028..000000000 --- a/dash-spv/src/sync/state.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Sync state management. - -use crate::sync::SyncComponent; -use std::collections::HashSet; -use std::time::SystemTime; - -/// Manages the state of synchronization processes. -#[derive(Debug, Clone)] -pub struct SyncState { - /// Components currently syncing. - syncing: HashSet, - - /// Last sync times for each component. - last_sync: std::collections::HashMap, - - /// Sync start time. - sync_start: Option, -} - -impl Default for SyncState { - fn default() -> Self { - Self::new() - } -} - -impl SyncState { - /// Create a new sync state. - pub fn new() -> Self { - Self { - syncing: HashSet::new(), - last_sync: std::collections::HashMap::new(), - sync_start: None, - } - } - - /// Start sync for a component. - pub fn start_sync(&mut self, component: SyncComponent) { - self.syncing.insert(component); - if self.sync_start.is_none() { - self.sync_start = Some(SystemTime::now()); - } - } - - /// Finish sync for a component. - pub fn finish_sync(&mut self, component: SyncComponent) { - self.syncing.remove(&component); - self.last_sync.insert(component, SystemTime::now()); - - if self.syncing.is_empty() { - self.sync_start = None; - } - } - - /// Check if a component is syncing. - pub fn is_syncing(&self, component: SyncComponent) -> bool { - self.syncing.contains(&component) - } - - /// Check if any component is syncing. - pub fn is_any_syncing(&self) -> bool { - !self.syncing.is_empty() - } - - /// Get all syncing components. - pub fn syncing_components(&self) -> Vec { - self.syncing.iter().copied().collect() - } - - /// Get last sync time for a component. - pub fn last_sync_time(&self, component: SyncComponent) -> Option { - self.last_sync.get(&component).copied() - } - - /// Get sync start time. - pub fn sync_start_time(&self) -> Option { - self.sync_start - } - - /// Reset all sync state. - pub fn reset(&mut self) { - self.syncing.clear(); - self.last_sync.clear(); - self.sync_start = None; - } -} diff --git a/dash-spv/src/sync/validation.rs b/dash-spv/src/sync/validation.rs deleted file mode 100644 index e1c5c7bd7..000000000 --- a/dash-spv/src/sync/validation.rs +++ /dev/null @@ -1,559 +0,0 @@ -//! Comprehensive validation for masternode sync operations. -//! -//! This module provides a validation engine for verifying masternode lists, -//! chain locks, and quorum information during sync operations. It integrates -//! with the existing masternode list engine to provide additional validation -//! layers for improved security and reliability. - -use crate::error::{SyncError, SyncResult}; -use dashcore::{ - network::message_qrinfo::QRInfo, - network::message_sml::MnListDiff, - sml::{ - llmq_entry_verification::LLMQEntryVerificationStatus, llmq_type::LLMQType, - masternode_list_engine::MasternodeListEngine, - quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry, - }, - BlockHash, -}; -use std::collections::HashMap; -use std::time::{Duration, Instant}; -use tracing; - -/// Configuration for validation behavior -#[derive(Debug, Clone)] -pub struct ValidationConfig { - /// Enable comprehensive validation - pub enabled: bool, - /// Validate chain locks for all blocks - pub validate_chain_locks: bool, - /// Validate rotating quorums - pub validate_rotating_quorums: bool, - /// Validate non-rotating quorums - pub validate_non_rotating_quorums: bool, - /// Maximum age for cached validation results - pub cache_ttl: Duration, - /// Maximum number of validation errors before failing - pub max_validation_errors: usize, - /// Retry failed validations - pub retry_failed_validations: bool, - /// Number of retries for failed validations - pub max_retries: usize, -} - -impl Default for ValidationConfig { - fn default() -> Self { - Self { - enabled: true, - validate_chain_locks: true, - validate_rotating_quorums: true, - validate_non_rotating_quorums: true, - cache_ttl: Duration::from_secs(3600), // 1 hour - max_validation_errors: 10, - retry_failed_validations: true, - max_retries: 3, - } - } -} - -impl ValidationConfig { - /// Create a minimal validation configuration for testing - pub fn minimal() -> Self { - Self { - enabled: true, - validate_chain_locks: false, - validate_rotating_quorums: false, - validate_non_rotating_quorums: true, - cache_ttl: Duration::from_secs(60), - max_validation_errors: 100, - retry_failed_validations: false, - max_retries: 0, - } - } -} - -/// Result of a validation operation -#[derive(Debug, Clone)] -pub struct ValidationResult { - /// Whether validation passed - pub success: bool, - /// Errors encountered during validation - pub errors: Vec, - /// Warnings that don't fail validation - pub warnings: Vec, - /// Time taken for validation - pub duration: Duration, - /// Number of items validated - pub items_validated: usize, -} - -impl ValidationResult { - /// Create a successful validation result - pub fn success(items_validated: usize, duration: Duration) -> Self { - Self { - success: true, - errors: Vec::new(), - warnings: Vec::new(), - duration, - items_validated, - } - } - - /// Create a failed validation result - pub fn failure(errors: Vec, duration: Duration) -> Self { - Self { - success: false, - errors, - warnings: Vec::new(), - duration, - items_validated: 0, - } - } - - /// Add a warning to the result - pub fn add_warning(&mut self, warning: ValidationWarning) { - self.warnings.push(warning); - } -} - -/// Validation error types -#[derive(Debug, Clone)] -pub enum ValidationError { - /// Invalid chain lock signature - InvalidChainLock(BlockHash), - /// Missing required masternode list - MissingMasternodeList(u32), - /// Quorum validation failed - QuorumValidationFailed(LLMQType, String), - /// Invalid masternode list diff - InvalidMnListDiff(u32, String), - /// State consistency error - StateInconsistency(String), - /// Timeout during validation - ValidationTimeout(String), -} - -impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidChainLock(hash) => write!(f, "Invalid chain lock for block {:x}", hash), - Self::MissingMasternodeList(height) => { - write!(f, "Missing masternode list at height {}", height) - } - Self::QuorumValidationFailed(qtype, msg) => { - write!(f, "Quorum validation failed for type {:?}: {}", qtype, msg) - } - Self::InvalidMnListDiff(height, msg) => { - write!(f, "Invalid MnListDiff at height {}: {}", height, msg) - } - Self::StateInconsistency(msg) => write!(f, "State inconsistency: {}", msg), - Self::ValidationTimeout(msg) => write!(f, "Validation timeout: {}", msg), - } - } -} - -/// Validation warning types -#[derive(Debug, Clone)] -pub enum ValidationWarning { - /// Quorum close to expiration - QuorumNearExpiration(LLMQType, u32), - /// High number of banned masternodes - HighBannedMasternodeCount(usize), - /// Unusual masternode list size change - UnusualListSizeChange(i32), -} - -/// Comprehensive validation engine -pub struct ValidationEngine { - /// Configuration for validation - config: ValidationConfig, - /// Cache of recent validation results - validation_cache: HashMap, - /// Validation statistics - stats: ValidationStats, - /// Current validation errors count - error_count: usize, -} - -/// Key for validation cache -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -enum ValidationCacheKey { - #[allow(dead_code)] - ChainLock(BlockHash), - Quorum(LLMQType, BlockHash), - MasternodeList(u32), -} - -/// Cached validation result -#[derive(Debug, Clone)] -struct CachedValidationResult { - result: bool, - timestamp: Instant, -} - -/// Validation statistics -#[derive(Debug, Default)] -pub struct ValidationStats { - total_validations: usize, - successful_validations: usize, - failed_validations: usize, - cache_hits: usize, - cache_misses: usize, -} - -impl ValidationEngine { - /// Create a new validation engine - pub fn new(config: ValidationConfig) -> Self { - Self { - config, - validation_cache: HashMap::new(), - stats: ValidationStats::default(), - error_count: 0, - } - } - - /// Validate a QRInfo message comprehensively - pub fn validate_qr_info( - &mut self, - qr_info: &QRInfo, - engine: &MasternodeListEngine, - ) -> SyncResult { - if !self.config.enabled { - return Ok(ValidationResult::success(0, Duration::from_secs(0))); - } - - let start = Instant::now(); - let mut errors = Vec::new(); - let mut items_validated = 0; - - tracing::debug!( - "Starting QRInfo validation with {} diffs and {} snapshots", - qr_info.mn_list_diff_list.len(), - qr_info.quorum_snapshot_list.len() - ); - - // Validate masternode list diffs - for diff in &qr_info.mn_list_diff_list { - let block_height = engine.block_container.get_height(&diff.block_hash).unwrap_or(0); - match self.validate_mn_list_diff(diff, engine) { - Ok(true) => items_validated += 1, - Ok(false) => errors.push(ValidationError::InvalidMnListDiff( - block_height, - "Validation failed".to_string(), - )), - Err(e) => { - errors.push(ValidationError::InvalidMnListDiff(block_height, e.to_string())) - } - } - } - - // Validate quorum snapshots - for snapshot in &qr_info.quorum_snapshot_list { - items_validated += snapshot.active_quorum_members.len(); - // TODO: Implement quorum snapshot validation - } - - // Check error threshold - self.error_count += errors.len(); - if self.error_count > self.config.max_validation_errors { - return Err(SyncError::Validation(format!( - "Validation error threshold exceeded: {} errors", - self.error_count - ))); - } - - let duration = start.elapsed(); - self.update_stats(errors.is_empty(), items_validated); - - if errors.is_empty() { - Ok(ValidationResult::success(items_validated, duration)) - } else { - Ok(ValidationResult::failure(errors, duration)) - } - } - - /// Validate a masternode list diff - fn validate_mn_list_diff( - &mut self, - diff: &MnListDiff, - engine: &MasternodeListEngine, - ) -> SyncResult { - let block_height = engine.block_container.get_height(&diff.block_hash).unwrap_or(0); - let cache_key = ValidationCacheKey::MasternodeList(block_height); - - // Check cache - if let Some(cached) = self.get_cached_result(&cache_key) { - return Ok(cached); - } - - // Perform validation - let result = self.perform_mn_list_diff_validation(diff, engine)?; - - // Cache result - self.cache_result(cache_key, result); - - Ok(result) - } - - /// Perform actual masternode list diff validation - fn perform_mn_list_diff_validation( - &self, - diff: &MnListDiff, - engine: &MasternodeListEngine, - ) -> SyncResult { - // Check if we have the base list - // We can resolve block height from the diff's block hash using the engine's block container - let _block_height = engine.block_container.get_height(&diff.block_hash).unwrap_or(0); - - // Validate merkle root matches - // TODO: Implement merkle root validation - - // Check for unusual changes - let added_count = diff.new_masternodes.len(); - let removed_count = diff.deleted_masternodes.len(); - let updated_count = 0; // No separate updated field in MnListDiff - - let total_changes = added_count + removed_count + updated_count; - if total_changes > 100 { - tracing::warn!( - "Unusual number of masternode changes for block {:?}: {} total", - diff.block_hash, - total_changes - ); - } - - Ok(true) - } - - /// Validate quorums for a specific height - pub fn validate_quorums_at_height( - &mut self, - height: u32, - engine: &MasternodeListEngine, - ) -> SyncResult { - if !self.config.enabled { - return Ok(ValidationResult::success(0, Duration::from_secs(0))); - } - - let start = Instant::now(); - let mut errors = Vec::new(); - let mut items_validated = 0; - - // Get masternode list at height - let mn_list = engine.masternode_lists.get(&height).ok_or_else(|| { - SyncError::Validation(format!("No masternode list at height {}", height)) - })?; - - // Validate each quorum type - for (quorum_type, quorums) in &mn_list.quorums { - if self.should_validate_quorum_type(quorum_type) { - for (quorum_hash, entry) in quorums { - match self.validate_quorum_entry(quorum_type, quorum_hash, entry, engine) { - Ok(true) => items_validated += 1, - Ok(false) => errors.push(ValidationError::QuorumValidationFailed( - *quorum_type, - format!("Quorum {:x} validation failed", quorum_hash), - )), - Err(e) => errors.push(ValidationError::QuorumValidationFailed( - *quorum_type, - e.to_string(), - )), - } - } - } - } - - let duration = start.elapsed(); - self.update_stats(errors.is_empty(), items_validated); - - if errors.is_empty() { - Ok(ValidationResult::success(items_validated, duration)) - } else { - Ok(ValidationResult::failure(errors, duration)) - } - } - - /// Check if we should validate a specific quorum type - fn should_validate_quorum_type(&self, quorum_type: &LLMQType) -> bool { - match quorum_type { - LLMQType::Llmqtype50_60 | LLMQType::Llmqtype400_60 | LLMQType::Llmqtype400_85 => { - self.config.validate_rotating_quorums - } - _ => self.config.validate_non_rotating_quorums, - } - } - - /// Validate a single quorum entry - fn validate_quorum_entry( - &mut self, - quorum_type: &LLMQType, - quorum_hash: &BlockHash, - entry: &QualifiedQuorumEntry, - engine: &MasternodeListEngine, - ) -> SyncResult { - let cache_key = ValidationCacheKey::Quorum(*quorum_type, *quorum_hash); - - // Check cache - if let Some(cached) = self.get_cached_result(&cache_key) { - return Ok(cached); - } - - // Check verification status - let is_valid = match &entry.verified { - LLMQEntryVerificationStatus::Verified => true, - LLMQEntryVerificationStatus::Invalid(_) => false, - LLMQEntryVerificationStatus::Unknown => { - // Try to verify using engine - self.verify_quorum_with_engine(quorum_type, quorum_hash, entry, engine)? - } - LLMQEntryVerificationStatus::Skipped(_) => { - // Skipped entries are treated as unknown - try to verify - self.verify_quorum_with_engine(quorum_type, quorum_hash, entry, engine)? - } - }; - - // Cache result - self.cache_result(cache_key, is_valid); - - Ok(is_valid) - } - - /// Verify a quorum using the engine - fn verify_quorum_with_engine( - &self, - quorum_type: &LLMQType, - quorum_hash: &BlockHash, - entry: &QualifiedQuorumEntry, - _engine: &MasternodeListEngine, - ) -> SyncResult { - // Verify basic quorum properties - if entry.quorum_entry.llmq_type != *quorum_type { - tracing::warn!( - "Quorum type mismatch: expected {:?}, got {:?}", - quorum_type, - entry.quorum_entry.llmq_type - ); - return Ok(false); - } - - if entry.quorum_entry.quorum_hash != *quorum_hash { - tracing::warn!( - "Quorum hash mismatch: expected {:x}, got {:x}", - quorum_hash, - entry.quorum_entry.quorum_hash - ); - return Ok(false); - } - - // Check if the quorum public key is valid (non-zero) - if entry.quorum_entry.quorum_public_key.is_zeroed() { - tracing::warn!("Invalid quorum public key (all zeros) for quorum {:x}", quorum_hash); - return Ok(false); - } - - // For now, we trust the engine's quorum data as it should already be validated - // when it was added to the engine. More complex validation could include: - // - Verifying the threshold signature shares - // - Checking member validity against the masternode list - // - Validating the commitment hash - - Ok(true) - } - - /// Get cached validation result if still valid - fn get_cached_result(&mut self, key: &ValidationCacheKey) -> Option { - if let Some(cached) = self.validation_cache.get(key) { - if cached.timestamp.elapsed() < self.config.cache_ttl { - self.stats.cache_hits += 1; - return Some(cached.result); - } - } - self.stats.cache_misses += 1; - None - } - - /// Cache a validation result - fn cache_result(&mut self, key: ValidationCacheKey, result: bool) { - self.validation_cache.insert( - key, - CachedValidationResult { - result, - timestamp: Instant::now(), - }, - ); - - // Clean up old entries if cache is too large - if self.validation_cache.len() > 10000 { - self.cleanup_cache(); - } - } - - /// Clean up expired cache entries - fn cleanup_cache(&mut self) { - let now = Instant::now(); - self.validation_cache - .retain(|_, v| now.duration_since(v.timestamp) < self.config.cache_ttl); - } - - /// Update validation statistics - fn update_stats(&mut self, success: bool, items: usize) { - self.stats.total_validations += items; - if success { - self.stats.successful_validations += items; - } else { - self.stats.failed_validations += items; - } - } - - /// Get current validation statistics - pub fn stats(&self) -> ValidationStats { - ValidationStats { - total_validations: self.stats.total_validations, - successful_validations: self.stats.successful_validations, - failed_validations: self.stats.failed_validations, - cache_hits: self.stats.cache_hits, - cache_misses: self.stats.cache_misses, - } - } - - /// Reset error count (e.g., after successful sync phase) - pub fn reset_error_count(&mut self) { - self.error_count = 0; - } -} - -/// Summary of validation results for reporting -#[derive(Debug, Clone)] -pub struct ValidationSummary { - /// Total items validated - pub total_validated: usize, - /// Number of validation failures - pub failures: usize, - /// Number of warnings - pub warnings: usize, - /// Total time spent validating - pub total_time: Duration, - /// Cache hit rate - pub cache_hit_rate: f64, -} - -impl ValidationSummary { - /// Create from validation engine stats - pub fn from_engine(engine: &ValidationEngine) -> Self { - let stats = &engine.stats; - let cache_attempts = stats.cache_hits + stats.cache_misses; - let cache_hit_rate = if cache_attempts > 0 { - stats.cache_hits as f64 / cache_attempts as f64 - } else { - 0.0 - }; - - Self { - total_validated: stats.total_validations, - failures: stats.failed_validations, - warnings: 0, // TODO: Track warnings - total_time: Duration::from_secs(0), // TODO: Track total time - cache_hit_rate, - } - } -} diff --git a/dash-spv/src/sync/validation_state.rs b/dash-spv/src/sync/validation_state.rs deleted file mode 100644 index f37d177bb..000000000 --- a/dash-spv/src/sync/validation_state.rs +++ /dev/null @@ -1,488 +0,0 @@ -//! Validation state management with snapshot and rollback capabilities. -//! -//! This module provides state management for validation operations, allowing -//! for safe rollback in case of validation failures and maintaining consistency -//! across sync operations. - -use crate::error::{SyncError, SyncResult}; -use dashcore::{sml::llmq_type::LLMQType, BlockHash}; -use std::collections::{HashMap, VecDeque}; -use std::time::{Duration, Instant}; -use tracing; - -type ValidationStateListener = Box; - -/// Maximum number of state snapshots to maintain -const MAX_SNAPSHOTS: usize = 10; - -/// Validation state that can be snapshotted and rolled back -#[derive(Debug, Clone)] -pub struct ValidationState { - /// Current sync height - pub current_height: u32, - /// Last validated height - pub last_validated_height: u32, - /// Pending validations by height - pub pending_validations: HashMap, - /// Validation failures by height - pub validation_failures: HashMap>, - /// Active quorum validations - pub active_quorum_validations: HashMap<(LLMQType, BlockHash), QuorumValidationState>, - /// Chain lock validation checkpoint - pub chain_lock_checkpoint: Option, - /// State version for consistency checking - pub version: u64, - /// Timestamp of last state update - pub last_update: Instant, -} - -/// Pending validation information -#[derive(Debug, Clone)] -pub struct PendingValidation { - /// Height being validated - pub height: u32, - /// Type of validation - pub validation_type: ValidationType, - /// Number of retry attempts - pub retry_count: usize, - /// Time when validation was queued - pub queued_at: Instant, -} - -/// Types of validation -#[derive(Debug, Clone, PartialEq)] -pub enum ValidationType { - MasternodeList, - ChainLock, - Quorum(LLMQType), - QRInfo, -} - -/// Validation failure information -#[derive(Debug, Clone)] -pub struct ValidationFailure { - /// Type of validation that failed - pub validation_type: ValidationType, - /// Error message - pub error: String, - /// Time of failure - pub failed_at: Instant, - /// Whether this failure is recoverable - pub recoverable: bool, -} - -/// State of quorum validation -#[derive(Debug, Clone)] -pub struct QuorumValidationState { - /// Quorum type - pub quorum_type: LLMQType, - /// Quorum hash - pub quorum_hash: BlockHash, - /// Validation status - pub status: QuorumValidationStatus, - /// Number of members validated - pub members_validated: usize, - /// Total members - pub total_members: usize, -} - -/// Quorum validation status -#[derive(Debug, Clone, PartialEq)] -pub enum QuorumValidationStatus { - Pending, - InProgress, - Completed, - Failed(String), -} - -/// Chain lock validation checkpoint -#[derive(Debug, Clone)] -pub struct ChainLockCheckpoint { - /// Height of the checkpoint - pub height: u32, - /// Block hash - pub block_hash: BlockHash, - /// Time when checkpoint was created - pub created_at: Instant, -} - -/// Validation state manager with snapshot and rollback support -pub struct ValidationStateManager { - /// Current state - current_state: ValidationState, - /// State snapshots for rollback - snapshots: VecDeque, - /// Maximum age for snapshots - snapshot_ttl: Duration, - /// State change listeners - change_listeners: Vec, -} - -/// State snapshot for rollback -#[derive(Debug, Clone)] -struct StateSnapshot { - /// The saved state - state: ValidationState, - /// Snapshot ID - id: u64, - /// When the snapshot was created - created_at: Instant, - /// Description of why snapshot was created - description: String, -} - -impl Default for ValidationState { - fn default() -> Self { - Self { - current_height: 0, - last_validated_height: 0, - pending_validations: HashMap::new(), - validation_failures: HashMap::new(), - active_quorum_validations: HashMap::new(), - chain_lock_checkpoint: None, - version: 0, - last_update: Instant::now(), - } - } -} - -impl Default for ValidationStateManager { - fn default() -> Self { - Self::new() - } -} - -impl ValidationStateManager { - /// Create a new validation state manager - pub fn new() -> Self { - Self { - current_state: ValidationState::default(), - snapshots: VecDeque::new(), - snapshot_ttl: Duration::from_secs(3600), // 1 hour - change_listeners: Vec::new(), - } - } - - /// Create a snapshot of the current state - pub fn create_snapshot(&mut self, description: impl Into) -> u64 { - let snapshot_id = self.current_state.version; - - let snapshot = StateSnapshot { - state: self.current_state.clone(), - id: snapshot_id, - created_at: Instant::now(), - description: description.into(), - }; - - self.snapshots.push_back(snapshot); - - // Remove old snapshots - while self.snapshots.len() > MAX_SNAPSHOTS { - self.snapshots.pop_front(); - } - - // Clean up expired snapshots - self.cleanup_expired_snapshots(); - - tracing::debug!( - "Created state snapshot {} with {} pending validations", - snapshot_id, - self.current_state.pending_validations.len() - ); - - snapshot_id - } - - /// Rollback to a specific snapshot - pub fn rollback_to_snapshot(&mut self, snapshot_id: u64) -> SyncResult<()> { - let snapshot = self.snapshots.iter().find(|s| s.id == snapshot_id).ok_or_else(|| { - SyncError::InvalidState(format!("Snapshot {} not found", snapshot_id)) - })?; - - let old_height = self.current_state.current_height; - self.current_state = snapshot.state.clone(); - - tracing::info!( - "Rolled back state from height {} to {} (snapshot: {})", - old_height, - self.current_state.current_height, - snapshot.description - ); - - // Notify listeners - self.notify_listeners(); - - Ok(()) - } - - /// Rollback to the most recent snapshot - pub fn rollback_to_latest(&mut self) -> SyncResult<()> { - let snapshot = self.snapshots.back().ok_or_else(|| { - SyncError::InvalidState("No snapshots available for rollback".to_string()) - })?; - - let snapshot_id = snapshot.id; - self.rollback_to_snapshot(snapshot_id) - } - - /// Update current sync height - pub fn update_sync_height(&mut self, height: u32) { - self.current_state.current_height = height; - self.current_state.version += 1; - self.current_state.last_update = Instant::now(); - self.notify_listeners(); - } - - /// Add a pending validation - pub fn add_pending_validation(&mut self, height: u32, validation_type: ValidationType) { - self.current_state.pending_validations.insert( - height, - PendingValidation { - height, - validation_type, - retry_count: 0, - queued_at: Instant::now(), - }, - ); - self.current_state.version += 1; - self.notify_listeners(); - } - - /// Complete a pending validation - pub fn complete_validation(&mut self, height: u32) -> Option { - let result = self.current_state.pending_validations.remove(&height); - if result.is_some() { - self.current_state.last_validated_height = - self.current_state.last_validated_height.max(height); - self.current_state.version += 1; - self.notify_listeners(); - } - result - } - - /// Record a validation failure - pub fn record_validation_failure( - &mut self, - height: u32, - validation_type: ValidationType, - error: String, - recoverable: bool, - ) { - let failure = ValidationFailure { - validation_type, - error, - failed_at: Instant::now(), - recoverable, - }; - - self.current_state.validation_failures.entry(height).or_default().push(failure); - - self.current_state.version += 1; - self.notify_listeners(); - } - - /// Update quorum validation state - pub fn update_quorum_validation( - &mut self, - quorum_type: LLMQType, - quorum_hash: BlockHash, - status: QuorumValidationStatus, - ) { - let key = (quorum_type, quorum_hash); - - if let Some(state) = self.current_state.active_quorum_validations.get_mut(&key) { - state.status = status; - } else { - self.current_state.active_quorum_validations.insert( - key, - QuorumValidationState { - quorum_type, - quorum_hash, - status, - members_validated: 0, - total_members: 0, - }, - ); - } - - self.current_state.version += 1; - self.notify_listeners(); - } - - /// Set chain lock checkpoint - pub fn set_chain_lock_checkpoint(&mut self, height: u32, block_hash: BlockHash) { - self.current_state.chain_lock_checkpoint = Some(ChainLockCheckpoint { - height, - block_hash, - created_at: Instant::now(), - }); - self.current_state.version += 1; - self.notify_listeners(); - } - - /// Check state consistency - pub fn validate_consistency(&self) -> SyncResult<()> { - // Check that last validated height doesn't exceed current height - if self.current_state.last_validated_height > self.current_state.current_height { - return Err(SyncError::InvalidState(format!( - "Last validated height {} exceeds current height {}", - self.current_state.last_validated_height, self.current_state.current_height - ))); - } - - // Check that pending validations are within reasonable range - for height in self.current_state.pending_validations.keys() { - if *height > self.current_state.current_height + 1000 { - return Err(SyncError::InvalidState(format!( - "Pending validation at height {} is too far ahead of current height {}", - height, self.current_state.current_height - ))); - } - } - - // Check chain lock checkpoint consistency - if let Some(checkpoint) = &self.current_state.chain_lock_checkpoint { - if checkpoint.height > self.current_state.current_height { - return Err(SyncError::InvalidState(format!( - "Chain lock checkpoint height {} exceeds current height {}", - checkpoint.height, self.current_state.current_height - ))); - } - } - - // Validate active quorum states - for (key, state) in &self.current_state.active_quorum_validations { - if state.members_validated > state.total_members { - return Err(SyncError::InvalidState(format!( - "Quorum {:?} has more validated members ({}) than total members ({})", - key, state.members_validated, state.total_members - ))); - } - } - - // Check for expired pending validations - let now = Instant::now(); - let stale_timeout = Duration::from_secs(300); // 5 minutes - for (height, pending) in &self.current_state.pending_validations { - if now.duration_since(pending.queued_at) > stale_timeout { - return Err(SyncError::InvalidState(format!( - "Pending validation at height {} has been queued for too long", - height - ))); - } - } - - Ok(()) - } - - /// Get current state - pub fn current_state(&self) -> &ValidationState { - &self.current_state - } - - /// Get mutable current state - pub fn current_state_mut(&mut self) -> &mut ValidationState { - &mut self.current_state - } - - /// Add a state change listener - pub fn add_listener(&mut self, listener: F) - where - F: Fn(&ValidationState) + Send + 'static, - { - self.change_listeners.push(Box::new(listener)); - } - - /// Clean up expired snapshots - fn cleanup_expired_snapshots(&mut self) { - let now = Instant::now(); - self.snapshots - .retain(|snapshot| now.duration_since(snapshot.created_at) < self.snapshot_ttl); - } - - /// Notify all listeners of state change - fn notify_listeners(&self) { - for listener in &self.change_listeners { - listener(&self.current_state); - } - } - - /// Get validation statistics - pub fn get_stats(&self) -> ValidationStats { - ValidationStats { - pending_validations: self.current_state.pending_validations.len(), - total_failures: self.current_state.validation_failures.values().map(|v| v.len()).sum(), - active_quorum_validations: self.current_state.active_quorum_validations.len(), - snapshots_available: self.snapshots.len(), - state_version: self.current_state.version, - } - } -} - -/// Validation statistics -#[derive(Debug, Clone)] -pub struct ValidationStats { - /// Number of pending validations - pub pending_validations: usize, - /// Total validation failures - pub total_failures: usize, - /// Number of active quorum validations - pub active_quorum_validations: usize, - /// Number of snapshots available - pub snapshots_available: usize, - /// Current state version - pub state_version: u64, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_snapshot_and_rollback() { - let mut manager = ValidationStateManager::new(); - - // Update state - manager.update_sync_height(100); - manager.add_pending_validation(101, ValidationType::MasternodeList); - - // Create snapshot - let snapshot_id = manager.create_snapshot("Before validation"); - - // Make more changes - manager.update_sync_height(200); - manager.record_validation_failure( - 150, - ValidationType::ChainLock, - "Test failure".to_string(), - true, - ); - - assert_eq!(manager.current_state().current_height, 200); - assert_eq!(manager.current_state().validation_failures.len(), 1); - - // Rollback - manager.rollback_to_snapshot(snapshot_id).unwrap(); - - assert_eq!(manager.current_state().current_height, 100); - assert_eq!(manager.current_state().validation_failures.len(), 0); - assert_eq!(manager.current_state().pending_validations.len(), 1); - } - - #[test] - fn test_consistency_validation() { - let mut manager = ValidationStateManager::new(); - - manager.update_sync_height(100); - manager.current_state_mut().last_validated_height = 50; - - // Should pass - assert!(manager.validate_consistency().is_ok()); - - // Set invalid state - manager.current_state_mut().last_validated_height = 200; - - // Should fail - assert!(manager.validate_consistency().is_err()); - } -} diff --git a/dash-spv/src/sync/validation_test.rs b/dash-spv/src/sync/validation_test.rs deleted file mode 100644 index b68323cdf..000000000 --- a/dash-spv/src/sync/validation_test.rs +++ /dev/null @@ -1,248 +0,0 @@ -//! Integration tests for comprehensive validation functionality. - -#[cfg(test)] -mod tests { - use crate::client::ClientConfig; - use crate::network::PeerNetworkManager; - use crate::storage::MemoryStorageManager; - use crate::sync::chainlock_validation::{ChainLockValidationConfig, ChainLockValidator}; - use crate::sync::masternodes::MasternodeSyncManager; - use crate::sync::validation::{ValidationConfig, ValidationEngine}; - use crate::sync::validation_state::{ValidationStateManager, ValidationType}; - use crate::types::ValidationMode; - use dashcore::network::message_qrinfo::{QRInfo, QuorumSnapshot}; - use dashcore::network::message_sml::MnListDiff; - use dashcore::Transaction; - use dashcore::{BlockHash, Network}; - - /// Create a test client config with validation enabled - fn create_test_config() -> ClientConfig { - ClientConfig { - network: Network::Testnet, - validation_mode: ValidationMode::Full, - enable_masternodes: true, - ..Default::default() - } - } - - /// Create a mock QRInfo for testing - pub fn create_mock_qr_info() -> QRInfo { - let create_snapshot = || QuorumSnapshot { - skip_list_mode: dashcore::network::message_qrinfo::MNSkipListMode::NoSkipping, - active_quorum_members: vec![true; 10], - skip_list: vec![], - }; - - QRInfo { - quorum_snapshot_at_h_minus_c: create_snapshot(), - quorum_snapshot_at_h_minus_2c: create_snapshot(), - quorum_snapshot_at_h_minus_3c: create_snapshot(), - mn_list_diff_tip: create_mock_mn_list_diff(100), - mn_list_diff_h: create_mock_mn_list_diff(100), - mn_list_diff_at_h_minus_c: create_mock_mn_list_diff(100), - mn_list_diff_at_h_minus_2c: create_mock_mn_list_diff(100), - mn_list_diff_at_h_minus_3c: create_mock_mn_list_diff(100), - quorum_snapshot_and_mn_list_diff_at_h_minus_4c: None, - last_commitment_per_index: vec![], - quorum_snapshot_list: vec![], - mn_list_diff_list: vec![create_mock_mn_list_diff(100), create_mock_mn_list_diff(200)], - } - } - - /// Create a mock MnListDiff for testing - pub fn create_mock_mn_list_diff(_height: u32) -> MnListDiff { - MnListDiff { - version: 1, - base_block_hash: BlockHash::from([0u8; 32]), - block_hash: BlockHash::from([0; 32]), - total_transactions: 1, - merkle_hashes: vec![], - merkle_flags: vec![], - coinbase_tx: Transaction { - version: 1, - lock_time: 0, - input: vec![], - output: vec![], - special_transaction_payload: None, - }, - deleted_masternodes: vec![], - new_masternodes: vec![], - deleted_quorums: vec![], - new_quorums: vec![], - quorums_chainlock_signatures: vec![], - } - } - - #[tokio::test] - async fn test_validation_engine_creation() { - let config = ValidationConfig::default(); - let engine = ValidationEngine::new(config); - - // Note: ValidationStats fields are private, so we can only test that - // the engine is created successfully - let _stats = engine.stats(); - // Test passes if no panic occurs - } - - #[tokio::test] - async fn test_chain_lock_validator_creation() { - let config = ChainLockValidationConfig::default(); - let validator = ChainLockValidator::new(config); - - assert_eq!(validator.cache_hit_rate(), 0.0); - } - - #[tokio::test] - async fn test_validation_state_manager() { - let mut manager = ValidationStateManager::new(); - - // Update state - manager.update_sync_height(100); - manager.add_pending_validation(101, ValidationType::MasternodeList); - - // Create snapshot - let snapshot_id = manager.create_snapshot("Test snapshot"); - - // Make changes - manager.update_sync_height(200); - manager.record_validation_failure( - 150, - ValidationType::ChainLock, - "Test failure".to_string(), - true, - ); - - // Check current state - assert_eq!(manager.current_state().current_height, 200); - assert_eq!(manager.current_state().validation_failures.len(), 1); - - // Rollback - manager.rollback_to_snapshot(snapshot_id).unwrap(); - - // Verify rollback - assert_eq!(manager.current_state().current_height, 100); - assert_eq!(manager.current_state().validation_failures.len(), 0); - assert_eq!(manager.current_state().pending_validations.len(), 1); - } - - #[tokio::test] - async fn test_masternode_sync_with_validation() { - let config = create_test_config(); - let _sync_manager = - MasternodeSyncManager::::new(&config); - - // Note: get_validation_summary method was removed from MasternodeSyncManager - // Test that manager is created successfully - } - - #[tokio::test] - async fn test_qr_info_validation() { - let config = create_test_config(); - let _sync_manager = - MasternodeSyncManager::::new(&config); - let _storage = - MemoryStorageManager::new().await.expect("Failed to create MemoryStorageManager"); - - // Create mock QRInfo - let _qr_info = create_mock_qr_info(); - - // Note: handle_qr_info method was removed from MasternodeSyncManager - // Test that components are created successfully - } - - #[tokio::test] - async fn test_validation_enable_disable() { - let mut config = create_test_config(); - config.validation_mode = ValidationMode::None; - - let _sync_manager = - MasternodeSyncManager::::new(&config); - - // Note: set_validation_enabled and get_validation_summary methods were removed - // Test that manager is created successfully with validation disabled - } - - #[tokio::test] - async fn test_validation_state_consistency() { - let mut manager = ValidationStateManager::new(); - - // Set valid state - manager.update_sync_height(100); - manager.current_state_mut().last_validated_height = 50; - - // Should pass consistency check - assert!(manager.validate_consistency().is_ok()); - - // Set invalid state - manager.current_state_mut().last_validated_height = 200; - - // Should fail consistency check - assert!(manager.validate_consistency().is_err()); - } - - #[tokio::test] - async fn test_validation_with_retries() { - let config = ValidationConfig { - retry_failed_validations: true, - max_retries: 3, - ..Default::default() - }; - - let engine = ValidationEngine::new(config); - - // Verify retry configuration - let _stats = engine.stats(); - // Note: ValidationStats fields are private - } - - #[tokio::test] - async fn test_validation_cache() { - let config = ValidationConfig::default(); - let engine = ValidationEngine::new(config); - - // Perform validations to populate cache - // Note: Actual validation requires proper engine setup - - let _stats = engine.stats(); - // Note: ValidationStats fields are private - } -} - -/// Performance tests for validation -#[cfg(test)] -mod perf_tests { - - use crate::sync::chainlock_validation::{ChainLockValidationConfig, ChainLockValidator}; - - use dashcore::BlockHash; - use std::time::Instant; - - // TODO: Implement performance test for validation - // This test should measure performance of validation with large QRInfo datasets - // and verify that validation completes within acceptable time limits - - #[tokio::test] - #[ignore] - async fn test_cache_performance() { - let config = ChainLockValidationConfig { - cache_size: 10000, - ..Default::default() - }; - - let validator = ChainLockValidator::new(config); - - let start = Instant::now(); - - // Simulate many cache operations - for i in 0..10000 { - let _hash = BlockHash::from([i as u8; 32]); - // Cache operations would happen during validation - } - - let duration = start.elapsed(); - println!("Cache operations completed in {:?}", duration); - - let hit_rate = validator.cache_hit_rate(); - println!("Cache hit rate: {:.2}%", hit_rate * 100.0); - } -} diff --git a/dash-spv/tests/error_handling_test.rs b/dash-spv/tests/error_handling_test.rs deleted file mode 100644 index bc6cd1f44..000000000 --- a/dash-spv/tests/error_handling_test.rs +++ /dev/null @@ -1,1200 +0,0 @@ -#![cfg(feature = "skip_mock_implementation_incomplete")] - -//! Comprehensive error handling tests for dash-spv -//! -//! NOTE: This test file is currently ignored due to incomplete mock trait implementations. -//! TODO: Re-enable once StorageManager and NetworkManager trait methods are fully implemented. - -//! Comprehensive error handling tests for dash-spv -//! -//! This test suite validates error scenarios across all major components: -//! - Network errors (connection failures, timeouts, invalid data) -//! - Storage errors (disk full, permissions, corruption) -//! - Validation errors (invalid headers, failed verification) -//! - Recovery mechanisms (automatic retries, graceful degradation) -//! - Error propagation through layers - -use std::collections::HashMap; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::sync::Arc; - -use dashcore::{ - block::{Header as BlockHeader, Version}, - hash_types::FilterHeader, - pow::CompactTarget, - BlockHash, Network, OutPoint, Txid, -}; -use dashcore_hashes::Hash; -use tokio::sync::RwLock; - -use dash_spv::error::*; -use dash_spv::network::{NetworkManager, Peer}; -use dash_spv::storage::{DiskStorageManager, StorageManager}; -use dash_spv::sync::sequential::phases::SyncPhase; -use dash_spv::sync::sequential::recovery::{RecoveryManager, RecoveryStrategy}; -use dash_spv::types::{ChainState, MempoolState, UnconfirmedTransaction}; - -/// Mock network manager for testing error scenarios -struct MockNetworkManager { - fail_on_connect: bool, - timeout_on_message: bool, - return_invalid_data: bool, - disconnect_after_n_messages: Option, - messages_sent: usize, -} - -impl MockNetworkManager { - fn new() -> Self { - Self { - fail_on_connect: false, - timeout_on_message: false, - return_invalid_data: false, - disconnect_after_n_messages: None, - messages_sent: 0, - } - } - - // Removed unused set_fail_on_connect; use flags directly where needed - - fn set_timeout_on_message(&mut self) { - self.timeout_on_message = true; - } - - fn set_return_invalid_data(&mut self) { - self.return_invalid_data = true; - } - - fn set_disconnect_after_n_messages(&mut self, n: usize) { - self.disconnect_after_n_messages = Some(n); - } -} - -#[async_trait::async_trait] -impl dash_spv::network::NetworkManager for MockNetworkManager { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - async fn connect(&mut self) -> NetworkResult<()> { - if self.fail_on_connect { - Err(NetworkError::ConnectionFailed("Mock connection failure".to_string())) - } else { - Ok(()) - } - } - - async fn disconnect(&mut self) -> NetworkResult<()> { - Ok(()) - } - - async fn send_message( - &mut self, - _msg: dashcore::network::message::NetworkMessage, - ) -> NetworkResult<()> { - if let Some(n) = self.disconnect_after_n_messages { - if self.messages_sent >= n { - return Err(NetworkError::PeerDisconnected); - } - } - - self.messages_sent += 1; - - if self.timeout_on_message { - Err(NetworkError::Timeout) - } else { - Ok(()) - } - } - - async fn receive_message( - &mut self, - ) -> NetworkResult> { - if self.return_invalid_data { - // Return data that will fail validation - Err(NetworkError::ProtocolError("Invalid message format".to_string())) - } else if self.timeout_on_message { - Err(NetworkError::Timeout) - } else { - Ok(None) - } - } - - fn is_connected(&self) -> bool { - !self.fail_on_connect - } - - fn peer_count(&self) -> usize { - if self.fail_on_connect { - 0 - } else { - 1 - } - } - - fn peer_info(&self) -> Vec { - vec![] - } - - async fn get_peer_best_height(&self) -> NetworkResult> { - Ok(Some(1000000)) - } - - async fn has_peer_with_service( - &self, - _service_flags: dashcore::network::constants::ServiceFlags, - ) -> bool { - true - } - - async fn update_peer_dsq_preference(&mut self, _wants_dsq: bool) -> NetworkResult<()> { - Ok(()) - } -} - -/// Mock storage manager for testing error scenarios -struct MockStorageManager { - fail_on_write: bool, - fail_on_read: bool, - corrupt_data: bool, - disk_full: bool, - permission_denied: bool, - lock_poisoned: bool, -} - -impl MockStorageManager { - fn new() -> Self { - Self { - fail_on_write: false, - fail_on_read: false, - corrupt_data: false, - disk_full: false, - permission_denied: false, - lock_poisoned: false, - } - } - - fn set_fail_on_write(&mut self) { - self.fail_on_write = true; - } - - fn set_fail_on_read(&mut self) { - self.fail_on_read = true; - } - - fn set_corrupt_data(&mut self) { - self.corrupt_data = true; - } - - fn set_disk_full(&mut self) { - self.disk_full = true; - } - - fn set_permission_denied(&mut self) { - self.permission_denied = true; - } - - fn set_lock_poisoned(&mut self) { - self.lock_poisoned = true; - } -} - -#[async_trait::async_trait] -impl StorageManager for MockStorageManager { - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } - - async fn store_headers(&mut self, _headers: &[BlockHeader]) -> StorageResult<()> { - if self.lock_poisoned { - return Err(StorageError::LockPoisoned("Mock lock poisoned".to_string())); - } - if self.permission_denied { - return Err(StorageError::WriteFailed("Permission denied".to_string())); - } - if self.disk_full { - return Err(StorageError::WriteFailed("No space left on device".to_string())); - } - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_headers(&self, _range: std::ops::Range) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(vec![]) - } - - async fn get_header(&self, _height: u32) -> StorageResult> { - if self.lock_poisoned { - return Err(StorageError::LockPoisoned("Mock lock poisoned".to_string())); - } - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - if self.corrupt_data { - return Err(StorageError::Corruption("Mock data corruption".to_string())); - } - Ok(None) - } - - async fn get_tip_height(&self) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(Some(0)) - } - - async fn store_filter_headers(&mut self, _headers: &[FilterHeader]) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_filter_headers( - &self, - _range: std::ops::Range, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(vec![]) - } - - async fn get_filter_header(&self, _height: u32) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn get_filter_tip_height(&self) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(Some(0)) - } - - async fn store_masternode_state( - &mut self, - _state: &dash_spv::storage::MasternodeState, - ) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_masternode_state( - &self, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn store_chain_state(&mut self, _state: &ChainState) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_chain_state(&self) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn store_filter(&mut self, _height: u32, _filter: &[u8]) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_filter(&self, _height: u32) -> StorageResult>> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn store_metadata(&mut self, _key: &str, _value: &[u8]) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_metadata(&self, _key: &str) -> StorageResult>> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn clear(&mut self) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn clear_filters(&mut self) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn stats(&self) -> StorageResult { - Ok(dash_spv::storage::StorageStats { - header_count: 0, - filter_header_count: 0, - filter_count: 0, - total_size: 0, - component_sizes: std::collections::HashMap::new(), - }) - } - - async fn get_header_height_by_hash( - &self, - _hash: &dashcore::BlockHash, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn get_headers_batch( - &self, - _start_height: u32, - _end_height: u32, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(vec![]) - } - - async fn store_sync_state( - &mut self, - _state: &dash_spv::storage::PersistentSyncState, - ) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_sync_state( - &self, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn clear_sync_state(&mut self) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn store_sync_checkpoint( - &mut self, - _height: u32, - _checkpoint: &dash_spv::storage::sync_state::SyncCheckpoint, - ) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn get_sync_checkpoints( - &self, - _start_height: u32, - _end_height: u32, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(vec![]) - } - - async fn store_chain_lock( - &mut self, - _height: u32, - _chain_lock: &dashcore::ChainLock, - ) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_chain_lock(&self, _height: u32) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn get_chain_locks( - &self, - _start_height: u32, - _end_height: u32, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(vec![]) - } - - // InstantLock storage methods removed from trait; no-op here - - async fn store_mempool_transaction( - &mut self, - _txid: &Txid, - _tx: &UnconfirmedTransaction, - ) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn remove_mempool_transaction(&mut self, _txid: &Txid) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn get_mempool_transaction( - &self, - _txid: &Txid, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn get_all_mempool_transactions( - &self, - ) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(HashMap::new()) - } - - async fn store_mempool_state(&mut self, _state: &MempoolState) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn load_mempool_state(&self) -> StorageResult> { - if self.fail_on_read { - return Err(StorageError::ReadFailed("Mock read failure".to_string())); - } - Ok(None) - } - - async fn clear_mempool(&mut self) -> StorageResult<()> { - if self.fail_on_write { - return Err(StorageError::WriteFailed("Mock write failure".to_string())); - } - Ok(()) - } - - async fn shutdown(&mut self) -> StorageResult<()> { - Ok(()) - } -} - -// ===== Network Error Tests ===== - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_network_connection_failure() { - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9999); - - // Test connection timeout - let result = Peer::connect(addr, 1, Network::Dash).await; - - match result { - Err(NetworkError::ConnectionFailed(msg)) => { - assert!(msg.contains("Failed to connect")); - } - _ => panic!("Expected ConnectionFailed error"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_network_timeout_recovery() { - let mut network = MockNetworkManager::new(); - network.set_timeout_on_message(); - - let mut recovery_manager = RecoveryManager::new(); - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100, - target_height: Some(1000), - headers_downloaded: 100, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let error = SyncError::Timeout("Network request timed out".to_string()); - let strategy = recovery_manager.determine_strategy(&phase, &error); - - match strategy { - RecoveryStrategy::Retry { - delay, - } => { - assert!(delay.as_secs() >= 1); - } - _ => panic!("Expected Retry strategy for timeout error"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_network_peer_disconnection() { - let mut network = MockNetworkManager::new(); - network.set_disconnect_after_n_messages(3); - - // Send messages until disconnection - let mut disconnect_occurred = false; - for i in 0..5 { - let msg = dashcore::network::message::NetworkMessage::Ping(i); - match network.send_message(msg).await { - Err(NetworkError::PeerDisconnected) => { - disconnect_occurred = true; - assert_eq!(i, 3); - break; - } - Ok(_) => assert!(i < 3), - Err(e) => panic!("Unexpected error: {:?}", e), - } - } - - assert!(disconnect_occurred, "Expected peer disconnection"); -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_network_invalid_data_handling() { - let mut network = MockNetworkManager::new(); - network.set_return_invalid_data(); - - match network.receive_message().await { - Err(NetworkError::ProtocolError(msg)) => { - assert!(msg.contains("Invalid message format")); - } - _ => panic!("Expected ProtocolError for invalid data"), - } -} - -// ===== Storage Error Tests ===== - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_storage_disk_full() { - let mut storage = MockStorageManager::new(); - storage.set_disk_full(); - - let header = create_test_header(0); - let result = storage.store_headers(&[header]).await; - - match result { - Err(StorageError::WriteFailed(msg)) => { - assert!(msg.contains("No space left on device")); - } - _ => panic!("Expected WriteFailed error for disk full"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_storage_permission_denied() { - let mut storage = MockStorageManager::new(); - storage.set_permission_denied(); - - let header = create_test_header(0); - let result = storage.store_headers(&[header]).await; - - match result { - Err(StorageError::WriteFailed(msg)) => { - assert!(msg.contains("Permission denied")); - } - _ => panic!("Expected WriteFailed error for permission denied"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_storage_corruption_detection() { - let mut storage = MockStorageManager::new(); - storage.set_corrupt_data(); - - let result = storage.get_header(0).await; - - match result { - Err(StorageError::Corruption(msg)) => { - assert!(msg.contains("Mock data corruption")); - } - _ => panic!("Expected Corruption error"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_storage_lock_poisoned() { - let mut storage = MockStorageManager::new(); - storage.set_lock_poisoned(); - - let header = create_test_header(0); - let result = storage.store_headers(&[header]).await; - - match result { - Err(StorageError::LockPoisoned(msg)) => { - assert!(msg.contains("Mock lock poisoned")); - } - _ => panic!("Expected LockPoisoned error"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_storage_recovery_strategy() { - let mut storage = MockStorageManager::new(); - storage.set_fail_on_write(); - - let mut recovery_manager = RecoveryManager::new(); - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100, - target_height: Some(1000), - headers_downloaded: 100, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let error = SyncError::Storage("Write failed".to_string()); - let strategy = recovery_manager.determine_strategy(&phase, &error); - - match strategy { - RecoveryStrategy::Abort { - error, - } => { - assert!(error.contains("Storage error")); - } - _ => panic!("Expected Abort strategy for storage error"), - } -} - -// ===== Validation Error Tests ===== - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_validation_invalid_proof_of_work() { - let mut header = create_test_header(0); - header.bits = CompactTarget::from_consensus(0x00000000); // Invalid difficulty - - let result = validate_header_pow(&header); - - match result { - Err(ValidationError::InvalidProofOfWork) => { - // Expected - } - _ => panic!("Expected InvalidProofOfWork error"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_validation_invalid_header_chain() { - let header1 = create_test_header(0); - let mut header2 = create_test_header(1); - header2.prev_blockhash = BlockHash::from_byte_array([0xFF; 32]); // Wrong previous hash - - let result = validate_header_chain(&header1, &header2); - - match result { - Err(ValidationError::InvalidHeaderChain(msg)) => { - assert!(msg.contains("previous block hash mismatch")); - } - _ => panic!("Expected InvalidHeaderChain error"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_validation_recovery_strategy() { - let mut recovery_manager = RecoveryManager::new(); - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 500, - target_height: Some(1000), - headers_downloaded: 500, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let error = SyncError::Validation("Invalid block header".to_string()); - let strategy = recovery_manager.determine_strategy(&phase, &error); - - match strategy { - RecoveryStrategy::RestartPhase { - checkpoint, - } => { - assert!(checkpoint.restart_height.is_some()); - let restart_height = checkpoint.restart_height.unwrap(); - assert!(restart_height < 500); // Should restart from earlier height - } - _ => panic!("Expected RestartPhase strategy for validation error"), - } -} - -// ===== Error Conversion Tests ===== - -#[test] -fn test_error_conversions() { - // Test NetworkError -> SpvError - let net_err = NetworkError::Timeout; - let spv_err: SpvError = net_err.into(); - match spv_err { - SpvError::Network(NetworkError::Timeout) => {} - _ => panic!("Incorrect error conversion"), - } - - // Test StorageError -> SpvError - let storage_err = StorageError::Corruption("test".to_string()); - let spv_err: SpvError = storage_err.into(); - match spv_err { - SpvError::Storage(StorageError::Corruption(_)) => {} - _ => panic!("Incorrect error conversion"), - } - - // Test ValidationError -> SpvError - let val_err = ValidationError::InvalidProofOfWork; - let spv_err: SpvError = val_err.into(); - match spv_err { - SpvError::Validation(ValidationError::InvalidProofOfWork) => {} - _ => panic!("Incorrect error conversion"), - } - - // Test SyncError -> SpvError - let sync_err = SyncError::SyncInProgress; - let spv_err: SpvError = sync_err.into(); - match spv_err { - SpvError::Sync(SyncError::SyncInProgress) => {} - _ => panic!("Incorrect error conversion"), - } -} - -// ===== Error Context and Messages Tests ===== - -#[test] -fn test_error_messages_contain_context() { - let err = NetworkError::ConnectionFailed( - "Failed to connect to 192.168.1.1:9999: Connection refused".to_string(), - ); - let msg = err.to_string(); - assert!(msg.contains("192.168.1.1:9999")); - assert!(msg.contains("Connection refused")); - - let err = StorageError::WriteFailed( - "/var/dash-spv/headers/segment_5.dat: Permission denied".to_string(), - ); - let msg = err.to_string(); - assert!(msg.contains("segment_5.dat")); - assert!(msg.contains("Permission denied")); - - let err = ValidationError::InvalidHeaderChain( - "Block 12345: timestamp is before previous block".to_string(), - ); - let msg = err.to_string(); - assert!(msg.contains("Block 12345")); - assert!(msg.contains("timestamp")); -} - -// ===== Recovery Mechanism Tests ===== - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_exponential_backoff() { - let mut recovery_manager = RecoveryManager::new(); - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100, - target_height: Some(1000), - headers_downloaded: 100, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let error = SyncError::Timeout("Test timeout".to_string()); - - // Test that retry delays increase exponentially - let mut delays = vec![]; - for _ in 0..3 { - let strategy = recovery_manager.determine_strategy(&phase, &error); - if let RecoveryStrategy::Retry { - delay, - } = strategy - { - delays.push(delay); - } - } - - assert_eq!(delays.len(), 3); - assert!(delays[1] > delays[0]); - assert!(delays[2] > delays[1]); -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_max_retry_limit() { - let mut recovery_manager = RecoveryManager::new(); - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100, - target_height: Some(1000), - headers_downloaded: 100, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let error = SyncError::Timeout("Test timeout".to_string()); - - // Exhaust retries - let mut abort_occurred = false; - for i in 0..10 { - let strategy = recovery_manager.determine_strategy(&phase, &error); - if let RecoveryStrategy::Abort { - .. - } = strategy - { - abort_occurred = true; - assert!(i > 3); // Should abort after some retries - break; - } - } - - assert!(abort_occurred, "Expected abort after max retries"); -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_recovery_statistics() { - let mut recovery_manager = RecoveryManager::new(); - let mut phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100, - target_height: Some(1000), - headers_downloaded: 100, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let mut network = MockNetworkManager::new(); - let mut storage = MockStorageManager::new(); - - // Execute some recoveries - let error = SyncError::Timeout("Test".to_string()); - let strategy = recovery_manager.determine_strategy(&phase, &error); - let _ = recovery_manager - .execute_recovery(&mut phase, strategy, &error, &mut network, &mut storage) - .await; - - let stats = recovery_manager.get_stats(); - assert_eq!(stats.total_recoveries, 1); - assert!(stats.recoveries_by_phase.contains_key("DownloadingHeaders")); -} - -// ===== Error Propagation Tests ===== - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_error_propagation_through_layers() { - // Create a storage error - let storage_err = StorageError::Corruption("Database corrupted".to_string()); - - // Convert to validation error (storage errors can occur during validation) - let val_err: ValidationError = storage_err.clone().into(); - match &val_err { - ValidationError::StorageError(StorageError::Corruption(msg)) => { - assert_eq!(msg, "Database corrupted"); - } - _ => panic!("Incorrect error propagation"), - } - - // Convert to SPV error - let spv_err: SpvError = val_err.into(); - match spv_err { - SpvError::Validation(ValidationError::StorageError(StorageError::Corruption(msg))) => { - assert_eq!(msg, "Database corrupted"); - } - _ => panic!("Incorrect error propagation"), - } -} - -// ===== Wallet Error Tests ===== - -#[test] -fn test_wallet_error_scenarios() { - // Test balance overflow - let err = WalletError::BalanceOverflow; - assert_eq!(err.to_string(), "Balance calculation overflow"); - - // Test UTXO not found - let outpoint = OutPoint { - txid: Txid::from_byte_array([0; 32]), - vout: 0, - }; - let err = WalletError::UtxoNotFound(outpoint); - assert!(err.to_string().contains("UTXO not found")); - - // Test unsupported address type - let err = WalletError::UnsupportedAddressType("P2WSH".to_string()); - assert!(err.to_string().contains("P2WSH")); -} - -// ===== SyncError Category Tests ===== - -#[test] -fn test_sync_error_categories() { - assert_eq!(SyncError::SyncInProgress.category(), "state"); - assert_eq!(SyncError::Timeout("test".to_string()).category(), "timeout"); - assert_eq!(SyncError::Network("test".to_string()).category(), "network"); - assert_eq!(SyncError::Validation("test".to_string()).category(), "validation"); - assert_eq!(SyncError::Storage("test".to_string()).category(), "storage"); - assert_eq!(SyncError::MissingDependency("test".to_string()).category(), "dependency"); - assert_eq!(SyncError::Headers2DecompressionFailed("test".to_string()).category(), "headers2"); -} - -// ===== Helper Functions ===== - -fn create_test_header(height: u32) -> BlockHeader { - BlockHeader { - version: Version::from_consensus(1), - prev_blockhash: if height == 0 { - BlockHash::from_byte_array([0; 32]) - } else { - BlockHash::from_byte_array([(height - 1) as u8; 32]) - }, - merkle_root: dashcore::hashes::sha256d::Hash::from_byte_array([height as u8; 32]).into(), - time: 1234567890 + height, - bits: CompactTarget::from_consensus(0x1d00ffff), - nonce: height, - } -} - -fn validate_header_pow(header: &BlockHeader) -> ValidationResult<()> { - if header.bits.to_consensus() == 0x00000000 { - return Err(ValidationError::InvalidProofOfWork); - } - Ok(()) -} - -fn validate_header_chain(prev: &BlockHeader, current: &BlockHeader) -> ValidationResult<()> { - if current.prev_blockhash != prev.block_hash() { - return Err(ValidationError::InvalidHeaderChain( - "previous block hash mismatch".to_string(), - )); - } - Ok(()) -} - -// ===== Parse Error Tests ===== - -#[test] -fn test_parse_errors() { - let err = ParseError::InvalidAddress("not_a_valid_address".to_string()); - assert!(err.to_string().contains("not_a_valid_address")); - - let err = ParseError::InvalidNetwork("testnet3".to_string()); - assert!(err.to_string().contains("testnet3")); - - let err = ParseError::MissingArgument("--peer".to_string()); - assert!(err.to_string().contains("--peer")); - - let err = ParseError::InvalidArgument("port".to_string(), "abc".to_string()); - assert!(err.to_string().contains("port")); - assert!(err.to_string().contains("abc")); -} - -// ===== Real-world Scenario Tests ===== - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_cascading_network_failures() { - let mut network = MockNetworkManager::new(); - let mut recovery_manager = RecoveryManager::new(); - - // Simulate a series of network failures - network.set_timeout_on_message(); - - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100, - target_height: Some(1000), - headers_downloaded: 100, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - // First few failures should trigger retries - for i in 0..3 { - let error = SyncError::Network(format!("Connection timeout #{}", i)); - let strategy = recovery_manager.determine_strategy(&phase, &error); - match strategy { - RecoveryStrategy::Retry { - .. - } => { - // Expected - } - _ => panic!("Expected retry strategy for failure #{}", i), - } - } - - // After multiple failures, should switch peer - let error = SyncError::Network("Connection timeout #3".to_string()); - let strategy = recovery_manager.determine_strategy(&phase, &error); - match strategy { - RecoveryStrategy::SwitchPeer => { - // Expected - } - _ => panic!("Expected peer switch after multiple failures"), - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_storage_corruption_recovery() { - let temp_dir = tempfile::tempdir().unwrap(); - let storage_path = temp_dir.path().to_path_buf(); - - // Create real storage manager - let mut storage = DiskStorageManager::new(storage_path.clone()).await.unwrap(); - - // Store some headers - for i in 0..10 { - let header = create_test_header(i); - storage.store_headers(&[header]).await.unwrap(); - } - - // Simulate corruption by modifying files directly - let headers_dir = storage_path.join("headers"); - if let Ok(entries) = std::fs::read_dir(&headers_dir) { - for entry in entries.flatten() { - if entry.path().extension().map(|e| e == "dat").unwrap_or(false) { - // Truncate file to simulate corruption - let _ = std::fs::OpenOptions::new().write(true).truncate(true).open(entry.path()); - break; - } - } - } - - // Try to read headers - should fail with corruption error - let result = storage.load_headers(0..10).await; - assert!(result.is_err()); -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_concurrent_error_handling() { - let storage = Arc::new(RwLock::new(MockStorageManager::new())); - let mut handles = vec![]; - - // Spawn multiple tasks that will encounter errors - for i in 0..5 { - let storage_clone = Arc::clone(&storage); - let handle = tokio::spawn(async move { - let mut storage = storage_clone.write().await; - if i % 2 == 0 { - storage.set_fail_on_write(); - } else { - storage.set_fail_on_read(); - } - drop(storage); - - // Try operations - let storage = storage_clone.read().await; - let result = if i % 2 == 0 { - let header = create_test_header(i); - drop(storage); - let mut storage = storage_clone.write().await; - storage.store_headers(&[header]).await - } else { - storage.get_header(i).await.map(|_| ()) - }; - - result - }); - handles.push(handle); - } - - // All tasks should complete with errors - for handle in handles { - let result = handle.await.unwrap(); - assert!(result.is_err()); - } -} - -// ===== Headers2 Specific Error Tests ===== - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_headers2_decompression_failure() { - let error = SyncError::Headers2DecompressionFailed("Invalid compressed data".to_string()); - assert_eq!(error.category(), "headers2"); - - let mut recovery_manager = RecoveryManager::new(); - let phase = SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100, - target_height: Some(1000), - headers_downloaded: 100, - headers_per_second: 10.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - // Headers2 decompression failures should trigger appropriate recovery - let strategy = recovery_manager.determine_strategy(&phase, &error); - // The specific strategy would depend on implementation details - assert!(matches!(strategy, RecoveryStrategy::Retry { .. } | RecoveryStrategy::SwitchPeer)); -} diff --git a/dash-spv/tests/error_recovery_integration_test.rs b/dash-spv/tests/error_recovery_integration_test.rs deleted file mode 100644 index a8ffaeb71..000000000 --- a/dash-spv/tests/error_recovery_integration_test.rs +++ /dev/null @@ -1,776 +0,0 @@ -//! Integration tests for error recovery mechanisms -//! -//! NOTE: This test file is currently disabled due to incomplete mock trait implementations. -//! TODO: Re-enable once StorageManager and NetworkManager trait methods are fully implemented. - -#![cfg(feature = "skip_mock_implementation_incomplete")] - -//! Integration tests for error recovery mechanisms -//! -//! These tests validate error recovery in more realistic scenarios, -//! including network interruptions, storage failures during sync, -//! and validation errors with real data. - -use std::sync::Arc; -use std::time::Duration; - -use dashcore::{block::Header as BlockHeader, hash_types::FilterHeader, BlockHash, Txid}; -use tokio::sync::{Mutex, RwLock}; - -use dash_spv::error::{StorageError, SyncError, ValidationError}; -use dash_spv::storage::{sync_state::SyncCheckpoint, DiskStorageManager, StorageManager}; -use dash_spv::sync::sequential::recovery::RecoveryManager; - -/// Test helper to simulate network interruptions -struct NetworkInterruptor { - should_interrupt: Arc>, - interrupt_after_messages: Arc>>, - messages_count: Arc>, -} - -impl NetworkInterruptor { - fn new() -> Self { - Self { - should_interrupt: Arc::new(Mutex::new(false)), - interrupt_after_messages: Arc::new(Mutex::new(None)), - messages_count: Arc::new(Mutex::new(0)), - } - } - - async fn set_interrupt_after(&self, count: usize) { - *self.interrupt_after_messages.lock().await = Some(count); - } - - async fn should_interrupt(&self) -> bool { - let mut count = self.messages_count.lock().await; - *count += 1; - - if let Some(limit) = *self.interrupt_after_messages.lock().await { - if *count >= limit { - *self.should_interrupt.lock().await = true; - } - } - - *self.should_interrupt.lock().await - } - - async fn reset(&self) { - *self.should_interrupt.lock().await = false; - *self.messages_count.lock().await = 0; - } -} - -/// Test helper to simulate storage failures -struct StorageFailureSimulator { - fail_at_height: Arc>>, - failure_type: Arc>, -} - -#[derive(Clone)] -enum FailureType { - None, - DiskFull, -} - -impl StorageFailureSimulator { - fn new() -> Self { - Self { - fail_at_height: Arc::new(RwLock::new(None)), - failure_type: Arc::new(RwLock::new(FailureType::None)), - } - } - - async fn set_fail_at_height(&self, height: u32, failure_type: FailureType) { - *self.fail_at_height.write().await = Some(height); - *self.failure_type.write().await = failure_type; - } - - async fn should_fail(&self, height: u32) -> Option { - if let Some(fail_height) = *self.fail_at_height.read().await { - if height >= fail_height { - return match &*self.failure_type.read().await { - FailureType::DiskFull => { - Some(StorageError::WriteFailed("No space left on device".to_string())) - } - FailureType::None => None, - }; - } - } - None - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_recovery_from_network_interruption_during_header_sync() { - // This test simulates a network interruption during header synchronization - // and verifies that the client can recover and continue from where it left off - - // Create storage manager - let storage = Arc::new(RwLock::new( - DiskStorageManager::new(tempfile::tempdir().unwrap().path().to_path_buf()).await.unwrap(), - )); - - // Create network interruptor - let interruptor = Arc::new(NetworkInterruptor::new()); - - // Set up to interrupt after 100 headers - interruptor.set_interrupt_after(100).await; - - // Create recovery manager - let mut recovery_manager = RecoveryManager::new(); - - // Track recovery attempts - let mut recovery_count = 0; - let max_recoveries = 3; - - // Simulate header sync with interruptions - let mut current_height = 0u32; - let target_height = 500u32; - - while current_height < target_height && recovery_count < max_recoveries { - // Simulate downloading headers - let mut headers_in_batch = 0; - - loop { - if interruptor.should_interrupt().await { - // Simulate network error - let error = SyncError::Network("Connection lost".to_string()); - - // Determine recovery strategy - let phase = dash_spv::sync::sequential::phases::SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height, - target_height: Some(target_height), - headers_downloaded: current_height, - headers_per_second: 50.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let strategy = recovery_manager.determine_strategy(&phase, &error); - - // Log recovery attempt - recovery_count += 1; - eprintln!("Recovery attempt {} at height {}", recovery_count, current_height); - - // Reset interruptor for next attempt - interruptor.reset().await; - interruptor.set_interrupt_after(100).await; - - // Apply recovery delay - if let dash_spv::sync::sequential::recovery::RecoveryStrategy::Retry { - delay, - } = strategy - { - tokio::time::sleep(delay).await; - } - - break; - } - - // Simulate storing a header - let header = create_test_header(current_height); - storage.write().await.store_headers(&[header]).await.unwrap(); - - current_height += 1; - headers_in_batch += 1; - - if current_height >= target_height { - break; - } - - // Simulate network delay - if headers_in_batch % 10 == 0 { - tokio::time::sleep(Duration::from_millis(1)).await; - } - } - - if current_height >= target_height { - break; - } - } - - // Verify we reached the target despite interruptions - assert_eq!(current_height, target_height); - assert!(recovery_count > 0, "Should have had at least one recovery"); - - // Verify all headers were stored correctly - let stored_headers = storage.read().await.load_headers(0..target_height).await.unwrap(); - assert_eq!(stored_headers.len(), target_height as usize); -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_recovery_from_storage_failure_during_sync() { - // This test simulates storage failures during synchronization - // and verifies appropriate error handling and recovery - - // No temp directory needed in this simulated test - - // Create storage with failure simulator - let failure_sim = Arc::new(StorageFailureSimulator::new()); - - // Set up to fail at height 250 with disk full - failure_sim.set_fail_at_height(250, FailureType::DiskFull).await; - - // Track storage operations - let mut last_successful_height = 0u32; - let target_height = 500u32; - - // Simulate sync with storage failures - for height in 0..target_height { - // Check if we should simulate a failure - if let Some(error) = failure_sim.should_fail(height).await { - eprintln!("Storage failure at height {}: {:?}", height, error); - - // In a real scenario, this would trigger recovery - // For this test, we'll simulate clearing some space and retrying - if matches!(error, StorageError::WriteFailed(ref msg) if msg.contains("No space left")) - { - // Simulate clearing space by resetting failure simulator - failure_sim.set_fail_at_height(350, FailureType::None).await; - - // Retry the operation - // In real implementation, this would be handled by recovery manager - continue; - } - - break; - } - - last_successful_height = height; - } - - // Verify we handled the disk full error appropriately - assert!(last_successful_height >= 250, "Should have processed headers up to failure point"); -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_recovery_from_validation_errors() { - // This test simulates validation errors and verifies recovery behavior - - let mut recovery_manager = RecoveryManager::new(); - - // Test various validation error scenarios - let validation_errors = [ - ValidationError::InvalidProofOfWork, - ValidationError::InvalidHeaderChain("Timestamp before previous block".to_string()), - ValidationError::InvalidFilterHeaderChain("Filter header mismatch".to_string()), - ValidationError::Consensus("Block too large".to_string()), - ]; - - for (i, val_error) in validation_errors.iter().enumerate() { - let sync_error = SyncError::Validation(val_error.to_string()); - - let phase = dash_spv::sync::sequential::phases::SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 1000 + (i as u32 * 100), - target_height: Some(2000), - headers_downloaded: 1000, - headers_per_second: 100.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let strategy = recovery_manager.determine_strategy(&phase, &sync_error); - - // Validation errors should typically trigger phase restart from checkpoint - match strategy { - dash_spv::sync::sequential::recovery::RecoveryStrategy::RestartPhase { - checkpoint, - } => { - assert!(checkpoint.restart_height.is_some()); - let restart_height = checkpoint.restart_height.unwrap(); - // Note: current_height method doesn't exist on SyncPhase - // assert!(restart_height < phase.current_height()); - eprintln!( - "Validation error '{}' triggers restart from height {}", - val_error, restart_height - ); - } - dash_spv::sync::sequential::recovery::RecoveryStrategy::Retry { - .. - } => { - // Some validation errors might trigger retry first - eprintln!("Validation error '{}' triggers retry", val_error); - } - _ => panic!("Unexpected recovery strategy for validation error"), - } - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_concurrent_error_recovery() { - // This test simulates multiple concurrent errors and verifies - // that the recovery mechanisms handle them correctly - - let recovery_manager = Arc::new(Mutex::new(RecoveryManager::new())); - - // Spawn multiple tasks that encounter different errors - let mut handles = vec![]; - - for i in 0..5 { - let recovery_clone = Arc::clone(&recovery_manager); - - let handle = tokio::spawn(async move { - let error = match i % 3 { - 0 => SyncError::Timeout(format!("Task {} timeout", i)), - 1 => SyncError::Network(format!("Task {} network error", i)), - _ => SyncError::Validation(format!("Task {} validation error", i)), - }; - - let phase = dash_spv::sync::sequential::phases::SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100 * i, - target_height: Some(1000), - headers_downloaded: 100 * i, - headers_per_second: 50.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let mut recovery = recovery_clone.lock().await; - let strategy = recovery.determine_strategy(&phase, &error); - - (i, error.category().to_string(), strategy) - }); - - handles.push(handle); - } - - // Collect results - let mut results = vec![]; - for handle in handles { - results.push(handle.await.unwrap()); - } - - // Verify each task got appropriate recovery strategy - for (task_id, error_category, strategy) in results { - eprintln!("Task {} with {} error got strategy: {:?}", task_id, error_category, strategy); - - match error_category.as_str() { - "timeout" => { - assert!(matches!( - strategy, - dash_spv::sync::sequential::recovery::RecoveryStrategy::Retry { .. } - )); - } - "network" => { - assert!(matches!( - strategy, - dash_spv::sync::sequential::recovery::RecoveryStrategy::Retry { .. } - | dash_spv::sync::sequential::recovery::RecoveryStrategy::SwitchPeer - )); - } - "validation" => { - assert!(matches!( - strategy, - dash_spv::sync::sequential::recovery::RecoveryStrategy::RestartPhase { .. } - | dash_spv::sync::sequential::recovery::RecoveryStrategy::Retry { .. } - )); - } - _ => {} - } - } -} - -#[ignore = "mock implementation incomplete"] -#[tokio::test] -async fn test_recovery_statistics_tracking() { - // This test verifies that recovery statistics are properly tracked - - let mut recovery_manager = RecoveryManager::new(); - let mut network = MockNetworkManager::new(); - let mut storage = MockStorageManager::new(); - - // Simulate various recovery scenarios - let scenarios = [ - (SyncError::Timeout("Test timeout".to_string()), true), - (SyncError::Network("Connection failed".to_string()), true), - (SyncError::Validation("Invalid header".to_string()), false), - (SyncError::Storage("Write failed".to_string()), false), - ]; - - for (i, (error, _expected_success)) in scenarios.iter().enumerate() { - let mut phase = dash_spv::sync::sequential::phases::SyncPhase::DownloadingHeaders { - start_time: std::time::Instant::now(), - start_height: 0, - current_height: 100 * i as u32, - target_height: Some(1000), - headers_downloaded: 100 * i as u32, - headers_per_second: 50.0, - received_empty_response: false, - last_progress: std::time::Instant::now(), - }; - - let strategy = recovery_manager.determine_strategy(&phase, error); - let _ = recovery_manager - .execute_recovery(&mut phase, strategy, error, &mut network, &mut storage) - .await; - } - - // Get and verify statistics - let stats = recovery_manager.get_stats(); - assert_eq!(stats.total_recoveries, scenarios.len()); - assert!(stats.recoveries_by_phase.contains_key("DownloadingHeaders")); - assert_eq!(stats.recoveries_by_phase["DownloadingHeaders"], scenarios.len()); - - // Verify retry counts are tracked - assert!(!stats.current_retry_counts.is_empty()); -} - -// Helper functions - -fn create_test_header(height: u32) -> BlockHeader { - use dashcore::block::Version; - use dashcore::pow::CompactTarget; - use dashcore_hashes::Hash; - - BlockHeader { - version: Version::from_consensus(1), - prev_blockhash: if height == 0 { - BlockHash::from_byte_array([0; 32]) - } else { - BlockHash::from_byte_array([(height - 1) as u8; 32]) - }, - merkle_root: dashcore::hashes::sha256d::Hash::from_byte_array([height as u8; 32]).into(), - time: 1234567890 + height, - bits: CompactTarget::from_consensus(0x1d00ffff), - nonce: height, - } -} - -// Mock implementations for testing - -struct MockNetworkManager { - messages_sent: usize, -} - -impl MockNetworkManager { - fn new() -> Self { - Self { - messages_sent: 0, - } - } -} - -#[async_trait::async_trait] -impl dash_spv::network::NetworkManager for MockNetworkManager { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - async fn disconnect(&mut self) -> dash_spv::error::NetworkResult<()> { - Ok(()) - } - - fn is_connected(&self) -> bool { - true - } - - fn peer_info(&self) -> Vec { - vec![] - } - - async fn get_peer_best_height(&self) -> dash_spv::error::NetworkResult> { - Ok(Some(1000000)) - } - - async fn has_peer_with_service( - &self, - _service_flags: dashcore::network::constants::ServiceFlags, - ) -> bool { - true - } - - async fn update_peer_dsq_preference( - &mut self, - _wants_dsq: bool, - ) -> dash_spv::error::NetworkResult<()> { - Ok(()) - } - fn peer_count(&self) -> usize { - 1 - } - - async fn connect(&mut self) -> dash_spv::error::NetworkResult<()> { - Ok(()) - } - - async fn send_message( - &mut self, - _msg: dashcore::network::message::NetworkMessage, - ) -> dash_spv::error::NetworkResult<()> { - self.messages_sent += 1; - Ok(()) - } - - async fn receive_message( - &mut self, - ) -> dash_spv::error::NetworkResult> { - Ok(None) - } -} - -struct MockStorageManager; - -impl MockStorageManager { - fn new() -> Self { - Self - } -} - -#[async_trait::async_trait] -impl StorageManager for MockStorageManager { - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } - - async fn store_headers( - &mut self, - _headers: &[BlockHeader], - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_headers( - &self, - _range: std::ops::Range, - ) -> dash_spv::error::StorageResult> { - Ok(vec![]) - } - - async fn get_header( - &self, - _height: u32, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn get_tip_height(&self) -> dash_spv::error::StorageResult> { - Ok(Some(0)) - } - - async fn store_filter_headers( - &mut self, - _headers: &[FilterHeader], - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_filter_headers( - &self, - _range: std::ops::Range, - ) -> dash_spv::error::StorageResult> { - Ok(vec![]) - } - - async fn get_filter_header( - &self, - _height: u32, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn get_filter_tip_height(&self) -> dash_spv::error::StorageResult> { - Ok(Some(0)) - } - - async fn store_chain_state( - &mut self, - _state: &dash_spv::types::ChainState, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_chain_state( - &self, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn store_filter( - &mut self, - _height: u32, - _filter: &[u8], - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_filter(&self, _height: u32) -> dash_spv::error::StorageResult>> { - Ok(None) - } - - async fn store_metadata( - &mut self, - _key: &str, - _value: &[u8], - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_metadata(&self, _key: &str) -> dash_spv::error::StorageResult>> { - Ok(None) - } - - async fn clear(&mut self) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn clear_filters(&mut self) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn stats(&self) -> dash_spv::error::StorageResult { - Ok(dash_spv::storage::StorageStats { - header_count: 0, - filter_header_count: 0, - filter_count: 0, - total_size: 0, - component_sizes: std::collections::HashMap::new(), - }) - } - - async fn get_header_height_by_hash( - &self, - _hash: &BlockHash, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn get_headers_batch( - &self, - _start_height: u32, - _end_height: u32, - ) -> dash_spv::error::StorageResult> { - Ok(vec![]) - } - - async fn store_masternode_state( - &mut self, - _state: &dash_spv::storage::MasternodeState, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_masternode_state( - &self, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn store_sync_state( - &mut self, - _state: &dash_spv::storage::PersistentSyncState, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_sync_state( - &self, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn clear_sync_state(&mut self) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn store_sync_checkpoint( - &mut self, - _height: u32, - _checkpoint: &SyncCheckpoint, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn get_sync_checkpoints( - &self, - _start_height: u32, - _end_height: u32, - ) -> dash_spv::error::StorageResult> { - Ok(vec![]) - } - - async fn store_chain_lock( - &mut self, - _height: u32, - _chain_lock: &dashcore::ChainLock, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_chain_lock( - &self, - _height: u32, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn get_chain_locks( - &self, - _start_height: u32, - _end_height: u32, - ) -> dash_spv::error::StorageResult> { - Ok(vec![]) - } - - // InstantLock storage methods removed from trait; no-op here - - async fn store_mempool_transaction( - &mut self, - _txid: &Txid, - _tx: &dash_spv::types::UnconfirmedTransaction, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn remove_mempool_transaction( - &mut self, - _txid: &Txid, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn get_mempool_transaction( - &self, - _txid: &Txid, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn get_all_mempool_transactions( - &self, - ) -> dash_spv::error::StorageResult< - std::collections::HashMap, - > { - Ok(std::collections::HashMap::new()) - } - - async fn store_mempool_state( - &mut self, - _state: &dash_spv::types::MempoolState, - ) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn load_mempool_state( - &self, - ) -> dash_spv::error::StorageResult> { - Ok(None) - } - - async fn clear_mempool(&mut self) -> dash_spv::error::StorageResult<()> { - Ok(()) - } - - async fn shutdown(&mut self) -> dash_spv::error::StorageResult<()> { - Ok(()) - } -} diff --git a/dash-spv/tests/header_sync_test.rs b/dash-spv/tests/header_sync_test.rs index eff06ead1..1941e9004 100644 --- a/dash-spv/tests/header_sync_test.rs +++ b/dash-spv/tests/header_sync_test.rs @@ -6,7 +6,6 @@ use dash_spv::{ client::{ClientConfig, DashSpvClient}, network::PeerNetworkManager, storage::{MemoryStorageManager, StorageManager}, - sync::headers::HeaderSyncManager, types::{ChainState, ValidationMode}, }; use dashcore::{block::Header as BlockHeader, block::Version, Network}; @@ -17,21 +16,6 @@ use log::{debug, info}; use std::sync::Arc; use tokio::sync::RwLock; -#[tokio::test] -async fn test_header_sync_manager_creation() { - let _ = env_logger::try_init(); - - let _storage = MemoryStorageManager::new().await.expect("Failed to create storage"); - - let config = ClientConfig::new(Network::Dash).with_validation_mode(ValidationMode::Basic); - - let _sync_manager = HeaderSyncManager::new(&config); - // HeaderSyncManager::new returns a HeaderSyncManager directly, not a Result - // So we just verify it was created successfully by not panicking - - info!("Header sync manager created successfully"); -} - #[tokio::test] async fn test_basic_header_sync_from_genesis() { let _ = env_logger::try_init(); @@ -86,40 +70,6 @@ async fn test_header_sync_continuation() { info!("Header sync continuation test completed"); } -#[tokio::test] -async fn test_header_validation_modes() { - let _ = env_logger::try_init(); - - // Test ValidationMode::None - should accept any headers - { - let config = ClientConfig::new(Network::Dash).with_validation_mode(ValidationMode::None); - - let _storage = MemoryStorageManager::new().await.unwrap(); - let _sync_manager = HeaderSyncManager::new(&config); - debug!("ValidationMode::None test passed"); - } - - // Test ValidationMode::Basic - should do basic validation - { - let config = ClientConfig::new(Network::Dash).with_validation_mode(ValidationMode::Basic); - - let _storage = MemoryStorageManager::new().await.unwrap(); - let _sync_manager = HeaderSyncManager::new(&config); - debug!("ValidationMode::Basic test passed"); - } - - // Test ValidationMode::Full - should do full validation - { - let config = ClientConfig::new(Network::Dash).with_validation_mode(ValidationMode::Full); - - let _storage = MemoryStorageManager::new().await.unwrap(); - let _sync_manager = HeaderSyncManager::new(&config); - debug!("ValidationMode::Full test passed"); - } - - info!("All validation mode tests completed"); -} - #[tokio::test] async fn test_header_batch_processing() { let _ = env_logger::try_init(); @@ -352,24 +302,6 @@ fn create_test_header_chain_from(start: usize, count: usize) -> Vec headers } -#[tokio::test] -async fn test_header_sync_error_handling() { - let _ = env_logger::try_init(); - - // Test various error conditions in header sync - let _storage = MemoryStorageManager::new().await.expect("Failed to create storage"); - - // Test with invalid configuration - let invalid_config = - ClientConfig::new(Network::Dash).with_validation_mode(ValidationMode::None); // Valid config for this test - - let _sync_manager = HeaderSyncManager::new(&invalid_config); - // Note: HeaderSyncManager creation is straightforward and doesn't validate config - // The actual error handling happens during sync operations - - info!("Header sync error handling test completed"); -} - #[tokio::test] async fn test_header_storage_consistency() { let _ = env_logger::try_init();