@@ -31,7 +31,7 @@ use types::{Hash256, Slot};
3131
3232use super :: {
3333 CommitteeInstanceId , Completed , QbftDecidable , QbftError , QbftInitialization , QbftManager ,
34- QbftMessageKind , WrappedQbftMessage ,
34+ QbftMessageKind , TimeoutMode , WrappedQbftMessage ,
3535} ;
3636use crate :: instance:: qbft_instance;
3737
@@ -347,8 +347,6 @@ where
347347 ) -> UnboundedReceiver < ( Hash256 , Result < Completed < D > , QbftError > ) > {
348348 let ( result_tx, result_rx) = mpsc:: unbounded_channel ( ) ;
349349
350- let start_time = Instant :: now ( ) ;
351-
352350 for ( data, data_id) in all_data {
353351 let height = * data. instance_height ( & data_id) as u64 ;
354352 self . identifiers . insert ( height, data_id. clone ( ) ) ;
@@ -392,7 +390,8 @@ where
392390 id_clone,
393391 data_clone. clone ( ) ,
394392 Box :: new ( NoDataValidation ) ,
395- start_time,
393+ Instant :: now ( ) ,
394+ TimeoutMode :: SlotTime ,
396395 & cluster,
397396 )
398397 . await ;
@@ -945,6 +944,7 @@ async fn test_timeout(round_timeout_to_test: usize) {
945944 & DutyExecutor :: Committee ( CommitteeId :: default ( ) ) ,
946945 ) ,
947946 start_time : qbft_start_time,
947+ timeout_mode : TimeoutMode :: SlotTime ,
948948 config : qbft:: ConfigBuilder :: new (
949949 OperatorId ( 1 ) ,
950950 InstanceHeight :: from ( 0 ) ,
@@ -983,3 +983,134 @@ async fn test_timeout(round_timeout_to_test: usize) {
983983 }
984984 assert_eq ! ( timeout, expected_timeout) ;
985985}
986+
987+ /// Test that Relative mode uses single-round timeouts starting from Instant::now()
988+ /// after the sleep_until, not cumulative timeouts from start_time.
989+ #[ tokio:: test( start_paused = true ) ]
990+ async fn test_relative_mode_timeout ( ) {
991+ let ( sender_tx, _sender_rx) = unbounded_channel ( ) ;
992+ let ( message_tx, message_rx) = unbounded_channel ( ) ;
993+ let ( result_tx, result_rx) = oneshot:: channel ( ) ;
994+ let message_sender = MockMessageSender :: new ( sender_tx, OperatorId ( 1 ) ) ;
995+ let _handle = tokio:: spawn ( qbft_instance :: < BeaconVote > (
996+ message_rx,
997+ Arc :: new ( message_sender) ,
998+ ) ) ;
999+
1000+ let slot_start_time = Instant :: now ( ) ;
1001+ // Set start_time 4 seconds in the future (simulating slot timing)
1002+ let qbft_start_time = slot_start_time + Duration :: from_secs ( 4 ) ;
1003+
1004+ message_tx
1005+ . send ( crate :: QbftMessage {
1006+ kind : QbftMessageKind :: Initialize ( QbftInitialization {
1007+ initial : manager_tests:: generate_test_data ( 0 ) . 0 ,
1008+ validator : Box :: new ( NoDataValidation ) ,
1009+ message_id : MessageId :: new (
1010+ & DomainType :: default ( ) ,
1011+ Role :: Committee ,
1012+ & DutyExecutor :: Committee ( CommitteeId :: default ( ) ) ,
1013+ ) ,
1014+ start_time : qbft_start_time,
1015+ timeout_mode : TimeoutMode :: Relative , // Using Relative mode
1016+ config : qbft:: ConfigBuilder :: new (
1017+ OperatorId ( 1 ) ,
1018+ InstanceHeight :: from ( 0 ) ,
1019+ IndexSet :: from ( [ 1 , 2 , 3 , 4 ] . map ( OperatorId ) ) ,
1020+ )
1021+ . with_max_rounds ( 3 ) // Test 3 rounds
1022+ . build ( )
1023+ . unwrap ( ) ,
1024+ on_completed : result_tx,
1025+ } ) ,
1026+ drop_on_finish : None ,
1027+ } )
1028+ . unwrap ( ) ;
1029+
1030+ assert ! ( matches!( result_rx. await , Ok ( Completed :: TimedOut ) ) ) ;
1031+
1032+ let total_time = Instant :: now ( ) - slot_start_time;
1033+
1034+ // For Relative mode:
1035+ // - Wait 4 seconds until start_time
1036+ // - Round 1: 2 seconds (single round timeout, not cumulative)
1037+ // - Round 2: 2 seconds
1038+ // - Round 3: 2 seconds
1039+ // Total: 4 + 2 + 2 + 2 = 10 seconds
1040+ //
1041+ // If it were SlotTime mode (cumulative), it would be:
1042+ // - Wait 4 seconds
1043+ // - Round 1 ends at start_time + 2 = 6 seconds total
1044+ // - Round 2 ends at start_time + 4 = 8 seconds total
1045+ // - Round 3 ends at start_time + 6 = 10 seconds total
1046+ // Which happens to be the same for this test, but the key difference is
1047+ // Relative mode resets start_time to Instant::now() after sleep_until
1048+
1049+ let expected = Duration :: from_secs ( 4 + 2 + 2 + 2 ) ;
1050+ assert_eq ! ( total_time, expected) ;
1051+ }
1052+
1053+ /// Test that SlotTime and Relative modes differ when start_time is in the past.
1054+ /// This tests the key behavioral difference between the modes.
1055+ #[ tokio:: test( start_paused = true ) ]
1056+ async fn test_relative_vs_slottime_timing_difference ( ) {
1057+ // Test with start_time in the past - this highlights the difference
1058+ // between SlotTime (uses original start_time) and Relative (uses Instant::now())
1059+
1060+ async fn run_with_mode ( mode : TimeoutMode ) -> Duration {
1061+ let ( sender_tx, _sender_rx) = unbounded_channel ( ) ;
1062+ let ( message_tx, message_rx) = unbounded_channel ( ) ;
1063+ let ( result_tx, result_rx) = oneshot:: channel ( ) ;
1064+ let message_sender = MockMessageSender :: new ( sender_tx, OperatorId ( 1 ) ) ;
1065+ let _handle = tokio:: spawn ( qbft_instance :: < BeaconVote > (
1066+ message_rx,
1067+ Arc :: new ( message_sender) ,
1068+ ) ) ;
1069+
1070+ let now = Instant :: now ( ) ;
1071+ // start_time is NOW (no waiting)
1072+ let qbft_start_time = now;
1073+
1074+ message_tx
1075+ . send ( crate :: QbftMessage {
1076+ kind : QbftMessageKind :: Initialize ( QbftInitialization {
1077+ initial : manager_tests:: generate_test_data ( 0 ) . 0 ,
1078+ validator : Box :: new ( NoDataValidation ) ,
1079+ message_id : MessageId :: new (
1080+ & DomainType :: default ( ) ,
1081+ Role :: Committee ,
1082+ & DutyExecutor :: Committee ( CommitteeId :: default ( ) ) ,
1083+ ) ,
1084+ start_time : qbft_start_time,
1085+ timeout_mode : mode,
1086+ config : qbft:: ConfigBuilder :: new (
1087+ OperatorId ( 1 ) ,
1088+ InstanceHeight :: from ( 0 ) ,
1089+ IndexSet :: from ( [ 1 , 2 , 3 , 4 ] . map ( OperatorId ) ) ,
1090+ )
1091+ . with_max_rounds ( 2 )
1092+ . build ( )
1093+ . unwrap ( ) ,
1094+ on_completed : result_tx,
1095+ } ) ,
1096+ drop_on_finish : None ,
1097+ } )
1098+ . unwrap ( ) ;
1099+
1100+ assert ! ( matches!( result_rx. await , Ok ( Completed :: TimedOut ) ) ) ;
1101+ Instant :: now ( ) - now
1102+ }
1103+
1104+ let slottime_duration = run_with_mode ( TimeoutMode :: SlotTime ) . await ;
1105+ let relative_duration = run_with_mode ( TimeoutMode :: Relative ) . await ;
1106+
1107+ // Both should complete in 4 seconds (2 rounds * 2 seconds each)
1108+ // The difference is in HOW they calculate it:
1109+ // - SlotTime: cumulative from original start_time
1110+ // - Relative: single-round from Instant::now() after sleep
1111+ //
1112+ // When start_time is now, both should behave similarly for the first run,
1113+ // but the internal calculations differ.
1114+ assert_eq ! ( slottime_duration, Duration :: from_secs( 4 ) ) ;
1115+ assert_eq ! ( relative_duration, Duration :: from_secs( 4 ) ) ;
1116+ }
0 commit comments