@@ -73,10 +73,24 @@ const EPOCH_1: Epoch = Epoch { epoch_id: 1, start_block: BlockNumber(1), epoch_l
7373const EPOCH_2 : Epoch = Epoch { epoch_id : 2 , start_block : BlockNumber ( 1001 ) , epoch_length : 1000 } ;
7474const EPOCH_3 : Epoch = Epoch { epoch_id : 3 , start_block : BlockNumber ( 2001 ) , epoch_length : 1000 } ;
7575
76+ // Epochs shorter than MIN_EPOCH_LENGTH, matching the config-backed mock contract's epochs.
77+ const SHORT_EPOCH_LENGTH : u64 = 30 ;
78+ const SHORT_EPOCH_4 : Epoch =
79+ Epoch { epoch_id : 4 , start_block : BlockNumber ( 120 ) , epoch_length : SHORT_EPOCH_LENGTH } ;
80+ const SHORT_EPOCH_5 : Epoch =
81+ Epoch { epoch_id : 5 , start_block : BlockNumber ( 150 ) , epoch_length : SHORT_EPOCH_LENGTH } ;
82+ const SHORT_EPOCH_6 : Epoch =
83+ Epoch { epoch_id : 6 , start_block : BlockNumber ( 180 ) , epoch_length : SHORT_EPOCH_LENGTH } ;
84+ const SHORT_EPOCH_7 : Epoch =
85+ Epoch { epoch_id : 7 , start_block : BlockNumber ( 210 ) , epoch_length : SHORT_EPOCH_LENGTH } ;
86+
7687const E1_H1 : BlockNumber = EPOCH_1 . start_block ;
7788const E1_H2 : BlockNumber = BlockNumber ( EPOCH_1 . start_block . 0 + EPOCH_1 . epoch_length - 1 ) ;
7889const E2_H1 : BlockNumber = EPOCH_2 . start_block ;
79- const E2_H2 : BlockNumber = BlockNumber ( EPOCH_2 . start_block . 0 + MIN_EPOCH_LENGTH + 1 ) ;
90+ // The last height within the next epoch's min bounds, and the first height past them.
91+ const E2_H_LAST_WITHIN_MIN_BOUNDS : BlockNumber =
92+ BlockNumber ( EPOCH_2 . start_block . 0 + MIN_EPOCH_LENGTH - 1 ) ;
93+ const E2_H2 : BlockNumber = BlockNumber ( EPOCH_2 . start_block . 0 + MIN_EPOCH_LENGTH ) ;
8094const E3_H1 : BlockNumber = EPOCH_3 . start_block ;
8195
8296fn test_config_with_committee_size ( committee_size : usize ) -> StakingManagerDynamicConfig {
@@ -362,7 +376,10 @@ async fn get_committee_for_next_epoch(
362376 let committee = committee_manager. get_committee ( E2_H1 ) . await . unwrap ( ) . members ( ) . clone ( ) ;
363377 assert_eq ! ( committee. into_iter( ) . collect:: <HashSet <_>>( ) , HashSet :: from( [ STAKER_1 , STAKER_2 ] ) ) ;
364378
365- // 2. Invalid Query: E2_H2 exceeds the min bounds of the next epoch.
379+ // 2. Valid Query: the last height within the next epoch's min bounds.
380+ assert ! ( committee_manager. get_committee( E2_H_LAST_WITHIN_MIN_BOUNDS ) . await . is_ok( ) ) ;
381+
382+ // 3. Invalid Query: E2_H2 exceeds the min bounds of the next epoch.
366383 // Since the next epoch's length is not known at this point, we cannot know if this height
367384 // belongs to Epoch 2 or a future Epoch > 2.
368385 let err = committee_manager. get_committee ( E2_H2 ) . await . err ( ) . unwrap ( ) ;
@@ -936,3 +953,61 @@ async fn get_actual_proposer_invalid_height(
936953 let err = committee_manager. get_committee ( E2_H2 ) . await . err ( ) . unwrap ( ) ;
937954 assert_matches ! ( err, CommitteeProviderError :: InvalidHeight { .. } ) ;
938955}
956+
957+ #[ rstest]
958+ #[ tokio:: test]
959+ async fn get_committee_short_epochs_override_activates_at_start_epoch (
960+ default_config : StakingManagerConfig ,
961+ mut contract : MockStakingContract ,
962+ ) {
963+ // Regression test: with epochs shorter than MIN_EPOCH_LENGTH, a node whose epoch cache
964+ // was synced a few epochs before an override's start_epoch must still apply the override
965+ // exactly from the override's first height, rather than serving the default committee
966+ // under a stale epoch ID.
967+ set_current_epoch ( & mut contract, SHORT_EPOCH_5 ) ;
968+ set_current_epoch ( & mut contract, SHORT_EPOCH_6 ) ;
969+ set_previous_epoch ( & mut contract, Some ( SHORT_EPOCH_4 ) ) ;
970+ set_stakers ( & mut contract, SHORT_EPOCH_6 , vec ! [ STAKER_1 , STAKER_2 , STAKER_3 ] ) ;
971+ set_stakers ( & mut contract, SHORT_EPOCH_7 , vec ! [ STAKER_1 , STAKER_2 , STAKER_3 ] ) ;
972+
973+ let default_committee = CommitteeConfig {
974+ start_epoch : 0 ,
975+ committee_size : 3 ,
976+ stakers : vec ! [
977+ create_configured_staker( & STAKER_1 , true ) ,
978+ create_configured_staker( & STAKER_2 , true ) ,
979+ create_configured_staker( & STAKER_3 , true ) ,
980+ ] ,
981+ } ;
982+ // Shrink the committee to 2 members starting at epoch 7.
983+ let override_committee = Some ( CommitteeConfig {
984+ start_epoch : SHORT_EPOCH_7 . epoch_id ,
985+ committee_size : 2 ,
986+ stakers : vec ! [
987+ create_configured_staker( & STAKER_2 , true ) ,
988+ create_configured_staker( & STAKER_3 , true ) ,
989+ ] ,
990+ } ) ;
991+
992+ let committee_manager = StakingManager :: new (
993+ Arc :: new ( contract) ,
994+ Arc :: new ( create_batcher_client_with_block_hash ( ) ) ,
995+ Arc :: new ( create_state_sync_client_with_block_hash ( ) ) ,
996+ Arc :: new ( make_generator_factory ( 0 ) ) ,
997+ StakingManagerConfig {
998+ dynamic_config : StakingManagerDynamicConfig { default_committee, override_committee } ,
999+ ..default_config
1000+ } ,
1001+ None ,
1002+ ) ;
1003+
1004+ // The last height of epoch 6 primes the epoch cache at epoch 5 and still gets the
1005+ // default committee.
1006+ let last_height_before_override = BlockNumber ( SHORT_EPOCH_7 . start_block . 0 - 1 ) ;
1007+ let committee = committee_manager. get_committee ( last_height_before_override) . await . unwrap ( ) ;
1008+ assert_eq ! ( * committee. members( ) , vec![ STAKER_3 , STAKER_2 , STAKER_1 ] ) ;
1009+
1010+ // The first height of epoch 7 must already get the override committee.
1011+ let committee = committee_manager. get_committee ( SHORT_EPOCH_7 . start_block ) . await . unwrap ( ) ;
1012+ assert_eq ! ( * committee. members( ) , vec![ STAKER_3 , STAKER_2 ] ) ;
1013+ }
0 commit comments