Skip to content

Commit 0e23a1d

Browse files
authored
fix: Validation for prepared round changes before insertion (#528)
If we have prepared a value, we include it in a round change messages and include the quorum of prepare justifications proving that value was prepared in the round_change_justifications field. These round change messages have to have the round_change_justifications prepare messages validated to ensure that these messages actually prove that the value was prepared. Otherwise, you could claim to prepare some value but not properly be able to support/prove it. Implement the missing validation and change existing code a bit to be able to reuse `is_valid_prepare_justification_for_round_and_root`.
1 parent 02e41ca commit 0e23a1d

File tree

2 files changed

+299
-51
lines changed

2 files changed

+299
-51
lines changed

anchor/common/qbft/src/lib.rs

Lines changed: 166 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::{collections::HashMap, sync::Arc};
1+
use std::{
2+
collections::{HashMap, HashSet},
3+
sync::Arc,
4+
};
25

36
// Re-Exports for Manager
47
pub 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

Comments
 (0)