@@ -115,39 +115,40 @@ impl OperatorDoppelgangerService {
115115 }
116116
117117 /// Spawn a background task to end monitoring after the configured wait period
118- ///
119- /// The task sleeps for the grace period plus the monitoring duration, then transitions
120- /// to active mode.
121- ///
122- /// # Arguments
123- ///
124- /// * `grace_period` - Duration to wait before starting twin detection. Should be slightly
125- /// longer than the gossip message cache window (history_length × heartbeat_interval ≈ 4.2s)
126- /// to ensure our own old messages have expired from the network. See `DoppelgangerState`
127- /// documentation for details on why this prevents false positives.
128- /// * `wait_epochs` - Number of epochs to monitor for doppelgängers after grace period ends
129- pub fn spawn_monitor_task (
130- self : Arc < Self > ,
131- grace_period : Duration ,
132- wait_epochs : u64 ,
133- executor : & TaskExecutor ,
134- ) {
135- // Calculate monitoring duration (after grace period)
136- let monitoring_duration =
137- Duration :: from_secs ( wait_epochs * self . slots_per_epoch * self . slot_duration . as_secs ( ) ) ;
118+ pub fn spawn_monitor_task ( self : Arc < Self > , wait_epochs : u64 , executor : & TaskExecutor ) {
119+ // Grace period must match the full message TTL window to prevent false positives
120+ // from late-arriving messages after restart.
121+ //
122+ // Full TTL = (slots_per_epoch + LATE_SLOT_ALLOWANCE) × slot_duration + LATE_MESSAGE_MARGIN
123+ // This matches the complete deadline calculation in message validation.
124+ let grace_period_slots = self . slots_per_epoch + message_validator:: LATE_SLOT_ALLOWANCE ;
125+ let grace_period_secs = grace_period_slots * self . slot_duration . as_secs ( ) ;
126+ let grace_period =
127+ Duration :: from_secs ( grace_period_secs) + message_validator:: LATE_MESSAGE_MARGIN ;
128+
129+ let monitoring_slots = wait_epochs * self . slots_per_epoch ;
130+ let monitoring_secs = monitoring_slots * self . slot_duration . as_secs ( ) ;
131+ let monitoring_duration = Duration :: from_secs ( monitoring_secs) ;
138132
139133 executor. spawn_without_exit (
140134 async move {
141- // Wait for grace period - prevents false positives from own old messages
135+ info ! (
136+ grace_period_slots = grace_period_slots,
137+ grace_period_secs = grace_period. as_secs( ) ,
138+ "Operator doppelgänger: entering grace period"
139+ ) ;
142140 tokio:: time:: sleep ( grace_period) . await ;
143141
144- // Grace period complete - start detecting twins
142+ info ! (
143+ monitoring_epochs = wait_epochs,
144+ monitoring_secs = monitoring_duration. as_secs( ) ,
145+ "Operator doppelgänger: grace period complete, starting monitoring"
146+ ) ;
145147 self . state . write ( ) . end_grace_period ( ) ;
146148
147- // Wait for monitoring period to complete
148149 tokio:: time:: sleep ( monitoring_duration) . await ;
149150
150- // Monitoring complete - stop checking for doppelgängers
151+ info ! ( "Operator doppelgänger: monitoring period complete" ) ;
151152 self . end_monitoring_period ( ) ;
152153 } ,
153154 "doppelganger-monitor" ,
@@ -273,25 +274,26 @@ mod tests {
273274 TaskExecutor :: new ( handle, exit, shutdown_tx, "doppelganger_test" . into ( ) )
274275 }
275276
276- /// Helper to spawn monitor task and advance time past grace period
277- ///
278- /// Returns the service in monitoring mode with grace period complete,
279- /// ready for twin detection tests.
280277 async fn spawn_and_advance_past_grace_period (
281278 service : Arc < OperatorDoppelgangerService > ,
282279 executor : & TaskExecutor ,
283280 ) {
284- let grace_period = Duration :: from_secs ( 5 ) ;
285281 let wait_epochs = 2 ;
286282
287283 // Spawn monitor task
288- service
289- . clone ( )
290- . spawn_monitor_task ( grace_period, wait_epochs, executor) ;
284+ service. clone ( ) . spawn_monitor_task ( wait_epochs, executor) ;
291285
292286 // Give the spawned task a chance to start
293287 tokio:: task:: yield_now ( ) . await ;
294288
289+ // Calculate grace period from service configuration
290+ // Grace period = (slots_per_epoch + LATE_SLOT_ALLOWANCE) × slot_duration +
291+ // LATE_MESSAGE_MARGIN
292+ let grace_period_slots = service. slots_per_epoch + message_validator:: LATE_SLOT_ALLOWANCE ;
293+ let grace_period =
294+ Duration :: from_secs ( grace_period_slots * service. slot_duration . as_secs ( ) )
295+ + message_validator:: LATE_MESSAGE_MARGIN ;
296+
295297 // Advance time past grace period
296298 tokio:: time:: advance ( grace_period) . await ;
297299
@@ -430,12 +432,9 @@ mod tests {
430432 let committee_id = CommitteeId ( [ 1u8 ; 32 ] ) ;
431433 let executor = create_test_executor ( ) ;
432434
433- // Spawn the monitor task with grace period
434- let grace_period = Duration :: from_secs ( 5 ) ;
435+ // Spawn the monitor task
435436 let wait_epochs = 2 ;
436- service
437- . clone ( )
438- . spawn_monitor_task ( grace_period, wait_epochs, & executor) ;
437+ service. clone ( ) . spawn_monitor_task ( wait_epochs, & executor) ;
439438
440439 // Still in grace period (don't advance time)
441440 assert ! ( !service. is_monitoring( ) ) ;
@@ -505,17 +504,23 @@ mod tests {
505504 let executor = create_test_executor ( ) ;
506505
507506 // Spawn the monitor task
508- let grace_period = Duration :: from_secs ( 5 ) ;
509507 let wait_epochs = 2 ;
510- service
511- . clone ( )
512- . spawn_monitor_task ( grace_period, wait_epochs, & executor) ;
508+ service. clone ( ) . spawn_monitor_task ( wait_epochs, & executor) ;
513509
514510 // Give the spawned task a chance to start
515511 tokio:: task:: yield_now ( ) . await ;
516512
517- // Calculate monitoring duration
518- let monitoring_duration = Duration :: from_secs ( wait_epochs * 12 ) ; // epochs * (slots_per_epoch=1) * (slot_duration=12s)
513+ // Calculate durations from service configuration
514+ // Grace period = (slots_per_epoch + LATE_SLOT_ALLOWANCE) × slot_duration +
515+ // LATE_MESSAGE_MARGIN
516+ let grace_period_slots = service. slots_per_epoch + message_validator:: LATE_SLOT_ALLOWANCE ;
517+ let grace_period =
518+ Duration :: from_secs ( grace_period_slots * service. slot_duration . as_secs ( ) )
519+ + message_validator:: LATE_MESSAGE_MARGIN ;
520+
521+ let monitoring_slots = wait_epochs * service. slots_per_epoch ;
522+ let monitoring_duration =
523+ Duration :: from_secs ( monitoring_slots * service. slot_duration . as_secs ( ) ) ;
519524
520525 // Advance time past grace period first
521526 tokio:: time:: advance ( grace_period) . await ;
@@ -532,11 +537,11 @@ mod tests {
532537 let ( signed_message, qbft_message) =
533538 create_test_message ( committee_id, vec ! [ OperatorId ( 1 ) ] , 10 , 0 ) ;
534539
535- // This should NOT detect a twin (monitoring period ended via timer)
540+ // This should NOT detect a twin (monitoring period completed via timer)
536541 let result = service. is_doppelganger ( & signed_message, Some ( & qbft_message) ) ;
537542 assert ! (
538543 !result,
539- "Message after monitoring period should NOT detect twin"
544+ "Message after monitoring period should NOT detect twin (monitoring complete) "
540545 ) ;
541546 }
542547}
0 commit comments