diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index 10703d5cc..c7e478f7f 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -298,7 +298,7 @@ typedef void (*OnManagerErrorCallback)(enum FFIManagerId manager_id, /** * Callback for SyncEvent::SyncComplete */ -typedef void (*OnSyncCompleteCallback)(uint32_t header_tip, void *user_data); +typedef void (*OnSyncCompleteCallback)(uint32_t header_tip, uint32_t cycle, void *user_data); /** * Sync event callbacks - one callback per SyncEvent variant. diff --git a/dash-spv-ffi/src/bin/ffi_cli.rs b/dash-spv-ffi/src/bin/ffi_cli.rs index ce669189c..305e71f10 100644 --- a/dash-spv-ffi/src/bin/ffi_cli.rs +++ b/dash-spv-ffi/src/bin/ffi_cli.rs @@ -122,8 +122,8 @@ extern "C" fn on_manager_error( println!("[Sync] Manager error: {:?} - {}", manager_id, error_str); } -extern "C" fn on_sync_complete(header_tip: u32, _user_data: *mut c_void) { - println!("[Sync] Sync complete at height: {}", header_tip); +extern "C" fn on_sync_complete(header_tip: u32, cycle: u32, _user_data: *mut c_void) { + println!("[Sync] Sync complete at height: {} (cycle {})", header_tip, cycle); } // ============================================================================ diff --git a/dash-spv-ffi/src/callbacks.rs b/dash-spv-ffi/src/callbacks.rs index 39960a4a5..9ae788032 100644 --- a/dash-spv-ffi/src/callbacks.rs +++ b/dash-spv-ffi/src/callbacks.rs @@ -209,7 +209,8 @@ pub type OnManagerErrorCallback = Option; /// Callback for SyncEvent::SyncComplete -pub type OnSyncCompleteCallback = Option; +pub type OnSyncCompleteCallback = + Option; /// Sync event callbacks - one callback per SyncEvent variant. /// @@ -409,9 +410,10 @@ impl FFISyncEventCallbacks { } SyncEvent::SyncComplete { header_tip, + cycle, } => { if let Some(cb) = self.on_sync_complete { - cb(*header_tip, self.user_data); + cb(*header_tip, *cycle, self.user_data); } } } diff --git a/dash-spv-ffi/tests/unit/test_async_operations.rs b/dash-spv-ffi/tests/unit/test_async_operations.rs index 14c974ba6..60f94eb5f 100644 --- a/dash-spv-ffi/tests/unit/test_async_operations.rs +++ b/dash-spv-ffi/tests/unit/test_async_operations.rs @@ -269,7 +269,7 @@ mod tests { data.headers_stored.store(true, Ordering::SeqCst); } - extern "C" fn on_sync_complete(_header_tip: u32, user_data: *mut c_void) { + extern "C" fn on_sync_complete(_header_tip: u32, _cycle: u32, user_data: *mut c_void) { let data = unsafe { &*(user_data as *const EventData) }; data.sync_complete.store(true, Ordering::SeqCst); } diff --git a/dash-spv/src/sync/events.rs b/dash-spv/src/sync/events.rs index 11c9f66ac..c5da811b2 100644 --- a/dash-spv/src/sync/events.rs +++ b/dash-spv/src/sync/events.rs @@ -144,11 +144,17 @@ pub enum SyncEvent { /// Sync has reached the chain tip (all managers idle). /// + /// Emitted on every not-synced to synced transition. Cycle 0 is the + /// initial sync while subsequent cycles are incremental syncs triggered by + /// new blocks arriving from the network. + /// /// Emitted by: Coordinator /// Consumed by: External listeners SyncComplete { /// Final header tip height header_tip: u32, + /// Sync cycle (0 = initial, 1+ = incremental) + cycle: u32, }, } @@ -235,8 +241,9 @@ impl SyncEvent { } SyncEvent::SyncComplete { header_tip, + cycle, } => { - format!("SyncComplete(tip={})", header_tip) + format!("SyncComplete(tip={}, cycle={})", header_tip, cycle) } } } diff --git a/dash-spv/src/sync/sync_coordinator.rs b/dash-spv/src/sync/sync_coordinator.rs index c32fb3cfa..31e409377 100644 --- a/dash-spv/src/sync/sync_coordinator.rs +++ b/dash-spv/src/sync/sync_coordinator.rs @@ -339,7 +339,9 @@ async fn run_progress_task( let mut merged = select_all(streams); let mut progress = SyncProgress::default(); - let mut sync_complete_emitted = false; + let mut was_synced = false; + let mut sync_cycle: u32 = 0; + let mut cycle_start = sync_start_time; loop { tokio::select! { @@ -349,14 +351,18 @@ async fn run_progress_task( let _ = progress_sender.send(progress.clone()); - if progress.is_synced() && !sync_complete_emitted { - let duration = sync_start_time.elapsed(); - tracing::info!("Initial sync complete in {:.2}s", duration.as_secs_f64()); + let is_synced = progress.is_synced(); + if is_synced && !was_synced { + let duration = cycle_start.elapsed(); + tracing::info!("Sync complete in {:.2}s (cycle {})", duration.as_secs_f64(), sync_cycle); let header_tip = progress.headers().ok().map(|h| h.current_height()).unwrap_or(0); - let _ = sync_event_sender.send(SyncEvent::SyncComplete { header_tip }); - sync_complete_emitted = true; + let _ = sync_event_sender.send(SyncEvent::SyncComplete { header_tip, cycle: sync_cycle }); + sync_cycle += 1; + } else if !is_synced && was_synced { + cycle_start = Instant::now(); } + was_synced = is_synced; } } }