//! Types used in [crate::threshold_simplex]. use crate::Viewable; use bytes::{Buf, BufMut}; use commonware_codec::{ varint::UInt, Encode, EncodeSize, Error, Read, ReadExt, ReadRangeExt, Write, }; use commonware_cryptography::{ bls12381::primitives::{ group::Share, ops::{ aggregate_signatures, aggregate_verify_multiple_messages, partial_sign_message, partial_verify_multiple_public_keys_precomputed, verify_message, }, poly::PartialSignature, variant::Variant, }, Digest, }; use commonware_utils::union; use std::{ collections::{BTreeSet, HashMap, HashSet}, hash::Hash, }; /// View is a monotonically increasing counter that represents the current focus of consensus. /// Each View corresponds to a round in the consensus protocol where validators attempt to agree /// on a block to commit. pub type View = u64; /// Context is a collection of metadata from consensus about a given payload. /// It provides information about the current view and the parent payload that new proposals are built on. #[derive(Clone)] pub struct Context { /// Current view (round) of consensus. pub view: View, /// Parent the payload is built on. /// /// If there is a gap between the current view and the parent view, the participant /// must possess a nullification for each discarded view to safely vote on the proposed /// payload (any view without a nullification may eventually be finalized and skipping /// it would result in a fork). pub parent: (View, D), } /// Attributable is a trait that provides access to the signer index. /// This is used to identify which participant signed a given message. pub trait Attributable { /// Returns the index of the signer (validator) who produced this message. fn signer(&self) -> u32; } /// Seedable is a trait that provides access to the seed associated with a message. pub trait Seedable { /// Returns the seed associated with this object. fn seed(&self) -> Seed; } // Constants for domain separation in signature verification // These are used to prevent cross-protocol attacks and message-type confusion pub const SEED_SUFFIX: &[u8] = b"_SEED"; pub const NOTARIZE_SUFFIX: &[u8] = b"_NOTARIZE"; pub const NULLIFY_SUFFIX: &[u8] = b"_NULLIFY"; pub const FINALIZE_SUFFIX: &[u8] = b"_FINALIZE"; /// Creates a message to be signed containing just the view number #[inline] pub fn view_message(view: View) -> Vec { View::encode(&view).into() } /// Creates a namespace for seed messages by appending the SEED_SUFFIX /// The seed is used for leader election and randomness generation #[inline] pub fn seed_namespace(namespace: &[u8]) -> Vec { union(namespace, SEED_SUFFIX) } /// Creates a namespace for notarize messages by appending the NOTARIZE_SUFFIX /// Domain separation prevents cross-protocol attacks #[inline] pub fn notarize_namespace(namespace: &[u8]) -> Vec { union(namespace, NOTARIZE_SUFFIX) } /// Creates a namespace for nullify messages by appending the NULLIFY_SUFFIX /// Domain separation prevents cross-protocol attacks #[inline] pub fn nullify_namespace(namespace: &[u8]) -> Vec { union(namespace, NULLIFY_SUFFIX) } /// Creates a namespace for finalize messages by appending the FINALIZE_SUFFIX /// Domain separation prevents cross-protocol attacks #[inline] pub fn finalize_namespace(namespace: &[u8]) -> Vec { union(namespace, FINALIZE_SUFFIX) } /// `BatchVerifier` is a utility for tracking and batch verifying consensus messages. /// /// In consensus, verifying multiple signatures at the same time can be much more efficient /// than verifying them one by one. This struct collects messages from participants in consensus /// and signals they are ready to be verified when certain conditions are met (e.g., enough messages /// to potentially reach a quorum, or when a leader's message is received). /// /// To avoid unnecessary verification, it also tracks the number of already verified messages (ensuring /// we no longer attempt to verify messages after a quorum of valid messages have already been verified). pub struct BatchVerifier { quorum: Option, leader: Option, leader_proposal: Option>, notarizes: Vec>, notarizes_force: bool, notarizes_verified: usize, nullifies: Vec>, nullifies_verified: usize, finalizes: Vec>, finalizes_verified: usize, } impl BatchVerifier { /// Creates a new `BatchVerifier`. /// /// # Arguments /// /// * `quorum` - An optional `u32` specifying the number of votes (2f+1) /// required to reach a quorum. If `None`, batch verification readiness /// checks based on quorum size are skipped. pub fn new(quorum: Option) -> Self { Self { quorum: quorum.map(|q| q as usize), leader: None, leader_proposal: None, notarizes: Vec::new(), notarizes_force: false, notarizes_verified: 0, nullifies: Vec::new(), nullifies_verified: 0, finalizes: Vec::new(), finalizes_verified: 0, } } /// Clears any pending messages that are not for the leader's proposal and forces /// the notarizes to be verified. /// /// We force verification because we need to know the leader's proposal /// to begin verifying it. fn set_leader_proposal(&mut self, proposal: Proposal) { // Drop all notarizes/finalizes that aren't for the leader proposal self.notarizes.retain(|n| n.proposal == proposal); self.finalizes.retain(|f| f.proposal == proposal); // Set the leader proposal self.leader_proposal = Some(proposal); // Force the notarizes to be verified self.notarizes_force = true; } /// Adds a [Voter] message to the batch for later verification. /// /// If the message has already been verified (e.g., we built it), it increments /// the count of verified messages directly. Otherwise, it adds the message to /// the appropriate pending queue. /// /// If a leader is known and the message is a [Voter::Notarize] from that leader, /// this method may trigger `set_leader_proposal`. /// /// Recovered messages (e.g., [Voter::Notarization], [Voter::Nullification], [Voter::Finalization]) /// are not expected here and will cause a panic. /// /// # Arguments /// /// * `msg` - The [Voter] message to add. /// * `verified` - A boolean indicating if the message has already been verified. pub fn add(&mut self, msg: Voter, verified: bool) { match msg { Voter::Notarize(notarize) => { if let Some(ref leader_proposal) = self.leader_proposal { // If leader proposal is set and the message is not for it, drop it if leader_proposal != ¬arize.proposal { return; } } else if let Some(leader) = self.leader { // If leader is set but leader proposal is not, set it if leader == notarize.signer() { // Set the leader proposal self.set_leader_proposal(notarize.proposal.clone()); } } // If we've made it this far, add the notarize if verified { self.notarizes_verified += 1; } else { self.notarizes.push(notarize); } } Voter::Nullify(nullify) => { if verified { self.nullifies_verified += 1; } else { self.nullifies.push(nullify); } } Voter::Finalize(finalize) => { // If leader proposal is set and the message is not for it, drop it if let Some(ref leader_proposal) = self.leader_proposal { if leader_proposal != &finalize.proposal { return; } } // If we've made it this far, add the finalize if verified { self.finalizes_verified += 1; } else { self.finalizes.push(finalize); } } Voter::Notarization(_) | Voter::Nullification(_) | Voter::Finalization(_) => { unreachable!("should not be adding recovered messages to partial verifier"); } } } /// Sets the leader for the current consensus view. /// /// If the leader is found, we may call `set_leader_proposal` to clear any pending /// messages that are not for the leader's proposal and to force verification of said /// proposal. /// /// # Arguments /// /// * `leader` - The `u32` identifier of the leader. pub fn set_leader(&mut self, leader: u32) { // Set the leader assert!(self.leader.is_none()); self.leader = Some(leader); // Look for a notarize from the leader let Some(notarize) = self.notarizes.iter().find(|n| n.signer() == leader) else { return; }; // Set the leader proposal self.set_leader_proposal(notarize.proposal.clone()); } /// Verifies a batch of pending [Voter::Notarize] messages. /// /// It uses `Notarize::verify_multiple` for efficient batch verification against /// the provided `polynomial`. /// /// # Arguments /// /// * `namespace` - The namespace for signature domain separation. /// * `polynomial` - The public polynomial (`Poly`) of the DKG. /// /// # Returns /// /// A tuple containing: /// * A `Vec>` of successfully verified [Voter::Notarize] messages (wrapped as [Voter]). /// * A `Vec` of signer indices for whom verification failed. pub fn verify_notarizes( &mut self, namespace: &[u8], polynomial: &[V::Public], ) -> (Vec>, Vec) { self.notarizes_force = false; let (notarizes, failed) = Notarize::verify_multiple(namespace, polynomial, std::mem::take(&mut self.notarizes)); self.notarizes_verified += notarizes.len(); (notarizes.into_iter().map(Voter::Notarize).collect(), failed) } /// Checks if there are [Voter::Notarize] messages ready for batch verification. /// /// Verification is considered "ready" if: /// 1. `notarizes_force` is true (e.g., after a leader's proposal is set). /// 2. A leader and their proposal are known, and: /// a. The quorum (if set) has not yet been met by verified messages. /// b. The sum of verified and pending messages is enough to potentially reach the quorum. /// 3. There are pending [Voter::Notarize] messages to verify. /// /// # Returns /// /// `true` if [Voter::Notarize] messages should be verified, `false` otherwise. pub fn ready_notarizes(&self) -> bool { // If there are no pending notarizes, there is nothing to do. if self.notarizes.is_empty() { return false; } // If we have the leader's notarize, we should verify immediately to start // block verification. if self.notarizes_force { return true; } // If we don't yet know the leader, notarizes may contain messages for // a number of different proposals. if self.leader.is_none() || self.leader_proposal.is_none() { return false; } // If we have a quorum, we need to check if we have enough verified and pending if let Some(quorum) = self.quorum { // If we have already performed sufficient verifications, there is nothing more // to do. if self.notarizes_verified >= quorum { return false; } // If we don't have enough to reach the quorum, there is nothing to do yet. if self.notarizes_verified + self.notarizes.len() < quorum { return false; } } // If there is no required quorum and we have pending notarizes, we should verify. true } /// Verifies a batch of pending [Voter::Nullify] messages. /// /// It uses `Nullify::verify_multiple` for efficient batch verification against /// the provided `polynomial`. /// /// # Arguments /// /// * `namespace` - The namespace for signature domain separation. /// * `polynomial` - The public polynomial (`Poly`) of the DKG. /// /// # Returns /// /// A tuple containing: /// * A `Vec>` of successfully verified [Voter::Nullify] messages (wrapped as [Voter]). /// * A `Vec` of signer indices for whom verification failed. pub fn verify_nullifies( &mut self, namespace: &[u8], polynomial: &[V::Public], ) -> (Vec>, Vec) { let (nullifies, failed) = Nullify::verify_multiple(namespace, polynomial, std::mem::take(&mut self.nullifies)); self.nullifies_verified += nullifies.len(); (nullifies.into_iter().map(Voter::Nullify).collect(), failed) } /// Checks if there are [Voter::Nullify] messages ready for batch verification. /// /// Verification is considered "ready" if: /// 1. The quorum (if set) has not yet been met by verified messages. /// 2. The sum of verified and pending messages is enough to potentially reach the quorum. /// 3. There are pending [Voter::Nullify] messages to verify. /// /// # Returns /// /// `true` if [Voter::Nullify] messages should be verified, `false` otherwise. pub fn ready_nullifies(&self) -> bool { // If there are no pending nullifies, there is nothing to do. if self.nullifies.is_empty() { return false; } if let Some(quorum) = self.quorum { // If we have already performed sufficient verifications, there is nothing more // to do. if self.nullifies_verified >= quorum { return false; } // If we don't have enough to reach the quorum, there is nothing to do yet. if self.nullifies_verified + self.nullifies.len() < quorum { return false; } } // If there is no required quorum and we have pending nullifies, we should verify. true } /// Verifies a batch of pending [Voter::Finalize] messages. /// /// It uses `Finalize::verify_multiple` for efficient batch verification against /// the provided `polynomial`. /// /// # Arguments /// /// * `namespace` - The namespace for signature domain separation. /// * `polynomial` - The public polynomial (`Poly`) of the DKG. /// /// # Returns /// /// A tuple containing: /// * A `Vec>` of successfully verified [Voter::Finalize] messages (wrapped as [Voter]). /// * A `Vec` of signer indices for whom verification failed. pub fn verify_finalizes( &mut self, namespace: &[u8], polynomial: &[V::Public], ) -> (Vec>, Vec) { let (finalizes, failed) = Finalize::verify_multiple(namespace, polynomial, std::mem::take(&mut self.finalizes)); self.finalizes_verified += finalizes.len(); (finalizes.into_iter().map(Voter::Finalize).collect(), failed) } /// Checks if there are [Voter::Finalize] messages ready for batch verification. /// /// Verification is considered "ready" if: /// 1. A leader and their proposal are known (finalizes are proposal-specific). /// 2. The quorum (if set) has not yet been met by verified messages. /// 3. The sum of verified and pending messages is enough to potentially reach the quorum. /// 4. There are pending [Voter::Finalize] messages to verify. /// /// # Returns /// /// `true` if [Voter::Finalize] messages should be verified, `false` otherwise. pub fn ready_finalizes(&self) -> bool { // If there are no pending finalizes, there is nothing to do. if self.finalizes.is_empty() { return false; } // If we don't yet know the leader, finalizers may contain messages for // a number of different proposals. if self.leader.is_none() || self.leader_proposal.is_none() { return false; } if let Some(quorum) = self.quorum { // If we have already performed sufficient verifications, there is nothing more // to do. if self.finalizes_verified >= quorum { return false; } // If we don't have enough to reach the quorum, there is nothing to do yet. if self.finalizes_verified + self.finalizes.len() < quorum { return false; } } // If there is no required quorum and we have pending finalizes, we should verify. true } } /// Voter represents all possible message types that can be sent by validators /// in the consensus protocol. #[derive(Clone, Debug, PartialEq)] pub enum Voter { /// A single validator notarize over a proposal Notarize(Notarize), /// A recovered threshold signature for a notarization Notarization(Notarization), /// A single validator nullify to skip the current view (usually when leader is unresponsive) Nullify(Nullify), /// A recovered threshold signature for a nullification Nullification(Nullification), /// A single validator finalize over a proposal Finalize(Finalize), /// A recovered threshold signature for a finalization Finalization(Finalization), } impl Write for Voter { fn write(&self, writer: &mut impl BufMut) { match self { Voter::Notarize(v) => { 0u8.write(writer); v.write(writer); } Voter::Notarization(v) => { 1u8.write(writer); v.write(writer); } Voter::Nullify(v) => { 2u8.write(writer); v.write(writer); } Voter::Nullification(v) => { 3u8.write(writer); v.write(writer); } Voter::Finalize(v) => { 4u8.write(writer); v.write(writer); } Voter::Finalization(v) => { 5u8.write(writer); v.write(writer); } } } } impl EncodeSize for Voter { fn encode_size(&self) -> usize { 1 + match self { Voter::Notarize(v) => v.encode_size(), Voter::Notarization(v) => v.encode_size(), Voter::Nullify(v) => v.encode_size(), Voter::Nullification(v) => v.encode_size(), Voter::Finalize(v) => v.encode_size(), Voter::Finalization(v) => v.encode_size(), } } } impl Read for Voter { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let tag = ::read(reader)?; match tag { 0 => { let v = Notarize::read(reader)?; Ok(Voter::Notarize(v)) } 1 => { let v = Notarization::read(reader)?; Ok(Voter::Notarization(v)) } 2 => { let v = Nullify::read(reader)?; Ok(Voter::Nullify(v)) } 3 => { let v = Nullification::read(reader)?; Ok(Voter::Nullification(v)) } 4 => { let v = Finalize::read(reader)?; Ok(Voter::Finalize(v)) } 5 => { let v = Finalization::read(reader)?; Ok(Voter::Finalization(v)) } _ => Err(Error::Invalid( "consensus::threshold_simplex::Voter", "Invalid type", )), } } } impl Viewable for Voter { type View = View; fn view(&self) -> View { match self { Voter::Notarize(v) => v.view(), Voter::Notarization(v) => v.view(), Voter::Nullify(v) => v.view(), Voter::Nullification(v) => v.view(), Voter::Finalize(v) => v.view(), Voter::Finalization(v) => v.view(), } } } /// Proposal represents a proposed block in the protocol. /// It includes the view number, the parent view, and the actual payload (typically a digest of block data). #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Proposal { /// The view (round) in which this proposal is made pub view: View, /// The view of the parent proposal that this one builds upon pub parent: View, /// The actual payload/content of the proposal (typically a digest of the block data) pub payload: D, } impl Proposal { /// Creates a new proposal with the specified view, parent view, and payload. pub fn new(view: View, parent: View, payload: D) -> Self { Proposal { view, parent, payload, } } } impl Write for Proposal { fn write(&self, writer: &mut impl BufMut) { UInt(self.view).write(writer); UInt(self.parent).write(writer); self.payload.write(writer) } } impl Read for Proposal { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let view = UInt::read(reader)?.into(); let parent = UInt::read(reader)?.into(); let payload = D::read(reader)?; Ok(Self { view, parent, payload, }) } } impl EncodeSize for Proposal { fn encode_size(&self) -> usize { UInt(self.view).encode_size() + UInt(self.parent).encode_size() + self.payload.encode_size() } } impl Viewable for Proposal { type View = View; fn view(&self) -> View { self.view } } /// Notarize represents a validator's vote to notarize a proposal. /// In threshold_simplex, it contains a partial signature on the proposal and a partial signature for the seed. /// The seed is used for leader election and as a source of randomness. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Notarize { /// The proposal that is being notarized pub proposal: Proposal, /// The validator's partial signature on the proposal pub proposal_signature: PartialSignature, /// The validator's partial signature on the seed (for leader election/randomness) pub seed_signature: PartialSignature, } impl Notarize { /// Creates a new notarize with the given proposal and signatures. pub fn new( proposal: Proposal, proposal_signature: PartialSignature, seed_signature: PartialSignature, ) -> Self { Notarize { proposal, proposal_signature, seed_signature, } } /// Verifies the [PartialSignature]s on this [Notarize]. /// /// This ensures that: /// 1. The notarize signature is valid for the claimed proposal /// 2. The seed signature is valid for the view /// 3. Both signatures are from the same signer pub fn verify(&self, namespace: &[u8], polynomial: &[V::Public]) -> bool { let notarize_namespace = notarize_namespace(namespace); let notarize_message = self.proposal.encode(); let notarize_message = (Some(notarize_namespace.as_ref()), notarize_message.as_ref()); let seed_namespace = seed_namespace(namespace); let seed_message = view_message(self.proposal.view); let seed_message = (Some(seed_namespace.as_ref()), seed_message.as_ref()); let Some(evaluated) = polynomial.get(self.signer() as usize) else { return false; }; let signature = aggregate_signatures::(&[ self.proposal_signature.value, self.seed_signature.value, ]); aggregate_verify_multiple_messages::( evaluated, &[notarize_message, seed_message], &signature, 1, ) .is_ok() } /// Verifies a batch of [Notarize] messages using BLS aggregate verification. /// /// This function verifies a batch of [Notarize] messages using BLS aggregate verification. /// It returns a tuple containing: /// * A vector of successfully verified [Notarize] messages. /// * A vector of signer indices for whom verification failed. pub fn verify_multiple( namespace: &[u8], polynomial: &[V::Public], notarizes: Vec>, ) -> (Vec>, Vec) { // Prepare to verify if notarizes.is_empty() { return (notarizes, vec![]); } else if notarizes.len() == 1 { // If there is only one notarize, verify it directly (will perform // inner aggregation) let valid = notarizes[0].verify(namespace, polynomial); if valid { return (notarizes, vec![]); } else { return (vec![], vec![notarizes[0].signer()]); } } let proposal = ¬arizes[0].proposal; let mut invalid = BTreeSet::new(); // Verify proposal signatures let notarize_namespace = notarize_namespace(namespace); let notarize_message = proposal.encode(); let notarize_signatures = notarizes.iter().map(|n| &n.proposal_signature); if let Err(err) = partial_verify_multiple_public_keys_precomputed::( polynomial, Some(¬arize_namespace), ¬arize_message, notarize_signatures, ) { for signature in err.iter() { invalid.insert(signature.index); } } // Verify seed signatures let seed_namespace = seed_namespace(namespace); let seed_message = view_message(proposal.view); let seed_signatures = notarizes .iter() .filter(|n| !invalid.contains(&n.seed_signature.index)) .map(|n| &n.seed_signature); if let Err(err) = partial_verify_multiple_public_keys_precomputed::( polynomial, Some(&seed_namespace), &seed_message, seed_signatures, ) { for signature in err.iter() { invalid.insert(signature.index); } } // Remove invalid notarizes ( notarizes .into_iter() .filter(|n| !invalid.contains(&n.signer())) .collect(), invalid.into_iter().collect(), ) } /// Creates a [PartialSignature] over this [Notarize]. pub fn sign(namespace: &[u8], share: &Share, proposal: Proposal) -> Self { let notarize_namespace = notarize_namespace(namespace); let proposal_message = proposal.encode(); let proposal_signature = partial_sign_message::(share, Some(notarize_namespace.as_ref()), &proposal_message); let seed_namespace = seed_namespace(namespace); let seed_message = view_message(proposal.view); let seed_signature = partial_sign_message::(share, Some(seed_namespace.as_ref()), &seed_message); Notarize::new(proposal, proposal_signature, seed_signature) } } impl Attributable for Notarize { fn signer(&self) -> u32 { self.proposal_signature.index } } impl Viewable for Notarize { type View = View; fn view(&self) -> View { self.proposal.view() } } impl Write for Notarize { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.proposal_signature.write(writer); self.seed_signature.write(writer); } } impl Read for Notarize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let proposal = Proposal::read(reader)?; let proposal_signature = PartialSignature::::read(reader)?; let seed_signature = PartialSignature::::read(reader)?; if proposal_signature.index != seed_signature.index { return Err(Error::Invalid( "consensus::threshold_simplex::Notarize", "mismatched signatures", )); } Ok(Notarize { proposal, proposal_signature, seed_signature, }) } } impl EncodeSize for Notarize { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.proposal_signature.encode_size() + self.seed_signature.encode_size() } } /// Notarization represents a recovered threshold signature certifying a proposal. /// When a proposal is notarized, it means at least 2f+1 validators have voted for it. /// The threshold signatures provide compact verification compared to collecting individual signatures. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Notarization { /// The proposal that has been notarized pub proposal: Proposal, /// The recovered threshold signature on the proposal pub proposal_signature: V::Signature, /// The recovered threshold signature on the seed (for leader election/randomness) pub seed_signature: V::Signature, } impl Notarization { /// Creates a new notarization with the given proposal and aggregated signatures. pub fn new( proposal: Proposal, proposal_signature: V::Signature, seed_signature: V::Signature, ) -> Self { Notarization { proposal, proposal_signature, seed_signature, } } /// Verifies the threshold signatures on this [Notarization]. /// /// This ensures that: /// 1. The notarization signature is a valid threshold signature for the proposal /// 2. The seed signature is a valid threshold signature for the view pub fn verify(&self, namespace: &[u8], identity: &V::Public) -> bool { let notarize_namespace = notarize_namespace(namespace); let notarize_message = self.proposal.encode(); let notarize_message = (Some(notarize_namespace.as_ref()), notarize_message.as_ref()); let seed_namespace = seed_namespace(namespace); let seed_message = view_message(self.proposal.view); let seed_message = (Some(seed_namespace.as_ref()), seed_message.as_ref()); let signature = aggregate_signatures::(&[self.proposal_signature, self.seed_signature]); aggregate_verify_multiple_messages::( identity, &[notarize_message, seed_message], &signature, 1, ) .is_ok() } } impl Viewable for Notarization { type View = View; fn view(&self) -> View { self.proposal.view() } } impl Write for Notarization { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.proposal_signature.write(writer); self.seed_signature.write(writer) } } impl Read for Notarization { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let proposal = Proposal::read(reader)?; let proposal_signature = V::Signature::read(reader)?; let seed_signature = V::Signature::read(reader)?; Ok(Notarization { proposal, proposal_signature, seed_signature, }) } } impl EncodeSize for Notarization { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.proposal_signature.encode_size() + self.seed_signature.encode_size() } } impl Seedable for Notarization { fn seed(&self) -> Seed { Seed::new(self.view(), self.seed_signature) } } /// Nullify represents a validator's vote to skip the current view. /// This is typically used when the leader is unresponsive or fails to propose a valid block. /// It contains partial signatures for the view and seed. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Nullify { /// The view to be nullified (skipped) pub view: View, /// The validator's partial signature on the view pub view_signature: PartialSignature, /// The validator's partial signature on the seed (for leader election/randomness) pub seed_signature: PartialSignature, } impl Nullify { /// Creates a new nullify with the given view and signatures. pub fn new( view: View, view_signature: PartialSignature, seed_signature: PartialSignature, ) -> Self { Nullify { view, view_signature, seed_signature, } } /// Verifies the [PartialSignature]s on this [Nullify]. /// /// This ensures that: /// 1. The view signature is valid for the given view /// 2. The seed signature is valid for the view /// 3. Both signatures are from the same signer pub fn verify(&self, namespace: &[u8], polynomial: &[V::Public]) -> bool { let nullify_namespace = nullify_namespace(namespace); let view_message = view_message(self.view); let nullify_message = (Some(nullify_namespace.as_ref()), view_message.as_ref()); let seed_namespace = seed_namespace(namespace); let seed_message = (Some(seed_namespace.as_ref()), view_message.as_ref()); let Some(evaluated) = polynomial.get(self.signer() as usize) else { return false; }; let signature = aggregate_signatures::(&[self.view_signature.value, self.seed_signature.value]); aggregate_verify_multiple_messages::( evaluated, &[nullify_message, seed_message], &signature, 1, ) .is_ok() } /// Verifies a batch of [Nullify] messages using BLS aggregate verification. /// /// This function verifies a batch of [Nullify] messages using BLS aggregate verification. /// It returns a tuple containing: /// * A vector of successfully verified [Nullify] messages. /// * A vector of signer indices for whom verification failed. pub fn verify_multiple( namespace: &[u8], polynomial: &[V::Public], nullifies: Vec>, ) -> (Vec>, Vec) { // Prepare to verify if nullifies.is_empty() { return (nullifies, vec![]); } else if nullifies.len() == 1 { let valid = nullifies[0].verify(namespace, polynomial); if valid { return (nullifies, vec![]); } else { return (vec![], vec![nullifies[0].signer()]); } } let selected = &nullifies[0]; let mut invalid = BTreeSet::new(); // Verify view signature let nullify_namespace = nullify_namespace(namespace); let view_message = view_message(selected.view); let view_signatures = nullifies.iter().map(|n| &n.view_signature); if let Err(err) = partial_verify_multiple_public_keys_precomputed::( polynomial, Some(&nullify_namespace), &view_message, view_signatures, ) { for signature in err.iter() { invalid.insert(signature.index); } } // Verify seed signature let seed_namespace = seed_namespace(namespace); let seed_signatures = nullifies .iter() .filter(|n| !invalid.contains(&n.seed_signature.index)) .map(|n| &n.seed_signature); if let Err(err) = partial_verify_multiple_public_keys_precomputed::( polynomial, Some(&seed_namespace), &view_message, seed_signatures, ) { for signature in err.iter() { invalid.insert(signature.index); } } // Return valid nullifies and invalid signers ( nullifies .into_iter() .filter(|n| !invalid.contains(&n.signer())) .collect(), invalid.into_iter().collect(), ) } /// Creates a [PartialSignature] over this [Nullify]. pub fn sign(namespace: &[u8], share: &Share, view: View) -> Self { let nullify_namespace = nullify_namespace(namespace); let view_message = view_message(view); let view_signature = partial_sign_message::(share, Some(nullify_namespace.as_ref()), &view_message); let seed_namespace = seed_namespace(namespace); let seed_signature = partial_sign_message::(share, Some(seed_namespace.as_ref()), &view_message); Nullify::new(view, view_signature, seed_signature) } } impl Attributable for Nullify { fn signer(&self) -> u32 { self.view_signature.index } } impl Viewable for Nullify { type View = View; fn view(&self) -> View { self.view } } impl Write for Nullify { fn write(&self, writer: &mut impl BufMut) { UInt(self.view).write(writer); self.view_signature.write(writer); self.seed_signature.write(writer); } } impl Read for Nullify { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let view = UInt::read(reader)?.into(); let view_signature = PartialSignature::::read(reader)?; let seed_signature = PartialSignature::::read(reader)?; if view_signature.index != seed_signature.index { return Err(Error::Invalid( "consensus::threshold_simplex::Nullify", "mismatched signatures", )); } Ok(Nullify { view, view_signature, seed_signature, }) } } impl EncodeSize for Nullify { fn encode_size(&self) -> usize { UInt(self.view).encode_size() + self.view_signature.encode_size() + self.seed_signature.encode_size() } } /// Nullification represents a recovered threshold signature to skip a view. /// When a view is nullified, the consensus moves to the next view without finalizing a block. /// The threshold signatures provide compact verification compared to collecting individual signatures. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Nullification { /// The view that has been nullified pub view: View, /// The recovered threshold signature on the view pub view_signature: V::Signature, /// The recovered threshold signature on the seed (for leader election/randomness) pub seed_signature: V::Signature, } impl Nullification { /// Creates a new nullification with the given view and aggregated signatures. pub fn new(view: View, view_signature: V::Signature, seed_signature: V::Signature) -> Self { Nullification { view, view_signature, seed_signature, } } /// Verifies the threshold signatures on this [Nullification]. /// /// This ensures that: /// 1. The view signature is a valid threshold signature for the view /// 2. The seed signature is a valid threshold signature for the view pub fn verify(&self, namespace: &[u8], identity: &V::Public) -> bool { let nullify_namespace = nullify_namespace(namespace); let view_message = view_message(self.view); let nullify_message = (Some(nullify_namespace.as_ref()), view_message.as_ref()); let seed_namespace = seed_namespace(namespace); let seed_message = (Some(seed_namespace.as_ref()), view_message.as_ref()); let signature = aggregate_signatures::(&[self.view_signature, self.seed_signature]); aggregate_verify_multiple_messages::( identity, &[nullify_message, seed_message], &signature, 1, ) .is_ok() } } impl Viewable for Nullification { type View = View; fn view(&self) -> View { self.view } } impl Write for Nullification { fn write(&self, writer: &mut impl BufMut) { UInt(self.view).write(writer); self.view_signature.write(writer); self.seed_signature.write(writer); } } impl Read for Nullification { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let view = UInt::read(reader)?.into(); let view_signature = V::Signature::read(reader)?; let seed_signature = V::Signature::read(reader)?; Ok(Nullification { view, view_signature, seed_signature, }) } } impl EncodeSize for Nullification { fn encode_size(&self) -> usize { UInt(self.view).encode_size() + self.view_signature.encode_size() + self.seed_signature.encode_size() } } impl Seedable for Nullification { fn seed(&self) -> Seed { Seed::new(self.view(), self.seed_signature) } } /// Finalize represents a validator's vote to finalize a proposal. /// This happens after a proposal has been notarized, confirming it as the canonical block for this view. /// It contains a partial signature on the proposal. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Finalize { /// The proposal to be finalized pub proposal: Proposal, /// The validator's partial signature on the proposal pub proposal_signature: PartialSignature, } impl Finalize { /// Creates a new finalize with the given proposal and signature. pub fn new(proposal: Proposal, proposal_signature: PartialSignature) -> Self { Finalize { proposal, proposal_signature, } } /// Verifies the [PartialSignature] on this [Finalize]. /// /// This ensures that the signature is valid for the given proposal. pub fn verify(&self, namespace: &[u8], polynomial: &[V::Public]) -> bool { let finalize_namespace = finalize_namespace(namespace); let message = self.proposal.encode(); let Some(evaluated) = polynomial.get(self.signer() as usize) else { return false; }; verify_message::( evaluated, Some(finalize_namespace.as_ref()), &message, &self.proposal_signature.value, ) .is_ok() } /// Verifies a batch of [Finalize] messages using BLS aggregate verification. /// /// This function verifies a batch of [Finalize] messages using BLS aggregate verification. /// It returns a tuple containing: /// * A vector of successfully verified [Finalize] messages. /// * A vector of signer indices for whom verification failed. pub fn verify_multiple( namespace: &[u8], polynomial: &[V::Public], finalizes: Vec>, ) -> (Vec>, Vec) { // Prepare to verify if finalizes.is_empty() { return (finalizes, vec![]); } else if finalizes.len() == 1 { let valid = finalizes[0].verify(namespace, polynomial); if valid { return (finalizes, vec![]); } else { return (vec![], vec![finalizes[0].signer()]); } } let proposal = &finalizes[0].proposal; let mut invalid = BTreeSet::new(); // Verify proposal signature let finalize_namespace = finalize_namespace(namespace); let finalize_message = proposal.encode(); let finalize_signatures = finalizes.iter().map(|f| &f.proposal_signature); if let Err(err) = partial_verify_multiple_public_keys_precomputed::( polynomial, Some(&finalize_namespace), &finalize_message, finalize_signatures, ) { for signature in err.iter() { invalid.insert(signature.index); } } // Return valid finalizes and invalid signers ( finalizes .into_iter() .filter(|f| !invalid.contains(&f.signer())) .collect(), invalid.into_iter().collect(), ) } /// Creates a [PartialSignature] over this [Finalize]. pub fn sign(namespace: &[u8], share: &Share, proposal: Proposal) -> Self { let finalize_namespace = finalize_namespace(namespace); let message = proposal.encode(); let proposal_signature = partial_sign_message::(share, Some(finalize_namespace.as_ref()), &message); Finalize::new(proposal, proposal_signature) } } impl Attributable for Finalize { fn signer(&self) -> u32 { self.proposal_signature.index } } impl Viewable for Finalize { type View = View; fn view(&self) -> View { self.proposal.view() } } impl Write for Finalize { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.proposal_signature.write(writer); } } impl Read for Finalize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let proposal = Proposal::read(reader)?; let proposal_signature = PartialSignature::::read(reader)?; Ok(Finalize { proposal, proposal_signature, }) } } impl EncodeSize for Finalize { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.proposal_signature.encode_size() } } /// Finalization represents a recovered threshold signature to finalize a proposal. /// When a proposal is finalized, it becomes the canonical block for its view. /// The threshold signatures provide compact verification compared to collecting individual signatures. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Finalization { /// The proposal that has been finalized pub proposal: Proposal, /// The recovered threshold signature on the proposal pub proposal_signature: V::Signature, /// The recovered threshold signature on the seed (for leader election/randomness) pub seed_signature: V::Signature, } impl Finalization { /// Creates a new finalization with the given proposal and aggregated signatures. pub fn new( proposal: Proposal, proposal_signature: V::Signature, seed_signature: V::Signature, ) -> Self { Finalization { proposal, proposal_signature, seed_signature, } } /// Verifies the threshold signatures on this [Finalization]. /// /// This ensures that: /// 1. The proposal signature is a valid threshold signature for the proposal /// 2. The seed signature is a valid threshold signature for the view pub fn verify(&self, namespace: &[u8], identity: &V::Public) -> bool { let finalize_namespace = finalize_namespace(namespace); let finalize_message = self.proposal.encode(); let finalize_message = (Some(finalize_namespace.as_ref()), finalize_message.as_ref()); let seed_namespace = seed_namespace(namespace); let seed_message = view_message(self.proposal.view); let seed_message = (Some(seed_namespace.as_ref()), seed_message.as_ref()); let signature = aggregate_signatures::(&[self.proposal_signature, self.seed_signature]); aggregate_verify_multiple_messages::( identity, &[finalize_message, seed_message], &signature, 1, ) .is_ok() } } impl Viewable for Finalization { type View = View; fn view(&self) -> View { self.proposal.view() } } impl Write for Finalization { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.proposal_signature.write(writer); self.seed_signature.write(writer); } } impl Read for Finalization { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let proposal = Proposal::read(reader)?; let proposal_signature = V::Signature::read(reader)?; let seed_signature = V::Signature::read(reader)?; Ok(Finalization { proposal, proposal_signature, seed_signature, }) } } impl EncodeSize for Finalization { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.proposal_signature.encode_size() + self.seed_signature.encode_size() } } impl Seedable for Finalization { fn seed(&self) -> Seed { Seed::new(self.view(), self.seed_signature) } } /// Backfiller is a message type for requesting and receiving missing consensus artifacts. /// This is used to synchronize validators that have fallen behind or just joined the network. #[derive(Clone, Debug, PartialEq)] pub enum Backfiller { /// Request for missing notarizations and nullifications Request(Request), /// Response containing requested notarizations and nullifications Response(Response), } impl Write for Backfiller { fn write(&self, writer: &mut impl BufMut) { match self { Backfiller::Request(v) => { 0u8.write(writer); v.write(writer); } Backfiller::Response(v) => { 1u8.write(writer); v.write(writer); } } } } impl EncodeSize for Backfiller { fn encode_size(&self) -> usize { 1 + match self { Backfiller::Request(v) => v.encode_size(), Backfiller::Response(v) => v.encode_size(), } } } impl Read for Backfiller { type Cfg = usize; fn read_cfg(reader: &mut impl Buf, cfg: &usize) -> Result { let tag = ::read(reader)?; match tag { 0 => { let v = Request::read_cfg(reader, cfg)?; Ok(Backfiller::Request(v)) } 1 => { let v = Response::::read_cfg(reader, cfg)?; Ok(Backfiller::Response(v)) } _ => Err(Error::Invalid( "consensus::threshold_simplex::Backfiller", "Invalid type", )), } } } /// Request is a message to request missing notarizations and nullifications. /// This is used by validators who need to catch up with the consensus state. #[derive(Clone, Debug, PartialEq)] pub struct Request { /// Unique identifier for this request (used to match responses) pub id: u64, /// Views for which notarizations are requested pub notarizations: Vec, /// Views for which nullifications are requested pub nullifications: Vec, } impl Request { /// Creates a new request for missing notarizations and nullifications. pub fn new(id: u64, notarizations: Vec, nullifications: Vec) -> Self { Request { id, notarizations, nullifications, } } } impl Write for Request { fn write(&self, writer: &mut impl BufMut) { UInt(self.id).write(writer); self.notarizations.write(writer); self.nullifications.write(writer); } } impl EncodeSize for Request { fn encode_size(&self) -> usize { UInt(self.id).encode_size() + self.notarizations.encode_size() + self.nullifications.encode_size() } } impl Read for Request { type Cfg = usize; fn read_cfg(reader: &mut impl Buf, max_len: &usize) -> Result { let id = UInt::read(reader)?.into(); let mut views = HashSet::new(); let notarizations = Vec::::read_range(reader, ..=*max_len)?; for view in notarizations.iter() { if !views.insert(view) { return Err(Error::Invalid( "consensus::threshold_simplex::Request", "Duplicate notarization", )); } } let remaining = max_len - notarizations.len(); views.clear(); let nullifications = Vec::::read_range(reader, ..=remaining)?; for view in nullifications.iter() { if !views.insert(view) { return Err(Error::Invalid( "consensus::threshold_simplex::Request", "Duplicate nullification", )); } } Ok(Request { id, notarizations, nullifications, }) } } /// Response is a message containing the requested notarizations and nullifications. /// This is sent in response to a Request message. #[derive(Clone, Debug, PartialEq)] pub struct Response { /// Identifier matching the original request pub id: u64, /// Notarizations for the requested views pub notarizations: Vec>, /// Nullifications for the requested views pub nullifications: Vec>, } impl Response { /// Creates a new response with the given id, notarizations, and nullifications. pub fn new( id: u64, notarizations: Vec>, nullifications: Vec>, ) -> Self { Response { id, notarizations, nullifications, } } /// Verifies the signatures on this response using BLS aggregate verification. pub fn verify(&self, namespace: &[u8], identity: &V::Public) -> bool { // Prepare to verify if self.notarizations.is_empty() && self.nullifications.is_empty() { return true; } let mut seeds = HashMap::new(); let mut messages = Vec::new(); let mut signatures = Vec::new(); // Parse all notarizations let notarize_namespace = notarize_namespace(namespace); let seed_namespace = seed_namespace(namespace); for notarization in self.notarizations.iter() { // Prepare notarize message let notarize_message = notarization.proposal.encode().to_vec(); let notarize_message = (Some(notarize_namespace.as_slice()), notarize_message); messages.push(notarize_message); signatures.push(¬arization.proposal_signature); // Add seed message (if not already present) if let Some(previous) = seeds.get(¬arization.proposal.view) { if *previous != ¬arization.seed_signature { return false; } } else { let seed_message = view_message(notarization.proposal.view); let seed_message = (Some(seed_namespace.as_slice()), seed_message); messages.push(seed_message); signatures.push(¬arization.seed_signature); seeds.insert(notarization.proposal.view, ¬arization.seed_signature); } } // Parse all nullifications let nullify_namespace = nullify_namespace(namespace); for nullification in self.nullifications.iter() { // Prepare nullify message let nullify_message = view_message(nullification.view); let nullify_message = (Some(nullify_namespace.as_slice()), nullify_message); messages.push(nullify_message); signatures.push(&nullification.view_signature); // Add seed message (if not already present) if let Some(previous) = seeds.get(&nullification.view) { if *previous != &nullification.seed_signature { return false; } } else { let seed_message = view_message(nullification.view); let seed_message = (Some(seed_namespace.as_slice()), seed_message); messages.push(seed_message); signatures.push(&nullification.seed_signature); seeds.insert(nullification.view, &nullification.seed_signature); } } // Aggregate signatures let signature = aggregate_signatures::(signatures); aggregate_verify_multiple_messages::( identity, &messages .iter() .map(|(namespace, message)| (namespace.as_deref(), message.as_ref())) .collect::>(), &signature, 1, ) .is_ok() } } impl Write for Response { fn write(&self, writer: &mut impl BufMut) { UInt(self.id).write(writer); self.notarizations.write(writer); self.nullifications.write(writer); } } impl EncodeSize for Response { fn encode_size(&self) -> usize { UInt(self.id).encode_size() + self.notarizations.encode_size() + self.nullifications.encode_size() } } impl Read for Response { type Cfg = usize; fn read_cfg(reader: &mut impl Buf, max_len: &usize) -> Result { let id = UInt::read(reader)?.into(); let mut views = HashSet::new(); let notarizations = Vec::>::read_range(reader, ..=*max_len)?; for notarization in notarizations.iter() { if !views.insert(notarization.proposal.view) { return Err(Error::Invalid( "consensus::threshold_simplex::Response", "Duplicate notarization", )); } } let remaining = max_len - notarizations.len(); views.clear(); let nullifications = Vec::>::read_range(reader, ..=remaining)?; for nullification in nullifications.iter() { if !views.insert(nullification.view) { return Err(Error::Invalid( "consensus::threshold_simplex::Response", "Duplicate nullification", )); } } Ok(Response { id, notarizations, nullifications, }) } } /// Activity represents all possible activities that can occur in the consensus protocol. /// This includes both regular consensus messages and fault evidence. /// /// Some activities issued by consensus are not verified. To determine if an activity has been verified, /// use the `verified` method. /// /// # Warning /// /// After collecting `t` [PartialSignature]s for the same [Activity], an attacker can derive /// the [PartialSignature] for the `n-t` remaining participants. /// /// For this reason, it is not sound to use [PartialSignature]-backed [Activity] to reward participants /// for their contributions (as an attacker, for example, could forge contributions from offline participants). #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub enum Activity { /// A single validator notarize over a proposal Notarize(Notarize), /// A threshold signature for a notarization Notarization(Notarization), /// A single validator nullify to skip the current view Nullify(Nullify), /// A threshold signature for a nullification Nullification(Nullification), /// A single validator finalize over a proposal Finalize(Finalize), /// A threshold signature for a finalization Finalization(Finalization), /// Evidence of a validator sending conflicting notarizes (Byzantine behavior) ConflictingNotarize(ConflictingNotarize), /// Evidence of a validator sending conflicting finalizes (Byzantine behavior) ConflictingFinalize(ConflictingFinalize), /// Evidence of a validator sending both nullify and finalize for the same view (Byzantine behavior) NullifyFinalize(NullifyFinalize), } impl Activity { /// Indicates whether the activity has been verified by consensus. pub fn verified(&self) -> bool { match self { Activity::Notarize(_) => false, Activity::Notarization(_) => true, Activity::Nullify(_) => false, Activity::Nullification(_) => true, Activity::Finalize(_) => false, Activity::Finalization(_) => true, Activity::ConflictingNotarize(_) => false, Activity::ConflictingFinalize(_) => false, Activity::NullifyFinalize(_) => false, } } } impl Write for Activity { fn write(&self, writer: &mut impl BufMut) { match self { Activity::Notarize(v) => { 0u8.write(writer); v.write(writer); } Activity::Notarization(v) => { 1u8.write(writer); v.write(writer); } Activity::Nullify(v) => { 2u8.write(writer); v.write(writer); } Activity::Nullification(v) => { 3u8.write(writer); v.write(writer); } Activity::Finalize(v) => { 4u8.write(writer); v.write(writer); } Activity::Finalization(v) => { 5u8.write(writer); v.write(writer); } Activity::ConflictingNotarize(v) => { 6u8.write(writer); v.write(writer); } Activity::ConflictingFinalize(v) => { 7u8.write(writer); v.write(writer); } Activity::NullifyFinalize(v) => { 8u8.write(writer); v.write(writer); } } } } impl EncodeSize for Activity { fn encode_size(&self) -> usize { 1 + match self { Activity::Notarize(v) => v.encode_size(), Activity::Notarization(v) => v.encode_size(), Activity::Nullify(v) => v.encode_size(), Activity::Nullification(v) => v.encode_size(), Activity::Finalize(v) => v.encode_size(), Activity::Finalization(v) => v.encode_size(), Activity::ConflictingNotarize(v) => v.encode_size(), Activity::ConflictingFinalize(v) => v.encode_size(), Activity::NullifyFinalize(v) => v.encode_size(), } } } impl Read for Activity { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let tag = ::read(reader)?; match tag { 0 => { let v = Notarize::::read(reader)?; Ok(Activity::Notarize(v)) } 1 => { let v = Notarization::::read(reader)?; Ok(Activity::Notarization(v)) } 2 => { let v = Nullify::::read(reader)?; Ok(Activity::Nullify(v)) } 3 => { let v = Nullification::::read(reader)?; Ok(Activity::Nullification(v)) } 4 => { let v = Finalize::::read(reader)?; Ok(Activity::Finalize(v)) } 5 => { let v = Finalization::::read(reader)?; Ok(Activity::Finalization(v)) } 6 => { let v = ConflictingNotarize::::read(reader)?; Ok(Activity::ConflictingNotarize(v)) } 7 => { let v = ConflictingFinalize::::read(reader)?; Ok(Activity::ConflictingFinalize(v)) } 8 => { let v = NullifyFinalize::::read(reader)?; Ok(Activity::NullifyFinalize(v)) } _ => Err(Error::Invalid( "consensus::threshold_simplex::Activity", "Invalid type", )), } } } impl Viewable for Activity { type View = View; fn view(&self) -> View { match self { Activity::Notarize(v) => v.view(), Activity::Notarization(v) => v.view(), Activity::Nullify(v) => v.view(), Activity::Nullification(v) => v.view(), Activity::Finalize(v) => v.view(), Activity::Finalization(v) => v.view(), Activity::ConflictingNotarize(v) => v.view(), Activity::ConflictingFinalize(v) => v.view(), Activity::NullifyFinalize(v) => v.view(), } } } /// Seed represents a threshold signature over the current view. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Seed { /// The view for which this seed is generated pub view: View, /// The partial signature on the seed pub signature: V::Signature, } impl Seed { /// Creates a new seed with the given view and signature. pub fn new(view: View, signature: V::Signature) -> Self { Seed { view, signature } } /// Verifies the threshold signature on this [Seed]. pub fn verify(&self, namespace: &[u8], identity: &V::Public) -> bool { let seed_namespace = seed_namespace(namespace); let message = view_message(self.view); verify_message::(identity, Some(&seed_namespace), &message, &self.signature).is_ok() } } impl Viewable for Seed { type View = View; fn view(&self) -> View { self.view } } impl Write for Seed { fn write(&self, writer: &mut impl BufMut) { UInt(self.view).write(writer); self.signature.write(writer); } } impl Read for Seed { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let view = UInt::read(reader)?.into(); let signature = V::Signature::read(reader)?; Ok(Seed { view, signature }) } } impl EncodeSize for Seed { fn encode_size(&self) -> usize { UInt(self.view).encode_size() + self.signature.encode_size() } } /// ConflictingNotarize represents evidence of a Byzantine validator sending conflicting notarizes. /// This is used to prove that a validator has equivocated (voted for different proposals in the same view). #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ConflictingNotarize { /// The view in which the conflict occurred pub view: View, /// The parent view of the first conflicting proposal pub parent_1: View, /// The payload of the first conflicting proposal pub payload_1: D, /// The signature on the first conflicting proposal pub signature_1: PartialSignature, /// The parent view of the second conflicting proposal pub parent_2: View, /// The payload of the second conflicting proposal pub payload_2: D, /// The signature on the second conflicting proposal pub signature_2: PartialSignature, } impl ConflictingNotarize { /// Creates a new conflicting notarize evidence from two conflicting notarizes. pub fn new(notarize_1: Notarize, notarize_2: Notarize) -> Self { assert_eq!(notarize_1.view(), notarize_2.view()); assert_eq!(notarize_1.signer(), notarize_2.signer()); ConflictingNotarize { view: notarize_1.view(), parent_1: notarize_1.proposal.parent, payload_1: notarize_1.proposal.payload, signature_1: notarize_1.proposal_signature, parent_2: notarize_2.proposal.parent, payload_2: notarize_2.proposal.payload, signature_2: notarize_2.proposal_signature, } } /// Reconstructs the original proposals from this evidence. pub fn proposals(&self) -> (Proposal, Proposal) { ( Proposal::new(self.view, self.parent_1, self.payload_1), Proposal::new(self.view, self.parent_2, self.payload_2), ) } /// Verifies that both conflicting signatures are valid, proving Byzantine behavior. pub fn verify(&self, namespace: &[u8], polynomial: &[V::Public]) -> bool { let (proposal_1, proposal_2) = self.proposals(); let notarize_namespace = notarize_namespace(namespace); let notarize_message_1 = proposal_1.encode(); let notarize_message_1 = ( Some(notarize_namespace.as_ref()), notarize_message_1.as_ref(), ); let notarize_message_2 = proposal_2.encode(); let notarize_message_2 = ( Some(notarize_namespace.as_ref()), notarize_message_2.as_ref(), ); let Some(evaluated) = polynomial.get(self.signer() as usize) else { return false; }; let signature = aggregate_signatures::(&[self.signature_1.value, self.signature_2.value]); aggregate_verify_multiple_messages::( evaluated, &[notarize_message_1, notarize_message_2], &signature, 1, ) .is_ok() } } impl Attributable for ConflictingNotarize { fn signer(&self) -> u32 { self.signature_1.index } } impl Viewable for ConflictingNotarize { type View = View; fn view(&self) -> View { self.view } } impl Write for ConflictingNotarize { fn write(&self, writer: &mut impl BufMut) { UInt(self.view).write(writer); UInt(self.parent_1).write(writer); self.payload_1.write(writer); self.signature_1.write(writer); UInt(self.parent_2).write(writer); self.payload_2.write(writer); self.signature_2.write(writer); } } impl Read for ConflictingNotarize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let view = UInt::read(reader)?.into(); let parent_1 = UInt::read(reader)?.into(); let payload_1 = D::read(reader)?; let signature_1 = PartialSignature::::read(reader)?; let parent_2 = UInt::read(reader)?.into(); let payload_2 = D::read(reader)?; let signature_2 = PartialSignature::::read(reader)?; if signature_1.index != signature_2.index { return Err(Error::Invalid( "consensus::threshold_simplex::ConflictingNotarize", "mismatched signatures", )); } Ok(ConflictingNotarize { view, parent_1, payload_1, signature_1, parent_2, payload_2, signature_2, }) } } impl EncodeSize for ConflictingNotarize { fn encode_size(&self) -> usize { UInt(self.view).encode_size() + UInt(self.parent_1).encode_size() + self.payload_1.encode_size() + self.signature_1.encode_size() + UInt(self.parent_2).encode_size() + self.payload_2.encode_size() + self.signature_2.encode_size() } } /// ConflictingFinalize represents evidence of a Byzantine validator sending conflicting finalizes. /// Similar to ConflictingNotarize, but for finalizes. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ConflictingFinalize { /// The view in which the conflict occurred pub view: View, /// The parent view of the first conflicting proposal pub parent_1: View, /// The payload of the first conflicting proposal pub payload_1: D, /// The signature on the first conflicting proposal pub signature_1: PartialSignature, /// The parent view of the second conflicting proposal pub parent_2: View, /// The payload of the second conflicting proposal pub payload_2: D, /// The signature on the second conflicting proposal pub signature_2: PartialSignature, } impl ConflictingFinalize { /// Creates a new conflicting finalize evidence from two conflicting finalizes. pub fn new(finalize_1: Finalize, finalize_2: Finalize) -> Self { assert_eq!(finalize_1.view(), finalize_2.view()); assert_eq!(finalize_1.signer(), finalize_2.signer()); ConflictingFinalize { view: finalize_1.view(), parent_1: finalize_1.proposal.parent, payload_1: finalize_1.proposal.payload, signature_1: finalize_1.proposal_signature, parent_2: finalize_2.proposal.parent, payload_2: finalize_2.proposal.payload, signature_2: finalize_2.proposal_signature, } } /// Reconstructs the original proposals from this evidence. pub fn proposals(&self) -> (Proposal, Proposal) { ( Proposal::new(self.view, self.parent_1, self.payload_1), Proposal::new(self.view, self.parent_2, self.payload_2), ) } /// Verifies that both conflicting signatures are valid, proving Byzantine behavior. pub fn verify(&self, namespace: &[u8], polynomial: &[V::Public]) -> bool { let (proposal_1, proposal_2) = self.proposals(); let finalize_namespace = finalize_namespace(namespace); let finalize_message_1 = proposal_1.encode(); let finalize_message_1 = ( Some(finalize_namespace.as_ref()), finalize_message_1.as_ref(), ); let finalize_message_2 = proposal_2.encode(); let finalize_message_2 = ( Some(finalize_namespace.as_ref()), finalize_message_2.as_ref(), ); let Some(evaluated) = polynomial.get(self.signer() as usize) else { return false; }; let signature = aggregate_signatures::(&[self.signature_1.value, self.signature_2.value]); aggregate_verify_multiple_messages::( evaluated, &[finalize_message_1, finalize_message_2], &signature, 1, ) .is_ok() } } impl Attributable for ConflictingFinalize { fn signer(&self) -> u32 { self.signature_1.index } } impl Viewable for ConflictingFinalize { type View = View; fn view(&self) -> View { self.view } } impl Write for ConflictingFinalize { fn write(&self, writer: &mut impl BufMut) { UInt(self.view).write(writer); UInt(self.parent_1).write(writer); self.payload_1.write(writer); self.signature_1.write(writer); UInt(self.parent_2).write(writer); self.payload_2.write(writer); self.signature_2.write(writer); } } impl Read for ConflictingFinalize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let view = UInt::read(reader)?.into(); let parent_1 = UInt::read(reader)?.into(); let payload_1 = D::read(reader)?; let signature_1 = PartialSignature::::read(reader)?; let parent_2 = UInt::read(reader)?.into(); let payload_2 = D::read(reader)?; let signature_2 = PartialSignature::::read(reader)?; if signature_1.index != signature_2.index { return Err(Error::Invalid( "consensus::threshold_simplex::ConflictingFinalize", "mismatched signatures", )); } Ok(ConflictingFinalize { view, parent_1, payload_1, signature_1, parent_2, payload_2, signature_2, }) } } impl EncodeSize for ConflictingFinalize { fn encode_size(&self) -> usize { UInt(self.view).encode_size() + UInt(self.parent_1).encode_size() + self.payload_1.encode_size() + self.signature_1.encode_size() + UInt(self.parent_2).encode_size() + self.payload_2.encode_size() + self.signature_2.encode_size() } } /// NullifyFinalize represents evidence of a Byzantine validator sending both a nullify and finalize /// for the same view, which is contradictory behavior (a validator should either try to skip a view OR /// finalize a proposal, not both). #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct NullifyFinalize { /// The proposal that the validator tried to finalize pub proposal: Proposal, /// The signature on the nullify pub view_signature: PartialSignature, /// The signature on the finalize pub finalize_signature: PartialSignature, } impl NullifyFinalize { /// Creates a new nullify-finalize evidence from a nullify and a finalize. pub fn new(nullify: Nullify, finalize: Finalize) -> Self { assert_eq!(nullify.view(), finalize.view()); assert_eq!(nullify.signer(), finalize.signer()); NullifyFinalize { proposal: finalize.proposal, view_signature: nullify.view_signature, finalize_signature: finalize.proposal_signature, } } /// Verifies that both the nullify and finalize signatures are valid, proving Byzantine behavior. pub fn verify(&self, namespace: &[u8], polynomial: &[V::Public]) -> bool { let nullify_namespace = nullify_namespace(namespace); let nullify_message = view_message(self.proposal.view); let nullify_message = (Some(nullify_namespace.as_ref()), nullify_message.as_ref()); let finalize_namespace = finalize_namespace(namespace); let finalize_message = self.proposal.encode(); let finalize_message = (Some(finalize_namespace.as_ref()), finalize_message.as_ref()); let Some(evaluated) = polynomial.get(self.signer() as usize) else { return false; }; let signature = aggregate_signatures::(&[ self.view_signature.value, self.finalize_signature.value, ]); aggregate_verify_multiple_messages::( evaluated, &[nullify_message, finalize_message], &signature, 1, ) .is_ok() } } impl Attributable for NullifyFinalize { fn signer(&self) -> u32 { self.view_signature.index } } impl Viewable for NullifyFinalize { type View = View; fn view(&self) -> View { self.proposal.view() } } impl Write for NullifyFinalize { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.view_signature.write(writer); self.finalize_signature.write(writer); } } impl Read for NullifyFinalize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let proposal = Proposal::read(reader)?; let view_signature = PartialSignature::::read(reader)?; let finalize_signature = PartialSignature::::read(reader)?; if view_signature.index != finalize_signature.index { return Err(Error::Invalid( "consensus::threshold_simplex::NullifyFinalize", "mismatched signatures", )); } Ok(NullifyFinalize { proposal, view_signature, finalize_signature, }) } } impl EncodeSize for NullifyFinalize { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.view_signature.encode_size() + self.finalize_signature.encode_size() } } #[cfg(test)] mod tests { use super::*; use commonware_codec::{Decode, DecodeExt, Encode}; use commonware_cryptography::{ bls12381::{ dkg::ops::{self, evaluate_all}, primitives::{ group::{Element, Share}, ops::threshold_signature_recover, poly, variant::MinSig, }, }, sha256::Digest as Sha256, }; use commonware_utils::quorum; use rand::{rngs::StdRng, SeedableRng}; const NAMESPACE: &[u8] = b"test"; // Helper function to create a sample digest fn sample_digest(v: u8) -> Sha256 { Sha256::from([v; 32]) // Simple fixed digest for testing } // Helper function to generate BLS shares and polynomial fn generate_test_data( n: u32, t: u32, seed: u64, ) -> ( ::Public, Vec<::Public>, Vec, ) { let mut rng = StdRng::seed_from_u64(seed); let (polynomial, shares) = ops::generate_shares::<_, MinSig>(&mut rng, None, n, t); let identity = poly::public::(&polynomial); let polynomial = evaluate_all::(&polynomial, n); (*identity, polynomial, shares) } #[test] fn test_proposal_encode_decode() { let proposal = Proposal::new(10, 5, sample_digest(1)); let encoded = proposal.encode(); let decoded = Proposal::::decode(encoded).unwrap(); assert_eq!(proposal, decoded); } #[test] fn test_notarize_encode_decode() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); let notarize = Notarize::::sign(NAMESPACE, &shares[0], proposal); let encoded = notarize.encode(); let decoded = Notarize::::decode(encoded).unwrap(); assert_eq!(notarize, decoded); assert!(decoded.verify(NAMESPACE, &polynomial)); } #[test] fn test_notarization_encode_decode() { let n = 5; let t = quorum(n); let (identity, _, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); // Create notarizes let notarizes: Vec<_> = shares .iter() .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); // Recover threshold signature let proposal_partials = notarizes.iter().map(|n| &n.proposal_signature); let proposal_signature = threshold_signature_recover::(t, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); // Create notarization let notarization = Notarization::new(proposal, proposal_signature, seed_signature); let encoded = notarization.encode(); let decoded = Notarization::::decode(encoded).unwrap(); assert_eq!(notarization, decoded); // Verify the notarization assert!(decoded.verify(NAMESPACE, &identity)); // Create seed let seed = notarization.seed(); let encoded = seed.encode(); let decoded = Seed::::decode(encoded).unwrap(); assert_eq!(seed, decoded); // Verify the seed assert!(decoded.verify(NAMESPACE, &identity)); } #[test] fn test_nullify_encode_decode() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let nullify = Nullify::::sign(NAMESPACE, &shares[0], 10); let encoded = nullify.encode(); let decoded = Nullify::::decode(encoded).unwrap(); assert_eq!(nullify, decoded); assert!(decoded.verify(NAMESPACE, &polynomial)); } #[test] fn test_nullification_encode_decode() { let n = 5; let t = quorum(n); let (identity, _, shares) = generate_test_data(n, t, 0); // Create nullifies let nullifies: Vec<_> = shares .iter() .map(|s| Nullify::::sign(NAMESPACE, s, 10)) .collect(); // Recover threshold signature let view_partials = nullifies.iter().map(|n| &n.view_signature); let view_signature = threshold_signature_recover::(t, view_partials).unwrap(); let seed_partials = nullifies.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); // Create nullification let nullification = Nullification::new(10, view_signature, seed_signature); let encoded = nullification.encode(); let decoded = Nullification::::decode(encoded).unwrap(); assert_eq!(nullification, decoded); // Verify the nullification assert!(decoded.verify(NAMESPACE, &identity)); // Create seed let seed = nullification.seed(); let encoded = seed.encode(); let decoded = Seed::::decode(encoded).unwrap(); assert_eq!(seed, decoded); // Verify the seed assert!(decoded.verify(NAMESPACE, &identity)); } #[test] fn test_finalize_encode_decode() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); let finalize = Finalize::::sign(NAMESPACE, &shares[0], proposal); let encoded = finalize.encode(); let decoded = Finalize::::decode(encoded).unwrap(); assert_eq!(finalize, decoded); assert!(decoded.verify(NAMESPACE, &polynomial)); } #[test] fn test_finalization_encode_decode() { let n = 5; let t = quorum(n); let (identity, _, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); // Create finalizes let notarizes: Vec<_> = shares .iter() .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); let finalizes: Vec<_> = shares .iter() .map(|s| Finalize::::sign(NAMESPACE, s, proposal.clone())) .collect(); // Recover threshold signatures let proposal_partials = finalizes.iter().map(|f| &f.proposal_signature); let proposal_signature = threshold_signature_recover::(t, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); // Create finalization let finalization = Finalization::new(proposal, proposal_signature, seed_signature); let encoded = finalization.encode(); let decoded = Finalization::::decode(encoded).unwrap(); assert_eq!(finalization, decoded); // Verify the finalization assert!(decoded.verify(NAMESPACE, &identity)); // Create seed let seed = finalization.seed(); let encoded = seed.encode(); let decoded = Seed::::decode(encoded).unwrap(); assert_eq!(seed, decoded); // Verify the seed assert!(decoded.verify(NAMESPACE, &identity)); } #[test] fn test_backfiller_encode_decode() { // Test Request let request = Request::new(1, vec![10, 11], vec![12, 13]); let backfiller = Backfiller::::Request(request.clone()); let encoded = backfiller.encode(); let decoded = Backfiller::::decode_cfg(encoded, &usize::MAX).unwrap(); assert!(matches!(decoded, Backfiller::Request(r) if r == request)); // Test Response let n = 5; let t = quorum(n); let (_, _, shares) = generate_test_data(n, t, 0); // Create a notarization let proposal = Proposal::new(10, 5, sample_digest(1)); let notarizes: Vec<_> = shares .iter() .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); let proposal_partials = notarizes.iter().map(|n| &n.proposal_signature); let proposal_signature = threshold_signature_recover::(t, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); let notarization = Notarization::new(proposal, proposal_signature, seed_signature); // Create a nullification let nullifies: Vec<_> = shares .iter() .map(|s| Nullify::::sign(NAMESPACE, s, 11)) .collect(); let view_partials = nullifies.iter().map(|n| &n.view_signature); let view_signature = threshold_signature_recover::(t, view_partials).unwrap(); let seed_partials = nullifies.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); let nullification = Nullification::new(11, view_signature, seed_signature); // Create a response let response = Response::new(1, vec![notarization], vec![nullification]); let backfiller = Backfiller::::Response(response.clone()); let encoded = backfiller.encode(); let decoded = Backfiller::::decode_cfg(encoded, &usize::MAX).unwrap(); assert!(matches!(decoded, Backfiller::Response(r) if r.id == response.id)); } #[test] fn test_request_encode_decode() { let request = Request::new(1, vec![10, 11], vec![12, 13]); let encoded = request.encode(); let decoded = Request::decode_cfg(encoded, &usize::MAX).unwrap(); assert_eq!(request, decoded); } #[test] fn test_response_encode_decode() { let n = 5; let t = quorum(n); let (identity, _, shares) = generate_test_data(n, t, 0); // Create a notarization let proposal = Proposal::new(10, 5, sample_digest(1)); let notarizes: Vec<_> = shares .iter() .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); let proposal_partials = notarizes.iter().map(|n| &n.proposal_signature); let proposal_signature = threshold_signature_recover::(t, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); let notarization = Notarization::new(proposal, proposal_signature, seed_signature); // Create a nullification let nullifies: Vec<_> = shares .iter() .map(|s| Nullify::::sign(NAMESPACE, s, 11)) .collect(); let view_partials = nullifies.iter().map(|n| &n.view_signature); let view_signature = threshold_signature_recover::(t, view_partials).unwrap(); let seed_partials = nullifies.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); let nullification = Nullification::new(11, view_signature, seed_signature); // Create a response let response = Response::::new(1, vec![notarization], vec![nullification]); let encoded = response.encode(); let mut decoded = Response::::decode_cfg(encoded, &usize::MAX).unwrap(); assert_eq!(response.id, decoded.id); assert_eq!(response.notarizations.len(), decoded.notarizations.len()); assert_eq!(response.nullifications.len(), decoded.nullifications.len()); // Verify the response assert!(decoded.verify(NAMESPACE, &identity)); // Modify the response decoded.nullifications[0] .view_signature .add(&::Signature::one()); // Verify the modified response assert!(!decoded.verify(NAMESPACE, &identity)); } #[test] fn test_conflicting_notarize_encode_decode() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let proposal1 = Proposal::new(10, 5, sample_digest(1)); let proposal2 = Proposal::new(10, 5, sample_digest(2)); let notarize1 = Notarize::::sign(NAMESPACE, &shares[0], proposal1); let notarize2 = Notarize::::sign(NAMESPACE, &shares[0], proposal2); let conflicting_notarize = ConflictingNotarize::new(notarize1, notarize2); let encoded = conflicting_notarize.encode(); let decoded = ConflictingNotarize::::decode(encoded).unwrap(); assert_eq!(conflicting_notarize, decoded); assert!(decoded.verify(NAMESPACE, &polynomial)); } #[test] fn test_conflicting_finalize_encode_decode() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let proposal1 = Proposal::new(10, 5, sample_digest(1)); let proposal2 = Proposal::new(10, 5, sample_digest(2)); let finalize1 = Finalize::::sign(NAMESPACE, &shares[0], proposal1); let finalize2 = Finalize::::sign(NAMESPACE, &shares[0], proposal2); let conflicting_finalize = ConflictingFinalize::new(finalize1, finalize2); let encoded = conflicting_finalize.encode(); let decoded = ConflictingFinalize::::decode(encoded).unwrap(); assert_eq!(conflicting_finalize, decoded); assert!(decoded.verify(NAMESPACE, &polynomial)); } #[test] fn test_nullify_finalize_encode_decode() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); let nullify = Nullify::::sign(NAMESPACE, &shares[0], 10); let finalize = Finalize::::sign(NAMESPACE, &shares[0], proposal); let nullify_finalize = NullifyFinalize::new(nullify, finalize); let encoded = nullify_finalize.encode(); let decoded = NullifyFinalize::::decode(encoded).unwrap(); assert_eq!(nullify_finalize, decoded); assert!(decoded.verify(NAMESPACE, &polynomial)); } #[test] fn test_notarize_verify_wrong_namespace() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); let notarize = Notarize::::sign(NAMESPACE, &shares[0], proposal); // Verify with correct namespace and polynomial - should pass assert!(notarize.verify(NAMESPACE, &polynomial)); // Verify with wrong namespace - should fail assert!(!notarize.verify(b"wrong_namespace", &polynomial)); } #[test] fn test_notarize_verify_wrong_polynomial() { let n = 5; let t = quorum(n); let (_, polynomial1, shares1) = generate_test_data(n, t, 0); // Generate a different set of BLS keys/shares let (_, polynomial2, _) = generate_test_data(n, t, 1); let proposal = Proposal::new(10, 5, sample_digest(1)); let notarize = Notarize::::sign(NAMESPACE, &shares1[0], proposal); // Verify with correct polynomial - should pass assert!(notarize.verify(NAMESPACE, &polynomial1)); // Verify with wrong polynomial - should fail assert!(!notarize.verify(NAMESPACE, &polynomial2)); } #[test] fn test_notarization_verify_wrong_keys() { let n = 5; let t = quorum(n); let (identity, _, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); // Create notarizes let notarizes: Vec<_> = shares .iter() .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); // Recover threshold signature let proposal_partials = notarizes.iter().map(|n| &n.proposal_signature); let proposal_signature = threshold_signature_recover::(t, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); // Create notarization let notarization = Notarization::::new(proposal, proposal_signature, seed_signature); // Verify with correct public key - should pass assert!(notarization.verify(NAMESPACE, &identity)); // Generate a different set of BLS keys/shares let (wrong_identity, _, _) = generate_test_data(n, t, 1); // Verify with wrong public key - should fail assert!(!notarization.verify(NAMESPACE, &wrong_identity)); } #[test] fn test_notarization_verify_wrong_namespace() { let n = 5; let t = quorum(n); let (identity, _, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); // Create notarizes let notarizes: Vec<_> = shares .iter() .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); // Recover threshold signature let proposal_partials = notarizes.iter().map(|n| &n.proposal_signature); let proposal_signature = threshold_signature_recover::(t, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); // Create notarization let notarization = Notarization::::new(proposal, proposal_signature, seed_signature); // Verify with correct namespace - should pass assert!(notarization.verify(NAMESPACE, &identity)); // Verify with wrong namespace - should fail assert!(!notarization.verify(b"wrong_namespace", &identity)); } #[test] fn test_threshold_recover_insufficient_signatures() { let n = 5; let t = quorum(n); // For n=5, t should be 4 (2f+1 where f=1) let (_, _, shares) = generate_test_data(n, t, 0); let proposal = Proposal::new(10, 5, sample_digest(1)); // Create notarizes, but only collect t-1 of them let notarizes: Vec<_> = shares .iter() .take((t as usize) - 1) // One less than the threshold .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); // Try to recover threshold signature with insufficient partials - should fail let proposal_partials = notarizes.iter().map(|n| &n.proposal_signature); let result = threshold_signature_recover::(t, proposal_partials); // Should not be able to recover the threshold signature assert!(result.is_err()); } #[test] fn test_conflicting_notarize_detection() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); // Create two different proposals for the same view let proposal1 = Proposal::new(10, 5, sample_digest(1)); let proposal2 = Proposal::new(10, 5, sample_digest(2)); // Same view, different payload // Create notarizes for both proposals from the same validator let notarize1 = Notarize::::sign(NAMESPACE, &shares[0], proposal1.clone()); let notarize2 = Notarize::::sign(NAMESPACE, &shares[0], proposal2); // Create conflict evidence let conflict = ConflictingNotarize::new(notarize1, notarize2.clone()); // Verify the evidence is valid assert!(conflict.verify(NAMESPACE, &polynomial)); // Now create invalid evidence using different validator keys let notarize3 = Notarize::::sign(NAMESPACE, &shares[1], proposal1.clone()); // This should compile but verification should fail because the signatures // are from different validators let invalid_conflict: ConflictingNotarize = ConflictingNotarize { view: conflict.view, parent_1: conflict.parent_1, payload_1: conflict.payload_1, signature_1: conflict.signature_1.clone(), parent_2: notarize3.proposal.parent, payload_2: notarize3.proposal.payload, signature_2: notarize3.proposal_signature, }; // Verification should still fail even with correct polynomial assert!(!invalid_conflict.verify(NAMESPACE, &polynomial)); } #[test] fn test_nullify_finalize_detection() { let n = 5; let t = quorum(n); let (_, polynomial, shares) = generate_test_data(n, t, 0); let view = 10; // Create a nullify for view 10 let nullify = Nullify::::sign(NAMESPACE, &shares[0], view); // Create a finalize for the same view let proposal = Proposal::new(view, 5, sample_digest(1)); let finalize = Finalize::::sign(NAMESPACE, &shares[0], proposal); // Create nullify+finalize evidence let conflict = NullifyFinalize::new(nullify, finalize.clone()); // Verify the evidence is valid assert!(conflict.verify(NAMESPACE, &polynomial)); // Now try with wrong namespace assert!(!conflict.verify(b"wrong_namespace", &polynomial)); // Now create invalid evidence with different validators let nullify2 = Nullify::::sign(NAMESPACE, &shares[1], view); // Compile but verification should fail because signatures are from different validators let invalid_conflict: NullifyFinalize = NullifyFinalize { proposal: finalize.proposal.clone(), view_signature: conflict.view_signature.clone(), finalize_signature: nullify2.view_signature, }; // Verification should fail assert!(!invalid_conflict.verify(NAMESPACE, &polynomial)); } #[test] fn test_finalization_wrong_signature() { let n = 5; let t = quorum(n); let (identity, _, shares) = generate_test_data(n, t, 0); // Create a completely different key set let (wrong_identity, _, _) = generate_test_data(n, t, 1); let proposal = Proposal::new(10, 5, sample_digest(1)); // Create finalizes and notarizes for threshold signatures let finalizes: Vec<_> = shares .iter() .map(|s| Finalize::::sign(NAMESPACE, s, proposal.clone())) .collect(); let notarizes: Vec<_> = shares .iter() .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); // Recover threshold signatures let proposal_partials = finalizes.iter().map(|f| &f.proposal_signature); let proposal_signature = threshold_signature_recover::(t, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(t, seed_partials).unwrap(); // Create finalization let finalization = Finalization::::new(proposal, proposal_signature, seed_signature); // Verify with correct public key - should pass assert!(finalization.verify(NAMESPACE, &identity)); // Verify with wrong public key - should fail assert!(!finalization.verify(NAMESPACE, &wrong_identity)); } // Helper to create a Notarize message fn create_notarize( share: &Share, view: View, parent_view: View, payload_val: u8, ) -> Notarize { let proposal = Proposal::new(view, parent_view, sample_digest(payload_val)); Notarize::::sign(NAMESPACE, share, proposal) } // Helper to create a Nullify message fn create_nullify(share: &Share, view: View) -> Nullify { Nullify::::sign(NAMESPACE, share, view) } // Helper to create a Finalize message fn create_finalize( share: &Share, view: View, parent_view: View, payload_val: u8, ) -> Finalize { let proposal = Proposal::new(view, parent_view, sample_digest(payload_val)); Finalize::::sign(NAMESPACE, share, proposal) } // Helper to create a Notarization (for panic test) fn create_notarization( proposal_view: View, parent_view: View, payload_val: u8, shares: &[Share], threshold: u32, ) -> Notarization { let proposal = Proposal::new(proposal_view, parent_view, sample_digest(payload_val)); let notarizes: Vec<_> = shares .iter() .take(threshold as usize) .map(|s| Notarize::::sign(NAMESPACE, s, proposal.clone())) .collect(); let proposal_partials = notarizes.iter().map(|n| &n.proposal_signature); let proposal_signature = threshold_signature_recover::(threshold, proposal_partials).unwrap(); let seed_partials = notarizes.iter().map(|n| &n.seed_signature); let seed_signature = threshold_signature_recover::(threshold, seed_partials).unwrap(); Notarization::new(proposal, proposal_signature, seed_signature) } #[test] fn test_batch_verifier_add_notarize() { let n_validators = 5; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 123); let mut verifier = BatchVerifier::::new(Some(threshold)); let notarize1_s0 = create_notarize(&shares[0], 1, 0, 1); // validator 0 let notarize2_s1 = create_notarize(&shares[1], 1, 0, 1); // validator 1 (same proposal) let notarize_diff_prop_s2 = create_notarize(&shares[2], 1, 0, 2); // validator 2 (different proposal) // Add notarize1 (unverified) verifier.add(Voter::Notarize(notarize1_s0.clone()), false); assert_eq!(verifier.notarizes.len(), 1); assert_eq!(verifier.notarizes_verified, 0); // Add notarize1 again (verified) verifier.add(Voter::Notarize(notarize1_s0.clone()), true); assert_eq!(verifier.notarizes.len(), 1); // Still 1 pending assert_eq!(verifier.notarizes_verified, 1); // Verified count increases // Set leader to validator 0 (signer of notarize1) // This should trigger set_leader_proposal with notarize1's proposal verifier.set_leader(shares[0].index); assert!(verifier.leader_proposal.is_some()); assert_eq!( verifier.leader_proposal.as_ref().unwrap(), ¬arize1_s0.proposal ); assert!(verifier.notarizes_force); // Force verification assert_eq!(verifier.notarizes.len(), 1); // notarize1 still there // Add notarize2 (matches leader proposal) verifier.add(Voter::Notarize(notarize2_s1.clone()), false); assert_eq!(verifier.notarizes.len(), 2); // Add notarize_diff_prop (does not match leader proposal, should be dropped) verifier.add(Voter::Notarize(notarize_diff_prop_s2.clone()), false); assert_eq!(verifier.notarizes.len(), 2); // Should not have been added // Test adding when leader is set, but proposal comes from non-leader first let mut verifier2 = BatchVerifier::::new(Some(threshold)); let notarize_s1_v2 = create_notarize(&shares[1], 2, 1, 3); // from validator 1 let notarize_s0_v2_leader = create_notarize(&shares[0], 2, 1, 3); // from validator 0 (leader) verifier2.set_leader(shares[0].index); // Leader is 0 verifier2.add(Voter::Notarize(notarize_s1_v2.clone()), false); // Add non-leader's msg assert!(verifier2.leader_proposal.is_none()); // Leader proposal not set yet assert_eq!(verifier2.notarizes.len(), 1); verifier2.add(Voter::Notarize(notarize_s0_v2_leader.clone()), false); // Add leader's msg assert!(verifier2.leader_proposal.is_some()); // Now set assert_eq!( verifier2.leader_proposal.as_ref().unwrap(), ¬arize_s0_v2_leader.proposal ); assert_eq!(verifier2.notarizes.len(), 2); // Both should be there } #[test] fn test_batch_verifier_set_leader() { let n_validators = 5; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 124); let mut verifier = BatchVerifier::::new(Some(threshold)); let notarize_s0 = create_notarize(&shares[0], 1, 0, 1); let notarize_s1 = create_notarize(&shares[1], 1, 0, 1); // Add notarize from non-leader first verifier.add(Voter::Notarize(notarize_s1.clone()), false); assert_eq!(verifier.notarizes.len(), 1); // Set leader to s0 (no notarize from s0 yet) verifier.set_leader(shares[0].index); assert_eq!(verifier.leader, Some(shares[0].index)); assert!(verifier.leader_proposal.is_none()); // No proposal from leader yet assert!(!verifier.notarizes_force); assert_eq!(verifier.notarizes.len(), 1); // notarize_s1 still there // Add notarize from leader (s0) verifier.add(Voter::Notarize(notarize_s0.clone()), false); assert!(verifier.leader_proposal.is_some()); // Leader proposal now set assert_eq!( verifier.leader_proposal.as_ref().unwrap(), ¬arize_s0.proposal ); assert!(verifier.notarizes_force); // Force verification assert_eq!(verifier.notarizes.len(), 2); // Both notarizes present (assuming same proposal) } #[test] fn test_batch_verifier_ready_and_verify_notarizes() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, polynomial, shares) = generate_test_data(n_validators, threshold, 125); let mut verifier = BatchVerifier::::new(Some(threshold)); let proposal = Proposal::new(1, 0, sample_digest(1)); let notarize_s0 = Notarize::::sign(NAMESPACE, &shares[0], proposal.clone()); let notarize_s1 = Notarize::::sign(NAMESPACE, &shares[1], proposal.clone()); let notarize_s2 = Notarize::::sign(NAMESPACE, &shares[2], proposal.clone()); let notarize_s3 = Notarize::::sign(NAMESPACE, &shares[3], proposal.clone()); // Enough for quorum // Not ready - no leader/proposal (This specific check is now in test_ready_notarizes_without_leader_or_proposal) assert!(!verifier.ready_notarizes()); // Set leader and add leader's notarize verifier.set_leader(shares[0].index); verifier.add(Voter::Notarize(notarize_s0.clone()), false); assert!(verifier.ready_notarizes()); // notarizes_force is true (Covered by test_ready_notarizes_behavior_with_force_flag) assert_eq!(verifier.notarizes.len(), 1); let (verified_n, failed_n) = verifier.verify_notarizes(NAMESPACE, &polynomial); assert_eq!(verified_n.len(), 1); assert!(failed_n.is_empty()); assert_eq!(verifier.notarizes_verified, 1); assert!(verifier.notarizes.is_empty()); assert!(!verifier.notarizes_force); // Reset after verify (Covered by test_ready_notarizes_behavior_with_force_flag) // Not ready - not enough verifier.add(Voter::Notarize(notarize_s1.clone()), false); // Verified: 1, Pending: 1. Total: 2 < 4 assert!(!verifier.ready_notarizes()); verifier.add(Voter::Notarize(notarize_s2.clone()), false); // Verified: 1, Pending: 2. Total: 3 < 4 assert!(!verifier.ready_notarizes()); verifier.add(Voter::Notarize(notarize_s3.clone()), false); // Verified: 1, Pending: 3. Total: 4 == 4 assert!(verifier.ready_notarizes()); // (Covered by test_ready_notarizes_exact_quorum) assert_eq!(verifier.notarizes.len(), 3); let (verified_n, failed_n) = verifier.verify_notarizes(NAMESPACE, &polynomial); assert_eq!(verified_n.len(), 3); assert!(failed_n.is_empty()); assert_eq!(verifier.notarizes_verified, 1 + 3); // 1 previous + 3 new assert!(verifier.notarizes.is_empty()); // Not ready - quorum met by verified (Covered by test_ready_notarizes_quorum_already_met_by_verified) assert!(!verifier.ready_notarizes()); // Scenario: Verification with a faulty signature let mut verifier2 = BatchVerifier::::new(Some(threshold)); verifier2.set_leader(shares[0].index); // Set leader let leader_notarize = create_notarize(&shares[0], 2, 1, 10); verifier2.add(Voter::Notarize(leader_notarize.clone()), false); // Add leader's notarize let mut faulty_notarize = create_notarize(&shares[1], 2, 1, 10); // Same proposal as leader // Corrupt a signature let (_, _, other_shares) = generate_test_data(n_validators, threshold, 126); faulty_notarize.proposal_signature = Notarize::::sign( NAMESPACE, &other_shares[1], faulty_notarize.proposal.clone(), ) // Sign with a "wrong" share for that index .proposal_signature; verifier2.add(Voter::Notarize(faulty_notarize.clone()), false); // Add invalid notarize assert!(verifier2.ready_notarizes()); // Force is true let (verified_n, failed_n) = verifier2.verify_notarizes(NAMESPACE, &polynomial); assert_eq!(verified_n.len(), 1); // Only leader's should verify assert!(verified_n.contains(&Voter::Notarize(leader_notarize))); assert_eq!(failed_n.len(), 1); assert_eq!(failed_n[0], shares[1].index); // s1's should fail } #[test] fn test_batch_verifier_add_nullify() { let n_validators = 5; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 127); let mut verifier = BatchVerifier::::new(Some(threshold)); let nullify1_s0 = create_nullify(&shares[0], 1); // Add unverified verifier.add(Voter::Nullify(nullify1_s0.clone()), false); assert_eq!(verifier.nullifies.len(), 1); assert_eq!(verifier.nullifies_verified, 0); // Add verified verifier.add(Voter::Nullify(nullify1_s0.clone()), true); assert_eq!(verifier.nullifies.len(), 1); assert_eq!(verifier.nullifies_verified, 1); } #[test] fn test_batch_verifier_ready_and_verify_nullifies() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, polynomial, shares) = generate_test_data(n_validators, threshold, 128); let mut verifier = BatchVerifier::::new(Some(threshold)); let nullify_s0 = create_nullify(&shares[0], 1); let nullify_s1 = create_nullify(&shares[1], 1); let nullify_s2 = create_nullify(&shares[2], 1); let nullify_s3 = create_nullify(&shares[3], 1); // Enough for quorum // Not ready, not enough verifier.add(Voter::Nullify(nullify_s0.clone()), true); // Verified: 1 assert_eq!(verifier.nullifies_verified, 1); verifier.add(Voter::Nullify(nullify_s1.clone()), false); // Verified: 1, Pending: 1. Total: 2 < 4 assert!(!verifier.ready_nullifies()); verifier.add(Voter::Nullify(nullify_s2.clone()), false); // Verified: 1, Pending: 2. Total: 3 < 4 assert!(!verifier.ready_nullifies()); // Ready, enough for quorum verifier.add(Voter::Nullify(nullify_s3.clone()), false); // Verified: 1, Pending: 3. Total: 4 == 4 assert!(verifier.ready_nullifies()); assert_eq!(verifier.nullifies.len(), 3); let (verified_null, failed_null) = verifier.verify_nullifies(NAMESPACE, &polynomial); assert_eq!(verified_null.len(), 3); assert!(failed_null.is_empty()); assert_eq!(verifier.nullifies_verified, 1 + 3); // Nothing to do after verify assert!(verifier.nullifies.is_empty()); assert!(!verifier.ready_nullifies()); } #[test] fn test_batch_verifier_add_finalize() { let n_validators = 5; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 129); let mut verifier = BatchVerifier::::new(Some(threshold)); let finalize_s0_prop_a = create_finalize(&shares[0], 1, 0, 1); // Proposal A let finalize_s1_prop_b = create_finalize(&shares[1], 1, 0, 2); // Proposal B // Add finalize_s1_propB (unverified) - No leader proposal yet, so it's added verifier.add(Voter::Finalize(finalize_s1_prop_b.clone()), false); assert_eq!(verifier.finalizes.len(), 1); assert_eq!(verifier.finalizes_verified, 0); // Add finalize_s0_prop_a (unverified) verifier.add(Voter::Finalize(finalize_s0_prop_a.clone()), false); assert_eq!(verifier.finalizes.len(), 2); // Both are present // Set leader and leader proposal to Proposal A // This specific call to set_leader won't set leader_proposal because no notarize from leader exists. verifier.set_leader(shares[0].index); assert!(verifier.leader_proposal.is_none()); // Manually set leader_proposal for finalize_s0_propA verifier.set_leader_proposal(finalize_s0_prop_a.proposal.clone()); // Now, finalize_s1_propB should have been removed. assert_eq!(verifier.finalizes.len(), 1); assert_eq!(verifier.finalizes[0], finalize_s0_prop_a); assert_eq!(verifier.finalizes_verified, 0); // Add finalize_s0_propA (verified) verifier.add(Voter::Finalize(finalize_s0_prop_a.clone()), true); assert_eq!(verifier.finalizes.len(), 1); // Still finalize_s0_propA assert_eq!(verifier.finalizes_verified, 1); // Verified count increased // Add finalize_s1_propB (unverified) - should be dropped as it doesn't match leader proposal verifier.add(Voter::Finalize(finalize_s1_prop_b.clone()), false); assert_eq!(verifier.finalizes.len(), 1); // Should still be 1 (finalize_s0_propA) assert_eq!(verifier.finalizes_verified, 1); } #[test] fn test_batch_verifier_ready_and_verify_finalizes() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, polynomial, shares) = generate_test_data(n_validators, threshold, 130); let mut verifier = BatchVerifier::::new(Some(threshold)); let leader_proposal = Proposal::new(1, 0, sample_digest(1)); let finalize_s0 = Finalize::::sign(NAMESPACE, &shares[0], leader_proposal.clone()); let finalize_s1 = Finalize::::sign(NAMESPACE, &shares[1], leader_proposal.clone()); let finalize_s2 = Finalize::::sign(NAMESPACE, &shares[2], leader_proposal.clone()); let finalize_s3 = Finalize::::sign(NAMESPACE, &shares[3], leader_proposal.clone()); // Not ready - no leader/proposal set (Covered by test_ready_finalizes_without_leader_or_proposal) assert!(!verifier.ready_finalizes()); // Set leader and leader proposal verifier.set_leader(shares[0].index); // Leader is s0 // Manually set leader proposal, as set_leader won't do it without a notarize from leader. verifier.set_leader_proposal(leader_proposal.clone()); // Add some (verified and unverified) verifier.add(Voter::Finalize(finalize_s0.clone()), true); // Verified: 1 assert_eq!(verifier.finalizes_verified, 1); assert_eq!(verifier.finalizes.len(), 0); verifier.add(Voter::Finalize(finalize_s1.clone()), false); // Verified: 1, Pending: 1. Total: 2 < 4 assert!(!verifier.ready_finalizes()); verifier.add(Voter::Finalize(finalize_s2.clone()), false); // Verified: 1, Pending: 2. Total: 3 < 4 assert!(!verifier.ready_finalizes()); // Ready for finalizes verifier.add(Voter::Finalize(finalize_s3.clone()), false); // Verified: 1, Pending: 3. Total: 4 == 4 assert!(verifier.ready_finalizes()); // (Covered by test_ready_finalizes_exact_quorum) let (verified_fin, failed_fin) = verifier.verify_finalizes(NAMESPACE, &polynomial); assert_eq!(verified_fin.len(), 3); assert!(failed_fin.is_empty()); assert_eq!(verifier.finalizes_verified, 1 + 3); assert!(verifier.finalizes.is_empty()); // Not ready, quorum met (Covered by test_ready_finalizes_quorum_already_met_by_verified) assert!(!verifier.ready_finalizes()); } #[test] fn test_batch_verifier_quorum_none() { let n_validators = 3; let threshold = quorum(n_validators); // Not strictly used by BatchVerifier logic when quorum is None let (_, polynomial, shares) = generate_test_data(n_validators, threshold, 200); // Test with Notarizes let mut verifier_n = BatchVerifier::::new(None); let prop1 = Proposal::new(1, 0, sample_digest(1)); let notarize1 = create_notarize(&shares[0], 1, 0, 1); assert!(!verifier_n.ready_notarizes()); // No leader/proposal verifier_n.set_leader(shares[0].index); verifier_n.add(Voter::Notarize(notarize1.clone()), false); // Sets leader proposal and notarizes_force assert!(verifier_n.ready_notarizes()); // notarizes_force is true, and notarizes is not empty let (verified, failed) = verifier_n.verify_notarizes(NAMESPACE, &polynomial); assert_eq!(verified.len(), 1); assert!(failed.is_empty()); assert_eq!(verifier_n.notarizes_verified, 1); assert!(!verifier_n.ready_notarizes()); // notarizes_force is false, list is empty // Test with Nullifies let mut verifier_null = BatchVerifier::::new(None); let nullify1 = create_nullify(&shares[0], 1); assert!(!verifier_null.ready_nullifies()); // List is empty verifier_null.add(Voter::Nullify(nullify1.clone()), false); assert!(verifier_null.ready_nullifies()); // List is not empty let (verified, failed) = verifier_null.verify_nullifies(NAMESPACE, &polynomial); assert_eq!(verified.len(), 1); assert!(failed.is_empty()); assert_eq!(verifier_null.nullifies_verified, 1); assert!(!verifier_null.ready_nullifies()); // List is empty // Test with Finalizes let mut verifier_f = BatchVerifier::::new(None); let finalize1 = create_finalize(&shares[0], 1, 0, 1); assert!(!verifier_f.ready_finalizes()); // No leader/proposal verifier_f.set_leader(shares[0].index); verifier_f.set_leader_proposal(prop1.clone()); // Assume prop1 is the leader's proposal verifier_f.add(Voter::Finalize(finalize1.clone()), false); assert!(verifier_f.ready_finalizes()); // Leader/proposal set, list not empty let (verified, failed) = verifier_f.verify_finalizes(NAMESPACE, &polynomial); assert_eq!(verified.len(), 1); assert!(failed.is_empty()); assert_eq!(verifier_f.finalizes_verified, 1); assert!(!verifier_f.ready_finalizes()); // List is empty } #[test] fn test_batch_verifier_leader_proposal_filters_messages() { let n_validators = 3; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 201); let mut verifier = BatchVerifier::::new(Some(threshold)); let proposal_a = Proposal::new(1, 0, sample_digest(10)); let proposal_b = Proposal::new(1, 0, sample_digest(20)); let notarize_a_s0 = Notarize::::sign(NAMESPACE, &shares[0], proposal_a.clone()); let notarize_b_s1 = Notarize::::sign(NAMESPACE, &shares[1], proposal_b.clone()); let finalize_a_s0 = Finalize::::sign(NAMESPACE, &shares[0], proposal_a.clone()); let finalize_b_s1 = Finalize::::sign(NAMESPACE, &shares[1], proposal_b.clone()); verifier.add(Voter::Notarize(notarize_a_s0.clone()), false); verifier.add(Voter::Notarize(notarize_b_s1.clone()), false); verifier.add(Voter::Finalize(finalize_a_s0.clone()), false); verifier.add(Voter::Finalize(finalize_b_s1.clone()), false); assert_eq!(verifier.notarizes.len(), 2); assert_eq!(verifier.finalizes.len(), 2); // Set leader proposal to proposal_A // To make set_leader_proposal get called from set_leader, a notarize from the leader must exist. // Or, call it directly. verifier.set_leader(shares[0].index); assert!(verifier.notarizes_force); assert_eq!(verifier.notarizes.len(), 1); assert_eq!(verifier.notarizes[0].proposal, proposal_a); assert_eq!(verifier.finalizes.len(), 1); assert_eq!(verifier.finalizes[0].proposal, proposal_a); } #[test] #[should_panic(expected = "self.leader.is_none()")] fn test_batch_verifier_set_leader_twice_panics() { let mut verifier = BatchVerifier::::new(Some(3)); verifier.set_leader(0); verifier.set_leader(1); // This should panic } #[test] #[should_panic(expected = "should not be adding recovered messages to partial verifier")] fn test_batch_verifier_add_recovered_message_panics() { let n_validators = 3; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 202); let mut verifier = BatchVerifier::::new(Some(threshold)); let notarization = create_notarization(1, 0, 1, &shares, threshold); verifier.add(Voter::Notarization(notarization), false); // This should panic } #[test] fn test_ready_notarizes_behavior_with_force_flag() { let n_validators = 3; let threshold = quorum(n_validators); let (_, polynomial, shares) = generate_test_data(n_validators, threshold, 203); let mut verifier = BatchVerifier::::new(Some(threshold)); let leader_notarize = create_notarize(&shares[0], 1, 0, 1); // Set leader and add leader's notarize verifier.set_leader(shares[0].index); // Manually add leader's notarize for it to pick up leader_proposal verifier.add(Voter::Notarize(leader_notarize.clone()), false); assert!( verifier.notarizes_force, "notarizes_force should be true after leader's proposal is set" ); assert!( verifier.ready_notarizes(), "Ready should be true when notarizes_force is true" ); // Assume leader's own notarize is processed. Let's verify it. let (verified, _) = verifier.verify_notarizes(NAMESPACE, &polynomial); assert_eq!(verified.len(), 1); assert!( !verifier.notarizes_force, "notarizes_force should be false after verification" ); assert!( !verifier.ready_notarizes(), "Ready should be false now (no pending, quorum not met by verified alone)" ); } #[test] fn test_ready_notarizes_without_leader_or_proposal() { let n_validators = 3; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 204); let mut verifier = BatchVerifier::::new(Some(threshold)); // Collect sufficient number of unverified notarizes for i in 0..threshold { verifier.add( Voter::Notarize(create_notarize(&shares[i as usize], 1, 0, 1)), false, ); } assert!( !verifier.ready_notarizes(), "Should not be ready without leader/proposal set" ); // Set leader verifier.set_leader(shares[0].index); assert!( verifier.ready_notarizes(), "Should be ready once leader is set" ); } #[test] fn test_ready_finalizes_without_leader_or_proposal() { let n_validators = 3; let threshold = quorum(n_validators); let (_, _, shares) = generate_test_data(n_validators, threshold, 205); let mut verifier = BatchVerifier::::new(Some(threshold)); for i in 0..threshold { verifier.add( Voter::Finalize(create_finalize(&shares[i as usize], 1, 0, 1)), false, ); } assert!( !verifier.ready_finalizes(), "Should not be ready without leader/proposal set" ); // Set leader, still not ready verifier.set_leader(shares[0].index); assert!( !verifier.ready_finalizes(), "Should not be ready without leader_proposal set" ); } #[test] fn test_verify_notarizes_empty_pending_when_forced() { let n_validators = 3; let threshold = quorum(n_validators); let mut verifier = BatchVerifier::::new(Some(threshold)); let leader_proposal = Proposal::new(1, 0, sample_digest(1)); verifier.set_leader_proposal(leader_proposal); // This sets notarizes_force = true assert!(verifier.notarizes_force); assert!(verifier.notarizes.is_empty()); assert!(!verifier.ready_notarizes()); } #[test] fn test_verify_nullifies_empty_pending() { let n_validators = 3; let threshold = quorum(n_validators); let (_, polynomial, _) = generate_test_data(n_validators, threshold, 207); let mut verifier = BatchVerifier::::new(Some(threshold)); assert!(verifier.nullifies.is_empty()); // ready_nullifies will be false if the list is empty and quorum is Some assert!(!verifier.ready_nullifies()); let (verified, failed) = verifier.verify_nullifies(NAMESPACE, &polynomial); assert!(verified.is_empty()); assert!(failed.is_empty()); assert_eq!(verifier.nullifies_verified, 0); } #[test] fn test_verify_finalizes_empty_pending() { let n_validators = 3; let threshold = quorum(n_validators); let (_, polynomial, shares) = generate_test_data(n_validators, threshold, 208); let mut verifier = BatchVerifier::::new(Some(threshold)); // ready_finalizes will be false if the list is empty and quorum is Some verifier.set_leader(shares[0].index); assert!(verifier.finalizes.is_empty()); assert!(!verifier.ready_finalizes()); let (verified, failed) = verifier.verify_finalizes(NAMESPACE, &polynomial); assert!(verified.is_empty()); assert!(failed.is_empty()); assert_eq!(verifier.finalizes_verified, 0); } #[test] fn test_ready_notarizes_exact_quorum() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, polynomial, shares) = generate_test_data(n_validators, threshold, 209); let mut verifier = BatchVerifier::::new(Some(threshold)); let leader_notarize = create_notarize(&shares[0], 1, 0, 1); verifier.set_leader(shares[0].index); verifier.add(Voter::Notarize(leader_notarize), true); // 1 verified assert_eq!(verifier.notarizes_verified, 1); // Add next verified notarize verifier.add(Voter::Notarize(create_notarize(&shares[1], 1, 0, 1)), false); // Perform forced verification assert!(verifier.ready_notarizes()); let (verified, failed) = verifier.verify_notarizes(NAMESPACE, &polynomial); assert_eq!(verified.len(), 1); assert!(failed.is_empty()); assert_eq!(verifier.notarizes_verified, 1 + 1); // Add threshold - 1 pending notarizes for share in shares.iter().take(threshold as usize).skip(2) { assert!(!verifier.ready_notarizes()); verifier.add(Voter::Notarize(create_notarize(share, 1, 0, 1)), false); } // Now, notarizes_verified = 2, notarizes.len() = 2. Total = 4 == threshold assert!(verifier.ready_notarizes()); } #[test] fn test_ready_nullifies_exact_quorum() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, _, shares) = generate_test_data(n_validators, threshold, 210); let mut verifier = BatchVerifier::::new(Some(threshold)); verifier.add(Voter::Nullify(create_nullify(&shares[0], 1)), true); // 1 verified assert_eq!(verifier.nullifies_verified, 1); for share in shares.iter().take(threshold as usize).skip(1) { assert!(!verifier.ready_nullifies()); verifier.add(Voter::Nullify(create_nullify(share, 1)), false); } assert!(verifier.ready_nullifies()); } #[test] fn test_ready_finalizes_exact_quorum() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, _, shares) = generate_test_data(n_validators, threshold, 211); let mut verifier = BatchVerifier::::new(Some(threshold)); let leader_proposal = Proposal::new(1, 0, sample_digest(1)); verifier.set_leader(shares[0].index); verifier.set_leader_proposal(leader_proposal.clone()); verifier.add(Voter::Finalize(create_finalize(&shares[0], 1, 0, 1)), true); // 1 verified assert_eq!(verifier.finalizes_verified, 1); for share in shares.iter().take(threshold as usize).skip(1) { assert!(!verifier.ready_finalizes()); verifier.add(Voter::Finalize(create_finalize(share, 1, 0, 1)), false); } assert!(verifier.ready_finalizes()); } #[test] fn test_ready_notarizes_quorum_already_met_by_verified() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, _, shares) = generate_test_data(n_validators, threshold, 212); let mut verifier = BatchVerifier::::new(Some(threshold)); let leader_notarize = create_notarize(&shares[0], 1, 0, 1); verifier.set_leader(shares[0].index); verifier.add(Voter::Notarize(leader_notarize), false); // This sets leader_proposal and notarizes_force // Manually set notarizes_force to false as if verify_notarizes was called. verifier.notarizes_force = false; for share in shares.iter().take(threshold as usize) { verifier.add(Voter::Notarize(create_notarize(share, 1, 0, 1)), true); } assert_eq!(verifier.notarizes_verified as u32, threshold); assert!( !verifier.ready_notarizes(), "Should not be ready if quorum already met by verified messages" ); // Add one more pending, still should not be ready verifier.add( Voter::Notarize(create_notarize(&shares[threshold as usize], 1, 0, 1)), false, ); assert!(!verifier.ready_notarizes()); } #[test] fn test_ready_nullifies_quorum_already_met_by_verified() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, _, shares) = generate_test_data(n_validators, threshold, 213); let mut verifier = BatchVerifier::::new(Some(threshold)); for share in shares.iter().take(threshold as usize) { verifier.add(Voter::Nullify(create_nullify(share, 1)), true); } assert_eq!(verifier.nullifies_verified as u32, threshold); assert!(!verifier.ready_nullifies()); verifier.add( Voter::Nullify(create_nullify(&shares[threshold as usize], 1)), false, ); assert!(!verifier.ready_nullifies()); } #[test] fn test_ready_finalizes_quorum_already_met_by_verified() { let n_validators = 5; let threshold = quorum(n_validators); // threshold = 4 let (_, _, shares) = generate_test_data(n_validators, threshold, 214); let mut verifier = BatchVerifier::::new(Some(threshold)); let leader_proposal = Proposal::new(1, 0, sample_digest(1)); verifier.set_leader(shares[0].index); verifier.set_leader_proposal(leader_proposal.clone()); for share in shares.iter().take(threshold as usize) { verifier.add(Voter::Finalize(create_finalize(share, 1, 0, 1)), true); } assert_eq!(verifier.finalizes_verified as u32, threshold); assert!(!verifier.ready_finalizes()); verifier.add( Voter::Finalize(create_finalize(&shares[threshold as usize], 1, 0, 1)), false, ); assert!(!verifier.ready_finalizes()); } }