@@ -658,3 +658,40 @@ fn number_of_required_votes(quorum_type: QuorumType) {
658658 ) ;
659659 assert ! ( wrapper. next_request( ) . is_none( ) ) ;
660660}
661+
662+ #[ test]
663+ fn observer_does_not_record_self_votes ( ) {
664+ // Set up as an observer.
665+ let id = * VALIDATOR_ID ;
666+ let mut wrapper = TestWrapper :: new ( id, 4 , |_: Round | * PROPOSER_ID , true , QuorumType :: Byzantine ) ;
667+
668+ // Start and receive proposal validation completion.
669+ wrapper. start ( ) ;
670+ assert_eq ! ( wrapper. next_request( ) . unwrap( ) , SMRequest :: ScheduleTimeoutPropose { round: ROUND } ) ;
671+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
672+ wrapper. send_finished_validation ( PROPOSAL_ID , ROUND ) ;
673+
674+ // Reach mixed prevote quorum with peer votes only (self not counted).
675+ wrapper. send_prevote ( PROPOSAL_ID , ROUND ) ;
676+ wrapper. send_prevote ( PROPOSAL_ID , ROUND ) ;
677+ // No quorum yet, we didn't vote.
678+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
679+ wrapper. send_prevote ( PROPOSAL_ID , ROUND ) ;
680+ assert_eq ! ( wrapper. next_request( ) . unwrap( ) , SMRequest :: ScheduleTimeoutPrevote { round: ROUND } ) ;
681+
682+ // Timeout prevote triggers self precommit(nil) path, which observers must not record/broadcast.
683+ wrapper. send_timeout_prevote ( ROUND ) ;
684+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
685+ assert_eq ! ( wrapper. state_machine. last_self_precommit( ) , None ) ;
686+
687+ // Reach mixed precommit quorum with peer votes only and ensure timeout is scheduled.
688+ wrapper. send_precommit ( PROPOSAL_ID , ROUND ) ;
689+ wrapper. send_precommit ( PROPOSAL_ID , ROUND ) ;
690+ // No quorum yet, we didn't vote.
691+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
692+ wrapper. send_precommit ( PROPOSAL_ID , ROUND ) ;
693+ assert_eq ! (
694+ wrapper. next_request( ) . unwrap( ) ,
695+ SMRequest :: ScheduleTimeoutPrecommit { round: ROUND }
696+ ) ;
697+ }
0 commit comments