//! BLS12-381 multi-signature implementation of the [`Scheme`] trait for `simplex`. //! //! [`Scheme`] is **attributable**: individual signatures can be //! used by an external observer as evidence of either liveness or of committing a fault. //! Certificates contain signer indices alongside an aggregated signature, //! enabling secure per-validator activity tracking and conflict detection. use crate::{ simplex::{ signing_scheme::{self, utils::Signers, vote_namespace_and_message}, types::{OrderedExt, Vote, VoteContext, VoteVerification}, }, types::Round, }; use bytes::{Buf, BufMut}; use commonware_codec::{EncodeSize, Error, Read, ReadExt, Write}; use commonware_cryptography::{ bls12381::primitives::{ group::Private, ops::{ aggregate_signatures, aggregate_verify_multiple_public_keys, compute_public, sign_message, verify_message, }, variant::Variant, }, Digest, PublicKey, }; use commonware_utils::set::{Ordered, OrderedAssociated}; use rand::{CryptoRng, Rng}; use std::{collections::BTreeSet, fmt::Debug}; /// BLS12-381 multi-signature implementation of the [`Scheme`] trait. #[derive(Clone, Debug)] pub struct Scheme { /// Participants in the committee. participants: OrderedAssociated, /// Key used for generating signatures. signer: Option<(u32, Private)>, } impl Scheme { /// Creates a new scheme instance with the provided key material. /// /// Participants have both an identity key and a consensus key. The identity key /// is used for committee ordering and indexing, while the consensus key is used for /// signing and verification. /// /// If the provided private key does not match any consensus key in the committee, /// the instance will act as a verifier (unable to generate signatures). pub fn new(participants: OrderedAssociated, private_key: Private) -> Self { let public_key = compute_public::(&private_key); let signer = participants .values() .iter() .position(|p| p == &public_key) .map(|index| (index as u32, private_key)); Self { participants, signer, } } /// Builds a verifier that can authenticate votes and certificates. /// /// Participants have both an identity key and a consensus key. The identity key /// is used for committee ordering and indexing, while the consensus key is used for /// verification. pub fn verifier(participants: OrderedAssociated) -> Self { Self { participants, signer: None, } } } /// Certificate formed by an aggregated BLS12-381 signature plus the signers that /// contributed to it. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Certificate { /// Bitmap of validator indices that contributed signatures. pub signers: Signers, /// Aggregated BLS signature covering all votes in this certificate. pub signature: V::Signature, } impl Write for Certificate { fn write(&self, writer: &mut impl BufMut) { self.signers.write(writer); self.signature.write(writer); } } impl EncodeSize for Certificate { fn encode_size(&self) -> usize { self.signers.encode_size() + self.signature.encode_size() } } impl Read for Certificate { type Cfg = usize; fn read_cfg(reader: &mut impl Buf, participants: &usize) -> Result { let signers = Signers::read_cfg(reader, participants)?; if signers.count() == 0 { return Err(Error::Invalid( "consensus::simplex::signing_scheme::bls12381_multisig::Certificate", "Certificate contains no signers", )); } let signature = V::Signature::read(reader)?; Ok(Self { signers, signature }) } } impl signing_scheme::Scheme for Scheme { type PublicKey = P; type Signature = V::Signature; type Certificate = Certificate; type Seed = (); fn me(&self) -> Option { self.signer.as_ref().map(|(index, _)| *index) } fn participants(&self) -> &Ordered { &self.participants } fn sign_vote( &self, namespace: &[u8], context: VoteContext<'_, D>, ) -> Option> { let (index, private_key) = self.signer.as_ref()?; let (namespace, message) = vote_namespace_and_message(namespace, context); let signature = sign_message::(private_key, Some(namespace.as_ref()), message.as_ref()); Some(Vote { signer: *index, signature, }) } fn verify_vote( &self, namespace: &[u8], context: VoteContext<'_, D>, vote: &Vote, ) -> bool { let Some(public_key) = self.participants.value(vote.signer as usize) else { return false; }; let (namespace, message) = vote_namespace_and_message(namespace, context); verify_message::( public_key, Some(namespace.as_ref()), message.as_ref(), &vote.signature, ) .is_ok() } fn verify_votes( &self, _rng: &mut R, namespace: &[u8], context: VoteContext<'_, D>, votes: I, ) -> VoteVerification where R: Rng + CryptoRng, D: Digest, I: IntoIterator>, { let mut invalid = BTreeSet::new(); let mut candidates = Vec::new(); let mut publics = Vec::new(); let mut signatures = Vec::new(); for vote in votes.into_iter() { let Some(public_key) = self.participants.value(vote.signer as usize) else { invalid.insert(vote.signer); continue; }; publics.push(*public_key); signatures.push(vote.signature); candidates.push(vote); } // If there are no candidates to verify, return before doing any work. if candidates.is_empty() { return VoteVerification::new(candidates, invalid.into_iter().collect()); } // Verify the aggregate signature. let (namespace, message) = vote_namespace_and_message(namespace, context); if aggregate_verify_multiple_public_keys::( publics.iter(), Some(namespace.as_ref()), message.as_ref(), &aggregate_signatures::(signatures.iter()), ) .is_err() { for (vote, public_key) in candidates.iter().zip(publics.iter()) { if verify_message::( public_key, Some(namespace.as_ref()), message.as_ref(), &vote.signature, ) .is_err() { invalid.insert(vote.signer); } } } // Collect the invalid signers. let verified = candidates .into_iter() .filter(|vote| !invalid.contains(&vote.signer)) .collect(); let invalid_signers: Vec<_> = invalid.into_iter().collect(); VoteVerification::new(verified, invalid_signers) } fn assemble_certificate(&self, votes: I) -> Option where I: IntoIterator>, { // Collect the signers and signatures. let mut entries = Vec::new(); for Vote { signer, signature } in votes { if signer as usize >= self.participants.len() { return None; } entries.push((signer, signature)); } if entries.len() < self.participants.quorum() as usize { return None; } // Produce signers and aggregate signature. let (signers, signatures): (Vec<_>, Vec<_>) = entries.into_iter().unzip(); let signers = Signers::from(self.participants.len(), signers); let signature = aggregate_signatures::(signatures.iter()); Some(Certificate { signers, signature }) } fn verify_certificate( &self, _rng: &mut R, namespace: &[u8], context: VoteContext<'_, D>, certificate: &Self::Certificate, ) -> bool { // If the certificate signers length does not match the participant set, return false. if certificate.signers.len() != self.participants.len() { return false; } // If the certificate does not meet the quorum, return false. if certificate.signers.count() < self.participants.quorum() as usize { return false; } // Collect the public keys. let mut publics = Vec::with_capacity(certificate.signers.count()); for signer in certificate.signers.iter() { let Some(public_key) = self.participants.value(signer as usize) else { return false; }; publics.push(*public_key); } // Verify the aggregate signature. let (namespace, message) = vote_namespace_and_message(namespace, context); aggregate_verify_multiple_public_keys::( publics.iter(), Some(namespace.as_ref()), message.as_ref(), &certificate.signature, ) .is_ok() } fn seed(&self, _: Round, _: &Self::Certificate) -> Option { None } fn is_attributable(&self) -> bool { true } fn certificate_codec_config(&self) -> ::Cfg { self.participants.len() } fn certificate_codec_config_unbounded() -> ::Cfg { u32::MAX as usize } } #[cfg(test)] mod tests { use super::*; use crate::{ simplex::{ mocks::fixtures::{bls12381_multisig, Fixture}, signing_scheme::Scheme as _, types::{Proposal, VoteContext}, }, types::Round, }; use commonware_codec::{Decode, Encode}; use commonware_cryptography::{ bls12381::primitives::{ group::Element, variant::{MinPk, MinSig, Variant}, }, ed25519, sha256::Digest as Sha256Digest, Hasher, Sha256, }; use commonware_utils::quorum; use rand::{ rngs::{OsRng, StdRng}, thread_rng, SeedableRng, }; const NAMESPACE: &[u8] = b"bls-multisig-signing-scheme"; #[allow(clippy::type_complexity)] fn setup_signers( n: u32, seed: u64, ) -> ( Vec>, OrderedAssociated, ) { let mut rng = StdRng::seed_from_u64(seed); let Fixture { schemes, .. } = bls12381_multisig::(&mut rng, n); let participants = schemes.first().unwrap().participants.clone(); (schemes, participants) } fn sample_proposal(round: u64, view: u64, tag: u8) -> Proposal { Proposal::new( Round::new(round, view), view.saturating_sub(1), Sha256::hash(&[tag]), ) } fn sign_vote_roundtrip_for_each_context() { let (schemes, _) = setup_signers::(4, 42); let scheme = &schemes[0]; let proposal = sample_proposal(0, 2, 1); let vote = scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .unwrap(); assert!(scheme.verify_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, &vote )); let vote = scheme .sign_vote::( NAMESPACE, VoteContext::Nullify { round: proposal.round, }, ) .unwrap(); assert!(scheme.verify_vote::( NAMESPACE, VoteContext::Nullify { round: proposal.round, }, &vote )); let vote = scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap(); assert!(scheme.verify_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &vote )); } #[test] fn test_sign_vote_roundtrip_for_each_context() { sign_vote_roundtrip_for_each_context::(); sign_vote_roundtrip_for_each_context::(); } fn verifier_cannot_sign() { let (_, participants) = setup_signers::(4, 42); let verifier = Scheme::::verifier(participants); let proposal = sample_proposal(0, 3, 2); assert!( verifier .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .is_none(), "verifier should not produce signatures" ); } #[test] fn test_verifier_cannot_sign_min() { verifier_cannot_sign::(); verifier_cannot_sign::(); } fn verify_votes_filters_bad_signers() { let (schemes, _) = setup_signers::(5, 42); let quorum = quorum(schemes.len() as u32) as usize; let proposal = sample_proposal(0, 5, 3); let mut votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .unwrap() }) .collect(); let verification = schemes[0].verify_votes( &mut thread_rng(), NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, votes.clone(), ); assert!(verification.invalid_signers.is_empty()); assert_eq!(verification.verified.len(), quorum); // Invalid signer index should be detected. votes[0].signer = 999; let verification = schemes[0].verify_votes( &mut thread_rng(), NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, votes.clone(), ); assert_eq!(verification.invalid_signers, vec![999]); assert_eq!(verification.verified.len(), quorum - 1); // Invalid signature should be detected. votes[0].signer = 0; votes[0].signature = votes[1].signature; let verification = schemes[0].verify_votes( &mut thread_rng(), NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, votes, ); assert_eq!(verification.invalid_signers, vec![0]); assert_eq!(verification.verified.len(), quorum - 1); } #[test] fn test_verify_votes_filters_bad_signers() { verify_votes_filters_bad_signers::(); verify_votes_filters_bad_signers::(); } fn assemble_certificate_sorts_signers() { let (schemes, _) = setup_signers::(4, 42); let proposal = sample_proposal(0, 7, 4); let votes = [ schemes[2] .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap(), schemes[0] .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap(), schemes[1] .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap(), ]; let certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); assert_eq!(certificate.signers.count(), 3); assert_eq!( certificate.signers.iter().collect::>(), vec![0, 1, 2] ); } #[test] fn test_assemble_certificate_sorts_signers() { assemble_certificate_sorts_signers::(); assemble_certificate_sorts_signers::(); } fn assemble_certificate_requires_quorum() { let (schemes, _) = setup_signers::(4, 42); let proposal = sample_proposal(0, 9, 5); let votes: Vec<_> = schemes .iter() .take(2) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .unwrap() }) .collect(); assert!(schemes[0].assemble_certificate(votes).is_none()); } #[test] fn test_assemble_certificate_requires_quorum() { assemble_certificate_requires_quorum::(); assemble_certificate_requires_quorum::(); } fn assemble_certificate_rejects_out_of_range_signer() { let (schemes, _) = setup_signers::(4, 42); let proposal = sample_proposal(0, 13, 7); let mut votes: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .unwrap() }) .collect(); votes[0].signer = 42; assert!(schemes[0].assemble_certificate(votes).is_none()); } #[test] fn test_assemble_certificate_rejects_out_of_range_signer() { assemble_certificate_rejects_out_of_range_signer::(); assemble_certificate_rejects_out_of_range_signer::(); } fn verify_certificate_detects_corruption() { let (schemes, participants) = setup_signers::(4, 42); let proposal = sample_proposal(0, 15, 8); let votes: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap() }) .collect(); let certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); let verifier = Scheme::verifier(participants); assert!(verifier.verify_certificate( &mut thread_rng(), NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &certificate, )); let mut corrupted = certificate.clone(); corrupted.signature = V::Signature::zero(); assert!(!verifier.verify_certificate( &mut thread_rng(), NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &corrupted, )); } #[test] fn test_verify_certificate_detects_corruption() { verify_certificate_detects_corruption::(); verify_certificate_detects_corruption::(); } fn certificate_codec_roundtrip() { let (schemes, _) = setup_signers::(4, 42); let proposal = sample_proposal(0, 21, 11); let votes: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .unwrap() }) .collect(); let certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); let encoded = certificate.encode(); let decoded = Certificate::::decode_cfg(encoded, &schemes.len()).expect("decode"); assert_eq!(decoded, certificate); } #[test] fn test_certificate_codec_roundtrip() { certificate_codec_roundtrip::(); certificate_codec_roundtrip::(); } fn scheme_clone_and_into_verifier() { let (schemes, participants) = setup_signers::(4, 42); let proposal = sample_proposal(0, 23, 12); let clone = schemes[0].clone(); assert!( clone .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .is_some(), "cloned signer should retain signing capability" ); let verifier = Scheme::::verifier(participants); assert!( verifier .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .is_none(), "verifier must not sign votes" ); } #[test] fn test_scheme_clone_and_into_verifier() { scheme_clone_and_into_verifier::(); scheme_clone_and_into_verifier::(); } fn verify_certificate() { let (schemes, participants) = setup_signers::(4, 42); let proposal = sample_proposal(0, 23, 12); let votes: Vec<_> = schemes .iter() .take(quorum(schemes.len() as u32) as usize) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap() }) .collect(); let certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); let verifier = Scheme::verifier(participants); assert!(verifier.verify_certificate( &mut OsRng, NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &certificate, )); } #[test] fn test_verify_certificate() { verify_certificate::(); verify_certificate::(); } fn verify_certificates_batch() { let (schemes, participants) = setup_signers::(4, 42); let proposal_a = sample_proposal(0, 23, 12); let proposal_b = sample_proposal(1, 24, 13); let votes_a: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal_a, }, ) .unwrap() }) .collect(); let votes_b: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal_b, }, ) .unwrap() }) .collect(); let certificate_a = schemes[0] .assemble_certificate(votes_a) .expect("assemble certificate"); let certificate_b = schemes[0] .assemble_certificate(votes_b) .expect("assemble certificate"); let verifier = Scheme::verifier(participants); let mut iter = [ ( VoteContext::Notarize { proposal: &proposal_a, }, &certificate_a, ), ( VoteContext::Finalize { proposal: &proposal_b, }, &certificate_b, ), ] .into_iter(); assert!(verifier.verify_certificates(&mut thread_rng(), NAMESPACE, &mut iter)); } #[test] fn test_verify_certificates_batch() { verify_certificates_batch::(); verify_certificates_batch::(); } fn verify_certificates_batch_detects_failure() { let (schemes, participants) = setup_signers::(4, 42); let proposal_a = sample_proposal(0, 25, 14); let proposal_b = sample_proposal(1, 26, 15); let votes_a: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal_a, }, ) .unwrap() }) .collect(); let votes_b: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal_b, }, ) .unwrap() }) .collect(); let certificate_a = schemes[0] .assemble_certificate(votes_a) .expect("assemble certificate"); let mut bad_certificate = schemes[0] .assemble_certificate(votes_b) .expect("assemble certificate"); bad_certificate.signature = certificate_a.signature; let verifier = Scheme::verifier(participants); let mut iter = [ ( VoteContext::Notarize { proposal: &proposal_a, }, &certificate_a, ), ( VoteContext::Finalize { proposal: &proposal_b, }, &bad_certificate, ), ] .into_iter(); assert!(!verifier.verify_certificates(&mut thread_rng(), NAMESPACE, &mut iter)); } #[test] fn test_verify_certificates_batch_detects_failure() { verify_certificates_batch_detects_failure::(); verify_certificates_batch_detects_failure::(); } fn verify_certificate_rejects_sub_quorum() { let (schemes, participants) = setup_signers::(4, 42); let proposal = sample_proposal(0, 17, 9); let votes: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap() }) .collect(); let certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); let mut truncated = certificate.clone(); let mut signers: Vec = truncated.signers.iter().collect(); signers.pop(); truncated.signers = Signers::from(participants.len(), signers); let verifier = Scheme::verifier(participants); assert!(!verifier.verify_certificate( &mut thread_rng(), NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &truncated, )); } #[test] fn test_verify_certificate_rejects_sub_quorum() { verify_certificate_rejects_sub_quorum::(); verify_certificate_rejects_sub_quorum::(); } fn verify_certificate_rejects_unknown_signer() { let (schemes, participants) = setup_signers::(4, 42); let proposal = sample_proposal(0, 19, 10); let votes: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap() }) .collect(); let mut certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); let mut signers: Vec = certificate.signers.iter().collect(); signers.push(participants.len() as u32); certificate.signers = Signers::from(participants.len() + 1, signers); let verifier = Scheme::verifier(participants); assert!(!verifier.verify_certificate( &mut thread_rng(), NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &certificate, )); } #[test] fn test_verify_certificate_rejects_unknown_signer() { verify_certificate_rejects_unknown_signer::(); verify_certificate_rejects_unknown_signer::(); } fn verify_certificate_rejects_invalid_certificate_signers_size() { let (schemes, participants) = setup_signers::(4, 42); let proposal = sample_proposal(0, 20, 11); let votes: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, ) .unwrap() }) .collect(); let mut certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); // The certificate is valid let verifier = Scheme::verifier(participants.clone()); assert!(verifier.verify_certificate( &mut thread_rng(), NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &certificate, )); // Make the signers bitmap size smaller let signers: Vec = certificate.signers.iter().collect(); certificate.signers = Signers::from(participants.len() - 1, signers); // The certificate verification should fail assert!(!verifier.verify_certificate( &mut thread_rng(), NAMESPACE, VoteContext::Finalize { proposal: &proposal, }, &certificate, )); } #[test] fn test_verify_certificate_rejects_invalid_certificate_signers_size() { verify_certificate_rejects_invalid_certificate_signers_size::(); verify_certificate_rejects_invalid_certificate_signers_size::(); } fn certificate_decode_checks_sorted_unique_signers() { let (schemes, participants) = setup_signers::(4, 42); let proposal = sample_proposal(0, 19, 10); let votes: Vec<_> = schemes .iter() .take(3) .map(|scheme| { scheme .sign_vote( NAMESPACE, VoteContext::Notarize { proposal: &proposal, }, ) .unwrap() }) .collect(); let certificate = schemes[0] .assemble_certificate(votes) .expect("assemble certificate"); // Well-formed certificate decodes successfully. let encoded = certificate.encode(); let mut cursor = &encoded[..]; let decoded = Certificate::::read_cfg(&mut cursor, &participants.len()) .expect("decode certificate"); assert_eq!(decoded, certificate); // Certificate with no signers is rejected. let empty = Certificate:: { signers: Signers::from(participants.len(), std::iter::empty::()), signature: certificate.signature, }; assert!(Certificate::::decode_cfg(empty.encode(), &participants.len()).is_err()); // Certificate containing more signers than the participant set is rejected. let mut signers = certificate.signers.iter().collect::>(); signers.push(participants.len() as u32); let extended = Certificate:: { signers: Signers::from(participants.len() + 1, signers), signature: certificate.signature, }; assert!(Certificate::::decode_cfg(extended.encode(), &participants.len()).is_err()); } #[test] fn test_certificate_decode_checks_sorted_unique_signers() { certificate_decode_checks_sorted_unique_signers::(); certificate_decode_checks_sorted_unique_signers::(); } }