@@ -610,3 +610,37 @@ fn number_of_required_votes(quorum_type: QuorumType) {
610610 ) ;
611611 assert ! ( wrapper. next_request( ) . is_none( ) ) ;
612612}
613+
614+ #[ test]
615+ fn observer_does_not_record_self_votes ( ) {
616+ // Set up as an observer.
617+ let id = * VALIDATOR_ID ;
618+ let mut wrapper = TestWrapper :: new ( id, 4 , |_: Round | * PROPOSER_ID , true , QuorumType :: Byzantine ) ;
619+
620+ // Start and receive proposal validation completion.
621+ wrapper. start ( ) ;
622+ assert_eq ! ( wrapper. next_request( ) . unwrap( ) , SMRequest :: ScheduleTimeoutPropose ( ROUND ) ) ;
623+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
624+ wrapper. send_finished_validation ( PROPOSAL_ID , ROUND ) ;
625+
626+ // Reach mixed prevote quorum with peer votes only (self not counted).
627+ wrapper. send_prevote ( PROPOSAL_ID , ROUND ) ;
628+ wrapper. send_prevote ( PROPOSAL_ID , ROUND ) ;
629+ // No quorum yet, we didn't vote.
630+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
631+ wrapper. send_prevote ( PROPOSAL_ID , ROUND ) ;
632+ assert_eq ! ( wrapper. next_request( ) . unwrap( ) , SMRequest :: ScheduleTimeoutPrevote ( ROUND ) ) ;
633+
634+ // Timeout prevote triggers self precommit(nil) path, which observers must not record/broadcast.
635+ wrapper. send_timeout_prevote ( ROUND ) ;
636+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
637+ assert_eq ! ( wrapper. state_machine. last_self_precommit( ) , None ) ;
638+
639+ // Reach mixed precommit quorum with peer votes only and ensure timeout is scheduled.
640+ wrapper. send_precommit ( PROPOSAL_ID , ROUND ) ;
641+ wrapper. send_precommit ( PROPOSAL_ID , ROUND ) ;
642+ // No quorum yet, we didn't vote.
643+ assert ! ( wrapper. next_request( ) . is_none( ) ) ;
644+ wrapper. send_precommit ( PROPOSAL_ID , ROUND ) ;
645+ assert_eq ! ( wrapper. next_request( ) . unwrap( ) , SMRequest :: ScheduleTimeoutPrecommit ( ROUND ) ) ;
646+ }
0 commit comments