@@ -63,6 +63,11 @@ impl ProposerSelector {
6363 ///
6464 /// This method advances the internal priority state to match the target
6565 /// (height, round) and returns the validator with highest priority.
66+ ///
67+ /// **IMPORTANT**: To ensure all nodes agree on the proposer regardless of their
68+ /// starting point (initial sync vs. running from genesis), advances are computed
69+ /// from the ABSOLUTE position (genesis height 1, round Nil), not from the node's
70+ /// `initial_height`. This ensures deterministic proposer selection across all nodes.
6671 pub fn select_proposer < ' a > (
6772 & self ,
6873 validator_set : & ' a ConsensusValidatorSet ,
@@ -76,8 +81,17 @@ impl ProposerSelector {
7681
7782 let ( last_height, last_round) = * last_synced;
7883
79- // Calculate total advances needed from last synced point to (height, round)
80- let advances = Self :: compute_advances ( last_height, last_round, height, round) ;
84+ // Calculate total advances needed from last synced point to (height, round).
85+ // If this is the first call (last_synced is at initial_height with Nil round),
86+ // we compute advances from GENESIS (height 1, Nil) to ensure all nodes get
87+ // the same result regardless of their starting height.
88+ let advances = if last_round == Round :: Nil && last_proposer. is_none ( ) {
89+ // First call: compute absolute advances from genesis
90+ Self :: compute_advances ( ConsensusHeight ( 1 ) , Round :: Nil , height, round)
91+ } else {
92+ // Subsequent calls: compute relative advances from last synced point
93+ Self :: compute_advances ( last_height, last_round, height, round)
94+ } ;
8195
8296 // Track the proposer from advances (the proposer is determined DURING advance,
8397 // not after, because advance_one_round penalizes the proposer)
@@ -461,4 +475,52 @@ mod tests {
461475 ) ;
462476 assert_eq ! ( advances, 4 , "(1, 0) -> (5, 0) should be 4 advances" ) ;
463477 }
478+
479+ #[ test]
480+ fn test_different_initial_heights_agree_on_proposer ( ) {
481+ // CRITICAL: Nodes that start at different initial heights (e.g., after syncing)
482+ // MUST agree on the proposer for the same (height, round).
483+ // This is the root cause of consensus stalls when nodes disagree on proposer.
484+ let vs = make_validator_set ( & [ 100 , 100 , 100 ] ) ;
485+
486+ // Node A: started from genesis (height 1)
487+ let selector_from_genesis = ProposerSelector :: new ( & vs, ConsensusHeight ( 1 ) ) ;
488+
489+ // Node B: synced and started at height 34 (simulating a node that joined later)
490+ let selector_synced = ProposerSelector :: new ( & vs, ConsensusHeight ( 34 ) ) ;
491+
492+ // Both query for the proposer at height 34, round 6
493+ let proposer_genesis = selector_from_genesis. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 6 ) ) ;
494+ let proposer_synced = selector_synced. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 6 ) ) ;
495+
496+ assert_eq ! (
497+ proposer_genesis. address, proposer_synced. address,
498+ "Nodes starting at different heights MUST agree on the proposer for the same (height, round)"
499+ ) ;
500+ }
501+
502+ #[ test]
503+ fn test_synced_node_continues_correctly ( ) {
504+ // After initial query, synced node should continue correctly for subsequent rounds
505+ let vs = make_validator_set ( & [ 100 , 100 , 100 ] ) ;
506+
507+ // Genesis node goes through all heights
508+ let selector_genesis = ProposerSelector :: new ( & vs, ConsensusHeight ( 1 ) ) ;
509+ for h in 1 ..34 {
510+ selector_genesis. select_proposer ( & vs, ConsensusHeight ( h) , Round :: new ( 0 ) ) ;
511+ }
512+ let gen_h34_r0 = selector_genesis. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 0 ) ) ;
513+ let gen_h34_r1 = selector_genesis. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 1 ) ) ;
514+ let gen_h34_r2 = selector_genesis. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 2 ) ) ;
515+
516+ // Synced node starts at height 34
517+ let selector_synced = ProposerSelector :: new ( & vs, ConsensusHeight ( 34 ) ) ;
518+ let sync_h34_r0 = selector_synced. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 0 ) ) ;
519+ let sync_h34_r1 = selector_synced. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 1 ) ) ;
520+ let sync_h34_r2 = selector_synced. select_proposer ( & vs, ConsensusHeight ( 34 ) , Round :: new ( 2 ) ) ;
521+
522+ assert_eq ! ( gen_h34_r0. address, sync_h34_r0. address, "Must agree on round 0" ) ;
523+ assert_eq ! ( gen_h34_r1. address, sync_h34_r1. address, "Must agree on round 1" ) ;
524+ assert_eq ! ( gen_h34_r2. address, sync_h34_r2. address, "Must agree on round 2" ) ;
525+ }
464526}
0 commit comments