//! Types used in [crate::simplex]. use crate::{ Epochable, Viewable, simplex::scheme::{self, CertificateVerifier}, types::{Epoch, Participant, Round, View}, }; use bytes::BufMut; use commonware_codec::{Buf, EncodeSize, Error, FixedSize, MaxEncodeSize, Read, ReadExt, Write}; use commonware_cryptography::{ Digest, PublicKey, certificate::{AssemblyError, Attestation, Scheme}, }; use commonware_parallel::Strategy; use commonware_utils::{iter::NonEmpty, max, non_empty}; use rand_core::CryptoRng; use std::{fmt::Debug, hash::Hash}; /// Context is a collection of metadata from consensus about a given payload. /// It provides information about the current epoch/view and the parent payload that new proposals are built on. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Context { /// Current round of consensus. pub round: Round, /// Leader of the current round. pub leader: P, /// Parent the payload is built on. /// /// When the current view is not a term start, the parent must be the immediately /// previous view. When the current view is a term start, the parent may be an older /// certified view as long as the participant possesses nullifications covering every /// skipped term (a nullification covers the view it was created for and the remainder /// of that term); any uncovered view may eventually be finalized and skipping it would /// result in a fork. A certified parent remains valid even if a later nullification in /// its own term covers the parent view. /// /// A pipelined handoff request instead names the previous view as its parent before that /// view certifies (see [`crate::CertifiableAutomaton::prepare`]). /// /// A certified parent may never be finalized. Later proposals can skip it once a nullification /// covers its view, and a competing payload can then be finalized in its place. Only treat the /// parent as final once the [`Reporter`](crate::Reporter) receives an /// [`Activity::Finalization`] for it or one of its descendants. pub parent: (View, D), } impl Epochable for Context { fn epoch(&self) -> Epoch { self.round.epoch() } } impl Viewable for Context { fn view(&self) -> View { self.round.view() } } impl Write for Context { fn write(&self, buf: &mut impl BufMut) { self.round.write(buf); self.leader.write(buf); self.parent.write(buf); } } impl EncodeSize for Context { fn encode_size(&self) -> usize { self.round.encode_size() + self.leader.encode_size() + self.parent.encode_size() } } impl MaxEncodeSize for Context { const MAX_ENCODE_SIZE: usize = Round::MAX_ENCODE_SIZE + P::SIZE + <(View, D)>::MAX_ENCODE_SIZE; } impl Read for Context { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let round = Round::read(reader)?; let leader = P::read(reader)?; let parent = <(View, D)>::read_cfg(reader, &((), ()))?; Ok(Self { round, leader, parent, }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Context where D: for<'a> arbitrary::Arbitrary<'a>, P: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self { round: Round::arbitrary(u)?, leader: P::arbitrary(u)?, parent: (View::arbitrary(u)?, D::arbitrary(u)?), }) } } /// 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) -> Participant; } /// A map of [Attributable] items keyed by their signer index. /// /// The key for each item is automatically inferred from [Attributable::signer()]. /// Each signer can insert at most one item. pub struct AttributableMap { participants: usize, data: Vec>, added: usize, } impl AttributableMap { /// Creates a new [AttributableMap] with the given number of participants. pub const fn new(participants: usize) -> Self { Self { participants, data: Vec::new(), added: 0, } } /// Clears all existing items and releases their storage. pub fn clear(&mut self) { self.data = Vec::new(); self.added = 0; } /// Inserts an item into the map, using [Attributable::signer()] as the key, /// if it has not been added yet. /// /// Returns `true` if the item was inserted, `false` if an item from this /// signer already exists or if the signer index is out of bounds. pub fn insert(&mut self, item: T) -> bool { let index: usize = item.signer().into(); if index >= self.participants { return false; } if self.data.is_empty() { // `resize_with` avoids requiring `T: Clone` while pre-filling with `None`. self.data.reserve_exact(self.participants); self.data.resize_with(self.participants, || None); } if self.data[index].is_some() { return false; } self.data[index] = Some(item); self.added += 1; true } /// Returns the number of items in the [AttributableMap]. pub const fn len(&self) -> usize { self.added } /// Returns `true` if the [AttributableMap] is empty. pub const fn is_empty(&self) -> bool { self.added == 0 } /// Returns a reference to the item associated with the given signer, if present. pub fn get(&self, signer: Participant) -> Option<&T> { self.data.get(::from(signer))?.as_ref() } /// Returns an iterator over items in the map, ordered by signer index /// ([Attributable::signer()]). pub fn iter(&self) -> impl Iterator { self.data.iter().filter_map(|o| o.as_ref()) } /// Returns the allocated slot capacity. #[cfg(test)] pub(crate) const fn capacity(&self) -> usize { self.data.capacity() } } /// Identifies the subject of a vote or certificate. /// /// Implementations use the subject to derive domain-separated message bytes for both /// individual votes and recovered certificates. #[derive(Copy, Clone, Debug)] pub enum Subject<'a, D: Digest> { /// Subject for notarize votes and certificates, carrying the proposal. Notarize { proposal: &'a Proposal }, /// Subject for nullify votes and certificates, scoped to a round. Nullify { round: Round }, /// Subject for finalize votes and certificates, carrying the proposal. Finalize { proposal: &'a Proposal }, } impl Viewable for Subject<'_, D> { fn view(&self) -> View { match self { Subject::Notarize { proposal } => proposal.view(), Subject::Nullify { round } => round.view(), Subject::Finalize { proposal } => proposal.view(), } } } /// Vote represents individual votes ([Notarize], [Nullify], [Finalize]). #[derive(Clone, Debug, PartialEq)] pub enum Vote { /// A validator's notarize vote over a proposal. Notarize(Notarize), /// A validator's nullify vote used to skip the current view. Nullify(Nullify), /// A validator's finalize vote over a proposal. Finalize(Finalize), } impl Write for Vote { fn write(&self, writer: &mut impl BufMut) { match self { Self::Notarize(v) => { 0u8.write(writer); v.write(writer); } Self::Nullify(v) => { 1u8.write(writer); v.write(writer); } Self::Finalize(v) => { 2u8.write(writer); v.write(writer); } } } } impl EncodeSize for Vote { fn encode_size(&self) -> usize { 1 + match self { Self::Notarize(v) => v.encode_size(), Self::Nullify(v) => v.encode_size(), Self::Finalize(v) => v.encode_size(), } } } impl MaxEncodeSize for Vote { // A tag and the widest variant. const MAX_ENCODE_SIZE: usize = u8::SIZE + max( max( Notarize::::MAX_ENCODE_SIZE, Nullify::::MAX_ENCODE_SIZE, ), Finalize::::MAX_ENCODE_SIZE, ); } impl Read for Vote { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let tag = ::read(reader)?; match tag { 0 => { let v = Notarize::read(reader)?; Ok(Self::Notarize(v)) } 1 => { let v = Nullify::read(reader)?; Ok(Self::Nullify(v)) } 2 => { let v = Finalize::read(reader)?; Ok(Self::Finalize(v)) } _ => Err(Error::Invalid("consensus::simplex::Vote", "Invalid type")), } } } impl Epochable for Vote { fn epoch(&self) -> Epoch { match self { Self::Notarize(v) => v.epoch(), Self::Nullify(v) => v.epoch(), Self::Finalize(v) => v.epoch(), } } } impl Viewable for Vote { fn view(&self) -> View { match self { Self::Notarize(v) => v.view(), Self::Nullify(v) => v.view(), Self::Finalize(v) => v.view(), } } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Vote where S::Signature: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let tag = u.int_in_range(0..=2)?; match tag { 0 => { let v = Notarize::arbitrary(u)?; Ok(Self::Notarize(v)) } 1 => { let v = Nullify::arbitrary(u)?; Ok(Self::Nullify(v)) } 2 => { let v = Finalize::arbitrary(u)?; Ok(Self::Finalize(v)) } _ => unreachable!(), } } } /// Certificate represents aggregated votes ([Notarization], [Nullification], [Finalization]). #[derive(Clone, Debug, PartialEq)] pub enum Certificate { /// A recovered certificate for a notarization. Notarization(Notarization), /// A recovered certificate for a nullification. Nullification(Nullification), /// A recovered certificate for a finalization. Finalization(Finalization), } /// The discriminant of a [Certificate], naming its kind without its contents. #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Kind { Notarization, Nullification, Finalization, } #[cfg(not(target_arch = "wasm32"))] impl std::fmt::Display for Kind { /// Writes the stable trace field value for this certificate type. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Notarization => "notarization", Self::Nullification => "nullification", Self::Finalization => "finalization", }) } } impl Certificate { /// Returns an upper bound on the encoded size of any certificate for a committee of at most /// `participants`. pub fn max_encode_size(participants: u32) -> usize { let notarization = Notarization::::max_encode_size(participants); let nullification = Nullification::::max_encode_size(participants); let finalization = Finalization::::max_encode_size(participants); notarization.max(nullification).max(finalization) + u8::SIZE } /// Returns this certificate's type. #[cfg(not(target_arch = "wasm32"))] pub(crate) const fn kind(&self) -> Kind { match self { Self::Notarization(_) => Kind::Notarization, Self::Nullification(_) => Kind::Nullification, Self::Finalization(_) => Kind::Finalization, } } /// Verifies this certificate against the provided signing scheme. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where S: scheme::Scheme, { match self { Self::Notarization(notarization) => notarization.verify(rng, scheme, strategy), Self::Nullification(nullification) => { nullification.verify::<_, D>(rng, scheme, strategy) } Self::Finalization(finalization) => finalization.verify(rng, scheme, strategy), } } } impl Write for Certificate { fn write(&self, writer: &mut impl BufMut) { match self { Self::Notarization(v) => { 0u8.write(writer); v.write(writer); } Self::Nullification(v) => { 1u8.write(writer); v.write(writer); } Self::Finalization(v) => { 2u8.write(writer); v.write(writer); } } } } impl EncodeSize for Certificate { fn encode_size(&self) -> usize { 1 + match self { Self::Notarization(v) => v.encode_size(), Self::Nullification(v) => v.encode_size(), Self::Finalization(v) => v.encode_size(), } } } impl Read for Certificate { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let tag = ::read(reader)?; match tag { 0 => { let v = Notarization::read_cfg(reader, cfg)?; Ok(Self::Notarization(v)) } 1 => { let v = Nullification::read_cfg(reader, cfg)?; Ok(Self::Nullification(v)) } 2 => { let v = Finalization::read_cfg(reader, cfg)?; Ok(Self::Finalization(v)) } _ => Err(Error::Invalid( "consensus::simplex::Certificate", "Invalid type", )), } } } impl Epochable for Certificate { fn epoch(&self) -> Epoch { match self { Self::Notarization(v) => v.epoch(), Self::Nullification(v) => v.epoch(), Self::Finalization(v) => v.epoch(), } } } impl Viewable for Certificate { fn view(&self) -> View { match self { Self::Notarization(v) => v.view(), Self::Nullification(v) => v.view(), Self::Finalization(v) => v.view(), } } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Certificate where S::Certificate: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let tag = u.int_in_range(0..=2)?; match tag { 0 => { let v = Notarization::arbitrary(u)?; Ok(Self::Notarization(v)) } 1 => { let v = Nullification::arbitrary(u)?; Ok(Self::Nullification(v)) } 2 => { let v = Finalization::arbitrary(u)?; Ok(Self::Finalization(v)) } _ => unreachable!(), } } } /// Artifact represents all consensus artifacts (votes and certificates) for storage. #[derive(Clone, Debug, PartialEq)] pub enum Artifact { /// A validator's notarize vote over a proposal. Notarize(Notarize), /// A recovered certificate for a notarization. Notarization(Notarization), /// A notarization was locally certified. Certification(Round, bool), /// A validator's nullify vote used to skip the current view. Nullify(Nullify), /// A recovered certificate for a nullification. Nullification(Nullification), /// A validator's finalize vote over a proposal. Finalize(Finalize), /// A recovered certificate for a finalization. Finalization(Finalization), } impl Write for Artifact { fn write(&self, writer: &mut impl BufMut) { match self { Self::Notarize(v) => { 0u8.write(writer); v.write(writer); } Self::Notarization(v) => { 1u8.write(writer); v.write(writer); } Self::Certification(r, b) => { 2u8.write(writer); r.write(writer); b.write(writer); } Self::Nullify(v) => { 3u8.write(writer); v.write(writer); } Self::Nullification(v) => { 4u8.write(writer); v.write(writer); } Self::Finalize(v) => { 5u8.write(writer); v.write(writer); } Self::Finalization(v) => { 6u8.write(writer); v.write(writer); } } } } impl EncodeSize for Artifact { fn encode_size(&self) -> usize { 1 + match self { Self::Notarize(v) => v.encode_size(), Self::Notarization(v) => v.encode_size(), Self::Certification(r, b) => r.encode_size() + b.encode_size(), Self::Nullify(v) => v.encode_size(), Self::Nullification(v) => v.encode_size(), Self::Finalize(v) => v.encode_size(), Self::Finalization(v) => v.encode_size(), } } } impl Read for Artifact { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let tag = ::read(reader)?; match tag { 0 => { let v = Notarize::read(reader)?; Ok(Self::Notarize(v)) } 1 => { let v = Notarization::read_cfg(reader, cfg)?; Ok(Self::Notarization(v)) } 2 => { let r = Round::read(reader)?; let b = bool::read(reader)?; Ok(Self::Certification(r, b)) } 3 => { let v = Nullify::read(reader)?; Ok(Self::Nullify(v)) } 4 => { let v = Nullification::read_cfg(reader, cfg)?; Ok(Self::Nullification(v)) } 5 => { let v = Finalize::read(reader)?; Ok(Self::Finalize(v)) } 6 => { let v = Finalization::read_cfg(reader, cfg)?; Ok(Self::Finalization(v)) } _ => Err(Error::Invalid( "consensus::simplex::Artifact", "Invalid type", )), } } } impl Epochable for Artifact { fn epoch(&self) -> Epoch { match self { Self::Notarize(v) => v.epoch(), Self::Notarization(v) => v.epoch(), Self::Certification(r, _) => r.epoch(), Self::Nullify(v) => v.epoch(), Self::Nullification(v) => v.epoch(), Self::Finalize(v) => v.epoch(), Self::Finalization(v) => v.epoch(), } } } impl Viewable for Artifact { fn view(&self) -> View { match self { Self::Notarize(v) => v.view(), Self::Notarization(v) => v.view(), Self::Certification(r, _) => r.view(), Self::Nullify(v) => v.view(), Self::Nullification(v) => v.view(), Self::Finalize(v) => v.view(), Self::Finalization(v) => v.view(), } } } impl From> for Artifact { fn from(vote: Vote) -> Self { match vote { Vote::Notarize(v) => Self::Notarize(v), Vote::Nullify(v) => Self::Nullify(v), Vote::Finalize(v) => Self::Finalize(v), } } } impl From> for Artifact { fn from(cert: Certificate) -> Self { match cert { Certificate::Notarization(v) => Self::Notarization(v), Certificate::Nullification(v) => Self::Nullification(v), Certificate::Finalization(v) => Self::Finalization(v), } } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Artifact where S::Signature: for<'a> arbitrary::Arbitrary<'a>, S::Certificate: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let tag = u.int_in_range(0..=6)?; match tag { 0 => { let v = Notarize::arbitrary(u)?; Ok(Self::Notarize(v)) } 1 => { let v = Notarization::arbitrary(u)?; Ok(Self::Notarization(v)) } 2 => { let r = Round::arbitrary(u)?; let b = bool::arbitrary(u)?; Ok(Self::Certification(r, b)) } 3 => { let v = Nullify::arbitrary(u)?; Ok(Self::Nullify(v)) } 4 => { let v = Nullification::arbitrary(u)?; Ok(Self::Nullification(v)) } 5 => { let v = Finalize::arbitrary(u)?; Ok(Self::Finalize(v)) } 6 => { let v = Finalization::arbitrary(u)?; Ok(Self::Finalization(v)) } _ => unreachable!(), } } } /// 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 round in which this proposal is made pub round: Round, /// 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 const fn new(round: Round, parent: View, payload: D) -> Self { Self { round, parent, payload, } } } impl Write for Proposal { fn write(&self, writer: &mut impl BufMut) { self.round.write(writer); self.parent.write(writer); self.payload.write(writer) } } impl Read for Proposal { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let round = Round::read(reader)?; let parent = View::read(reader)?; let payload = D::read(reader)?; Ok(Self { round, parent, payload, }) } } impl EncodeSize for Proposal { fn encode_size(&self) -> usize { self.round.encode_size() + self.parent.encode_size() + self.payload.encode_size() } } impl MaxEncodeSize for Proposal { const MAX_ENCODE_SIZE: usize = Round::MAX_ENCODE_SIZE + View::MAX_ENCODE_SIZE + D::SIZE; } impl Epochable for Proposal { fn epoch(&self) -> Epoch { self.round.epoch() } } impl Viewable for Proposal { fn view(&self) -> View { self.round.view() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Proposal where D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let round = Round::arbitrary(u)?; let parent = View::arbitrary(u)?; let payload = D::arbitrary(u)?; Ok(Self { round, parent, payload, }) } } /// Validator vote that endorses a proposal for notarization. #[derive(Clone, Debug)] pub struct Notarize { /// Proposal being notarized. pub proposal: Proposal, /// Scheme-specific attestation material. pub attestation: Attestation, } impl Notarize { /// Signs a notarize vote for the provided proposal. pub fn sign(scheme: &S, proposal: Proposal) -> Option where S: scheme::Scheme, { let attestation = scheme.sign::(Subject::Notarize { proposal: &proposal, })?; Some(Self { proposal, attestation, }) } /// Verifies the notarize vote against the provided signing scheme. /// /// This ensures that the notarize signature is valid for the claimed proposal. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where R: CryptoRng, S: scheme::Scheme, { scheme.verify_attestation::<_, D>( rng, Subject::Notarize { proposal: &self.proposal, }, &self.attestation, strategy, ) } /// Returns the round associated with this notarize vote. pub const fn round(&self) -> Round { self.proposal.round } } impl PartialEq for Notarize { fn eq(&self, other: &Self) -> bool { self.proposal == other.proposal && self.attestation == other.attestation } } impl Eq for Notarize {} impl Hash for Notarize { fn hash(&self, state: &mut H) { self.proposal.hash(state); self.attestation.hash(state); } } impl Write for Notarize { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.attestation.write(writer); } } impl EncodeSize for Notarize { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.attestation.encode_size() } } impl MaxEncodeSize for Notarize { const MAX_ENCODE_SIZE: usize = Proposal::::MAX_ENCODE_SIZE + Attestation::::MAX_ENCODE_SIZE; } impl Read for Notarize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let proposal = Proposal::read(reader)?; let attestation = Attestation::read(reader)?; Ok(Self { proposal, attestation, }) } } impl Attributable for Notarize { fn signer(&self) -> Participant { self.attestation.signer } } impl Epochable for Notarize { fn epoch(&self) -> Epoch { self.proposal.epoch() } } impl Viewable for Notarize { fn view(&self) -> View { self.proposal.view() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Notarize where S::Signature: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let proposal = Proposal::arbitrary(u)?; let attestation = Attestation::arbitrary(u)?; Ok(Self { proposal, attestation, }) } } /// Batch-verifies certificates and returns a per-item result. /// /// Uses bisection to efficiently identify invalid certificates when batch /// verification fails. pub fn verify_certificates<'a, R, S, D>( rng: &mut R, scheme: &S, certificates: &[(Subject<'a, D>, &'a S::Certificate)], strategy: &impl Strategy, ) -> Vec where R: CryptoRng, S: CertificateVerifier, D: Digest, { scheme.verify_certificates_bisect::<_, D>(rng, certificates, strategy) } /// Aggregated notarization certificate recovered from notarize votes. /// When a proposal is notarized, it means at least 2f+1 validators have voted for it. /// /// Some signing schemes (like [`super::scheme::bls12381_threshold::vrf`]) embed an additional /// randomness seed in the certificate. For threshold signatures, the seed can be accessed /// via [`super::scheme::bls12381_threshold::vrf::Seedable::seed`]. #[derive(Clone, Debug)] pub struct Notarization { /// The proposal that has been notarized. pub proposal: Proposal, /// The recovered certificate for the proposal. pub certificate: S::Certificate, } impl Notarization { /// Returns an upper bound on the encoded size of a notarization for a committee of at most /// `participants`. pub fn max_encode_size(participants: u32) -> usize { S::max_certificate_size(participants) + Proposal::::MAX_ENCODE_SIZE } /// Builds a notarization certificate from non-empty owned notarize votes for the same /// proposal, consuming the votes to avoid cloning each attestation. pub fn from_owned_notarizes( scheme: &S, notarizes: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send, { let (first, notarizes) = notarizes.into_parts(); let Notarize { proposal, attestation, } = first; let attestations = NonEmpty::new(attestation, notarizes.map(|notarize| notarize.attestation)); let certificate = scheme.assemble(attestations, strategy)?; Ok(Self { proposal, certificate, }) } /// Builds a notarization certificate from non-empty notarize votes for the same proposal. pub fn from_notarizes<'a, I>( scheme: &S, notarizes: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send, { Self::from_owned_notarizes( scheme, non_empty![@notarizes.into_iter().cloned()], strategy, ) } /// Verifies the notarization certificate against the provided signing scheme. /// /// This ensures that the certificate is valid for the claimed proposal. pub fn verify( &self, rng: &mut R, scheme: &impl CertificateVerifier, strategy: &impl Strategy, ) -> bool { scheme.verify_certificate::<_, D>( rng, Subject::Notarize { proposal: &self.proposal, }, &self.certificate, strategy, ) } /// Returns the round associated with the notarized proposal. pub const fn round(&self) -> Round { self.proposal.round } } impl PartialEq for Notarization { fn eq(&self, other: &Self) -> bool { self.proposal == other.proposal && self.certificate == other.certificate } } impl Eq for Notarization {} impl Hash for Notarization { fn hash(&self, state: &mut H) { self.proposal.hash(state); self.certificate.hash(state); } } impl Write for Notarization { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.certificate.write(writer); } } impl EncodeSize for Notarization { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.certificate.encode_size() } } impl Read for Notarization { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let proposal = Proposal::read(reader)?; let certificate = S::Certificate::read_cfg(reader, cfg)?; Ok(Self { proposal, certificate, }) } } impl Epochable for Notarization { fn epoch(&self) -> Epoch { self.proposal.epoch() } } impl Viewable for Notarization { fn view(&self) -> View { self.proposal.view() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Notarization where S::Certificate: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let proposal = Proposal::arbitrary(u)?; let certificate = S::Certificate::arbitrary(u)?; Ok(Self { proposal, certificate, }) } } /// Validator vote for nullifying the current round, i.e. skip the current round. /// This is typically used when the leader is unresponsive or fails to propose a valid block. #[derive(Clone, Debug)] pub struct Nullify { /// The round to be nullified (skipped). pub round: Round, /// Scheme-specific attestation material. pub attestation: Attestation, } impl PartialEq for Nullify { fn eq(&self, other: &Self) -> bool { self.round == other.round && self.attestation == other.attestation } } impl Eq for Nullify {} impl Hash for Nullify { fn hash(&self, state: &mut H) { self.round.hash(state); self.attestation.hash(state); } } impl Nullify { /// Signs a nullify vote for the given round. pub fn sign(scheme: &S, round: Round) -> Option where S: scheme::Scheme, { let attestation = scheme.sign::(Subject::Nullify { round })?; Some(Self { round, attestation }) } /// Verifies the nullify vote against the provided signing scheme. /// /// This ensures that the nullify signature is valid for the given round. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where R: CryptoRng, S: scheme::Scheme, { scheme.verify_attestation::<_, D>( rng, Subject::Nullify { round: self.round }, &self.attestation, strategy, ) } /// Returns the round associated with this nullify vote. pub const fn round(&self) -> Round { self.round } } impl Write for Nullify { fn write(&self, writer: &mut impl BufMut) { self.round.write(writer); self.attestation.write(writer); } } impl EncodeSize for Nullify { fn encode_size(&self) -> usize { self.round.encode_size() + self.attestation.encode_size() } } impl MaxEncodeSize for Nullify { const MAX_ENCODE_SIZE: usize = Round::MAX_ENCODE_SIZE + Attestation::::MAX_ENCODE_SIZE; } impl Read for Nullify { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let round = Round::read(reader)?; let attestation = Attestation::read(reader)?; Ok(Self { round, attestation }) } } impl Attributable for Nullify { fn signer(&self) -> Participant { self.attestation.signer } } impl Epochable for Nullify { fn epoch(&self) -> Epoch { self.round.epoch() } } impl Viewable for Nullify { fn view(&self) -> View { self.round.view() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Nullify where S::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let round = Round::arbitrary(u)?; let attestation = Attestation::arbitrary(u)?; Ok(Self { round, attestation }) } } /// Aggregated nullification certificate recovered from nullify votes. /// When a view is nullified, consensus moves to the first view of the next /// term without finalizing a block (the next view when `term_length` is 1); /// a nullification covers the nullified view and the rest of its term. #[derive(Clone, Debug)] pub struct Nullification { /// The round in which this nullification is made. pub round: Round, /// The recovered certificate for the nullification. pub certificate: S::Certificate, } impl Nullification { /// Returns an upper bound on the encoded size of a nullification for a committee of at most /// `participants`. pub fn max_encode_size(participants: u32) -> usize { S::max_certificate_size(participants) + Round::MAX_ENCODE_SIZE } /// Builds a nullification certificate from non-empty owned nullify votes from the same round, /// consuming the votes to avoid cloning each attestation. pub fn from_owned_nullifies( scheme: &S, nullifies: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send, { let (first, nullifies) = nullifies.into_parts(); let round = first.round; let attestations = NonEmpty::new( first.attestation, nullifies.map(|nullify| nullify.attestation), ); let certificate = scheme.assemble(attestations, strategy)?; Ok(Self { round, certificate }) } /// Builds a nullification certificate from non-empty nullify votes from the same round. pub fn from_nullifies<'a, I>( scheme: &S, nullifies: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send, { Self::from_owned_nullifies( scheme, non_empty![@nullifies.into_iter().cloned()], strategy, ) } /// Verifies the nullification certificate against the provided signing scheme. /// /// This ensures that the certificate is valid for the claimed round. pub fn verify( &self, rng: &mut R, scheme: &impl CertificateVerifier, strategy: &impl Strategy, ) -> bool { scheme.verify_certificate::<_, D>( rng, Subject::Nullify { round: self.round }, &self.certificate, strategy, ) } /// Returns the round associated with this nullification. pub const fn round(&self) -> Round { self.round } } impl PartialEq for Nullification { fn eq(&self, other: &Self) -> bool { self.round == other.round && self.certificate == other.certificate } } impl Eq for Nullification {} impl Hash for Nullification { fn hash(&self, state: &mut H) { self.round.hash(state); self.certificate.hash(state); } } impl Write for Nullification { fn write(&self, writer: &mut impl BufMut) { self.round.write(writer); self.certificate.write(writer); } } impl EncodeSize for Nullification { fn encode_size(&self) -> usize { self.round.encode_size() + self.certificate.encode_size() } } impl Read for Nullification { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let round = Round::read(reader)?; let certificate = S::Certificate::read_cfg(reader, cfg)?; Ok(Self { round, certificate }) } } impl Epochable for Nullification { fn epoch(&self) -> Epoch { self.round.epoch() } } impl Viewable for Nullification { fn view(&self) -> View { self.round.view() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Nullification where S::Certificate: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let round = Round::arbitrary(u)?; let certificate = S::Certificate::arbitrary(u)?; Ok(Self { round, certificate }) } } /// Validator vote to finalize a proposal. /// This happens after a proposal has been notarized, confirming it as the canonical block /// for this round. #[derive(Clone, Debug)] pub struct Finalize { /// Proposal being finalized. pub proposal: Proposal, /// Scheme-specific attestation material. pub attestation: Attestation, } impl Finalize { /// Signs a finalize vote for the provided proposal. pub fn sign(scheme: &S, proposal: Proposal) -> Option where S: scheme::Scheme, { let attestation = scheme.sign::(Subject::Finalize { proposal: &proposal, })?; Some(Self { proposal, attestation, }) } /// Verifies the finalize vote against the provided signing scheme. /// /// This ensures that the finalize signature is valid for the claimed proposal. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where R: CryptoRng, S: scheme::Scheme, { scheme.verify_attestation::<_, D>( rng, Subject::Finalize { proposal: &self.proposal, }, &self.attestation, strategy, ) } /// Returns the round associated with this finalize vote. pub const fn round(&self) -> Round { self.proposal.round } } impl PartialEq for Finalize { fn eq(&self, other: &Self) -> bool { self.proposal == other.proposal && self.attestation == other.attestation } } impl Eq for Finalize {} impl Hash for Finalize { fn hash(&self, state: &mut H) { self.proposal.hash(state); self.attestation.hash(state); } } impl Write for Finalize { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.attestation.write(writer); } } impl EncodeSize for Finalize { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.attestation.encode_size() } } impl MaxEncodeSize for Finalize { const MAX_ENCODE_SIZE: usize = Proposal::::MAX_ENCODE_SIZE + Attestation::::MAX_ENCODE_SIZE; } impl Read for Finalize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let proposal = Proposal::read(reader)?; let attestation = Attestation::read(reader)?; Ok(Self { proposal, attestation, }) } } impl Attributable for Finalize { fn signer(&self) -> Participant { self.attestation.signer } } impl Epochable for Finalize { fn epoch(&self) -> Epoch { self.proposal.epoch() } } impl Viewable for Finalize { fn view(&self) -> View { self.proposal.view() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Finalize where S::Signature: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let proposal = Proposal::arbitrary(u)?; let attestation = Attestation::arbitrary(u)?; Ok(Self { proposal, attestation, }) } } /// Aggregated finalization certificate recovered from finalize votes. /// When a proposal is finalized, it becomes the canonical block for its view. /// /// Some signing schemes (like [`super::scheme::bls12381_threshold::vrf`]) embed an additional /// randomness seed in the certificate. For threshold signatures, the seed can be accessed /// via [`super::scheme::bls12381_threshold::vrf::Seedable::seed`]. #[derive(Clone, Debug)] pub struct Finalization { /// The proposal that has been finalized. pub proposal: Proposal, /// The recovered certificate for the proposal. pub certificate: S::Certificate, } impl Finalization { /// Returns an upper bound on the encoded size of a finalization for a committee of at most /// `participants`. pub fn max_encode_size(participants: u32) -> usize { S::max_certificate_size(participants) + Proposal::::MAX_ENCODE_SIZE } /// Builds a finalization certificate from non-empty owned finalize votes for the same proposal, /// consuming the votes to avoid cloning each attestation. pub fn from_owned_finalizes( scheme: &S, finalizes: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send, { let (first, finalizes) = finalizes.into_parts(); let Finalize { proposal, attestation, } = first; let attestations = NonEmpty::new(attestation, finalizes.map(|finalize| finalize.attestation)); let certificate = scheme.assemble(attestations, strategy)?; Ok(Self { proposal, certificate, }) } /// Builds a finalization certificate from non-empty finalize votes for the same proposal. pub fn from_finalizes<'a, I>( scheme: &S, finalizes: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send, { Self::from_owned_finalizes( scheme, non_empty![@finalizes.into_iter().cloned()], strategy, ) } /// Verifies the finalization certificate against the provided signing scheme. /// /// This ensures that the certificate is valid for the claimed proposal. pub fn verify( &self, rng: &mut R, scheme: &impl CertificateVerifier, strategy: &impl Strategy, ) -> bool { scheme.verify_certificate::<_, D>( rng, Subject::Finalize { proposal: &self.proposal, }, &self.certificate, strategy, ) } /// Returns the round associated with the finalized proposal. pub const fn round(&self) -> Round { self.proposal.round } } impl PartialEq for Finalization { fn eq(&self, other: &Self) -> bool { self.proposal == other.proposal && self.certificate == other.certificate } } impl Eq for Finalization {} impl Hash for Finalization { fn hash(&self, state: &mut H) { self.proposal.hash(state); self.certificate.hash(state); } } impl Write for Finalization { fn write(&self, writer: &mut impl BufMut) { self.proposal.write(writer); self.certificate.write(writer); } } impl EncodeSize for Finalization { fn encode_size(&self) -> usize { self.proposal.encode_size() + self.certificate.encode_size() } } impl Read for Finalization { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let proposal = Proposal::read(reader)?; let certificate = S::Certificate::read_cfg(reader, cfg)?; Ok(Self { proposal, certificate, }) } } impl Epochable for Finalization { fn epoch(&self) -> Epoch { self.proposal.epoch() } } impl Viewable for Finalization { fn view(&self) -> View { self.proposal.view() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Finalization where S::Certificate: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let proposal = Proposal::arbitrary(u)?; let certificate = S::Certificate::arbitrary(u)?; Ok(Self { proposal, certificate, }) } } /// Activity represents all possible activities that can occur in the consensus protocol. /// This includes both regular consensus messages and fault evidence. /// /// # Verification /// /// Some activities issued by consensus are not guaranteed to be cryptographically verified. Use /// [`Activity::verified`] to check whether an activity may be unverified, and [`Activity::verify`] to /// perform verification. /// /// # Activity Filtering /// /// For **non-attributable** schemes like [`crate::simplex::scheme::bls12381_threshold`], exposing /// per-validator activity as fault evidence is not safe: with threshold cryptography, any `t` valid partial signatures can /// be used to forge a partial signature for any player. /// /// Use [`crate::simplex::scheme::reporter::AttributableReporter`] to automatically filter and /// verify activities based on [`Scheme::is_attributable`]. #[derive(Clone, Debug)] pub enum Activity { /// A validator's notarize vote over a proposal. Notarize(Notarize), /// A recovered certificate for a notarization (scheme-specific). Notarization(Notarization), /// A notarization was locally certified. Certification(Notarization), /// A validator's nullify vote used to skip the current view. Nullify(Nullify), /// A recovered certificate for a nullification (scheme-specific). Nullification(Nullification), /// A validator's finalize vote over a proposal. Finalize(Finalize), /// A recovered certificate for a finalization (scheme-specific). 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 PartialEq for Activity { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Notarize(a), Self::Notarize(b)) => a == b, (Self::Notarization(a), Self::Notarization(b)) => a == b, (Self::Certification(a), Self::Certification(b)) => a == b, (Self::Nullify(a), Self::Nullify(b)) => a == b, (Self::Nullification(a), Self::Nullification(b)) => a == b, (Self::Finalize(a), Self::Finalize(b)) => a == b, (Self::Finalization(a), Self::Finalization(b)) => a == b, (Self::ConflictingNotarize(a), Self::ConflictingNotarize(b)) => a == b, (Self::ConflictingFinalize(a), Self::ConflictingFinalize(b)) => a == b, (Self::NullifyFinalize(a), Self::NullifyFinalize(b)) => a == b, _ => false, } } } impl Eq for Activity {} impl Hash for Activity { fn hash(&self, state: &mut H) { match self { Self::Notarize(v) => { 0u8.hash(state); v.hash(state); } Self::Notarization(v) => { 1u8.hash(state); v.hash(state); } Self::Certification(v) => { 2u8.hash(state); v.hash(state); } Self::Nullify(v) => { 3u8.hash(state); v.hash(state); } Self::Nullification(v) => { 4u8.hash(state); v.hash(state); } Self::Finalize(v) => { 5u8.hash(state); v.hash(state); } Self::Finalization(v) => { 6u8.hash(state); v.hash(state); } Self::ConflictingNotarize(v) => { 7u8.hash(state); v.hash(state); } Self::ConflictingFinalize(v) => { 8u8.hash(state); v.hash(state); } Self::NullifyFinalize(v) => { 9u8.hash(state); v.hash(state); } } } } impl Activity { /// Indicates whether the activity is guaranteed to have been verified by consensus. pub const fn verified(&self) -> bool { match self { Self::Notarize(_) => false, Self::Notarization(_) => true, Self::Certification(_) => true, Self::Nullify(_) => false, Self::Nullification(_) => true, Self::Finalize(_) => false, Self::Finalization(_) => true, Self::ConflictingNotarize(_) => false, Self::ConflictingFinalize(_) => false, Self::NullifyFinalize(_) => false, } } /// Verifies the validity of this activity against the signing scheme. /// /// This method **always** performs verification regardless of whether the activity has been /// previously verified. Callers can use [`Activity::verified`] to check if verification is /// necessary before calling this method. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where S: scheme::Scheme, { match self { Self::Notarize(n) => n.verify(rng, scheme, strategy), Self::Notarization(n) => n.verify(rng, scheme, strategy), Self::Certification(n) => n.verify(rng, scheme, strategy), Self::Nullify(n) => n.verify(rng, scheme, strategy), Self::Nullification(n) => n.verify(rng, scheme, strategy), Self::Finalize(f) => f.verify(rng, scheme, strategy), Self::Finalization(f) => f.verify(rng, scheme, strategy), Self::ConflictingNotarize(c) => c.verify(rng, scheme, strategy), Self::ConflictingFinalize(c) => c.verify(rng, scheme, strategy), Self::NullifyFinalize(c) => c.verify(rng, scheme, strategy), } } } impl Write for Activity { fn write(&self, writer: &mut impl BufMut) { match self { Self::Notarize(v) => { 0u8.write(writer); v.write(writer); } Self::Notarization(v) => { 1u8.write(writer); v.write(writer); } Self::Certification(v) => { 2u8.write(writer); v.write(writer); } Self::Nullify(v) => { 3u8.write(writer); v.write(writer); } Self::Nullification(v) => { 4u8.write(writer); v.write(writer); } Self::Finalize(v) => { 5u8.write(writer); v.write(writer); } Self::Finalization(v) => { 6u8.write(writer); v.write(writer); } Self::ConflictingNotarize(v) => { 7u8.write(writer); v.write(writer); } Self::ConflictingFinalize(v) => { 8u8.write(writer); v.write(writer); } Self::NullifyFinalize(v) => { 9u8.write(writer); v.write(writer); } } } } impl EncodeSize for Activity { fn encode_size(&self) -> usize { 1 + match self { Self::Notarize(v) => v.encode_size(), Self::Notarization(v) => v.encode_size(), Self::Certification(v) => v.encode_size(), Self::Nullify(v) => v.encode_size(), Self::Nullification(v) => v.encode_size(), Self::Finalize(v) => v.encode_size(), Self::Finalization(v) => v.encode_size(), Self::ConflictingNotarize(v) => v.encode_size(), Self::ConflictingFinalize(v) => v.encode_size(), Self::NullifyFinalize(v) => v.encode_size(), } } } impl Read for Activity { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let tag = ::read(reader)?; match tag { 0 => { let v = Notarize::::read(reader)?; Ok(Self::Notarize(v)) } 1 => { let v = Notarization::::read_cfg(reader, cfg)?; Ok(Self::Notarization(v)) } 2 => { let v = Notarization::::read_cfg(reader, cfg)?; Ok(Self::Certification(v)) } 3 => { let v = Nullify::::read(reader)?; Ok(Self::Nullify(v)) } 4 => { let v = Nullification::::read_cfg(reader, cfg)?; Ok(Self::Nullification(v)) } 5 => { let v = Finalize::::read(reader)?; Ok(Self::Finalize(v)) } 6 => { let v = Finalization::::read_cfg(reader, cfg)?; Ok(Self::Finalization(v)) } 7 => { let v = ConflictingNotarize::::read(reader)?; Ok(Self::ConflictingNotarize(v)) } 8 => { let v = ConflictingFinalize::::read(reader)?; Ok(Self::ConflictingFinalize(v)) } 9 => { let v = NullifyFinalize::::read(reader)?; Ok(Self::NullifyFinalize(v)) } _ => Err(Error::Invalid( "consensus::simplex::Activity", "Invalid type", )), } } } impl Epochable for Activity { fn epoch(&self) -> Epoch { match self { Self::Notarize(v) => v.epoch(), Self::Notarization(v) => v.epoch(), Self::Certification(v) => v.epoch(), Self::Nullify(v) => v.epoch(), Self::Nullification(v) => v.epoch(), Self::Finalize(v) => v.epoch(), Self::Finalization(v) => v.epoch(), Self::ConflictingNotarize(v) => v.epoch(), Self::ConflictingFinalize(v) => v.epoch(), Self::NullifyFinalize(v) => v.epoch(), } } } impl Viewable for Activity { fn view(&self) -> View { match self { Self::Notarize(v) => v.view(), Self::Notarization(v) => v.view(), Self::Certification(v) => v.view(), Self::Nullify(v) => v.view(), Self::Nullification(v) => v.view(), Self::Finalize(v) => v.view(), Self::Finalization(v) => v.view(), Self::ConflictingNotarize(v) => v.view(), Self::ConflictingFinalize(v) => v.view(), Self::NullifyFinalize(v) => v.view(), } } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Activity where S::Signature: for<'a> arbitrary::Arbitrary<'a>, S::Certificate: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let tag = u.int_in_range(0..=9)?; match tag { 0 => { let v = Notarize::::arbitrary(u)?; Ok(Self::Notarize(v)) } 1 => { let v = Notarization::::arbitrary(u)?; Ok(Self::Notarization(v)) } 2 => { let v = Notarization::::arbitrary(u)?; Ok(Self::Certification(v)) } 3 => { let v = Nullify::::arbitrary(u)?; Ok(Self::Nullify(v)) } 4 => { let v = Nullification::::arbitrary(u)?; Ok(Self::Nullification(v)) } 5 => { let v = Finalize::::arbitrary(u)?; Ok(Self::Finalize(v)) } 6 => { let v = Finalization::::arbitrary(u)?; Ok(Self::Finalization(v)) } 7 => { let v = ConflictingNotarize::::arbitrary(u)?; Ok(Self::ConflictingNotarize(v)) } 8 => { let v = ConflictingFinalize::::arbitrary(u)?; Ok(Self::ConflictingFinalize(v)) } 9 => { let v = NullifyFinalize::::arbitrary(u)?; Ok(Self::NullifyFinalize(v)) } _ => unreachable!(), } } } /// 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)] pub struct ConflictingNotarize { /// The first conflicting notarize notarize_1: Notarize, /// The second conflicting notarize notarize_2: Notarize, } impl PartialEq for ConflictingNotarize { fn eq(&self, other: &Self) -> bool { self.notarize_1 == other.notarize_1 && self.notarize_2 == other.notarize_2 } } impl Eq for ConflictingNotarize {} impl Hash for ConflictingNotarize { fn hash(&self, state: &mut H) { self.notarize_1.hash(state); self.notarize_2.hash(state); } } impl ConflictingNotarize { /// Creates a new conflicting notarize evidence from two conflicting notarizes. /// /// # Panics /// /// Panics if the two notarizes do not have the same round and signer, or if they /// have identical proposals (which would not constitute conflicting evidence). pub fn new(notarize_1: Notarize, notarize_2: Notarize) -> Self { assert_eq!(notarize_1.round(), notarize_2.round()); assert_eq!(notarize_1.signer(), notarize_2.signer()); assert_ne!( notarize_1.proposal, notarize_2.proposal, "proposals must differ to constitute conflicting evidence" ); Self { notarize_1, notarize_2, } } /// Verifies that both conflicting signatures are valid, proving Byzantine behavior. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where R: CryptoRng, S: scheme::Scheme, { self.notarize_1.verify(rng, scheme, strategy) && self.notarize_2.verify(rng, scheme, strategy) } } impl Attributable for ConflictingNotarize { fn signer(&self) -> Participant { self.notarize_1.signer() } } impl Epochable for ConflictingNotarize { fn epoch(&self) -> Epoch { self.notarize_1.epoch() } } impl Viewable for ConflictingNotarize { fn view(&self) -> View { self.notarize_1.view() } } impl Write for ConflictingNotarize { fn write(&self, writer: &mut impl BufMut) { self.notarize_1.write(writer); self.notarize_2.write(writer); } } impl Read for ConflictingNotarize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let notarize_1 = Notarize::read(reader)?; let notarize_2 = Notarize::read(reader)?; if notarize_1.signer() != notarize_2.signer() || notarize_1.round() != notarize_2.round() || notarize_1.proposal == notarize_2.proposal { return Err(Error::Invalid( "consensus::simplex::ConflictingNotarize", "invalid conflicting notarize", )); } Ok(Self { notarize_1, notarize_2, }) } } impl EncodeSize for ConflictingNotarize { fn encode_size(&self) -> usize { self.notarize_1.encode_size() + self.notarize_2.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for ConflictingNotarize where S::Signature: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let notarize_1 = Notarize::arbitrary(u)?; let notarize_2 = Notarize::arbitrary(u)?; Ok(Self { notarize_1, notarize_2, }) } } /// ConflictingFinalize represents evidence of a Byzantine validator sending conflicting finalizes. /// Similar to ConflictingNotarize, but for finalizes. #[derive(Clone, Debug)] pub struct ConflictingFinalize { /// The second conflicting finalize finalize_1: Finalize, /// The second conflicting finalize finalize_2: Finalize, } impl PartialEq for ConflictingFinalize { fn eq(&self, other: &Self) -> bool { self.finalize_1 == other.finalize_1 && self.finalize_2 == other.finalize_2 } } impl Eq for ConflictingFinalize {} impl Hash for ConflictingFinalize { fn hash(&self, state: &mut H) { self.finalize_1.hash(state); self.finalize_2.hash(state); } } impl ConflictingFinalize { /// Creates a new conflicting finalize evidence from two conflicting finalizes. /// /// # Panics /// /// Panics if the two finalizes do not have the same round and signer, or if they /// have identical proposals (which would not constitute conflicting evidence). pub fn new(finalize_1: Finalize, finalize_2: Finalize) -> Self { assert_eq!(finalize_1.round(), finalize_2.round()); assert_eq!(finalize_1.signer(), finalize_2.signer()); assert_ne!( finalize_1.proposal, finalize_2.proposal, "proposals must differ to constitute conflicting evidence" ); Self { finalize_1, finalize_2, } } /// Verifies that both conflicting signatures are valid, proving Byzantine behavior. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where R: CryptoRng, S: scheme::Scheme, { self.finalize_1.verify(rng, scheme, strategy) && self.finalize_2.verify(rng, scheme, strategy) } } impl Attributable for ConflictingFinalize { fn signer(&self) -> Participant { self.finalize_1.signer() } } impl Epochable for ConflictingFinalize { fn epoch(&self) -> Epoch { self.finalize_1.epoch() } } impl Viewable for ConflictingFinalize { fn view(&self) -> View { self.finalize_1.view() } } impl Write for ConflictingFinalize { fn write(&self, writer: &mut impl BufMut) { self.finalize_1.write(writer); self.finalize_2.write(writer); } } impl Read for ConflictingFinalize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let finalize_1 = Finalize::read(reader)?; let finalize_2 = Finalize::read(reader)?; if finalize_1.signer() != finalize_2.signer() || finalize_1.round() != finalize_2.round() || finalize_1.proposal == finalize_2.proposal { return Err(Error::Invalid( "consensus::simplex::ConflictingFinalize", "invalid conflicting finalize", )); } Ok(Self { finalize_1, finalize_2, }) } } impl EncodeSize for ConflictingFinalize { fn encode_size(&self) -> usize { self.finalize_1.encode_size() + self.finalize_2.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for ConflictingFinalize where S::Signature: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let finalize_1 = Finalize::arbitrary(u)?; let finalize_2 = Finalize::arbitrary(u)?; Ok(Self { finalize_1, finalize_2, }) } } /// 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)] pub struct NullifyFinalize { /// The conflicting nullify nullify: Nullify, /// The conflicting finalize finalize: Finalize, } impl PartialEq for NullifyFinalize { fn eq(&self, other: &Self) -> bool { self.nullify == other.nullify && self.finalize == other.finalize } } impl Eq for NullifyFinalize {} impl Hash for NullifyFinalize { fn hash(&self, state: &mut H) { self.nullify.hash(state); self.finalize.hash(state); } } 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.round, finalize.round()); assert_eq!(nullify.signer(), finalize.signer()); Self { nullify, finalize } } /// Verifies that both the nullify and finalize signatures are valid, proving Byzantine behavior. pub fn verify(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool where R: CryptoRng, S: scheme::Scheme, { self.nullify.verify(rng, scheme, strategy) && self.finalize.verify(rng, scheme, strategy) } } impl Attributable for NullifyFinalize { fn signer(&self) -> Participant { self.nullify.signer() } } impl Epochable for NullifyFinalize { fn epoch(&self) -> Epoch { self.nullify.epoch() } } impl Viewable for NullifyFinalize { fn view(&self) -> View { self.nullify.view() } } impl Write for NullifyFinalize { fn write(&self, writer: &mut impl BufMut) { self.nullify.write(writer); self.finalize.write(writer); } } impl Read for NullifyFinalize { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let nullify = Nullify::read(reader)?; let finalize = Finalize::read(reader)?; if nullify.signer() != finalize.signer() || nullify.round != finalize.round() { return Err(Error::Invalid( "consensus::simplex::NullifyFinalize", "mismatched signatures", )); } Ok(Self { nullify, finalize }) } } impl EncodeSize for NullifyFinalize { fn encode_size(&self) -> usize { self.nullify.encode_size() + self.finalize.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for NullifyFinalize where S::Signature: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let nullify = Nullify::arbitrary(u)?; let finalize = Finalize::arbitrary(u)?; Ok(Self { nullify, finalize }) } } #[cfg(test)] mod tests { use super::*; use crate::simplex::{ quorum, scheme::{ Scheme, bls12381_multisig, bls12381_threshold::{ standard as bls12381_threshold_std, vrf as bls12381_threshold_vrf, }, ed25519, secp256r1, }, }; use commonware_codec::{Decode, DecodeExt, Encode}; use commonware_cryptography::{ bls12381::primitives::variant::{MinPk, MinSig}, certificate::mocks::Fixture, sha256::Digest as Sha256, }; use commonware_parallel::Sequential; use commonware_utils::{Faults, N3f1, TestRng, test_rng}; 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 } /// Generate a fixture using the provided generator function with a specific seed. fn setup_seeded(n: u32, seed: u64, fixture: F) -> Fixture where F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { setup_seeded_ns(n, seed, NAMESPACE, fixture) } /// Generate a fixture using the provided generator function with a specific seed and namespace. fn setup_seeded_ns(n: u32, seed: u64, namespace: &[u8], fixture: F) -> Fixture where F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = TestRng::new(seed); fixture(&mut rng, namespace, n) } #[test] fn test_proposal_encode_decode() { let proposal = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let encoded = proposal.encode(); let decoded = Proposal::::decode(encoded).unwrap(); assert_eq!(proposal, decoded); } fn notarize_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(1)); let notarize = Notarize::sign(&fixture.schemes[0], proposal).unwrap(); let encoded = notarize.encode(); let decoded = Notarize::decode(encoded).unwrap(); assert_eq!(notarize, decoded); assert!(decoded.verify(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_notarize_encode_decode() { notarize_encode_decode(ed25519::fixture); notarize_encode_decode(secp256r1::fixture); notarize_encode_decode(bls12381_multisig::fixture::); notarize_encode_decode(bls12381_multisig::fixture::); notarize_encode_decode(bls12381_threshold_vrf::fixture::); notarize_encode_decode(bls12381_threshold_vrf::fixture::); notarize_encode_decode(bls12381_threshold_std::fixture::); notarize_encode_decode(bls12381_threshold_std::fixture::); } fn notarization_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let proposal = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let notarizes: Vec<_> = fixture .schemes .iter() .map(|scheme| Notarize::sign(scheme, proposal.clone()).unwrap()) .collect(); let notarization = Notarization::from_notarizes(&fixture.schemes[0], non_empty![@¬arizes], &Sequential) .expect("quorum notarization"); let encoded = notarization.encode(); let cfg = fixture.schemes[0].certificate_codec_config(); let decoded = Notarization::decode_cfg(encoded, &cfg).unwrap(); assert_eq!(notarization, decoded); assert!(decoded.verify(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_notarization_encode_decode() { notarization_encode_decode(ed25519::fixture); notarization_encode_decode(secp256r1::fixture); notarization_encode_decode(bls12381_multisig::fixture::); notarization_encode_decode(bls12381_multisig::fixture::); notarization_encode_decode(bls12381_threshold_vrf::fixture::); notarization_encode_decode(bls12381_threshold_vrf::fixture::); notarization_encode_decode(bls12381_threshold_std::fixture::); notarization_encode_decode(bls12381_threshold_std::fixture::); } fn nullify_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let round = Round::new(Epoch::new(0), View::new(10)); let nullify = Nullify::sign::(&fixture.schemes[0], round).unwrap(); let encoded = nullify.encode(); let decoded = Nullify::decode(encoded).unwrap(); assert_eq!(nullify, decoded); assert!(decoded.verify::<_, Sha256>(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_nullify_encode_decode() { nullify_encode_decode(ed25519::fixture); nullify_encode_decode(secp256r1::fixture); nullify_encode_decode(bls12381_multisig::fixture::); nullify_encode_decode(bls12381_multisig::fixture::); nullify_encode_decode(bls12381_threshold_vrf::fixture::); nullify_encode_decode(bls12381_threshold_vrf::fixture::); nullify_encode_decode(bls12381_threshold_std::fixture::); nullify_encode_decode(bls12381_threshold_std::fixture::); } fn nullification_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let round = Round::new(Epoch::new(333), View::new(10)); let nullifies: Vec<_> = fixture .schemes .iter() .map(|scheme| Nullify::sign::(scheme, round).unwrap()) .collect(); let nullification = Nullification::from_nullifies( &fixture.schemes[0], non_empty![@&nullifies], &Sequential, ) .unwrap(); let encoded = nullification.encode(); let cfg = fixture.schemes[0].certificate_codec_config(); let decoded = Nullification::decode_cfg(encoded, &cfg).unwrap(); assert_eq!(nullification, decoded); assert!(decoded.verify::<_, Sha256>(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_nullification_encode_decode() { nullification_encode_decode(ed25519::fixture); nullification_encode_decode(secp256r1::fixture); nullification_encode_decode(bls12381_multisig::fixture::); nullification_encode_decode(bls12381_multisig::fixture::); nullification_encode_decode(bls12381_threshold_vrf::fixture::); nullification_encode_decode(bls12381_threshold_vrf::fixture::); nullification_encode_decode(bls12381_threshold_std::fixture::); nullification_encode_decode(bls12381_threshold_std::fixture::); } fn finalize_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(1)); let finalize = Finalize::sign(&fixture.schemes[0], proposal).unwrap(); let encoded = finalize.encode(); let decoded = Finalize::decode(encoded).unwrap(); assert_eq!(finalize, decoded); assert!(decoded.verify(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_finalize_encode_decode() { finalize_encode_decode(ed25519::fixture); finalize_encode_decode(secp256r1::fixture); finalize_encode_decode(bls12381_multisig::fixture::); finalize_encode_decode(bls12381_multisig::fixture::); finalize_encode_decode(bls12381_threshold_vrf::fixture::); finalize_encode_decode(bls12381_threshold_vrf::fixture::); finalize_encode_decode(bls12381_threshold_std::fixture::); finalize_encode_decode(bls12381_threshold_std::fixture::); } fn finalization_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(1)); let finalizes: Vec<_> = fixture .schemes .iter() .map(|scheme| Finalize::sign(scheme, proposal.clone()).unwrap()) .collect(); let finalization = Finalization::from_finalizes(&fixture.schemes[0], non_empty![@&finalizes], &Sequential) .unwrap(); let encoded = finalization.encode(); let cfg = fixture.schemes[0].certificate_codec_config(); let decoded = Finalization::decode_cfg(encoded, &cfg).unwrap(); assert_eq!(finalization, decoded); assert!(decoded.verify(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_finalization_encode_decode() { finalization_encode_decode(ed25519::fixture); finalization_encode_decode(secp256r1::fixture); finalization_encode_decode(bls12381_multisig::fixture::); finalization_encode_decode(bls12381_multisig::fixture::); finalization_encode_decode(bls12381_threshold_vrf::fixture::); finalization_encode_decode(bls12381_threshold_vrf::fixture::); finalization_encode_decode(bls12381_threshold_std::fixture::); finalization_encode_decode(bls12381_threshold_std::fixture::); } fn conflicting_notarize_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let proposal1 = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let proposal2 = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(2), ); let notarize1 = Notarize::sign(&fixture.schemes[0], proposal1).unwrap(); let notarize2 = Notarize::sign(&fixture.schemes[0], proposal2).unwrap(); let conflicting = ConflictingNotarize::new(notarize1, notarize2); let encoded = conflicting.encode(); let decoded = ConflictingNotarize::::decode(encoded).unwrap(); assert_eq!(conflicting, decoded); assert!(decoded.verify(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_conflicting_notarize_encode_decode() { conflicting_notarize_encode_decode(ed25519::fixture); conflicting_notarize_encode_decode(secp256r1::fixture); conflicting_notarize_encode_decode(bls12381_multisig::fixture::); conflicting_notarize_encode_decode(bls12381_multisig::fixture::); conflicting_notarize_encode_decode(bls12381_threshold_vrf::fixture::); conflicting_notarize_encode_decode(bls12381_threshold_vrf::fixture::); conflicting_notarize_encode_decode(bls12381_threshold_std::fixture::); conflicting_notarize_encode_decode(bls12381_threshold_std::fixture::); } fn conflicting_finalize_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let proposal1 = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let proposal2 = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(2), ); let finalize1 = Finalize::sign(&fixture.schemes[0], proposal1).unwrap(); let finalize2 = Finalize::sign(&fixture.schemes[0], proposal2).unwrap(); let conflicting = ConflictingFinalize::new(finalize1, finalize2); let encoded = conflicting.encode(); let decoded = ConflictingFinalize::::decode(encoded).unwrap(); assert_eq!(conflicting, decoded); assert!(decoded.verify(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_conflicting_finalize_encode_decode() { conflicting_finalize_encode_decode(ed25519::fixture); conflicting_finalize_encode_decode(secp256r1::fixture); conflicting_finalize_encode_decode(bls12381_multisig::fixture::); conflicting_finalize_encode_decode(bls12381_multisig::fixture::); conflicting_finalize_encode_decode(bls12381_threshold_vrf::fixture::); conflicting_finalize_encode_decode(bls12381_threshold_vrf::fixture::); conflicting_finalize_encode_decode(bls12381_threshold_std::fixture::); conflicting_finalize_encode_decode(bls12381_threshold_std::fixture::); } fn nullify_finalize_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(1)); let nullify = Nullify::sign::(&fixture.schemes[0], round).unwrap(); let finalize = Finalize::sign(&fixture.schemes[0], proposal).unwrap(); let conflict = NullifyFinalize::new(nullify, finalize); let encoded = conflict.encode(); let decoded = NullifyFinalize::::decode(encoded).unwrap(); assert_eq!(conflict, decoded); assert!(decoded.verify(&mut rng, &fixture.schemes[0], &Sequential)); } #[test] fn test_nullify_finalize_encode_decode() { nullify_finalize_encode_decode(ed25519::fixture); nullify_finalize_encode_decode(secp256r1::fixture); nullify_finalize_encode_decode(bls12381_multisig::fixture::); nullify_finalize_encode_decode(bls12381_multisig::fixture::); nullify_finalize_encode_decode(bls12381_threshold_vrf::fixture::); nullify_finalize_encode_decode(bls12381_threshold_vrf::fixture::); nullify_finalize_encode_decode(bls12381_threshold_std::fixture::); nullify_finalize_encode_decode(bls12381_threshold_std::fixture::); } fn notarize_verify_wrong_namespace(f: F) where S: Scheme, F: Fn(&mut TestRng, &[u8], u32) -> Fixture, { // Create two fixtures with different namespaces let mut rng = test_rng(); let fixture = setup_seeded_ns(5, 0, NAMESPACE, &f); let wrong_fixture = setup_seeded_ns(5, 0, b"wrong_namespace", &f); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(1)); let notarize = Notarize::sign(&fixture.schemes[0], proposal).unwrap(); assert!(notarize.verify(&mut rng, &fixture.schemes[0], &Sequential)); assert!(!notarize.verify(&mut rng, &wrong_fixture.schemes[0], &Sequential)); } #[test] fn test_notarize_verify_wrong_namespace() { notarize_verify_wrong_namespace(ed25519::fixture); notarize_verify_wrong_namespace(secp256r1::fixture); notarize_verify_wrong_namespace(bls12381_multisig::fixture::); notarize_verify_wrong_namespace(bls12381_multisig::fixture::); notarize_verify_wrong_namespace(bls12381_threshold_vrf::fixture::); notarize_verify_wrong_namespace(bls12381_threshold_vrf::fixture::); notarize_verify_wrong_namespace(bls12381_threshold_std::fixture::); notarize_verify_wrong_namespace(bls12381_threshold_std::fixture::); } fn notarize_verify_wrong_scheme(f: F) where S: Scheme, F: Fn(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = setup_seeded(5, 0, &f); let wrong_fixture = setup_seeded(5, 1, &f); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(2)); let notarize = Notarize::sign(&fixture.schemes[0], proposal).unwrap(); assert!(notarize.verify(&mut rng, &fixture.schemes[0], &Sequential)); assert!(!notarize.verify(&mut rng, &wrong_fixture.verifier, &Sequential)); } #[test] fn test_notarize_verify_wrong_scheme() { notarize_verify_wrong_scheme(ed25519::fixture); notarize_verify_wrong_scheme(secp256r1::fixture); notarize_verify_wrong_scheme(bls12381_multisig::fixture::); notarize_verify_wrong_scheme(bls12381_multisig::fixture::); notarize_verify_wrong_scheme(bls12381_threshold_vrf::fixture::); notarize_verify_wrong_scheme(bls12381_threshold_vrf::fixture::); notarize_verify_wrong_scheme(bls12381_threshold_std::fixture::); notarize_verify_wrong_scheme(bls12381_threshold_std::fixture::); } fn notarization_verify_wrong_scheme(f: F) where S: Scheme, F: Fn(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = setup_seeded(5, 0, &f); let wrong_fixture = setup_seeded(5, 1, &f); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(3)); let quorum = N3f1::quorum(fixture.schemes.len()) as usize; let notarizes: Vec<_> = fixture .schemes .iter() .take(quorum) .map(|scheme| Notarize::sign(scheme, proposal.clone()).unwrap()) .collect(); let notarization = Notarization::from_notarizes(&fixture.schemes[0], non_empty![@¬arizes], &Sequential) .unwrap(); assert!(notarization.verify(&mut rng, &fixture.schemes[0], &Sequential)); assert!(!notarization.verify(&mut rng, &wrong_fixture.verifier, &Sequential)); } #[test] fn test_notarization_verify_wrong_scheme() { notarization_verify_wrong_scheme(ed25519::fixture); notarization_verify_wrong_scheme(secp256r1::fixture); notarization_verify_wrong_scheme(bls12381_multisig::fixture::); notarization_verify_wrong_scheme(bls12381_multisig::fixture::); notarization_verify_wrong_scheme(bls12381_threshold_vrf::fixture::); notarization_verify_wrong_scheme(bls12381_threshold_vrf::fixture::); notarization_verify_wrong_scheme(bls12381_threshold_std::fixture::); notarization_verify_wrong_scheme(bls12381_threshold_std::fixture::); } fn notarization_verify_wrong_namespace(f: F) where S: Scheme, F: Fn(&mut TestRng, &[u8], u32) -> Fixture, { // Create two fixtures with different namespaces let fixture = setup_seeded_ns(5, 0, NAMESPACE, &f); let wrong_fixture = setup_seeded_ns(5, 0, b"wrong_namespace", &f); let mut rng = test_rng(); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(4)); let quorum = N3f1::quorum(fixture.schemes.len()) as usize; let notarizes: Vec<_> = fixture .schemes .iter() .take(quorum) .map(|scheme| Notarize::sign(scheme, proposal.clone()).unwrap()) .collect(); let notarization = Notarization::from_notarizes(&fixture.schemes[0], non_empty![@¬arizes], &Sequential) .unwrap(); assert!(notarization.verify(&mut rng, &fixture.schemes[0], &Sequential)); assert!(!notarization.verify(&mut rng, &wrong_fixture.schemes[0], &Sequential)); } #[test] fn test_notarization_verify_wrong_namespace() { notarization_verify_wrong_namespace(ed25519::fixture); notarization_verify_wrong_namespace(secp256r1::fixture); notarization_verify_wrong_namespace(bls12381_multisig::fixture::); notarization_verify_wrong_namespace(bls12381_multisig::fixture::); notarization_verify_wrong_namespace(bls12381_threshold_vrf::fixture::); notarization_verify_wrong_namespace(bls12381_threshold_vrf::fixture::); notarization_verify_wrong_namespace(bls12381_threshold_std::fixture::); notarization_verify_wrong_namespace(bls12381_threshold_std::fixture::); } fn notarization_recover_insufficient_signatures(fixture: F) where S: Scheme, F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = fixture(&mut rng, NAMESPACE, 5); let participant_count = u32::try_from(fixture.schemes.len()).expect("participant count exceeds u32::MAX"); let quorum_size = quorum(participant_count); let subquorum = usize::try_from(quorum_size - 1).expect("quorum exceeds usize::MAX"); assert!(quorum_size > 1, "test requires quorum larger than one"); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(5)); let notarizes: Vec<_> = fixture .schemes .iter() .take(subquorum) .map(|scheme| Notarize::sign(scheme, proposal.clone()).unwrap()) .collect(); assert_eq!( Notarization::from_notarizes(&fixture.schemes[0], non_empty![@¬arizes], &Sequential), Err(AssemblyError::InsufficientAttestations( quorum_size, quorum_size - 1 )), "insufficient votes should not form a notarization" ); } #[test] fn test_notarization_recover_insufficient_signatures() { notarization_recover_insufficient_signatures(ed25519::fixture); notarization_recover_insufficient_signatures(secp256r1::fixture); notarization_recover_insufficient_signatures(bls12381_multisig::fixture::); notarization_recover_insufficient_signatures(bls12381_multisig::fixture::); notarization_recover_insufficient_signatures(bls12381_threshold_vrf::fixture::); notarization_recover_insufficient_signatures(bls12381_threshold_vrf::fixture::); notarization_recover_insufficient_signatures(bls12381_threshold_std::fixture::); notarization_recover_insufficient_signatures(bls12381_threshold_std::fixture::); } fn conflicting_notarize_detection(f: F) where S: Scheme, F: Fn(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = setup_seeded(5, 0, &f); let wrong_ns_fixture = setup_seeded_ns(5, 0, b"wrong_namespace", &f); let wrong_scheme_fixture = setup_seeded(5, 1, &f); let round = Round::new(Epoch::new(0), View::new(10)); let proposal1 = Proposal::new(round, View::new(5), sample_digest(6)); let proposal2 = Proposal::new(round, View::new(5), sample_digest(7)); let notarize1 = Notarize::sign(&fixture.schemes[0], proposal1).unwrap(); let notarize2 = Notarize::sign(&fixture.schemes[0], proposal2).unwrap(); let conflict = ConflictingNotarize::new(notarize1, notarize2); assert!(conflict.verify(&mut rng, &fixture.schemes[0], &Sequential)); assert!(!conflict.verify(&mut rng, &wrong_ns_fixture.schemes[0], &Sequential)); assert!(!conflict.verify(&mut rng, &wrong_scheme_fixture.verifier, &Sequential)); } #[test] fn test_conflicting_notarize_detection() { conflicting_notarize_detection(ed25519::fixture); conflicting_notarize_detection(secp256r1::fixture); conflicting_notarize_detection(bls12381_multisig::fixture::); conflicting_notarize_detection(bls12381_multisig::fixture::); conflicting_notarize_detection(bls12381_threshold_vrf::fixture::); conflicting_notarize_detection(bls12381_threshold_vrf::fixture::); conflicting_notarize_detection(bls12381_threshold_std::fixture::); conflicting_notarize_detection(bls12381_threshold_std::fixture::); } fn nullify_finalize_detection(f: F) where S: Scheme, F: Fn(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = setup_seeded(5, 0, &f); let wrong_ns_fixture = setup_seeded_ns(5, 0, b"wrong_namespace", &f); let wrong_scheme_fixture = setup_seeded(5, 1, &f); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(8)); let nullify = Nullify::sign::(&fixture.schemes[0], round).unwrap(); let finalize = Finalize::sign(&fixture.schemes[0], proposal).unwrap(); let conflict = NullifyFinalize::new(nullify, finalize); assert!(conflict.verify(&mut rng, &fixture.schemes[0], &Sequential)); assert!(!conflict.verify(&mut rng, &wrong_ns_fixture.schemes[0], &Sequential)); assert!(!conflict.verify(&mut rng, &wrong_scheme_fixture.verifier, &Sequential)); } #[test] fn test_nullify_finalize_detection() { nullify_finalize_detection(ed25519::fixture); nullify_finalize_detection(secp256r1::fixture); nullify_finalize_detection(bls12381_multisig::fixture::); nullify_finalize_detection(bls12381_multisig::fixture::); nullify_finalize_detection(bls12381_threshold_vrf::fixture::); nullify_finalize_detection(bls12381_threshold_vrf::fixture::); nullify_finalize_detection(bls12381_threshold_std::fixture::); nullify_finalize_detection(bls12381_threshold_std::fixture::); } fn finalization_verify_wrong_scheme(f: F) where S: Scheme, F: Fn(&mut TestRng, &[u8], u32) -> Fixture, { let mut rng = test_rng(); let fixture = setup_seeded(5, 0, &f); let wrong_fixture = setup_seeded(5, 1, &f); let round = Round::new(Epoch::new(0), View::new(10)); let proposal = Proposal::new(round, View::new(5), sample_digest(9)); let quorum = N3f1::quorum(fixture.schemes.len()) as usize; let finalizes: Vec<_> = fixture .schemes .iter() .take(quorum) .map(|scheme| Finalize::sign(scheme, proposal.clone()).unwrap()) .collect(); let finalization = Finalization::from_finalizes(&fixture.schemes[0], non_empty![@&finalizes], &Sequential) .expect("quorum finalization"); assert!(finalization.verify(&mut rng, &fixture.schemes[0], &Sequential)); assert!(!finalization.verify(&mut rng, &wrong_fixture.verifier, &Sequential)); } #[test] fn test_finalization_wrong_scheme() { finalization_verify_wrong_scheme(ed25519::fixture); finalization_verify_wrong_scheme(secp256r1::fixture); finalization_verify_wrong_scheme(bls12381_multisig::fixture::); finalization_verify_wrong_scheme(bls12381_multisig::fixture::); finalization_verify_wrong_scheme(bls12381_threshold_vrf::fixture::); finalization_verify_wrong_scheme(bls12381_threshold_vrf::fixture::); finalization_verify_wrong_scheme(bls12381_threshold_std::fixture::); finalization_verify_wrong_scheme(bls12381_threshold_std::fixture::); } struct MockAttributable(Participant); impl Attributable for MockAttributable { fn signer(&self) -> Participant { self.0 } } #[test] fn test_attributable_map() { let mut map = AttributableMap::new(5); assert_eq!(map.len(), 0); assert!(map.is_empty()); assert_eq!(map.data.capacity(), 0, "empty maps should allocate lazily"); // Test get on empty map for i in 0..5 { assert!(map.get(Participant::new(i)).is_none()); } assert!(map.insert(MockAttributable(Participant::new(3)))); assert!(map.data.capacity() >= 5); assert_eq!(map.len(), 1); assert!(!map.is_empty()); let mut iter = map.iter(); assert!(matches!(iter.next(), Some(a) if a.signer() == Participant::new(3))); assert!(iter.next().is_none()); drop(iter); // Test get on existing item assert!( matches!(map.get(Participant::new(3)), Some(a) if a.signer() == Participant::new(3)) ); assert!(map.insert(MockAttributable(Participant::new(1)))); assert_eq!(map.len(), 2); assert!(!map.is_empty()); let mut iter = map.iter(); assert!(matches!(iter.next(), Some(a) if a.signer() == Participant::new(1))); assert!(matches!(iter.next(), Some(a) if a.signer() == Participant::new(3))); assert!(iter.next().is_none()); drop(iter); // Test get on both items assert!( matches!(map.get(Participant::new(1)), Some(a) if a.signer() == Participant::new(1)) ); assert!( matches!(map.get(Participant::new(3)), Some(a) if a.signer() == Participant::new(3)) ); // Test get on non-existing items assert!(map.get(Participant::new(0)).is_none()); assert!(map.get(Participant::new(2)).is_none()); assert!(map.get(Participant::new(4)).is_none()); assert!(!map.insert(MockAttributable(Participant::new(3)))); assert_eq!(map.len(), 2); assert!(!map.is_empty()); let mut iter = map.iter(); assert!(matches!(iter.next(), Some(a) if a.signer() == Participant::new(1))); assert!(matches!(iter.next(), Some(a) if a.signer() == Participant::new(3))); assert!(iter.next().is_none()); drop(iter); // Test out-of-bounds signer indices assert!(!map.insert(MockAttributable(Participant::new(5)))); assert!(!map.insert(MockAttributable(Participant::new(100)))); assert_eq!(map.len(), 2); // Test clear map.clear(); assert_eq!(map.len(), 0); assert!(map.is_empty()); assert!(map.iter().next().is_none()); assert_eq!(map.data.capacity(), 0, "clear should release vote storage"); // Verify can insert after clear assert!(map.insert(MockAttributable(Participant::new(2)))); assert_eq!(map.len(), 1); let mut iter = map.iter(); assert!(matches!(iter.next(), Some(a) if a.signer() == Participant::new(2))); assert!(iter.next().is_none()); } #[test] #[should_panic(expected = "proposals must differ")] fn issue_2944_regression_conflicting_notarize_new() { let mut rng = test_rng(); let fixture = ed25519::fixture(&mut rng, NAMESPACE, 1); let proposal = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let notarize = Notarize::sign(&fixture.schemes[0], proposal).unwrap(); let _ = ConflictingNotarize::new(notarize.clone(), notarize); } #[test] fn issue_2944_regression_conflicting_notarize_decode() { let mut rng = test_rng(); let fixture = ed25519::fixture(&mut rng, NAMESPACE, 1); let proposal = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let notarize = Notarize::sign(&fixture.schemes[0], proposal).unwrap(); // Manually encode two identical notarizes let mut buf = Vec::new(); notarize.write(&mut buf); notarize.write(&mut buf); // Decoding should fail let result = ConflictingNotarize::::decode(buf); assert!(result.is_err()); } #[test] #[should_panic(expected = "proposals must differ")] fn issue_2944_regression_conflicting_finalize_new() { let mut rng = test_rng(); let fixture = ed25519::fixture(&mut rng, NAMESPACE, 1); let proposal = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let finalize = Finalize::sign(&fixture.schemes[0], proposal).unwrap(); let _ = ConflictingFinalize::new(finalize.clone(), finalize); } #[test] fn issue_2944_regression_conflicting_finalize_decode() { let mut rng = test_rng(); let fixture = ed25519::fixture(&mut rng, NAMESPACE, 1); let proposal = Proposal::new( Round::new(Epoch::new(0), View::new(10)), View::new(5), sample_digest(1), ); let finalize = Finalize::sign(&fixture.schemes[0], proposal).unwrap(); // Manually encode two identical finalizes let mut buf = Vec::new(); finalize.write(&mut buf); finalize.write(&mut buf); // Decoding should fail let result = ConflictingFinalize::::decode(buf); assert!(result.is_err()); } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use crate::simplex::scheme::bls12381_threshold::vrf as bls12381_threshold_vrf; use commonware_codec::conformance::CodecConformance; use commonware_cryptography::{ed25519::PublicKey, sha256::Digest as Sha256Digest}; type Scheme = bls12381_threshold_vrf::Scheme; commonware_conformance::conformance_tests! { CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance> } } }