From 2e85243b2c179feb61ee83d506dcd4581888c229 Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Thu, 18 Dec 2025 17:58:37 +0000 Subject: [PATCH 1/3] removed chain storage an related code --- dash-spv/src/chain/fork_detector.rs | 249 +--------- dash-spv/src/chain/fork_detector_test.rs | 397 ---------------- dash-spv/src/chain/mod.rs | 6 +- dash-spv/src/chain/reorg.rs | 552 ----------------------- dash-spv/src/chain/reorg_test.rs | 129 ------ dash-spv/src/storage/mod.rs | 32 -- dash-spv/src/storage/sync_storage.rs | 92 ---- 7 files changed, 3 insertions(+), 1454 deletions(-) delete mode 100644 dash-spv/src/chain/fork_detector_test.rs delete mode 100644 dash-spv/src/chain/reorg_test.rs delete mode 100644 dash-spv/src/storage/sync_storage.rs diff --git a/dash-spv/src/chain/fork_detector.rs b/dash-spv/src/chain/fork_detector.rs index 7d02633cf..6dfe92822 100644 --- a/dash-spv/src/chain/fork_detector.rs +++ b/dash-spv/src/chain/fork_detector.rs @@ -3,18 +3,14 @@ //! This module detects when incoming headers create a fork in the blockchain //! rather than extending the current chain tip. -use super::{ChainWork, Fork}; -use crate::storage::ChainStorage; -use crate::types::ChainState; -use dashcore::{BlockHash, Header as BlockHeader}; +use super::Fork; +use dashcore::BlockHash; use std::collections::HashMap; /// Detects and manages blockchain forks pub struct ForkDetector { /// Currently known forks indexed by their tip hash forks: HashMap, - /// Maximum number of forks to track - max_forks: usize, } impl ForkDetector { @@ -24,164 +20,9 @@ impl ForkDetector { } Ok(Self { forks: HashMap::new(), - max_forks, }) } - /// Check if a header creates or extends a fork - pub fn check_header( - &mut self, - header: &BlockHeader, - chain_state: &ChainState, - storage: &CS, - ) -> ForkDetectionResult { - let header_hash = header.block_hash(); - let prev_hash = header.prev_blockhash; - - // Check if this extends the main chain - if let Some(tip_header) = chain_state.get_tip_header() { - tracing::trace!( - "Checking main chain extension - prev_hash: {}, tip_hash: {}", - prev_hash, - tip_header.block_hash() - ); - if prev_hash == tip_header.block_hash() { - return ForkDetectionResult::ExtendsMainChain; - } - } else { - // Special case: chain state is empty (shouldn't happen with genesis initialized) - // But handle it just in case - if chain_state.headers.is_empty() { - // Check if this is connecting to genesis in storage - if let Ok(Some(height)) = storage.get_header_height(&prev_hash) { - if height == 0 { - // This is the first header after genesis - return ForkDetectionResult::ExtendsMainChain; - } - } - } - } - - // Special case: Check if header connects to genesis which might be at height 0 - // This handles the case where chain_state has genesis but we're syncing the first real block - if chain_state.tip_height() == 0 { - if let Some(genesis_header) = chain_state.header_at_height(0) { - tracing::debug!( - "Checking if header connects to genesis - prev_hash: {}, genesis_hash: {}", - prev_hash, - genesis_header.block_hash() - ); - if prev_hash == genesis_header.block_hash() { - tracing::info!( - "Header extends genesis block - treating as main chain extension" - ); - return ForkDetectionResult::ExtendsMainChain; - } - } - } - - // Check if this extends a known fork - // Need to find a fork whose tip matches our prev_hash - let matching_fork = self - .forks - .iter() - .find(|(_, fork)| fork.tip_hash == prev_hash) - .map(|(_, fork)| fork.clone()); - - if let Some(mut fork) = matching_fork { - // Remove the old entry (indexed by old tip) - self.forks.remove(&fork.tip_hash); - - // Update the fork - fork.headers.push(*header); - fork.tip_hash = header_hash; - fork.tip_height += 1; - fork.chain_work = fork.chain_work.add_header(header); - - // Re-insert with new tip hash - let result_fork = fork.clone(); - self.forks.insert(header_hash, fork); - - return ForkDetectionResult::ExtendsFork(result_fork); - } - - // Check if this connects to the main chain (creates new fork) - if let Ok(Some(height)) = storage.get_header_height(&prev_hash) { - // Check if this would create a fork from before our checkpoint - if chain_state.synced_from_checkpoint() && height < chain_state.sync_base_height { - tracing::warn!( - "Rejecting header that would create fork from height {} (before checkpoint base {}). \ - This likely indicates headers from genesis were received during checkpoint sync.", - height, chain_state.sync_base_height - ); - return ForkDetectionResult::Orphan; - } - - // Found connection point - this creates a new fork - let fork_height = height; - let fork = Fork { - fork_point: prev_hash, - fork_height, - tip_hash: header_hash, - tip_height: fork_height + 1, - headers: vec![*header], - chain_work: ChainWork::from_height_and_header(fork_height, header), - }; - - self.add_fork(fork.clone()); - return ForkDetectionResult::CreatesNewFork(fork); - } - - // Additional check: see if header connects to any header in chain_state - // This helps when storage might be out of sync with chain_state - for (height, state_header) in chain_state.headers.iter().enumerate() { - if prev_hash == state_header.block_hash() { - // Calculate the actual blockchain height for this index - let actual_height = chain_state.sync_base_height + (height as u32); - - // This connects to a header in chain state but not in storage - // Treat it as extending main chain if it's the tip - if height == chain_state.headers.len() - 1 { - return ForkDetectionResult::ExtendsMainChain; - } else { - // Creates a fork from an earlier point - let fork = Fork { - fork_point: prev_hash, - fork_height: actual_height, - tip_hash: header_hash, - tip_height: actual_height + 1, - headers: vec![*header], - chain_work: ChainWork::from_height_and_header(actual_height, header), - }; - - self.add_fork(fork.clone()); - return ForkDetectionResult::CreatesNewFork(fork); - } - } - } - - // This header doesn't connect to anything we know - ForkDetectionResult::Orphan - } - - /// Add a new fork to track - fn add_fork(&mut self, fork: Fork) { - self.forks.insert(fork.tip_hash, fork); - - // Limit the number of forks we track - if self.forks.len() > self.max_forks { - // Remove the fork with least work - if let Some(weakest) = self.find_weakest_fork() { - self.forks.remove(&weakest); - } - } - } - - /// Find the fork with the least cumulative work - fn find_weakest_fork(&self) -> Option { - self.forks.iter().min_by_key(|(_, fork)| &fork.chain_work).map(|(hash, _)| *hash) - } - /// Get all known forks pub fn get_forks(&self) -> Vec<&Fork> { self.forks.values().collect() @@ -229,92 +70,6 @@ pub enum ForkDetectionResult { #[cfg(test)] mod tests { use super::*; - use crate::storage::MemoryStorage; - use dashcore::blockdata::constants::genesis_block; - use dashcore::Network; - use dashcore_hashes::Hash; - - fn create_test_header(prev_hash: BlockHash, nonce: u32) -> BlockHeader { - let mut header = genesis_block(Network::Dash).header; - header.prev_blockhash = prev_hash; - header.nonce = nonce; - header - } - - #[test] - fn test_fork_detection() { - let mut detector = ForkDetector::new(10).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Add genesis - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis header"); - chain_state.add_header(genesis); - - // Header that extends main chain - let header1 = create_test_header(genesis.block_hash(), 1); - let result = detector.check_header(&header1, &chain_state, &storage); - assert!(matches!(result, ForkDetectionResult::ExtendsMainChain)); - - // Add header1 to chain - storage.store_header(&header1, 1).expect("Failed to store header1"); - chain_state.add_header(header1); - - // Header that creates a fork from genesis - let fork_header = create_test_header(genesis.block_hash(), 2); - let result = detector.check_header(&fork_header, &chain_state, &storage); - - match result { - ForkDetectionResult::CreatesNewFork(fork) => { - assert_eq!(fork.fork_point, genesis.block_hash()); - assert_eq!(fork.fork_height, 0); - assert_eq!(fork.tip_height, 1); - assert_eq!(fork.headers.len(), 1); - } - result => panic!("Expected CreatesNewFork, got {:?}", result), - } - - // Header that extends the fork - let fork_header2 = create_test_header(fork_header.block_hash(), 3); - let result = detector.check_header(&fork_header2, &chain_state, &storage); - - assert!(matches!(result, ForkDetectionResult::ExtendsFork(_))); - assert_eq!(detector.get_forks().len(), 1); - - // Orphan header - let orphan = create_test_header( - BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::all_zeros()), - 4, - ); - let result = detector.check_header(&orphan, &chain_state, &storage); - assert!(matches!(result, ForkDetectionResult::Orphan)); - } - - #[test] - fn test_fork_limits() { - let mut detector = ForkDetector::new(2).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Add genesis - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis header"); - chain_state.add_header(genesis); - - // Add a header to extend the main chain past genesis - let header1 = create_test_header(genesis.block_hash(), 1); - storage.store_header(&header1, 1).expect("Failed to store header1"); - chain_state.add_header(header1); - - // Create 3 forks from genesis, should only keep 2 - for i in 0..3 { - let fork_header = create_test_header(genesis.block_hash(), i + 100); - detector.check_header(&fork_header, &chain_state, &storage); - } - - assert_eq!(detector.get_forks().len(), 2); - } #[test] fn test_fork_detector_zero_max_forks() { diff --git a/dash-spv/src/chain/fork_detector_test.rs b/dash-spv/src/chain/fork_detector_test.rs deleted file mode 100644 index f87a837a2..000000000 --- a/dash-spv/src/chain/fork_detector_test.rs +++ /dev/null @@ -1,397 +0,0 @@ -//! Comprehensive tests for fork detection functionality - -#[cfg(test)] -mod tests { - use super::super::*; - use crate::storage::{ChainStorage, MemoryStorage}; - use crate::types::ChainState; - use dashcore::blockdata::constants::genesis_block; - use dashcore::{BlockHash, Header as BlockHeader, Network}; - use dashcore_hashes::Hash; - use std::sync::{Arc, Mutex}; - use std::thread; - - fn create_test_header(prev_hash: BlockHash, nonce: u32) -> BlockHeader { - let mut header = genesis_block(Network::Dash).header; - header.prev_blockhash = prev_hash; - header.nonce = nonce; - header.time = 1390095618 + nonce * 600; // Increment time for each block - header - } - - fn create_test_header_with_time(prev_hash: BlockHash, nonce: u32, time: u32) -> BlockHeader { - let mut header = create_test_header(prev_hash, nonce); - header.time = time; - header - } - - #[test] - fn test_fork_detection_with_checkpoint_sync() { - let mut detector = ForkDetector::new(10).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Simulate checkpoint sync from height 1000 - chain_state.sync_base_height = 1000; - - // Add a checkpoint header at height 1000 - let checkpoint_header = create_test_header(BlockHash::from([0u8; 32]), 1000); - storage.store_header(&checkpoint_header, 1000).expect("Failed to store checkpoint"); - chain_state.add_header(checkpoint_header); - - // Add more headers building on checkpoint - let mut prev_hash = checkpoint_header.block_hash(); - for i in 1..5 { - let header = create_test_header(prev_hash, 1000 + i); - storage.store_header(&header, 1000 + i).expect("Failed to store header"); - chain_state.add_header(header); - prev_hash = header.block_hash(); - } - - // Try to create a fork from before the checkpoint (should be rejected) - let pre_checkpoint_hash = - BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::hash(&[99u8])); - storage.store_header(&checkpoint_header, 500).expect("Failed to store at height 500"); - - let fork_header = create_test_header(pre_checkpoint_hash, 999); - let result = detector.check_header(&fork_header, &chain_state, &storage); - - // Should be orphan since it tries to fork before checkpoint - assert!(matches!(result, ForkDetectionResult::Orphan)); - } - - #[test] - fn test_multiple_concurrent_forks() { - let mut detector = ForkDetector::new(5).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Setup genesis and main chain - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis"); - chain_state.add_header(genesis); - - // Build main chain - let mut main_chain_tip = genesis.block_hash(); - for i in 1..10 { - let header = create_test_header(main_chain_tip, i); - storage.store_header(&header, i).expect("Failed to store header"); - chain_state.add_header(header); - main_chain_tip = header.block_hash(); - } - - // Create multiple forks at different heights - let fork_points = vec![2, 4, 6, 8]; - let mut fork_tips = Vec::new(); - - for &height in &fork_points { - // Get the header at this height from storage - let fork_point_header = chain_state.header_at_height(height).unwrap(); - let fork_header = create_test_header(fork_point_header.block_hash(), 100 + height); - - let result = detector.check_header(&fork_header, &chain_state, &storage); - - match result { - ForkDetectionResult::CreatesNewFork(fork) => { - assert_eq!(fork.fork_height, height); - fork_tips.push(fork_header.block_hash()); - } - _ => panic!("Expected new fork creation at height {}", height), - } - } - - // Verify we have all forks tracked - assert_eq!(detector.get_forks().len(), 4); - - // Extend each fork - for (i, tip) in fork_tips.iter().enumerate() { - let extension = create_test_header(*tip, 200 + i as u32); - let result = detector.check_header(&extension, &chain_state, &storage); - - assert!(matches!(result, ForkDetectionResult::ExtendsFork(_))); - } - } - - #[test] - fn test_fork_limit_enforcement() { - let mut detector = ForkDetector::new(3).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Setup genesis and build a main chain - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis"); - chain_state.add_header(genesis); - - // Build main chain past genesis - let header1 = create_test_header(genesis.block_hash(), 1); - storage.store_header(&header1, 1).expect("Failed to store header"); - chain_state.add_header(header1); - - // Create more forks than the limit from genesis (not tip) - let mut created_forks = Vec::new(); - for i in 0..5 { - let fork_header = create_test_header(genesis.block_hash(), 100 + i); - detector.check_header(&fork_header, &chain_state, &storage); - created_forks.push(fork_header); - } - - // Should only track the maximum allowed - assert_eq!(detector.get_forks().len(), 3); - - // Verify we have 3 different forks - let remaining_forks = detector.get_forks(); - let mut fork_nonces: Vec = - remaining_forks.iter().map(|f| f.headers[0].nonce).collect(); - fork_nonces.sort(); - - // Since all forks have equal work, eviction order is not guaranteed - // Just verify we have 3 unique forks - assert_eq!(fork_nonces.len(), 3); - assert!(fork_nonces.iter().all(|&n| (100..=104).contains(&n))); - } - - #[test] - fn test_fork_chain_work_comparison() { - let mut detector = ForkDetector::new(10).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Setup genesis and build a main chain - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis"); - chain_state.add_header(genesis); - - // Build main chain past genesis - let header1 = create_test_header(genesis.block_hash(), 1); - storage.store_header(&header1, 1).expect("Failed to store header"); - chain_state.add_header(header1); - - // Create two forks from genesis (not tip) - let fork1_header = create_test_header(genesis.block_hash(), 100); - let fork2_header = create_test_header(genesis.block_hash(), 200); - - detector.check_header(&fork1_header, &chain_state, &storage); - detector.check_header(&fork2_header, &chain_state, &storage); - - // Extend fork1 with more headers - let mut fork1_tip = fork1_header.block_hash(); - for i in 0..5 { - let header = create_test_header(fork1_tip, 300 + i); - detector.check_header(&header, &chain_state, &storage); - fork1_tip = header.block_hash(); - } - - // Extend fork2 with fewer headers - let mut fork2_tip = fork2_header.block_hash(); - for i in 0..2 { - let header = create_test_header(fork2_tip, 400 + i); - detector.check_header(&header, &chain_state, &storage); - fork2_tip = header.block_hash(); - } - - // Get the strongest fork - let strongest = detector.get_strongest_fork().expect("Should have forks"); - assert_eq!(strongest.tip_hash, fork1_tip); - assert_eq!(strongest.headers.len(), 6); // Initial + 5 extensions - } - - #[test] - fn test_fork_detection_thread_safety() { - let detector = - Arc::new(Mutex::new(ForkDetector::new(50).expect("Failed to create fork detector"))); - let storage = Arc::new(MemoryStorage::new()); - let chain_state = Arc::new(Mutex::new(ChainState::new())); - - // Setup genesis - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis"); - chain_state.lock().unwrap().add_header(genesis); - - // Build a base chain - let mut prev_hash = genesis.block_hash(); - for i in 1..20 { - let header = create_test_header(prev_hash, i); - storage.store_header(&header, i).expect("Failed to store header"); - chain_state.lock().unwrap().add_header(header); - prev_hash = header.block_hash(); - } - - // Spawn multiple threads creating forks - let mut handles = vec![]; - - for thread_id in 0..5 { - let detector_clone = Arc::clone(&detector); - let storage_clone = Arc::clone(&storage); - let chain_state_clone = Arc::clone(&chain_state); - - let handle = thread::spawn(move || { - // Each thread creates forks at different heights - for i in 0..10 { - let fork_height = thread_id * 3 + i % 3; - let chain_state_lock = chain_state_clone.lock().unwrap(); - - if let Some(fork_point_header) = chain_state_lock.header_at_height(fork_height) - { - let fork_header = create_test_header( - fork_point_header.block_hash(), - 1000 + thread_id * 100 + i, - ); - - let mut detector_lock = detector_clone.lock().unwrap(); - detector_lock.check_header( - &fork_header, - &chain_state_lock, - storage_clone.as_ref(), - ); - } - } - }); - - handles.push(handle); - } - - // Wait for all threads to complete - for handle in handles { - handle.join().expect("Thread panicked"); - } - - // Verify the detector is in a consistent state - let detector_lock = detector.lock().unwrap(); - let forks = detector_lock.get_forks(); - - // Should have multiple forks but within the limit - assert!(!forks.is_empty()); - assert!(forks.len() <= 50); - - // All forks should have valid structure - for fork in forks { - assert!(!fork.headers.is_empty()); - assert_eq!(fork.tip_hash, fork.headers.last().unwrap().block_hash()); - assert_eq!(fork.tip_height, fork.fork_height + fork.headers.len() as u32); - } - } - - #[test] - fn test_orphan_detection_edge_cases() { - let mut detector = ForkDetector::new(10).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Test 1: Empty chain state (no genesis) - let orphan = create_test_header(BlockHash::from([0u8; 32]), 1); - let result = detector.check_header(&orphan, &chain_state, &storage); - assert!(matches!(result, ForkDetectionResult::Orphan)); - - // Add genesis - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis"); - chain_state.add_header(genesis); - - // Test 2: Header connecting to non-existent block - let phantom_hash = BlockHash::from_raw_hash(dashcore_hashes::hash_x11::Hash::hash(&[42u8])); - let orphan2 = create_test_header(phantom_hash, 2); - let result = detector.check_header(&orphan2, &chain_state, &storage); - assert!(matches!(result, ForkDetectionResult::Orphan)); - - // Test 3: Header with far future timestamp - let future_header = create_test_header_with_time(genesis.block_hash(), 3, u32::MAX); - let result = detector.check_header(&future_header, &chain_state, &storage); - assert!(matches!(result, ForkDetectionResult::ExtendsMainChain)); - } - - #[test] - fn test_fork_removal_and_cleanup() { - let mut detector = ForkDetector::new(10).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Setup genesis and build a main chain - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis"); - chain_state.add_header(genesis); - - // Build main chain past genesis - let header1 = create_test_header(genesis.block_hash(), 1); - storage.store_header(&header1, 1).expect("Failed to store header"); - chain_state.add_header(header1); - - // Create multiple forks from genesis (not tip) - let mut fork_tips = Vec::new(); - for i in 0..5 { - let fork_header = create_test_header(genesis.block_hash(), 100 + i); - detector.check_header(&fork_header, &chain_state, &storage); - fork_tips.push(fork_header.block_hash()); - } - - assert_eq!(detector.get_forks().len(), 5); - - // Remove specific forks - for tip in fork_tips.iter().take(3) { - let removed = detector.remove_fork(tip); - assert!(removed.is_some()); - } - - assert_eq!(detector.get_forks().len(), 2); - - // Verify removed forks can't be found - for tip in fork_tips.iter().take(3) { - assert!(detector.get_fork(tip).is_none()); - } - - // Clear all remaining forks - detector.clear_forks(); - assert_eq!(detector.get_forks().len(), 0); - assert!(!detector.has_forks()); - } - - #[test] - fn test_genesis_connection_special_case() { - let mut detector = ForkDetector::new(10).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Add genesis to storage and chain state - let genesis = genesis_block(Network::Dash).header; - storage.store_header(&genesis, 0).expect("Failed to store genesis"); - chain_state.add_header(genesis); - - // Chain state tip is at genesis (height 0) - assert_eq!(chain_state.tip_height(), 0); - - // Header connecting to genesis should extend main chain - let header1 = create_test_header(genesis.block_hash(), 1); - let result = detector.check_header(&header1, &chain_state, &storage); - assert!(matches!(result, ForkDetectionResult::ExtendsMainChain)); - } - - #[test] - fn test_chain_state_storage_mismatch() { - let mut detector = ForkDetector::new(10).expect("Failed to create fork detector"); - let storage = MemoryStorage::new(); - let mut chain_state = ChainState::new(); - - // Add headers to chain state but not storage (simulating sync issue) - let genesis = genesis_block(Network::Dash).header; - chain_state.add_header(genesis); - - let header1 = create_test_header(genesis.block_hash(), 1); - chain_state.add_header(header1); - - let header2 = create_test_header(header1.block_hash(), 2); - chain_state.add_header(header2); - - // Try to extend from header1 (in chain state but not storage) - let header3 = create_test_header(header1.block_hash(), 3); - let result = detector.check_header(&header3, &chain_state, &storage); - - // Should create a fork since it connects to non-tip header in chain state - match result { - ForkDetectionResult::CreatesNewFork(fork) => { - assert_eq!(fork.fork_point, header1.block_hash()); - assert_eq!(fork.fork_height, 1); - } - _ => panic!("Expected fork creation"), - } - } -} diff --git a/dash-spv/src/chain/mod.rs b/dash-spv/src/chain/mod.rs index 5fcabf106..61be1f963 100644 --- a/dash-spv/src/chain/mod.rs +++ b/dash-spv/src/chain/mod.rs @@ -18,11 +18,7 @@ pub mod reorg; #[cfg(test)] mod checkpoint_test; #[cfg(test)] -mod fork_detector_test; -#[cfg(test)] mod orphan_pool_test; -#[cfg(test)] -mod reorg_test; pub use chain_tip::{ChainTip, ChainTipManager}; pub use chain_work::ChainWork; @@ -30,7 +26,7 @@ pub use chainlock_manager::{ChainLockEntry, ChainLockManager, ChainLockStats}; pub use checkpoints::{Checkpoint, CheckpointManager}; pub use fork_detector::{ForkDetectionResult, ForkDetector}; pub use orphan_pool::{OrphanBlock, OrphanPool, OrphanPoolStats}; -pub use reorg::{ReorgEvent, ReorgManager}; +pub use reorg::ReorgEvent; use dashcore::{BlockHash, Header as BlockHeader}; diff --git a/dash-spv/src/chain/reorg.rs b/dash-spv/src/chain/reorg.rs index 4ba7005f2..026f7ccd0 100644 --- a/dash-spv/src/chain/reorg.rs +++ b/dash-spv/src/chain/reorg.rs @@ -3,13 +3,7 @@ //! This module implements the core logic for handling blockchain reorganizations, //! including finding common ancestors, rolling back transactions, and switching chains. -use super::chainlock_manager::ChainLockManager; -use super::{ChainTip, Fork}; -use crate::storage::ChainStorage; -use crate::types::ChainState; use dashcore::{BlockHash, Header as BlockHeader, Transaction, Txid}; -use std::sync::Arc; -use tracing; /// Event emitted when a reorganization occurs #[derive(Debug, Clone)] @@ -44,549 +38,3 @@ pub(crate) struct ReorgData { /// Actual transactions that were affected (if available) affected_transactions: Vec, } - -/// Manages chain reorganizations -pub struct ReorgManager { - /// Maximum depth of reorganization to handle - max_reorg_depth: u32, - /// Whether to allow reorgs past chain-locked blocks - respect_chain_locks: bool, - /// Chain lock manager for checking locked blocks - chain_lock_manager: Option>, -} - -impl ReorgManager { - /// Create a new reorganization manager - pub fn new(max_reorg_depth: u32, respect_chain_locks: bool) -> Self { - Self { - max_reorg_depth, - respect_chain_locks, - chain_lock_manager: None, - } - } - - /// Create a new reorganization manager with chain lock support - pub fn new_with_chain_locks( - max_reorg_depth: u32, - chain_lock_manager: Arc, - ) -> Self { - Self { - max_reorg_depth, - respect_chain_locks: true, - chain_lock_manager: Some(chain_lock_manager), - } - } - - /// Check if a fork has more work than the current chain and should trigger a reorg - pub fn should_reorganize( - &self, - current_tip: &ChainTip, - fork: &Fork, - storage: &CS, - ) -> Result { - self.should_reorganize_with_chain_state(current_tip, fork, storage, None) - } - - /// Check if a fork has more work than the current chain and should trigger a reorg - /// This version is checkpoint-aware when chain_state is provided - pub fn should_reorganize_with_chain_state( - &self, - current_tip: &ChainTip, - fork: &Fork, - storage: &CS, - chain_state: Option<&ChainState>, - ) -> Result { - // Check if fork has more work - if fork.chain_work <= current_tip.chain_work { - return Ok(false); - } - - // Check reorg depth - account for checkpoint sync - let reorg_depth = if let Some(state) = chain_state { - if state.synced_from_checkpoint() { - // During checkpoint sync, both current_tip.height and fork.fork_height - // should be interpreted relative to sync_base_height - - // For checkpoint sync: - // - current_tip.height is absolute blockchain height - // - fork.fork_height might be from genesis-based headers - // We need to compare relative depths only - - // If the fork is from headers that started at genesis, - // we shouldn't compare against the full checkpoint height - if fork.fork_height < state.sync_base_height { - // This fork is from before our checkpoint - likely from genesis-based headers - // This scenario should be rejected at header validation level, not here - tracing::warn!( - "Fork detected from height {} which is before checkpoint base height {}. \ - This suggests headers from genesis were received during checkpoint sync.", - fork.fork_height, - state.sync_base_height - ); - - // For now, reject forks that would reorg past the checkpoint - return Err(format!( - "Cannot reorg past checkpoint: fork height {} < checkpoint base {}", - fork.fork_height, state.sync_base_height - )); - } else { - // Normal case: both heights are relative to checkpoint - current_tip.height.saturating_sub(fork.fork_height) - } - } else { - // Normal sync mode - current_tip.height.saturating_sub(fork.fork_height) - } - } else { - // Fallback to original logic when no chain state provided - current_tip.height.saturating_sub(fork.fork_height) - }; - - if reorg_depth > self.max_reorg_depth { - return Err(format!( - "Reorg depth {} exceeds maximum {}", - reorg_depth, self.max_reorg_depth - )); - } - - // Check for chain locks if enabled - if self.respect_chain_locks { - if let Some(ref chain_lock_mgr) = self.chain_lock_manager { - // Check if reorg would violate chain locks - if chain_lock_mgr.would_violate_chain_lock(fork.fork_height, current_tip.height) { - return Err(format!( - "Cannot reorg: would violate chain lock between heights {} and {}", - fork.fork_height, current_tip.height - )); - } - } else { - // Fall back to checking individual blocks - for height in (fork.fork_height + 1)..=current_tip.height { - if let Ok(Some(header)) = storage.get_header_by_height(height) { - if self.is_chain_locked(&header, storage)? { - return Err(format!( - "Cannot reorg past chain-locked block at height {}", - height - )); - } - } - } - } - } - - Ok(true) - } - - /// Check if a block is chain-locked - pub fn is_chain_locked( - &self, - header: &BlockHeader, - storage: &CS, - ) -> Result { - if let Some(ref chain_lock_mgr) = self.chain_lock_manager { - // Get the height of this header - if let Ok(Some(height)) = storage.get_header_height(&header.block_hash()) { - return Ok(chain_lock_mgr.is_block_chain_locked(&header.block_hash(), height)); - } - } - // If no chain lock manager or height not found, assume not locked - Ok(false) - } -} - -// WalletState removed - reorganization should be handled by external wallet -/* -impl ReorgManager { - /// Perform a chain reorganization using a phased approach - pub async fn reorganize( - &self, - chain_state: &mut ChainState, - wallet_state: &mut WalletState, - fork: &Fork, - storage_manager: &mut S, - ) -> Result { - // Phase 1: Collect all necessary data (read-only) - let reorg_data = self.collect_reorg_data(chain_state, fork, storage_manager).await?; - - // Phase 2: Apply the reorganization (write-only) - self.apply_reorg_with_data(chain_state, wallet_state, fork, reorg_data, storage_manager) - .await - } - - /// Collect all data needed for reorganization (read-only phase) - #[cfg(test)] - pub async fn collect_reorg_data( - &self, - chain_state: &ChainState, - fork: &Fork, - storage_manager: &S, - ) -> Result { - self.collect_reorg_data_internal(chain_state, fork, storage_manager).await - } - - #[cfg(not(test))] - async fn collect_reorg_data( - &self, - chain_state: &ChainState, - fork: &Fork, - storage_manager: &S, - ) -> Result { - self.collect_reorg_data_internal(chain_state, fork, storage_manager).await - } - - async fn collect_reorg_data_internal( - &self, - chain_state: &ChainState, - fork: &Fork, - storage: &S, - ) -> Result { - // Find the common ancestor - let (common_ancestor, common_height) = - self.find_common_ancestor_with_fork(fork, storage).await?; - - // Collect headers to disconnect - let current_height = chain_state.get_height(); - let mut disconnected_headers = Vec::new(); - let mut disconnected_blocks = Vec::new(); - - // Walk back from current tip to common ancestor - for height in ((common_height + 1)..=current_height).rev() { - if let Ok(Some(header)) = storage.get_header(height).await { - let block_hash = header.block_hash(); - disconnected_blocks.push((block_hash, height)); - disconnected_headers.push(header); - } else { - return Err(format!("Missing header at height {}", height)); - } - } - - // Collect affected transaction IDs - let affected_tx_ids = Vec::new(); // Will be populated when we have transaction storage - let affected_transactions = Vec::new(); // Will be populated when we have transaction storage - - Ok(ReorgData { - common_ancestor, - common_height, - disconnected_headers, - disconnected_blocks, - affected_tx_ids, - affected_transactions, - }) - } - - /// Apply reorganization using collected data (write-only phase) - async fn apply_reorg_with_data( - &self, - chain_state: &mut ChainState, - wallet_state: &mut WalletState, - fork: &Fork, - reorg_data: ReorgData, - storage_manager: &mut S, - ) -> Result { - // Create a checkpoint of the current chain state before making any changes - let chain_state_checkpoint = chain_state.clone(); - - // Track headers that were successfully stored for potential rollback - let mut stored_headers: Vec = Vec::new(); - - // Perform all operations in a single atomic-like block - let result = async { - // Step 1: Rollback wallet state if UTXO rollback is available - if wallet_state.rollback_manager().is_some() { - wallet_state - .rollback_to_height(reorg_data.common_height, storage_manager) - .await - .map_err(|e| format!("Failed to rollback wallet state: {:?}", e))?; - } - - // Step 2: Disconnect blocks from the old chain - for header in &reorg_data.disconnected_headers { - // Mark transactions as unconfirmed if rollback manager not available - if wallet_state.rollback_manager().is_none() { - for txid in &reorg_data.affected_tx_ids { - wallet_state.mark_transaction_unconfirmed(txid); - } - } - - // Remove header from chain state - chain_state.remove_tip(); - } - - // Step 3: Connect blocks from the new chain and store them - let mut current_height = reorg_data.common_height; - for header in &fork.headers { - current_height += 1; - - // Add header to chain state - chain_state.add_header(*header); - - // Store the header - if this fails, we need to rollback everything - storage_manager.store_headers(&[*header]).await.map_err(|e| { - format!("Failed to store header at height {}: {:?}", current_height, e) - })?; - - // Only record successfully stored headers - stored_headers.push(*header); - } - - Ok::(ReorgEvent { - common_ancestor: reorg_data.common_ancestor, - common_height: reorg_data.common_height, - disconnected_headers: reorg_data.disconnected_headers, - connected_headers: fork.headers.clone(), - affected_transactions: reorg_data.affected_transactions, - }) - } - .await; - - // If any operation failed, attempt to restore the chain state - match result { - Ok(event) => Ok(event), - Err(e) => { - // Restore the chain state to its original state - *chain_state = chain_state_checkpoint; - - // Log the rollback attempt - tracing::error!( - "Reorg failed, restored chain state. Error: {}. \ - Successfully stored {} headers before failure.", - e, - stored_headers.len() - ); - - // Note: We cannot easily rollback the wallet state or storage operations - // that have already been committed. This is a limitation of not having - // true database transactions. The error message will indicate this partial - // state to the caller. - Err(format!( - "Reorg failed after partial application. Chain state restored, \ - but wallet/storage may be in inconsistent state. Error: {}. \ - Consider resyncing from a checkpoint.", - e - )) - } - } - } - - /// Find the common ancestor between current chain and a fork - async fn find_common_ancestor_with_fork( - &self, - fork: &Fork, - storage: &dyn StorageManager, - ) -> Result<(BlockHash, u32), String> { - // First check if the fork point itself is in our chain - if let Ok(Some(height)) = storage.get_header_height_by_hash(&fork.fork_point).await { - // The fork point is already in our chain, so it's the common ancestor - return Ok((fork.fork_point, height)); - } - - // If we have fork headers, check their parent blocks - if !fork.headers.is_empty() { - // Start from the first header in the fork and walk backwards - let first_fork_header = &fork.headers[0]; - let mut current_hash = first_fork_header.prev_blockhash; - - // Check if the parent of the first fork header is in our chain - if let Ok(Some(height)) = storage.get_header_height_by_hash(¤t_hash).await { - return Ok((current_hash, height)); - } - } - - // As a fallback, the fork should specify where it diverged from - // In a properly constructed Fork, fork_height should indicate where the split occurred - if fork.fork_height > 0 { - // Get the header at fork_height - 1 which should be the common ancestor - if let Ok(Some(header)) = storage.get_header(fork.fork_height.saturating_sub(1)).await { - let hash = header.block_hash(); - return Ok((hash, fork.fork_height.saturating_sub(1))); - } - } - - Err("Cannot find common ancestor between fork and main chain".to_string()) - } - - /// Find the common ancestor between current chain and a fork point (sync version for ChainStorage) - fn find_common_ancestor( - &self, - _chain_state: &ChainState, - fork_point: &BlockHash, - storage: &dyn ChainStorage, - ) -> Result<(BlockHash, u32), String> { - // Start from the fork point and walk back until we find a block in our chain - let mut current_hash = *fork_point; - let mut iterations = 0; - const MAX_ITERATIONS: u32 = 1_000_000; // Reasonable limit for chain traversal - - loop { - if let Ok(Some(height)) = storage.get_header_height(¤t_hash) { - // Found it in our chain - return Ok((current_hash, height)); - } - - // Get the previous block - if let Ok(Some(header)) = storage.get_header(¤t_hash) { - current_hash = header.prev_blockhash; - - // Safety check: don't go back too far - if current_hash == BlockHash::from([0u8; 32]) { - return Err("Reached genesis without finding common ancestor".to_string()); - } - - // Prevent infinite loops in case of corrupted chain - iterations += 1; - if iterations > MAX_ITERATIONS { - return Err(format!("Exceeded maximum iterations ({}) while searching for common ancestor - possible corrupted chain", MAX_ITERATIONS)); - } - } else { - return Err("Failed to find common ancestor".to_string()); - } - } - } - - /// Collect headers that need to be disconnected - fn collect_headers_to_disconnect( - &self, - chain_state: &ChainState, - common_height: u32, - storage: &dyn ChainStorage, - ) -> Result, String> { - let current_height = chain_state.get_height(); - let mut headers = Vec::new(); - - // Walk back from current tip to common ancestor - for height in ((common_height + 1)..=current_height).rev() { - if let Ok(Some(header)) = storage.get_header_by_height(height) { - headers.push(header); - } else { - return Err(format!("Missing header at height {}", height)); - } - } - - Ok(headers) - } - - /// Collect transactions affected by the reorganization - fn collect_affected_transactions( - &self, - disconnected_headers: &[BlockHeader], - _connected_headers: &[BlockHeader], - wallet_state: &WalletState, - storage: &dyn ChainStorage, - ) -> Result, String> { - let mut affected = Vec::new(); - - // Collect transactions from disconnected blocks - for header in disconnected_headers { - let block_hash = header.block_hash(); - if let Ok(Some(txids)) = storage.get_block_transactions(&block_hash) { - for txid in txids { - if wallet_state.is_wallet_transaction(&txid) { - if let Ok(Some(tx)) = storage.get_transaction(&txid) { - affected.push(tx); - } - } - } - } - } - - // Note: We don't have transactions from connected headers yet, - // they would need to be downloaded after the reorg - - Ok(affected) - } - - /// Check if a block is chain-locked - pub fn is_chain_locked( - &self, - header: &BlockHeader, - storage: &dyn ChainStorage, - ) -> Result { - if let Some(ref chain_lock_mgr) = self.chain_lock_manager { - // Get the height of this header - if let Ok(Some(height)) = storage.get_header_height(&header.block_hash()) { - return Ok(chain_lock_mgr.is_block_chain_locked(&header.block_hash(), height)); - } - } - // If no chain lock manager or height not found, assume not locked - Ok(false) - } - - /// Validate that a reorganization is safe to perform - pub fn validate_reorg(&self, current_tip: &ChainTip, fork: &Fork) -> Result<(), String> { - // Check maximum reorg depth - let reorg_depth = current_tip.height.saturating_sub(fork.fork_height); - if reorg_depth > self.max_reorg_depth { - return Err(format!( - "Reorg depth {} exceeds maximum allowed {}", - reorg_depth, self.max_reorg_depth - )); - } - - // Check that fork actually has more work - if fork.chain_work <= current_tip.chain_work { - return Err("Fork does not have more work than current chain".to_string()); - } - - // Additional validation could go here - - Ok(()) - } -} -*/ - -#[cfg(test)] -mod tests { - use super::*; - use crate::chain::ChainWork; - use crate::storage::MemoryStorage; - use dashcore::blockdata::constants::genesis_block; - use dashcore::Network; - use dashcore_hashes::Hash; - - #[test] - fn test_reorg_validation() { - let reorg_mgr = ReorgManager::new(100, false); - - let genesis = genesis_block(Network::Dash).header; - let tip = ChainTip::new(genesis, 0, ChainWork::from_header(&genesis)); - - // Create a fork with less work - should not reorg - let fork = Fork { - fork_point: BlockHash::from_byte_array([0; 32]), - fork_height: 0, - tip_hash: genesis.block_hash(), - tip_height: 1, - headers: vec![genesis], - chain_work: ChainWork::zero(), // Less work - }; - - let storage = MemoryStorage::new(); - let result = reorg_mgr.should_reorganize(&tip, &fork, &storage); - // Fork has less work, so should return Ok(false), not an error - assert!(result.is_ok()); - assert!(!result.unwrap()); - } - - #[test] - fn test_max_reorg_depth() { - let reorg_mgr = ReorgManager::new(10, false); - - let genesis = genesis_block(Network::Dash).header; - let tip = ChainTip::new(genesis, 100, ChainWork::from_header(&genesis)); - - // Create a fork that would require deep reorg - let fork = Fork { - fork_point: genesis.block_hash(), - fork_height: 0, // Fork from genesis - tip_hash: BlockHash::from_byte_array([0; 32]), - tip_height: 101, - headers: vec![], - chain_work: ChainWork::from_bytes([255u8; 32]), // Max work - }; - - let storage = MemoryStorage::new(); - let result = reorg_mgr.should_reorganize(&tip, &fork, &storage); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("exceeds maximum")); - } -} diff --git a/dash-spv/src/chain/reorg_test.rs b/dash-spv/src/chain/reorg_test.rs deleted file mode 100644 index 36a6d7d54..000000000 --- a/dash-spv/src/chain/reorg_test.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Tests for chain reorganization functionality - -#[cfg(test)] -mod tests { - use super::super::*; - use crate::chain::ChainWork; - use crate::storage::MemoryStorage; - use crate::types::ChainState; - use dashcore::{blockdata::constants::genesis_block, Network}; - use dashcore_hashes::Hash; - - fn create_test_header(prev: &BlockHeader, nonce: u32) -> BlockHeader { - let mut header = *prev; - header.prev_blockhash = prev.block_hash(); - header.nonce = nonce; - header.time = prev.time + 600; // 10 minutes later - header - } - - #[test] - fn test_should_reorganize() { - // Create test components - let network = Network::Dash; - let genesis = genesis_block(network).header; - let chain_state = ChainState::new_for_network(network); - let storage = MemoryStorage::new(); - - // Build main chain: genesis -> block1 -> block2 - let block1 = create_test_header(&genesis, 1); - let block2 = create_test_header(&block1, 2); - - // Create chain tip for main chain - let main_tip = ChainTip::new(block2, 2, ChainWork::from_header(&block2)); - - // Build fork chain: genesis -> block1' -> block2' -> block3' - let block1_fork = create_test_header(&genesis, 100); // Different nonce - let block2_fork = create_test_header(&block1_fork, 101); - let block3_fork = create_test_header(&block2_fork, 102); - - // Create fork with more work - let fork = Fork { - fork_point: genesis.block_hash(), - fork_height: 0, - tip_hash: block3_fork.block_hash(), - tip_height: 3, - headers: vec![block1_fork, block2_fork, block3_fork], - chain_work: ChainWork::from_bytes([255u8; 32]), // Max work - }; - - // Create reorg manager - let reorg_mgr = ReorgManager::new(100, false); - - // Should reorganize because fork has more work - let should_reorg = reorg_mgr - .should_reorganize_with_chain_state(&main_tip, &fork, &storage, Some(&chain_state)) - .unwrap(); - assert!(should_reorg); - } - - #[test] - fn test_max_reorg_depth() { - let network = Network::Dash; - let genesis = genesis_block(network).header; - let chain_state = ChainState::new_for_network(network); - let storage = MemoryStorage::new(); - - // Create a deep main chain - let main_tip = ChainTip::new(genesis, 100, ChainWork::from_header(&genesis)); - - // Create fork from genesis (depth 100) - let fork = Fork { - fork_point: genesis.block_hash(), - fork_height: 0, - tip_hash: BlockHash::from_byte_array([0; 32]), - tip_height: 101, - headers: vec![], - chain_work: ChainWork::from_bytes([255u8; 32]), // Max work - }; - - // Create reorg manager with max depth of 10 - let reorg_mgr = ReorgManager::new(10, false); - - // Should not reorganize due to depth limit - let result = reorg_mgr.should_reorganize_with_chain_state( - &main_tip, - &fork, - &storage, - Some(&chain_state), - ); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("exceeds maximum")); - } - - #[test] - fn test_checkpoint_sync_reorg_protection() { - let network = Network::Dash; - let genesis = genesis_block(network).header; - let mut chain_state = ChainState::new_for_network(network); - let storage = MemoryStorage::new(); - - // Simulate checkpoint sync from height 50000 - chain_state.sync_base_height = 50000; - - // Current tip at height 50100 - let main_tip = ChainTip::new(genesis, 50100, ChainWork::from_header(&genesis)); - - // Fork from before checkpoint (should be rejected) - let fork = Fork { - fork_point: genesis.block_hash(), - fork_height: 49999, // Before checkpoint - tip_hash: BlockHash::from_byte_array([0; 32]), - tip_height: 50101, - headers: vec![], - chain_work: ChainWork::from_bytes([255u8; 32]), // Max work - }; - - let reorg_mgr = ReorgManager::new(1000, false); - - // Should reject reorg past checkpoint - let result = reorg_mgr.should_reorganize_with_chain_state( - &main_tip, - &fork, - &storage, - Some(&chain_state), - ); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("checkpoint")); - } -} diff --git a/dash-spv/src/storage/mod.rs b/dash-spv/src/storage/mod.rs index 3dc462a12..aa8e0387a 100644 --- a/dash-spv/src/storage/mod.rs +++ b/dash-spv/src/storage/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod io; -pub mod sync_storage; pub mod types; mod headers; @@ -21,39 +20,8 @@ use crate::error::StorageResult; use crate::types::{ChainState, MempoolState, UnconfirmedTransaction}; pub use manager::DiskStorageManager; -pub use sync_storage::MemoryStorage; pub use types::*; -use crate::error::StorageError; -use dashcore::BlockHash; - -/// Synchronous storage trait for chain operations -pub trait ChainStorage: Send + Sync { - /// Get a header by its block hash - fn get_header(&self, hash: &BlockHash) -> Result, StorageError>; - - /// Get a header by its height - fn get_header_by_height(&self, height: u32) -> Result, StorageError>; - - /// Get the height of a block by its hash - fn get_header_height(&self, hash: &BlockHash) -> Result, StorageError>; - - /// Store a header at a specific height - fn store_header(&self, header: &BlockHeader, height: u32) -> Result<(), StorageError>; - - /// Get transaction IDs in a block - fn get_block_transactions( - &self, - block_hash: &BlockHash, - ) -> Result>, StorageError>; - - /// Get a transaction by its ID - fn get_transaction( - &self, - txid: &dashcore::Txid, - ) -> Result, StorageError>; -} - /// Storage manager trait for abstracting data persistence. /// /// # Thread Safety diff --git a/dash-spv/src/storage/sync_storage.rs b/dash-spv/src/storage/sync_storage.rs deleted file mode 100644 index 102114ca7..000000000 --- a/dash-spv/src/storage/sync_storage.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Synchronous storage wrapper for testing - -use super::ChainStorage; -use crate::error::StorageError; -use dashcore::{BlockHash, Header as BlockHeader, Transaction, Txid}; -use std::collections::HashMap; -use std::sync::RwLock; - -/// Simple in-memory storage for testing -pub struct MemoryStorage { - headers: RwLock>, - height_index: RwLock>, - transactions: RwLock>, - block_txs: RwLock>>, -} - -impl Default for MemoryStorage { - fn default() -> Self { - Self::new() - } -} - -impl MemoryStorage { - pub fn new() -> Self { - Self { - headers: RwLock::new(HashMap::new()), - height_index: RwLock::new(HashMap::new()), - transactions: RwLock::new(HashMap::new()), - block_txs: RwLock::new(HashMap::new()), - } - } -} - -impl ChainStorage for MemoryStorage { - fn get_header(&self, hash: &BlockHash) -> Result, StorageError> { - let headers = self.headers.read().map_err(|e| { - StorageError::LockPoisoned(format!("Failed to acquire read lock: {}", e)) - })?; - Ok(headers.get(hash).map(|(h, _)| *h)) - } - - fn get_header_by_height(&self, height: u32) -> Result, StorageError> { - let height_index = self.height_index.read().map_err(|e| { - StorageError::LockPoisoned(format!("Failed to acquire read lock: {}", e)) - })?; - if let Some(hash) = height_index.get(&height).cloned() { - drop(height_index); // Release lock before calling get_header - self.get_header(&hash) - } else { - Ok(None) - } - } - - fn get_header_height(&self, hash: &BlockHash) -> Result, StorageError> { - let headers = self.headers.read().map_err(|e| { - StorageError::LockPoisoned(format!("Failed to acquire read lock: {}", e)) - })?; - Ok(headers.get(hash).map(|(_, h)| *h)) - } - - fn store_header(&self, header: &BlockHeader, height: u32) -> Result<(), StorageError> { - let hash = header.block_hash(); - let mut headers = self.headers.write().map_err(|e| { - StorageError::LockPoisoned(format!("Failed to acquire write lock: {}", e)) - })?; - headers.insert(hash, (*header, height)); - drop(headers); // Release lock before acquiring the next one - - let mut height_index = self.height_index.write().map_err(|e| { - StorageError::LockPoisoned(format!("Failed to acquire write lock: {}", e)) - })?; - height_index.insert(height, hash); - Ok(()) - } - - fn get_block_transactions( - &self, - block_hash: &BlockHash, - ) -> Result>, StorageError> { - let block_txs = self.block_txs.read().map_err(|e| { - StorageError::LockPoisoned(format!("Failed to acquire read lock: {}", e)) - })?; - Ok(block_txs.get(block_hash).cloned()) - } - - fn get_transaction(&self, txid: &Txid) -> Result, StorageError> { - let transactions = self.transactions.read().map_err(|e| { - StorageError::LockPoisoned(format!("Failed to acquire read lock: {}", e)) - })?; - Ok(transactions.get(txid).cloned()) - } -} From 4be4cefb0e4e15758a5ee121ab6b356e10bbdd0d Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Thu, 18 Dec 2025 16:57:59 -0800 Subject: [PATCH 2/3] chore: removed fork detector and fork structs (#290) --- dash-spv/src/chain/fork_detector.rs | 80 ---------------------------- dash-spv/src/chain/mod.rs | 24 --------- dash-spv/src/chain/reorg.rs | 40 -------------- dash-spv/src/sync/headers/manager.rs | 9 +--- 4 files changed, 1 insertion(+), 152 deletions(-) delete mode 100644 dash-spv/src/chain/fork_detector.rs delete mode 100644 dash-spv/src/chain/reorg.rs diff --git a/dash-spv/src/chain/fork_detector.rs b/dash-spv/src/chain/fork_detector.rs deleted file mode 100644 index 6dfe92822..000000000 --- a/dash-spv/src/chain/fork_detector.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Fork detection logic for identifying blockchain forks -//! -//! This module detects when incoming headers create a fork in the blockchain -//! rather than extending the current chain tip. - -use super::Fork; -use dashcore::BlockHash; -use std::collections::HashMap; - -/// Detects and manages blockchain forks -pub struct ForkDetector { - /// Currently known forks indexed by their tip hash - forks: HashMap, -} - -impl ForkDetector { - pub fn new(max_forks: usize) -> Result { - if max_forks == 0 { - return Err("max_forks must be greater than 0"); - } - Ok(Self { - forks: HashMap::new(), - }) - } - - /// Get all known forks - pub fn get_forks(&self) -> Vec<&Fork> { - self.forks.values().collect() - } - - /// Get a specific fork by its tip hash - pub fn get_fork(&self, tip_hash: &BlockHash) -> Option<&Fork> { - self.forks.get(tip_hash) - } - - /// Remove a fork (e.g., after it's been processed) - pub fn remove_fork(&mut self, tip_hash: &BlockHash) -> Option { - self.forks.remove(tip_hash) - } - - /// Check if we have any forks - pub fn has_forks(&self) -> bool { - !self.forks.is_empty() - } - - /// Get the strongest fork (most cumulative work) - pub fn get_strongest_fork(&self) -> Option<&Fork> { - self.forks.values().max_by_key(|fork| &fork.chain_work) - } - - /// Clear all forks - pub fn clear_forks(&mut self) { - self.forks.clear(); - } -} - -/// Result of fork detection for a header -#[derive(Debug, Clone)] -pub enum ForkDetectionResult { - /// Header extends the current main chain tip - ExtendsMainChain, - /// Header extends an existing fork - ExtendsFork(Fork), - /// Header creates a new fork from the main chain - CreatesNewFork(Fork), - /// Header doesn't connect to any known chain - Orphan, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_fork_detector_zero_max_forks() { - let result = ForkDetector::new(0); - assert!(result.is_err()); - assert_eq!(result.err(), Some("max_forks must be greater than 0")); - } -} diff --git a/dash-spv/src/chain/mod.rs b/dash-spv/src/chain/mod.rs index 61be1f963..e533b7be3 100644 --- a/dash-spv/src/chain/mod.rs +++ b/dash-spv/src/chain/mod.rs @@ -1,7 +1,6 @@ //! Chain management module with reorganization support //! //! This module provides functionality for managing blockchain state including: -//! - Fork detection and handling //! - Chain reorganization //! - Multiple chain tip tracking //! - Chain work calculation @@ -11,9 +10,7 @@ pub mod chain_tip; pub mod chain_work; pub mod chainlock_manager; pub mod checkpoints; -pub mod fork_detector; pub mod orphan_pool; -pub mod reorg; #[cfg(test)] mod checkpoint_test; @@ -24,25 +21,4 @@ pub use chain_tip::{ChainTip, ChainTipManager}; pub use chain_work::ChainWork; pub use chainlock_manager::{ChainLockEntry, ChainLockManager, ChainLockStats}; pub use checkpoints::{Checkpoint, CheckpointManager}; -pub use fork_detector::{ForkDetectionResult, ForkDetector}; pub use orphan_pool::{OrphanBlock, OrphanPool, OrphanPoolStats}; -pub use reorg::ReorgEvent; - -use dashcore::{BlockHash, Header as BlockHeader}; - -/// Represents a potential chain fork -#[derive(Debug, Clone)] -pub struct Fork { - /// The block hash where the fork diverges from the main chain - pub fork_point: BlockHash, - /// The height of the fork point - pub fork_height: u32, - /// The tip of the forked chain - pub tip_hash: BlockHash, - /// The height of the fork tip - pub tip_height: u32, - /// Headers in the fork (from fork point to tip) - pub headers: Vec, - /// Cumulative chain work of this fork - pub chain_work: ChainWork, -} diff --git a/dash-spv/src/chain/reorg.rs b/dash-spv/src/chain/reorg.rs deleted file mode 100644 index 026f7ccd0..000000000 --- a/dash-spv/src/chain/reorg.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Chain reorganization handling -//! -//! This module implements the core logic for handling blockchain reorganizations, -//! including finding common ancestors, rolling back transactions, and switching chains. - -use dashcore::{BlockHash, Header as BlockHeader, Transaction, Txid}; - -/// Event emitted when a reorganization occurs -#[derive(Debug, Clone)] -pub struct ReorgEvent { - /// The common ancestor where chains diverged - pub common_ancestor: BlockHash, - /// Height of the common ancestor - pub common_height: u32, - /// Headers that were removed from the main chain - pub disconnected_headers: Vec, - /// Headers that were added to the main chain - pub connected_headers: Vec, - /// Transactions that may have changed confirmation status - pub affected_transactions: Vec, -} - -/// Data collected during the read phase of reorganization -#[allow(dead_code)] -#[derive(Debug)] -#[cfg_attr(test, derive(Clone))] -pub(crate) struct ReorgData { - /// The common ancestor where chains diverged - pub(crate) common_ancestor: BlockHash, - /// Height of the common ancestor - pub(crate) common_height: u32, - /// Headers that need to be disconnected from the main chain - disconnected_headers: Vec, - /// Block hashes and heights for disconnected blocks - disconnected_blocks: Vec<(BlockHash, u32)>, - /// Transaction IDs from disconnected blocks that affect the wallet - affected_tx_ids: Vec, - /// Actual transactions that were affected (if available) - affected_transactions: Vec, -} diff --git a/dash-spv/src/sync/headers/manager.rs b/dash-spv/src/sync/headers/manager.rs index 2db5fda4f..8e4a2f41b 100644 --- a/dash-spv/src/sync/headers/manager.rs +++ b/dash-spv/src/sync/headers/manager.rs @@ -7,7 +7,7 @@ use dashcore::{ use dashcore_hashes::Hash; use crate::chain::checkpoints::{mainnet_checkpoints, testnet_checkpoints, CheckpointManager}; -use crate::chain::{ChainTip, ChainTipManager, ChainWork, ForkDetector}; +use crate::chain::{ChainTip, ChainTipManager, ChainWork}; use crate::client::ClientConfig; use crate::error::{SyncError, SyncResult}; use crate::network::NetworkManager; @@ -47,7 +47,6 @@ pub struct HeaderSyncManager { _phantom_s: std::marker::PhantomData, _phantom_n: std::marker::PhantomData, config: ClientConfig, - fork_detector: ForkDetector, tip_manager: ChainTipManager, checkpoint_manager: CheckpointManager, reorg_config: ReorgConfig, @@ -83,8 +82,6 @@ impl Date: Fri, 19 Dec 2025 20:09:33 +0000 Subject: [PATCH 3/3] removed filters field from ChainState (ez) --- dash-spv-ffi/src/types.rs | 2 -- .../tests/unit/test_type_conversions.rs | 2 -- dash-spv/src/client/core.rs | 36 ------------------- dash-spv/src/storage/state.rs | 11 ------ dash-spv/src/types.rs | 25 +------------ 5 files changed, 1 insertion(+), 75 deletions(-) diff --git a/dash-spv-ffi/src/types.rs b/dash-spv-ffi/src/types.rs index c644c52da..ad9c45665 100644 --- a/dash-spv-ffi/src/types.rs +++ b/dash-spv-ffi/src/types.rs @@ -182,7 +182,6 @@ impl From for FFIDetailedSyncProgress { #[repr(C)] pub struct FFIChainState { pub header_height: u32, - pub filter_header_height: u32, pub masternode_height: u32, pub last_chainlock_height: u32, pub last_chainlock_hash: FFIString, @@ -193,7 +192,6 @@ impl From for FFIChainState { fn from(state: ChainState) -> Self { FFIChainState { header_height: state.headers.len() as u32, - filter_header_height: state.filter_headers.len() as u32, masternode_height: state.last_masternode_diff_height.unwrap_or(0), last_chainlock_height: state.last_chainlock_height.unwrap_or(0), last_chainlock_hash: FFIString::new( diff --git a/dash-spv-ffi/tests/unit/test_type_conversions.rs b/dash-spv-ffi/tests/unit/test_type_conversions.rs index 58e29ce5f..4f4c807f8 100644 --- a/dash-spv-ffi/tests/unit/test_type_conversions.rs +++ b/dash-spv-ffi/tests/unit/test_type_conversions.rs @@ -164,7 +164,6 @@ mod tests { fn test_chain_state_none_values() { let state = dash_spv::ChainState { headers: vec![], - filter_headers: vec![], last_chainlock_height: None, last_chainlock_hash: None, current_filter_tip: None, @@ -175,7 +174,6 @@ mod tests { let ffi_state = FFIChainState::from(state); assert_eq!(ffi_state.header_height, 0); - assert_eq!(ffi_state.filter_header_height, 0); assert_eq!(ffi_state.masternode_height, 0); assert_eq!(ffi_state.last_chainlock_height, 0); assert_eq!(ffi_state.current_filter_tip, 0); diff --git a/dash-spv/src/client/core.rs b/dash-spv/src/client/core.rs index e697ca462..1c375f9a5 100644 --- a/dash-spv/src/client/core.rs +++ b/dash-spv/src/client/core.rs @@ -271,42 +271,6 @@ impl< Ok(()) } - /// Clear all stored filter headers and compact filters while keeping other data intact. - pub async fn clear_filters(&mut self) -> Result<()> { - { - let mut storage = self.storage.lock().await; - storage.clear_filters().await.map_err(SpvError::Storage)?; - } - - // Reset in-memory chain state for filters - { - let mut state = self.state.write().await; - state.filter_headers.clear(); - state.current_filter_tip = None; - } - - // Reset filter sync manager tracking - self.sync_manager.filter_sync_mut().clear_filter_state().await; - - // Reset filter-related statistics - let received_heights = { - let stats = self.stats.read().await; - stats.received_filter_heights.clone() - }; - - { - let mut stats = self.stats.write().await; - stats.filter_headers_downloaded = 0; - stats.filter_height = 0; - stats.filters_downloaded = 0; - stats.filters_received = 0; - } - - received_heights.lock().await.clear(); - - Ok(()) - } - // ============ Configuration ============ /// Update the client configuration. diff --git a/dash-spv/src/storage/state.rs b/dash-spv/src/storage/state.rs index 937ac3d2a..56a3f346b 100644 --- a/dash-spv/src/storage/state.rs +++ b/dash-spv/src/storage/state.rs @@ -22,13 +22,6 @@ impl DiskStorageManager { // For checkpoint sync, we need to store headers starting from the checkpoint height self.store_headers_at_height(&state.headers, state.sync_base_height).await?; - // Store filter headers - self.filter_headers - .write() - .await - .store_items(&state.filter_headers, state.sync_base_height, self) - .await?; - // Store other state as JSON let state_data = serde_json::json!({ "last_chainlock_height": state.last_chainlock_height, @@ -87,10 +80,6 @@ impl DiskStorageManager { if let Some(tip_height) = self.get_tip_height().await? { state.headers = self.load_headers(range_start..tip_height + 1).await?; } - if let Some(filter_tip_height) = self.get_filter_tip_height().await? { - state.filter_headers = - self.load_filter_headers(range_start..filter_tip_height + 1).await?; - } Ok(Some(state)) } diff --git a/dash-spv/src/types.rs b/dash-spv/src/types.rs index 3b7e99958..5917208c3 100644 --- a/dash-spv/src/types.rs +++ b/dash-spv/src/types.rs @@ -250,16 +250,12 @@ impl DetailedSyncProgress { /// /// ## Memory Considerations /// - headers: ~80 bytes per header -/// - filter_headers: 32 bytes per filter header -/// - At 2M blocks: ~160MB for headers, ~64MB for filter headers +/// - At 2M blocks: ~160MB for headers #[derive(Clone, Default)] pub struct ChainState { /// Block headers indexed by height. pub headers: Vec, - /// Filter headers indexed by height. - pub filter_headers: Vec, - /// Last ChainLock height. pub last_chainlock_height: Option, @@ -355,28 +351,11 @@ impl ChainState { self.headers.get(index) } - /// Get filter header at the given height. - pub fn filter_header_at_height(&self, height: u32) -> Option<&FilterHeader> { - if height < self.sync_base_height { - return None; // Height is before our sync base - } - let index = (height - self.sync_base_height) as usize; - self.filter_headers.get(index) - } - /// Add headers to the chain. pub fn add_headers(&mut self, headers: Vec) { self.headers.extend(headers); } - /// Add filter headers to the chain. - pub fn add_filter_headers(&mut self, filter_headers: Vec) { - if let Some(last) = filter_headers.last() { - self.current_filter_tip = Some(*last); - } - self.filter_headers.extend(filter_headers); - } - /// Get the tip header pub fn get_tip_header(&self) -> Option { self.headers.last().copied() @@ -458,7 +437,6 @@ impl ChainState { ) { // Clear any existing headers self.headers.clear(); - self.filter_headers.clear(); // Set sync base height to checkpoint self.sync_base_height = checkpoint_height; @@ -498,7 +476,6 @@ impl std::fmt::Debug for ChainState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ChainState") .field("headers", &format!("{} headers", self.headers.len())) - .field("filter_headers", &format!("{} filter headers", self.filter_headers.len())) .field("last_chainlock_height", &self.last_chainlock_height) .field("last_chainlock_hash", &self.last_chainlock_hash) .field("current_filter_tip", &self.current_filter_tip)