Skip to content

Commit 8d8b6b3

Browse files
committed
fix: require height consistency in is_synced() to prevent race
When new blocks arrive after initial sync, there is a window between a manager completing and downstream managers processing the event. During this window all manager states show Synced but heights are inconsistent. Add checks that filter headers have caught up to block headers and filters have caught up to filter headers.
1 parent 54c07ef commit 8d8b6b3

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

dash-spv/src/sync/progress.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ impl SyncProgress {
7878
}
7979

8080
/// Check if all managers are idle (sync complete).
81+
///
82+
/// Each manager must report `Synced` and downstream managers must have caught
83+
/// up to their upstream counterparts. This prevents a false positive in the
84+
/// window between an upstream manager completing and the downstream manager
85+
/// processing the resulting event.
8186
pub fn is_synced(&self) -> bool {
8287
let states: Vec<SyncState> = [
8388
self.headers.as_ref().map(|h| h.state()),
@@ -95,7 +100,25 @@ impl SyncProgress {
95100
return false;
96101
}
97102

98-
states.iter().all(|state| *state == SyncState::Synced)
103+
if !states.iter().all(|state| *state == SyncState::Synced) {
104+
return false;
105+
}
106+
107+
// Filter headers must have caught up to block headers
108+
if let (Some(h), Some(fh)) = (&self.headers, &self.filter_headers) {
109+
if fh.current_height() < h.current_height() {
110+
return false;
111+
}
112+
}
113+
114+
// Filters must have caught up to filter headers
115+
if let (Some(fh), Some(fl)) = (&self.filter_headers, &self.filters) {
116+
if fl.current_height() < fh.current_height() {
117+
return false;
118+
}
119+
}
120+
121+
true
99122
}
100123

101124
/// Get overall completion percentage (0.0 to 1.0).

dash-spv/src/sync/sync_coordinator.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,64 @@ mod tests {
393393
assert_eq!(progress.percentage(), 1.0);
394394
}
395395

396+
#[test]
397+
fn test_is_synced_requires_height_consistency() {
398+
use crate::sync::{BlocksProgress, FilterHeadersProgress, MasternodesProgress};
399+
400+
let mut progress = SyncProgress::default();
401+
402+
// Set up all managers as Synced at height 1000
403+
let mut h = BlockHeadersProgress::default();
404+
h.set_state(SyncState::Synced);
405+
h.update_current_height(1000);
406+
h.update_target_height(1000);
407+
progress.update_headers(h);
408+
409+
let mut fh = FilterHeadersProgress::default();
410+
fh.set_state(SyncState::Synced);
411+
fh.update_current_height(1000);
412+
fh.update_target_height(1000);
413+
progress.update_filter_headers(fh);
414+
415+
let mut fl = FiltersProgress::default();
416+
fl.set_state(SyncState::Synced);
417+
fl.update_current_height(1000);
418+
fl.update_target_height(1000);
419+
progress.update_filters(fl);
420+
421+
let mut b = BlocksProgress::default();
422+
b.set_state(SyncState::Synced);
423+
progress.update_blocks(b);
424+
425+
let mut m = MasternodesProgress::default();
426+
m.set_state(SyncState::Synced);
427+
progress.update_masternodes(m);
428+
429+
assert!(progress.is_synced(), "should be synced when all heights match");
430+
431+
// Simulate filter headers advancing to 1006 (new blocks arrived)
432+
// while filters haven't processed the event yet
433+
let mut fh = FilterHeadersProgress::default();
434+
fh.set_state(SyncState::Synced);
435+
fh.update_current_height(1006);
436+
fh.update_target_height(1006);
437+
progress.update_filter_headers(fh);
438+
439+
assert!(
440+
!progress.is_synced(),
441+
"should not be synced when filters lag behind filter headers"
442+
);
443+
444+
// Now filters catch up
445+
let mut fl = FiltersProgress::default();
446+
fl.set_state(SyncState::Synced);
447+
fl.update_current_height(1006);
448+
fl.update_target_height(1006);
449+
progress.update_filters(fl);
450+
451+
assert!(progress.is_synced(), "should be synced after filters catch up");
452+
}
453+
396454
#[test]
397455
fn test_sync_percentage() {
398456
let mut progress = SyncProgress::default();

0 commit comments

Comments
 (0)