1- use std:: { collections:: HashMap , sync:: Arc } ;
1+ use std:: {
2+ collections:: { HashMap , HashSet } ,
3+ sync:: Arc ,
4+ } ;
25
36// Re-Exports for Manager
47pub use config:: { Config , ConfigBuilder } ;
@@ -229,6 +232,16 @@ where
229232 self . config . committee_members ( ) . contains ( operator_id)
230233 }
231234
235+ /// Checks if we have a quorum of unique committee operators from these messages.
236+ fn check_quorum < ' a > ( & self , msgs : impl IntoIterator < Item = & ' a SignedSSVMessage > ) -> bool {
237+ let unique_operators = msgs
238+ . into_iter ( )
239+ . flat_map ( |justification| justification. operator_ids ( ) )
240+ . filter ( |operator_id| self . check_committee ( operator_id) )
241+ . collect :: < HashSet < _ > > ( ) ;
242+ unique_operators. len ( ) >= self . config . quorum_size ( )
243+ }
244+
232245 // Perform base QBFT relevant message verification. This verfiication is applicable to all QBFT
233246 // message types
234247 // Return type expresses that we either have
@@ -465,7 +478,7 @@ where
465478 // received proposal
466479 if round > Round :: default ( ) {
467480 // validate the justifications
468- if !self . validate_justifications ( & wrapped_msg) {
481+ if !self . validate_proposal_justifications ( & wrapped_msg) {
469482 warn ! ( from = ?operator_id, "Justification validation failed for proposal" ) ;
470483 return ;
471484 }
@@ -517,27 +530,45 @@ where
517530 self . send_prepare ( wrapped_msg. qbft_message . root ) ;
518531 }
519532
520- // Validate the round change and prepare justifications. Returns true if the justifications
521- // correctly justify the proposal
533+ // Validate the round change and prepare justifications for proposal.
534+ // Returns true if the justifications correctly justify the proposal
522535 //
523536 // A QBFT Message contains fields to a list of round change justifications and prepare
524537 // justifications. We must go through each of these individually and verify the validity of each
525538 // one
526- fn validate_justifications ( & self , msg : & WrappedQbftMessage ) -> bool {
539+ //
540+ // Proposal
541+ // - round change justifications
542+ // - list of round change messages
543+ // - each round change message has list of prepare messages if it prepared a value
544+ // - prepare justifications
545+ // - list of prepare messages to
546+ fn validate_proposal_justifications ( & self , msg : & WrappedQbftMessage ) -> bool {
527547 // Record if any of the round change messages have a value that was prepared
528- let mut previously_prepared = false ;
529548 let mut max_prepared_round = 0 ;
530549 let mut max_prepared_msg = None ;
531550
532551 // Make sure we have a quorum of round change messages
533- if msg. qbft_message . round_change_justification . len ( ) < self . config . quorum_size ( ) {
552+ if ! self . check_quorum ( & msg. qbft_message . round_change_justification ) {
534553 warn ! ( "Did not receive a quorum of round change messages" ) ;
535554 return false ;
536555 }
537556
538557 // There was a quorum of round change justifications. We need to go though and verify each
539558 // one. Each will be a SignedSSVMessage
540559 for signed_round_change in & msg. qbft_message . round_change_justification {
560+ // Check for multi-signers - round change messages should only have 1 signer
561+ if signed_round_change. operator_ids ( ) . len ( ) > 1 {
562+ return false ;
563+ }
564+
565+ // make sure all signers in committee
566+ for signer in signed_round_change. operator_ids ( ) {
567+ if !self . check_committee ( signer) {
568+ return false ;
569+ }
570+ }
571+
541572 // The qbft message is represented as a Vec<u8> in the signed message, deserialize this
542573 // into a proper QbftMessage
543574 let round_change: QbftMessage =
@@ -552,34 +583,69 @@ where
552583 return false ;
553584 }
554585
555- // Convert to a wrapped message and perform verification
556- let wrapped = WrappedQbftMessage {
557- signed_message : signed_round_change. clone ( ) ,
558- qbft_message : round_change. clone ( ) ,
559- } ;
560-
561- if self . validate_message ( & wrapped) . is_none ( ) {
562- warn ! ( "ROUNDCHANGE message validation failed" ) ;
586+ // make sure the round change matches the round of the message
587+ if round_change. round != msg. qbft_message . round {
563588 return false ;
564589 }
565590
566- // If the data_round > 1, that means we have prepared a value in previous rounds
567- if round_change. data_round > 1 {
568- previously_prepared = true ;
591+ // For round change justifications, we need special validation that doesn't check
592+ // against current round since they're justifications from the proposal's round
593+ // Check height
594+ if round_change. height != * self . instance_height as u64 {
595+ return false ;
596+ }
569597
598+ // If the data_round > 0, that means we have prepared a value in previous rounds
599+ // We also have to go through all of the prepare justifications in the round change to
600+ // ensure that they are well formed and properly justify the prepared value
601+ if round_change. data_round > 0 {
570602 // also track the max prepared value and round
571603 if round_change. data_round > max_prepared_round {
572604 max_prepared_round = round_change. data_round ;
573- max_prepared_msg = Some ( round_change) ;
605+ max_prepared_msg = Some ( round_change. clone ( ) ) ;
606+ }
607+
608+ // Check that prepared round is not greater than current round
609+ if round_change. data_round > round_change. round {
610+ warn ! (
611+ "Round change has prepared round {} > round {}" ,
612+ round_change. data_round, round_change. round
613+ ) ;
614+ return false ;
615+ }
616+
617+ // Verify that if round change has full data, it matches the root
618+ if msg. qbft_message . root != round_change. root {
619+ warn ! ( "Proposal root doesn't match round change prepared root" ) ;
620+ return false ;
621+ }
622+
623+ if !self . check_quorum ( & round_change. round_change_justification ) {
624+ warn ! (
625+ num_justifications = round_change. round_change_justification. len( ) ,
626+ "Not enough prepare messages for quorum"
627+ ) ;
628+ return false ;
629+ }
630+
631+ // go through all of the round changes prepare justifications
632+ for signed_prepare in & round_change. round_change_justification {
633+ if !self . is_valid_prepare_justification_for_round_and_root (
634+ signed_prepare,
635+ round_change. data_round . into ( ) ,
636+ & round_change. root ,
637+ ) {
638+ return false ;
639+ }
574640 }
575641 }
576642 }
577643
578644 // If there was a value that was also previously prepared, we must also verify all of the
579645 // prepare justifications
580- if previously_prepared {
646+ if let Some ( max_prepared_msg ) = max_prepared_msg {
581647 // Make sure we have a quorum of prepare messages
582- if msg. qbft_message . prepare_justification . len ( ) < self . config . quorum_size ( ) {
648+ if ! self . check_quorum ( & msg. qbft_message . prepare_justification ) {
583649 warn ! (
584650 num_justifications = msg. qbft_message. prepare_justification. len( ) ,
585651 "Not enough prepare messages for quorum"
@@ -588,48 +654,59 @@ where
588654 }
589655
590656 // Make sure that the roots match
591- if msg. qbft_message . root
592- != max_prepared_msg
593- . clone ( )
594- . expect ( "Exists as we have a previously prepared value" )
595- . root
596- {
657+ if msg. qbft_message . root != max_prepared_msg. root {
597658 warn ! ( "Highest prepared does not match proposed data" ) ;
598659 return false ;
599660 }
600661
601662 // Validate each prepare message matches highest prepared round/value
602663 for signed_prepare in & msg. qbft_message . prepare_justification {
603- // The qbft message is represented as Vec<u8> in the signed message, deserialize
604- // this into a qbft message
605- let prepare = match QbftMessage :: from_ssz_bytes ( signed_prepare. ssv_message ( ) . data ( ) )
606- {
607- Ok ( data) => data,
608- Err ( _) => return false ,
609- } ;
610-
611- // Make sure this is a prepare message
612- if prepare. qbft_message_type != QbftMessageType :: Prepare {
613- warn ! ( "Expected a prepare message" ) ;
664+ if !self . is_valid_prepare_justification_for_round_and_root (
665+ signed_prepare,
666+ max_prepared_msg. data_round . into ( ) ,
667+ & max_prepared_msg. root ,
668+ ) {
614669 return false ;
615670 }
671+ }
672+ }
673+ true
674+ }
616675
617- let wrapped = WrappedQbftMessage {
618- signed_message : signed_prepare. clone ( ) ,
619- qbft_message : prepare. clone ( ) ,
620- } ;
676+ fn is_valid_prepare_justification_for_round_and_root (
677+ & self ,
678+ justification : & SignedSSVMessage ,
679+ round : Round ,
680+ root : & Hash256 ,
681+ ) -> bool {
682+ // The qbft message is represented as Vec<u8> in the signed message, deserialize this into
683+ // a qbft message
684+ let Ok ( prepare) = QbftMessage :: from_ssz_bytes ( justification. ssv_message ( ) . data ( ) ) else {
685+ warn ! ( "Failed to decode prepare justification message" ) ;
686+ return false ;
687+ } ;
621688
622- if self . validate_message ( & wrapped) . is_none ( ) {
623- warn ! ( "PREPARE message validation failed" ) ;
624- return false ;
625- }
689+ // Make sure this is a prepare message
690+ if prepare. qbft_message_type != QbftMessageType :: Prepare {
691+ warn ! ( "Expected a prepare message" ) ;
692+ return false ;
693+ }
626694
627- if prepare. root != msg. qbft_message . root {
628- warn ! ( "Proposed data mismatch" ) ;
629- return false ;
630- }
631- }
695+ if prepare. height != * self . instance_height as u64 {
696+ warn ! ( "PREPARE height incorrect" ) ;
697+ return false ;
632698 }
699+
700+ if prepare. round != round. get ( ) as u64 {
701+ warn ! ( "PREPARE round incorrect" ) ;
702+ return false ;
703+ }
704+
705+ if & prepare. root != root {
706+ warn ! ( "Proposed data mismatch" ) ;
707+ return false ;
708+ }
709+
633710 true
634711 }
635712
@@ -851,6 +928,44 @@ where
851928 return ;
852929 }
853930
931+ let qbft_msg = & wrapped_msg. qbft_message ;
932+ // If this is a "prepared" round change, we have to check the justifications.
933+ if qbft_msg. data_round > 0 {
934+ if !self . check_quorum ( & qbft_msg. round_change_justification ) {
935+ debug ! (
936+ from = * operator_id,
937+ justifications = qbft_msg. round_change_justification. len( ) ,
938+ quorum = self . config. quorum_size( ) ,
939+ "prepared ROUNDCHANGE has no quorum"
940+ ) ;
941+ return ;
942+ }
943+
944+ if qbft_msg. data_round > qbft_msg. round {
945+ debug ! (
946+ from = * operator_id,
947+ data_round = qbft_msg. data_round,
948+ round = qbft_msg. round,
949+ "ROUNDCHANGE has prepared round after round"
950+ ) ;
951+ return ;
952+ }
953+
954+ for justification in qbft_msg. round_change_justification . iter ( ) {
955+ if !self . is_valid_prepare_justification_for_round_and_root (
956+ justification,
957+ qbft_msg. data_round . into ( ) ,
958+ & qbft_msg. root ,
959+ ) {
960+ debug ! (
961+ from = * operator_id,
962+ "ROUNDCHANGE has invalid prepare justification"
963+ ) ;
964+ return ;
965+ }
966+ }
967+ }
968+
854969 debug ! ( from = ?operator_id, state = ?self . state, "ROUNDCHANGE received" ) ;
855970
856971 // Store the round changed message
0 commit comments