diff --git a/dash-spv-ffi/src/bin/ffi_cli.rs b/dash-spv-ffi/src/bin/ffi_cli.rs index 21ccdb71d..6b0406317 100644 --- a/dash-spv-ffi/src/bin/ffi_cli.rs +++ b/dash-spv-ffi/src/bin/ffi_cli.rs @@ -219,9 +219,6 @@ fn main() { break; } dash_spv_ffi_sync_progress_destroy(prog_ptr); - } else { - // If progress is unavailable, assume sync finished or errored - break; } thread::sleep(Duration::from_millis(300)); } diff --git a/dash-spv/src/sync/filters.rs b/dash-spv/src/sync/filters.rs index 3ff4ae24b..4b80ddb69 100644 --- a/dash-spv/src/sync/filters.rs +++ b/dash-spv/src/sync/filters.rs @@ -1,7 +1,7 @@ //! Filter synchronization functionality. use dashcore::{ - bip158::{BlockFilterReader, Error as Bip158Error}, + bip158::{BlockFilter, BlockFilterReader, Error as Bip158Error}, hash_types::FilterHeader, network::message::NetworkMessage, network::message_blockdata::Inventory, @@ -102,6 +102,53 @@ pub struct FilterSyncManager { impl FilterSyncManager { + /// Verify that the received compact filter hashes to the expected filter header + /// based on previously synchronized CFHeaders. + pub async fn verify_cfilter_against_headers( + &self, + filter_data: &[u8], + height: u32, + storage: &S, + ) -> SyncResult { + // We expect filter headers to be synced before requesting filters. + // If we're at height 0 (genesis), skip verification because there is no previous header. + if height == 0 { + tracing::debug!("Skipping cfilter verification at genesis height 0"); + return Ok(true); + } + + // Load previous and expected headers + let prev_header = storage.get_filter_header(height - 1).await.map_err(|e| { + SyncError::Storage(format!("Failed to load previous filter header: {}", e)) + })?; + let expected_header = storage.get_filter_header(height).await.map_err(|e| { + SyncError::Storage(format!("Failed to load expected filter header: {}", e)) + })?; + + let (Some(prev_header), Some(expected_header)) = (prev_header, expected_header) else { + tracing::warn!( + "Missing filter headers in storage for height {} (prev and/or expected)", + height + ); + return Ok(false); + }; + + // Compute the header from the received filter bytes and compare + let filter = BlockFilter::new(filter_data); + let computed_header = filter.filter_header(&prev_header); + + let matches = computed_header == expected_header; + if !matches { + tracing::error!( + "CFilter header mismatch at height {}: computed={:?}, expected={:?}", + height, + computed_header, + expected_header + ); + } + + Ok(matches) + } /// Scan backward from `abs_height` down to `min_abs_height` (inclusive) /// to find the nearest available block header stored in `storage`. /// Returns the found `(BlockHash, height)` or `None` if none available. diff --git a/dash-spv/src/sync/sequential/mod.rs b/dash-spv/src/sync/sequential/mod.rs index 3accd77f3..cb3a16a5d 100644 --- a/dash-spv/src/sync/sequential/mod.rs +++ b/dash-spv/src/sync/sequential/mod.rs @@ -1362,6 +1362,32 @@ impl< let mut wallet = self.wallet.write().await; // Check filter against wallet if available + // First, verify filter data matches expected filter header chain + let height = storage + .get_header_height_by_hash(&cfilter.block_hash) + .await + .map_err(|e| SyncError::Storage(format!("Failed to get filter block height: {}", e)))? + .ok_or_else(|| { + SyncError::Validation(format!( + "Block height not found for cfilter block {}", + cfilter.block_hash + )) + })?; + + let header_ok = self + .filter_sync + .verify_cfilter_against_headers(&cfilter.filter, height, &*storage) + .await?; + + if !header_ok { + tracing::warn!( + "Rejecting CFilter for block {} at height {} due to header mismatch", + cfilter.block_hash, + height + ); + return Ok(()); + } + let matches = self .filter_sync .check_filter_for_matches( @@ -1963,6 +1989,20 @@ impl< .map_err(|e| SyncError::Storage(format!("Failed to get filter block height: {}", e)))? .ok_or(SyncError::InvalidState("Filter block height not found".to_string()))?; + // Verify against expected header chain before storing + let header_ok = self + .filter_sync + .verify_cfilter_against_headers(&cfilter.filter, height, &*storage) + .await?; + if !header_ok { + tracing::warn!( + "Rejecting post-sync CFilter for block {} at height {} due to header mismatch", + cfilter.block_hash, + height + ); + return Ok(()); + } + // Store the filter storage .store_filter(height, &cfilter.filter)