Skip to content

Commit 2051bda

Browse files
feat: add CFHeader gap detection and auto-restart functionality
1 parent a029a98 commit 2051bda

15 files changed

+986
-51
lines changed

dash-spv/src/client/config.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,36 @@ pub struct ClientConfig {
6565

6666
/// Delay between filter requests in milliseconds (default: 50).
6767
pub filter_request_delay_ms: u64,
68+
69+
/// Enable automatic CFHeader gap detection and restart
70+
pub enable_cfheader_gap_restart: bool,
71+
72+
/// Interval for checking CFHeader gaps (seconds)
73+
pub cfheader_gap_check_interval_secs: u64,
74+
75+
/// Cooldown between CFHeader restart attempts (seconds)
76+
pub cfheader_gap_restart_cooldown_secs: u64,
77+
78+
/// Maximum CFHeader gap restart attempts
79+
pub max_cfheader_gap_restart_attempts: u32,
80+
81+
/// Enable automatic filter gap detection and restart
82+
pub enable_filter_gap_restart: bool,
83+
84+
/// Interval for checking filter gaps (seconds)
85+
pub filter_gap_check_interval_secs: u64,
86+
87+
/// Minimum filter gap size to trigger restart (blocks)
88+
pub min_filter_gap_size: u32,
89+
90+
/// Cooldown between filter restart attempts (seconds)
91+
pub filter_gap_restart_cooldown_secs: u64,
92+
93+
/// Maximum filter gap restart attempts
94+
pub max_filter_gap_restart_attempts: u32,
95+
96+
/// Maximum number of filters to sync in a single gap sync batch
97+
pub max_filter_gap_sync_size: u32,
6898
}
6999

70100
impl Default for ClientConfig {
@@ -88,6 +118,16 @@ impl Default for ClientConfig {
88118
max_concurrent_filter_requests: 16,
89119
enable_filter_flow_control: true,
90120
filter_request_delay_ms: 0,
121+
enable_cfheader_gap_restart: true,
122+
cfheader_gap_check_interval_secs: 15,
123+
cfheader_gap_restart_cooldown_secs: 30,
124+
max_cfheader_gap_restart_attempts: 5,
125+
enable_filter_gap_restart: true,
126+
filter_gap_check_interval_secs: 20,
127+
min_filter_gap_size: 10,
128+
filter_gap_restart_cooldown_secs: 30,
129+
max_filter_gap_restart_attempts: 5,
130+
max_filter_gap_sync_size: 50000,
91131
}
92132
}
93133
}

dash-spv/src/client/filter_sync.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ impl<'a> FilterSyncCoordinator<'a> {
9292
Ok(Vec::new())
9393
}
9494

95+
/// Sync filters for a specific height range.
96+
pub async fn sync_filters_range(&mut self, start_height: Option<u32>, count: Option<u32>) -> Result<()> {
97+
// Get filter tip height to determine default values
98+
let filter_tip_height = self.storage.get_filter_tip_height().await
99+
.map_err(|e| SpvError::Storage(e))?
100+
.unwrap_or(0);
101+
102+
let start = start_height.unwrap_or(filter_tip_height.saturating_sub(99));
103+
let num_blocks = count.unwrap_or(100);
104+
105+
tracing::info!("Starting filter sync for specific range from height {} ({} blocks)", start, num_blocks);
106+
107+
self.sync_filters_coordinated(start, num_blocks).await
108+
}
109+
95110
/// Sync filters in coordination with the monitoring loop using flow control processing
96111
async fn sync_filters_coordinated(&mut self, start_height: u32, count: u32) -> Result<()> {
97112
tracing::info!("Starting coordinated filter sync with flow control from height {} to {} ({} filters expected)",

dash-spv/src/client/mod.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ impl DashSpvClient {
451451

452452
// Timer for filter gap checking
453453
let mut last_filter_gap_check = Instant::now();
454-
let filter_gap_check_interval = std::time::Duration::from_secs(10);
454+
let filter_gap_check_interval = std::time::Duration::from_secs(self.config.cfheader_gap_check_interval_secs);
455455

456456
loop {
457457
// Check if we should stop
@@ -510,6 +510,17 @@ impl DashSpvClient {
510510
if last_status_update.elapsed() >= status_update_interval {
511511
self.update_status_display().await;
512512

513+
// Report CFHeader gap information if enabled
514+
if self.config.enable_filters {
515+
if let Ok((has_gap, block_height, filter_height, gap_size)) =
516+
self.sync_manager.filter_sync().check_cfheader_gap(&*self.storage).await {
517+
if has_gap && gap_size >= 100 { // Only log significant gaps
518+
tracing::info!("📏 CFHeader Gap: {} block headers vs {} filter headers (gap: {})",
519+
block_height, filter_height, gap_size);
520+
}
521+
}
522+
}
523+
513524
// Report enhanced filter sync progress if active
514525
let (filters_requested, filters_received, basic_progress, timeout, total_missing, actual_coverage, missing_ranges) =
515526
crate::sync::filters::FilterSyncManager::get_filter_sync_status_with_gaps(&self.stats, self.sync_manager.filter_sync()).await;
@@ -587,6 +598,68 @@ impl DashSpvClient {
587598
.check_and_retry_missing_filters(&mut *self.network, &*self.storage).await {
588599
tracing::warn!("Failed to check and retry missing filters: {}", e);
589600
}
601+
602+
// Check for CFHeader gaps and auto-restart if needed
603+
if self.config.enable_cfheader_gap_restart {
604+
match self.sync_manager.filter_sync_mut()
605+
.maybe_restart_cfheader_sync_for_gap(&mut *self.network, &mut *self.storage).await {
606+
Ok(restarted) => {
607+
if restarted {
608+
tracing::info!("🔄 Auto-restarted CFHeader sync due to detected gap");
609+
}
610+
}
611+
Err(e) => {
612+
tracing::warn!("Failed to check/restart CFHeader sync for gap: {}", e);
613+
}
614+
}
615+
}
616+
617+
// Check for filter gaps and auto-restart if needed
618+
if self.config.enable_filter_gap_restart && !self.watch_items.read().await.is_empty() {
619+
// Get current sync progress
620+
let progress = self.sync_progress().await?;
621+
622+
// Check if there's a gap between synced filters and filter headers
623+
match self.sync_manager.filter_sync()
624+
.check_filter_gap(&*self.storage, &progress).await {
625+
Ok((has_gap, filter_header_height, last_synced_filter, gap_size)) => {
626+
if has_gap && gap_size >= self.config.min_filter_gap_size {
627+
tracing::info!("🔍 Detected filter gap: filter headers at {}, last synced filter at {} (gap: {} blocks)",
628+
filter_header_height, last_synced_filter, gap_size);
629+
630+
// Check if we're not already syncing filters
631+
if !self.sync_manager.filter_sync().is_syncing_filters() {
632+
// Start filter sync for the missing range
633+
let start_height = last_synced_filter + 1;
634+
635+
// Limit the sync size to avoid overwhelming the system
636+
let max_sync_size = self.config.max_filter_gap_sync_size;
637+
let sync_count = gap_size.min(max_sync_size);
638+
639+
if sync_count < gap_size {
640+
tracing::info!("🔄 Auto-starting filter sync for gap from height {} ({} blocks of {} total gap)",
641+
start_height, sync_count, gap_size);
642+
} else {
643+
tracing::info!("🔄 Auto-starting filter sync for gap from height {} ({} blocks)",
644+
start_height, sync_count);
645+
}
646+
647+
match self.sync_filters_range(Some(start_height), Some(sync_count)).await {
648+
Ok(_) => {
649+
tracing::info!("✅ Successfully started filter sync for gap");
650+
}
651+
Err(e) => {
652+
tracing::warn!("Failed to start filter sync for gap: {}", e);
653+
}
654+
}
655+
}
656+
}
657+
}
658+
Err(e) => {
659+
tracing::debug!("Failed to check filter gap: {}", e);
660+
}
661+
}
662+
}
590663
}
591664
last_filter_gap_check = Instant::now();
592665
}
@@ -1271,6 +1344,19 @@ impl DashSpvClient {
12711344
);
12721345
coordinator.sync_and_check_filters(num_blocks).await
12731346
}
1347+
1348+
/// Sync filters for a specific height range.
1349+
pub async fn sync_filters_range(&mut self, start_height: Option<u32>, count: Option<u32>) -> Result<()> {
1350+
let mut coordinator = FilterSyncCoordinator::new(
1351+
&mut self.sync_manager,
1352+
&mut *self.storage,
1353+
&mut *self.network,
1354+
&self.watch_items,
1355+
&self.stats,
1356+
&self.running,
1357+
);
1358+
coordinator.sync_filters_range(start_height, count).await
1359+
}
12741360

12751361
/// Initialize genesis block if not already present in storage.
12761362
async fn initialize_genesis_block(&mut self) -> Result<()> {

dash-spv/src/client/status_display.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ impl<'a> StatusDisplay<'a> {
3939
/// Get current sync progress.
4040
pub async fn sync_progress(&self) -> Result<SyncProgress> {
4141
let state = self.state.read().await;
42+
let stats = self.stats.read().await;
43+
44+
// Calculate last synced filter height from received filter heights
45+
let last_synced_filter_height = if let Ok(heights) = stats.received_filter_heights.lock() {
46+
heights.iter().max().copied()
47+
} else {
48+
None
49+
};
50+
4251
Ok(SyncProgress {
4352
header_height: state.tip_height(),
4453
filter_header_height: state.filter_headers.len().saturating_sub(1) as u32,
@@ -47,7 +56,8 @@ impl<'a> StatusDisplay<'a> {
4756
headers_synced: false, // TODO: Implement
4857
filter_headers_synced: false, // TODO: Implement
4958
masternodes_synced: false, // TODO: Implement
50-
filters_downloaded: 0, // TODO: Track properly
59+
filters_downloaded: stats.filters_received,
60+
last_synced_filter_height,
5161
sync_start: std::time::SystemTime::now(), // TODO: Track properly
5262
last_update: std::time::SystemTime::now(),
5363
})

dash-spv/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
234234
let example_addresses = match network {
235235
dashcore::Network::Dash => vec![
236236
// Some example mainnet addresses (these are from block explorers/faucets)
237-
"XjbaGWaGnvEtuQAUoBgDxJWe8ZNv45upG2", // Crowdnode
237+
"Xesjop7V9xLndFMgZoCrckJ5ZPgJdJFbA3", // Crowdnode
238238
],
239239
dashcore::Network::Testnet => vec![
240240
// Testnet addresses
@@ -255,7 +255,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
255255
if let Ok(valid_addr) = addr.require_network(network) {
256256
// For the example mainnet address (Crowdnode), set earliest height to 1,000,000
257257
let watch_item = if network == dashcore::Network::Dash && addr_str == "XjbaGWaGnvEtuQAUoBgDxJWe8ZNv45upG2" {
258-
dash_spv::WatchItem::address_from_height(valid_addr, 500_000)
258+
dash_spv::WatchItem::address_from_height(valid_addr, 200_000)
259259
} else {
260260
dash_spv::WatchItem::address(valid_addr)
261261
};

dash-spv/src/network/tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ mod multi_peer_tests {
3030
enable_filter_flow_control: true,
3131
filter_request_delay_ms: 0,
3232
max_concurrent_filter_requests: 50,
33+
enable_cfheader_gap_restart: true,
34+
cfheader_gap_check_interval_secs: 15,
35+
cfheader_gap_restart_cooldown_secs: 30,
36+
max_cfheader_gap_restart_attempts: 5,
3337
}
3438
}
3539

0 commit comments

Comments
 (0)