//! BLS12-381 threshold implementation of the [`Scheme`] trait for `simplex`. //! //! [`Scheme`] is **non-attributable**: exposing partial signatures //! as evidence of either liveness or of committing a fault is not safe. With threshold signatures, //! any `t` valid partial signatures can be used to forge a partial signature for any other player, //! enabling equivocation attacks. Because peer connections are authenticated, evidence can be used locally //! (as it must be sent by said participant) but can't be used by an external observer. use crate::{ simplex::{ scheme::{seed_namespace, Namespace}, types::{Finalization, Notarization, Subject}, }, types::{Epoch, Participant, Round, View}, Epochable, Viewable, }; use bytes::{Buf, BufMut}; use commonware_codec::{Encode, EncodeSize, Error, FixedSize, Read, ReadExt, Write}; use commonware_cryptography::{ bls12381::{ primitives::{ group::Share, ops::{self, batch, threshold}, sharing::Sharing, variant::{PartialSignature, Variant}, }, tle, }, certificate::{self, Attestation, Subject as CertificateSubject, Verification}, Digest, PublicKey, }; use commonware_parallel::Strategy; use commonware_utils::{ordered::Set, Faults}; use rand::{rngs::StdRng, SeedableRng}; use rand_core::CryptoRngCore; use std::{ collections::{BTreeSet, HashMap}, fmt::Debug, }; /// The role-specific data for a BLS12-381 threshold scheme participant. #[derive(Clone, Debug)] enum Role { Signer { /// Participants in the committee. participants: Set

, /// The public polynomial, used for the group identity, and partial signatures. polynomial: Sharing, /// Local share used to generate partial signatures. share: Share, /// Pre-computed namespaces for domain separation. namespace: Namespace, }, Verifier { /// Participants in the committee. participants: Set

, /// The public polynomial, used for the group identity, and partial signatures. polynomial: Sharing, /// Pre-computed namespaces for domain separation. namespace: Namespace, }, CertificateVerifier { /// Public identity of the committee (constant across reshares). identity: V::Public, /// Pre-computed namespaces for domain separation. namespace: Namespace, }, } /// BLS12-381 threshold implementation of the [`certificate::Scheme`] trait. /// /// It is possible for a node to play one of the following roles: a signer (with its share), /// a verifier (with evaluated public polynomial), or an external verifier that /// only checks recovered certificates. #[derive(Clone, Debug)] pub struct Scheme { role: Role, } impl Scheme { /// Constructs a signer instance with a private share and evaluated public polynomial. /// /// The participant identity keys are used for committee ordering and indexing. /// The polynomial can be evaluated to obtain public verification keys for partial /// signatures produced by committee members. /// /// Returns `None` if the share's public key does not match any participant. /// /// * `namespace` - base namespace for domain separation /// * `participants` - ordered set of participant identity keys /// * `polynomial` - public polynomial for threshold verification /// * `share` - local threshold share for signing pub fn signer( namespace: &[u8], participants: Set

, polynomial: Sharing, share: Share, ) -> Option { assert_eq!( polynomial.total().get() as usize, participants.len(), "polynomial total must equal participant len" ); polynomial.precompute_partial_publics(); let partial_public = polynomial .partial_public(share.index) .expect("share index must match participant indices"); if partial_public == share.public::() { Some(Self { role: Role::Signer { participants, polynomial, share, namespace: Namespace::new(namespace), }, }) } else { None } } /// Produces a verifier that can authenticate votes but does not hold signing state. /// /// The participant identity keys are used for committee ordering and indexing. /// The polynomial can be evaluated to obtain public verification keys for partial /// signatures produced by committee members. /// /// * `namespace` - base namespace for domain separation /// * `participants` - ordered set of participant identity keys /// * `polynomial` - public polynomial for threshold verification pub fn verifier(namespace: &[u8], participants: Set

, polynomial: Sharing) -> Self { assert_eq!( polynomial.total().get() as usize, participants.len(), "polynomial total must equal participant len" ); polynomial.precompute_partial_publics(); Self { role: Role::Verifier { participants, polynomial, namespace: Namespace::new(namespace), }, } } /// Creates a verifier that only checks recovered certificates. /// /// This lightweight verifier can authenticate recovered threshold certificates but cannot /// verify individual votes or partial signatures. /// /// * `namespace` - base namespace for domain separation /// * `identity` - public identity of the committee (constant across reshares) pub fn certificate_verifier(namespace: &[u8], identity: V::Public) -> Self { Self { role: Role::CertificateVerifier { identity, namespace: Namespace::new(namespace), }, } } /// Returns the ordered set of participant public identity keys in the committee. pub fn participants(&self) -> &Set

{ match &self.role { Role::Signer { participants, .. } => participants, Role::Verifier { participants, .. } => participants, Role::CertificateVerifier { .. } => { panic!("can only be called for signer and verifier") } } } /// Returns the public identity of the committee (constant across reshares). pub fn identity(&self) -> &V::Public { match &self.role { Role::Signer { polynomial, .. } => polynomial.public(), Role::Verifier { polynomial, .. } => polynomial.public(), Role::CertificateVerifier { identity, .. } => identity, } } /// Returns the local share if this instance can generate partial signatures. pub const fn share(&self) -> Option<&Share> { match &self.role { Role::Signer { share, .. } => Some(share), _ => None, } } /// Returns the evaluated public polynomial for validating partial signatures produced by committee members. pub fn polynomial(&self) -> &Sharing { match &self.role { Role::Signer { polynomial, .. } => polynomial, Role::Verifier { polynomial, .. } => polynomial, Role::CertificateVerifier { .. } => { panic!("can only be called for signer and verifier") } } } /// Returns the pre-computed namespaces. const fn namespace(&self) -> &Namespace { match &self.role { Role::Signer { namespace, .. } => namespace, Role::Verifier { namespace, .. } => namespace, Role::CertificateVerifier { namespace, .. } => namespace, } } /// Encrypts a message for a target round using Timelock Encryption ([TLE](tle)). /// /// The encrypted message can only be decrypted using the seed signature /// from a certificate of the target round (i.e. notarization, finalization, /// or nullification). pub fn encrypt( &self, rng: &mut R, target: Round, message: impl Into, ) -> tle::Ciphertext { let block = message.into(); let target_message = target.encode(); tle::encrypt( rng, *self.identity(), (&self.namespace().seed, &target_message), &block, ) } } /// Encrypts a message for a future round using Timelock Encryption ([TLE](tle)). /// /// The encrypted message can only be decrypted using the seed signature /// from a certificate of the target round (i.e. notarization, finalization, /// or nullification). pub fn encrypt( rng: &mut R, identity: V::Public, namespace: &[u8], target: Round, message: impl Into, ) -> tle::Ciphertext { let block = message.into(); let seed_ns = seed_namespace(namespace); let target_message = target.encode(); tle::encrypt(rng, identity, (&seed_ns, &target_message), &block) } /// Generates a test fixture with Ed25519 identities and BLS12-381 threshold schemes. /// /// Returns a [`commonware_cryptography::certificate::mocks::Fixture`] whose keys and /// scheme instances share a consistent ordering. #[cfg(feature = "mocks")] pub fn fixture( rng: &mut R, namespace: &[u8], n: u32, ) -> commonware_cryptography::certificate::mocks::Fixture< Scheme, > where V: Variant, R: rand::RngCore + rand::CryptoRng, { commonware_cryptography::bls12381::certificate::threshold::mocks::fixture::<_, V, _>( rng, namespace, n, |namespace, participants, polynomial, share| { Scheme::signer(namespace, participants, polynomial, share) }, |namespace, participants, polynomial| Scheme::verifier(namespace, participants, polynomial), ) } /// Combined vote/seed signature pair emitted by the BLS12-381 threshold scheme. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Signature { /// Signature over the consensus vote message (partial or recovered aggregate). pub vote_signature: V::Signature, /// Signature over the per-view seed (partial or recovered aggregate). pub seed_signature: V::Signature, } impl Write for Signature { fn write(&self, writer: &mut impl BufMut) { self.vote_signature.write(writer); self.seed_signature.write(writer); } } impl Read for Signature { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let vote_signature = V::Signature::read(reader)?; let seed_signature = V::Signature::read(reader)?; Ok(Self { vote_signature, seed_signature, }) } } impl FixedSize for Signature { const SIZE: usize = V::Signature::SIZE * 2; } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Signature where V::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self { vote_signature: u.arbitrary()?, seed_signature: u.arbitrary()?, }) } } /// Seed represents a threshold signature over the current view. #[derive(Clone, Debug, PartialEq, Hash, Eq)] pub struct Seed { /// The round for which this seed is generated pub round: Round, /// The threshold signature on the seed. pub signature: V::Signature, } impl Seed { /// Creates a new seed with the given view and signature. pub const fn new(round: Round, signature: V::Signature) -> Self { Self { round, signature } } /// Verifies the threshold signature on this [Seed]. pub fn verify(&self, scheme: &Scheme) -> bool { let seed_message = self.round.encode(); ops::verify_message::( scheme.identity(), &scheme.namespace().seed, &seed_message, &self.signature, ) .is_ok() } /// Returns the round associated with this seed. pub const fn round(&self) -> Round { self.round } /// Decrypts a [TLE](tle) ciphertext using this seed. /// /// Returns `None` if the ciphertext is invalid or encrypted for a different /// round than this seed. pub fn decrypt(&self, ciphertext: &tle::Ciphertext) -> Option { decrypt(self, ciphertext) } } /// Decrypts a [TLE](tle) ciphertext using the seed from a certificate (i.e. /// notarization, finalization, or nullification). /// /// Returns `None` if the ciphertext is invalid or encrypted for a different /// round than the given seed. pub fn decrypt(seed: &Seed, ciphertext: &tle::Ciphertext) -> Option { tle::decrypt(&seed.signature, ciphertext) } impl Epochable for Seed { fn epoch(&self) -> Epoch { self.round.epoch() } } impl Viewable for Seed { fn view(&self) -> View { self.round.view() } } impl Write for Seed { fn write(&self, writer: &mut impl BufMut) { self.round.write(writer); self.signature.write(writer); } } impl Read for Seed { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let round = Round::read(reader)?; let signature = V::Signature::read(reader)?; Ok(Self { round, signature }) } } impl EncodeSize for Seed { fn encode_size(&self) -> usize { self.round.encode_size() + self.signature.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Seed where V::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self { round: u.arbitrary()?, signature: u.arbitrary()?, }) } } /// Seedable is a trait that provides access to the seed associated with a message. pub trait Seedable { /// Returns the seed associated with this object. fn seed(&self) -> Seed; } impl Seedable for Notarization, D> { fn seed(&self) -> Seed { Seed::new(self.proposal.round, self.certificate.seed_signature) } } impl Seedable for Finalization, D> { fn seed(&self) -> Seed { Seed::new(self.proposal.round, self.certificate.seed_signature) } } /// Extracts the seed message bytes from a Subject. /// /// The seed message is the round encoded as bytes, used for per-view randomness. fn seed_message_from_subject(subject: &Subject<'_, D>) -> bytes::Bytes { match subject { Subject::Notarize { proposal } | Subject::Finalize { proposal } => proposal.round.encode(), Subject::Nullify { round } => round.encode(), } } impl certificate::Scheme for Scheme { type Subject<'a, D: Digest> = Subject<'a, D>; type PublicKey = P; type Signature = Signature; type Certificate = Signature; fn me(&self) -> Option { match &self.role { Role::Signer { share, .. } => Some(share.index), _ => None, } } fn participants(&self) -> &Set { self.participants() } fn sign(&self, subject: Subject<'_, D>) -> Option> { let share = self.share()?; let namespace = self.namespace(); let vote_namespace = subject.namespace(namespace); let vote_message = subject.message(); let vote_signature = threshold::sign_message::(share, vote_namespace, &vote_message).value; let seed_message = seed_message_from_subject(&subject); let seed_signature = threshold::sign_message::(share, &namespace.seed, &seed_message).value; let signature = Signature { vote_signature, seed_signature, }; Some(Attestation { signer: share.index, signature, }) } fn verify_attestation( &self, rng: &mut R, subject: Subject<'_, D>, attestation: &Attestation, strategy: &impl Strategy, ) -> bool where R: CryptoRngCore, D: Digest, { let Ok(evaluated) = self.polynomial().partial_public(attestation.signer) else { return false; }; let namespace = self.namespace(); let vote_namespace = subject.namespace(namespace); let vote_message = subject.message(); let seed_message = seed_message_from_subject(&subject); let entries = &[ ( vote_namespace, vote_message.as_ref(), attestation.signature.vote_signature, ), ( &namespace.seed, seed_message.as_ref(), attestation.signature.seed_signature, ), ]; batch::verify_same_signer::<_, V, _>(rng, &evaluated, entries, strategy).is_ok() } fn verify_attestations( &self, rng: &mut R, subject: Subject<'_, D>, attestations: I, strategy: &impl Strategy, ) -> Verification where R: CryptoRngCore, D: Digest, I: IntoIterator>, { let namespace = self.namespace(); let (vote_partials, seed_partials): (Vec<_>, Vec<_>) = attestations .into_iter() .map(|attestation| { ( PartialSignature:: { index: attestation.signer, value: attestation.signature.vote_signature, }, PartialSignature:: { index: attestation.signer, value: attestation.signature.seed_signature, }, ) }) .unzip(); let polynomial = self.polynomial(); let vote_namespace = subject.namespace(namespace); let vote_message = subject.message(); let seed_message = seed_message_from_subject(&subject); // Generate independent RNG seeds for concurrent verification let mut vote_rng_seed = [0u8; 32]; let mut seed_rng_seed = [0u8; 32]; rng.fill_bytes(&mut vote_rng_seed); rng.fill_bytes(&mut seed_rng_seed); // Verify vote and seed signatures concurrently. let (vote_invalid, seed_invalid) = strategy.join( || { let mut vote_rng = StdRng::from_seed(vote_rng_seed); match threshold::batch_verify_same_message::<_, V, _>( &mut vote_rng, polynomial, vote_namespace, &vote_message, vote_partials.iter(), strategy, ) { Ok(()) => BTreeSet::new(), Err(errs) => errs.into_iter().map(|p| p.index).collect(), } }, || { let mut seed_rng = StdRng::from_seed(seed_rng_seed); match threshold::batch_verify_same_message::<_, V, _>( &mut seed_rng, polynomial, &namespace.seed, &seed_message, seed_partials.iter(), strategy, ) { Ok(()) => BTreeSet::new(), Err(errs) => errs.into_iter().map(|p| p.index).collect(), } }, ); // Merge invalid sets let invalid: BTreeSet<_> = vote_invalid.union(&seed_invalid).copied().collect(); let verified = vote_partials .into_iter() .zip(seed_partials) .map(|(vote, seed)| Attestation { signer: vote.index, signature: Signature { vote_signature: vote.value, seed_signature: seed.value, }, }) .filter(|attestation| !invalid.contains(&attestation.signer)) .collect(); Verification::new(verified, invalid.into_iter().collect()) } fn assemble(&self, attestations: I, strategy: &impl Strategy) -> Option where I: IntoIterator>, M: Faults, { let (vote_partials, seed_partials): (Vec<_>, Vec<_>) = attestations .into_iter() .map(|attestation| { ( PartialSignature:: { index: attestation.signer, value: attestation.signature.vote_signature, }, PartialSignature:: { index: attestation.signer, value: attestation.signature.seed_signature, }, ) }) .unzip(); let quorum = self.polynomial(); if vote_partials.len() < quorum.required::() as usize { return None; } let (vote_signature, seed_signature) = threshold::recover_pair::( quorum, vote_partials.iter(), seed_partials.iter(), strategy, ) .ok()?; Some(Signature { vote_signature, seed_signature, }) } fn verify_certificate( &self, rng: &mut R, subject: Subject<'_, D>, certificate: &Self::Certificate, strategy: &impl Strategy, ) -> bool where R: CryptoRngCore, D: Digest, M: Faults, { let identity = self.identity(); let namespace = self.namespace(); let vote_namespace = subject.namespace(namespace); let vote_message = subject.message(); let seed_message = seed_message_from_subject(&subject); let entries = &[ ( vote_namespace, vote_message.as_ref(), certificate.vote_signature, ), ( &namespace.seed, seed_message.as_ref(), certificate.seed_signature, ), ]; batch::verify_same_signer::<_, V, _>(rng, identity, entries, strategy).is_ok() } fn verify_certificates<'a, R, D, I, M>( &self, rng: &mut R, certificates: I, strategy: &impl Strategy, ) -> bool where R: CryptoRngCore, D: Digest, I: Iterator, &'a Self::Certificate)>, M: Faults, { let identity = self.identity(); let namespace = self.namespace(); let mut seeds = HashMap::new(); let mut entries: Vec<_> = Vec::new(); for (context, certificate) in certificates { // Prepare vote message with context-specific namespace let vote_namespace = context.namespace(namespace); let vote_message = context.message(); entries.push((vote_namespace, vote_message, certificate.vote_signature)); // Seed signatures are per-view, so multiple certificates for the same view // (e.g., notarization and finalization) share the same seed. We only include // each unique seed once in the aggregate, but verify all certificates for a // view have matching seeds. if let Some(previous) = seeds.get(&context.view()) { if *previous != certificate.seed_signature { return false; } } else { let seed_message = seed_message_from_subject(&context); entries.push((&namespace.seed, seed_message, certificate.seed_signature)); seeds.insert(context.view(), certificate.seed_signature); } } // We care about the correctness of each signature, so we use batch verification rather // than computing the aggregate signature and verifying it. let entries_refs: Vec<_> = entries .iter() .map(|(ns, msg, sig)| (*ns, msg.as_ref(), *sig)) .collect(); batch::verify_same_signer::<_, V, _>(rng, identity, &entries_refs, strategy).is_ok() } fn is_attributable() -> bool { false } fn is_batchable() -> bool { true } fn certificate_codec_config(&self) -> ::Cfg {} fn certificate_codec_config_unbounded() -> ::Cfg {} } #[cfg(test)] mod tests { use super::*; use crate::{ simplex::{ scheme::{bls12381_threshold, notarize_namespace, seed_namespace}, types::{Finalization, Finalize, Notarization, Notarize, Proposal, Subject}, }, types::{Round, View}, }; use commonware_codec::{Decode, Encode}; use commonware_cryptography::{ bls12381::{ dkg::{self, deal_anonymous}, primitives::{ group::Scalar, ops::threshold, variant::{MinPk, MinSig, Variant}, }, }, certificate::{mocks::Fixture, Scheme as _}, ed25519, ed25519::certificate::mocks::participants as ed25519_participants, sha256::Digest as Sha256Digest, Hasher, Sha256, }; use commonware_math::algebra::{CryptoGroup, Random}; use commonware_parallel::Sequential; use commonware_utils::{test_rng, Faults, N3f1, NZU32}; use rand::{rngs::StdRng, SeedableRng}; const NAMESPACE: &[u8] = b"bls-threshold-signing-scheme"; type Scheme = super::Scheme; type Signature = super::Signature; fn setup_signers(n: u32, seed: u64) -> (Vec>, Scheme) { let mut rng = StdRng::seed_from_u64(seed); let Fixture { schemes, verifier, .. } = bls12381_threshold::fixture::(&mut rng, NAMESPACE, n); (schemes, verifier) } fn sample_proposal(epoch: Epoch, view: View, tag: u8) -> Proposal { Proposal::new( Round::new(epoch, view), view.previous().unwrap(), Sha256::hash(&[tag]), ) } fn signer_shares_must_match_participant_indices() { let mut rng = test_rng(); let participants = ed25519_participants(&mut rng, 4); let (polynomial, mut shares) = dkg::deal_anonymous::(&mut rng, Default::default(), NZU32!(4)); shares[0].index = Participant::new(999); Scheme::::signer( NAMESPACE, participants.keys().clone(), polynomial, shares[0].clone(), ); } #[test] #[should_panic(expected = "share index must match participant indices")] fn test_signer_shares_must_match_participant_indices_min_pk() { signer_shares_must_match_participant_indices::(); } #[test] #[should_panic(expected = "share index must match participant indices")] fn test_signer_shares_must_match_participant_indices_min_sig() { signer_shares_must_match_participant_indices::(); } fn scheme_polynomial_threshold_must_equal_quorum() { let mut rng = test_rng(); let participants = ed25519_participants(&mut rng, 5); let (polynomial, shares) = deal_anonymous::(&mut rng, Default::default(), NZU32!(4)); Scheme::::signer( NAMESPACE, participants.keys().clone(), polynomial, shares[0].clone(), ); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_scheme_polynomial_threshold_must_equal_quorum_min_pk() { scheme_polynomial_threshold_must_equal_quorum::(); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_scheme_polynomial_threshold_must_equal_quorum_min_sig() { scheme_polynomial_threshold_must_equal_quorum::(); } fn verifier_polynomial_threshold_must_equal_quorum() { let mut rng = test_rng(); let participants = ed25519_participants(&mut rng, 5); let (polynomial, _) = deal_anonymous::(&mut rng, Default::default(), NZU32!(4)); Scheme::::verifier(NAMESPACE, participants.keys().clone(), polynomial); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_verifier_polynomial_threshold_must_equal_quorum_min_pk() { verifier_polynomial_threshold_must_equal_quorum::(); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_verifier_polynomial_threshold_must_equal_quorum_min_sig() { verifier_polynomial_threshold_must_equal_quorum::(); } #[test] fn test_is_not_attributable() { assert!(!Scheme::::is_attributable()); assert!(!Scheme::::is_attributable()); } #[test] fn test_is_batchable() { assert!(Scheme::::is_batchable()); assert!(Scheme::::is_batchable()); } fn sign_vote_roundtrip_for_each_context() { let (schemes, _) = setup_signers::(4, 7); let scheme = &schemes[0]; let mut rng = test_rng(); let proposal = sample_proposal(Epoch::new(0), View::new(2), 1); let notarize_vote = scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap(); assert!(scheme.verify_attestation::<_, Sha256Digest>( &mut rng, Subject::Notarize { proposal: &proposal, }, ¬arize_vote, &Sequential, )); let nullify_vote = scheme .sign::(Subject::Nullify { round: proposal.round, }) .unwrap(); assert!(scheme.verify_attestation::<_, Sha256Digest>( &mut rng, Subject::Nullify { round: proposal.round, }, &nullify_vote, &Sequential, )); let finalize_vote = scheme .sign(Subject::Finalize { proposal: &proposal, }) .unwrap(); assert!(scheme.verify_attestation::<_, Sha256Digest>( &mut rng, Subject::Finalize { proposal: &proposal, }, &finalize_vote, &Sequential, )); } #[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 (_, verifier) = setup_signers::(4, 11); let proposal = sample_proposal(Epoch::new(0), View::new(3), 2); assert!( verifier .sign(Subject::Notarize { proposal: &proposal, }) .is_none(), "verifier should not produce signatures" ); } #[test] fn test_verifier_cannot_sign() { verifier_cannot_sign::(); verifier_cannot_sign::(); } fn verifier_accepts_votes() { let (schemes, verifier) = setup_signers::(4, 11); let proposal = sample_proposal(Epoch::new(0), View::new(3), 2); let vote = schemes[1] .sign(Subject::Notarize { proposal: &proposal, }) .unwrap(); assert!(verifier.verify_attestation::<_, Sha256Digest>( &mut test_rng(), Subject::Notarize { proposal: &proposal, }, &vote, &Sequential, )); } #[test] fn test_verifier_accepts_votes() { verifier_accepts_votes::(); verifier_accepts_votes::(); } fn verify_votes_filters_bad_signers() { let mut rng = test_rng(); let (schemes, _) = setup_signers::(5, 13); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(5), 3); let mut votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap() }) .collect(); let verification = schemes[0].verify_attestations( &mut rng, Subject::Notarize { proposal: &proposal, }, votes.clone(), &Sequential, ); assert!(verification.invalid.is_empty()); assert_eq!(verification.verified.len(), quorum); votes[0].signer = Participant::new(999); let verification = schemes[0].verify_attestations( &mut rng, Subject::Notarize { proposal: &proposal, }, votes, &Sequential, ); assert_eq!(verification.invalid, vec![Participant::new(999)]); 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_requires_quorum() { let (schemes, _) = setup_signers::(4, 17); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(7), 4); let votes: Vec<_> = schemes .iter() .take(quorum - 1) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap() }) .collect(); assert!(schemes[0].assemble::<_, N3f1>(votes, &Sequential).is_none()); } #[test] fn test_assemble_certificate_requires_quorum() { assemble_certificate_requires_quorum::(); assemble_certificate_requires_quorum::(); } fn verify_certificate() { let (schemes, verifier) = setup_signers::(4, 19); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(9), 5); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Finalize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); assert!(verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut test_rng(), Subject::Finalize { proposal: &proposal, }, &certificate, &Sequential, )); } #[test] fn test_verify_certificate() { verify_certificate::(); verify_certificate::(); } fn verify_certificate_detects_corruption() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(4, 23); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(11), 6); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); assert!(verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut rng, Subject::Notarize { proposal: &proposal, }, &certificate, &Sequential, )); let mut corrupted = certificate; corrupted.vote_signature = corrupted.seed_signature; assert!(!verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut rng, Subject::Notarize { proposal: &proposal, }, &corrupted, &Sequential, )); } #[test] fn test_verify_certificate_detects_corruption() { verify_certificate_detects_corruption::(); verify_certificate_detects_corruption::(); } fn certificate_codec_roundtrip() { let (schemes, _) = setup_signers::(5, 29); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(13), 7); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); let encoded = certificate.encode(); let decoded = Signature::::decode_cfg(encoded, &()).expect("decode certificate"); assert_eq!(decoded, certificate); } #[test] fn test_certificate_codec_roundtrip() { certificate_codec_roundtrip::(); certificate_codec_roundtrip::(); } fn seed_codec_roundtrip() { let (schemes, _) = setup_signers::(4, 5); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(1), 0); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Finalize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); let seed = Seed::new(proposal.round, certificate.seed_signature); let encoded = seed.encode(); let decoded = Seed::::decode_cfg(encoded, &()).expect("decode seed"); assert_eq!(decoded, seed); } #[test] fn test_seed_codec_roundtrip() { seed_codec_roundtrip::(); seed_codec_roundtrip::(); } fn seed_verify() { let (schemes, _) = setup_signers::(4, 5); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(1), 0); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Finalize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); let seed = Seed::new(proposal.round, certificate.seed_signature); assert!(seed.verify(&schemes[0])); // Create an invalid seed with a mismatched round let invalid_seed = Seed::new( Round::new(proposal.epoch(), proposal.view().next()), certificate.seed_signature, ); assert!(!invalid_seed.verify(&schemes[0])); } #[test] fn test_seed_verify() { seed_verify::(); seed_verify::(); } fn seedable() { let (schemes, _) = setup_signers::(4, 5); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(1), 0); let notarizes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| Notarize::sign(scheme, proposal.clone()).unwrap()) .collect(); let notarization = Notarization::from_notarizes(&schemes[0], ¬arizes, &Sequential).unwrap(); let finalizes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| Finalize::sign(scheme, proposal.clone()).unwrap()) .collect(); let finalization = Finalization::from_finalizes(&schemes[0], &finalizes, &Sequential).unwrap(); assert_eq!(notarization.seed(), finalization.seed()); assert!(notarization.seed().verify(&schemes[0])); } #[test] fn test_seedable() { seedable::(); seedable::(); } fn scheme_clone_and_verifier() { let (schemes, verifier) = setup_signers::(4, 31); let signer = schemes[0].clone(); let proposal = sample_proposal(Epoch::new(0), View::new(21), 9); assert!( signer .sign(Subject::Notarize { proposal: &proposal, }) .is_some(), "signer should produce votes" ); assert!( verifier .sign(Subject::Notarize { proposal: &proposal, }) .is_none(), "verifier should not produce votes" ); } #[test] fn test_scheme_clone_and_verifier() { scheme_clone_and_verifier::(); scheme_clone_and_verifier::(); } fn certificate_verifier_accepts_certificates() { let (schemes, _) = setup_signers::(4, 37); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(15), 8); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Finalize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); let certificate_verifier = Scheme::::certificate_verifier(NAMESPACE, *schemes[0].identity()); assert!( certificate_verifier .sign(Subject::Finalize { proposal: &proposal, }) .is_none(), "certificate verifier should not produce votes" ); assert!( certificate_verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut test_rng(), Subject::Finalize { proposal: &proposal, }, &certificate, &Sequential, ) ); } #[test] fn test_certificate_verifier_accepts_certificates() { certificate_verifier_accepts_certificates::(); certificate_verifier_accepts_certificates::(); } fn certificate_verifier_panics_on_vote() { let (schemes, _) = setup_signers::(4, 37); let certificate_verifier = Scheme::::certificate_verifier(NAMESPACE, *schemes[0].identity()); let proposal = sample_proposal(Epoch::new(0), View::new(15), 8); let vote = schemes[1] .sign(Subject::Finalize { proposal: &proposal, }) .unwrap(); certificate_verifier.verify_attestation::<_, Sha256Digest>( &mut test_rng(), Subject::Finalize { proposal: &proposal, }, &vote, &Sequential, ); } #[test] #[should_panic(expected = "can only be called for signer and verifier")] fn test_certificate_verifier_panics_on_vote_min_pk() { certificate_verifier_panics_on_vote::(); } #[test] #[should_panic(expected = "can only be called for signer and verifier")] fn test_certificate_verifier_panics_on_vote_min_sig() { certificate_verifier_panics_on_vote::(); } fn verify_certificate_returns_seed_randomness() { let (schemes, _) = setup_signers::(4, 43); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(19), 10); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); let seed = Seed::::new(proposal.round, certificate.seed_signature); assert_eq!(seed.signature, certificate.seed_signature); } #[test] fn test_verify_certificate_returns_seed_randomness() { verify_certificate_returns_seed_randomness::(); verify_certificate_returns_seed_randomness::(); } fn certificate_decode_rejects_length_mismatch() { let (schemes, _) = setup_signers::(4, 47); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(21), 11); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign::(Subject::Nullify { round: proposal.round, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); let mut encoded = certificate.encode(); let truncated = encoded.split_to(encoded.len() - 1); assert!(Signature::::decode_cfg(truncated, &()).is_err()); } #[test] fn test_certificate_decode_rejects_length_mismatch() { certificate_decode_rejects_length_mismatch::(); certificate_decode_rejects_length_mismatch::(); } fn sign_vote_partial_matches_share() { let (schemes, _) = setup_signers::(4, 53); let scheme = &schemes[0]; let share = scheme.share().expect("has share"); let proposal = sample_proposal(Epoch::new(0), View::new(23), 12); let vote = scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap(); let notarize_namespace = notarize_namespace(NAMESPACE); let notarize_message = proposal.encode(); let expected_message = threshold::sign_message::( share, notarize_namespace.as_ref(), notarize_message.as_ref(), ) .value; let seed_namespace = seed_namespace(NAMESPACE); let seed_message = proposal.round.encode(); let expected_seed = threshold::sign_message::(share, seed_namespace.as_ref(), seed_message.as_ref()) .value; assert_eq!(vote.signer, share.index); assert_eq!(vote.signature.vote_signature, expected_message); assert_eq!(vote.signature.seed_signature, expected_seed); } #[test] fn test_sign_vote_partial_matches_share() { sign_vote_partial_matches_share::(); sign_vote_partial_matches_share::(); } fn verify_certificate_detects_seed_corruption() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(4, 59); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(25), 13); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign::(Subject::Nullify { round: proposal.round, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); assert!(verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut rng, Subject::Nullify { round: proposal.round, }, &certificate, &Sequential, )); let mut corrupted = certificate; corrupted.seed_signature = corrupted.vote_signature; assert!(!verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut rng, Subject::Nullify { round: proposal.round, }, &corrupted, &Sequential, )); } #[test] fn test_verify_certificate_detects_seed_corruption() { verify_certificate_detects_seed_corruption::(); verify_certificate_detects_seed_corruption::(); } fn encrypt_decrypt() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(4, 61); let quorum = N3f1::quorum_from_slice(&schemes) as usize; // Prepare a message to encrypt let message = b"Secret message for future view10"; // Target round for encryption let target = Round::new(Epoch::new(333), View::new(10)); // Encrypt using the scheme let ciphertext = schemes[0].encrypt(&mut rng, target, *message); // Can also encrypt with the verifier scheme let ciphertext_verifier = verifier.encrypt(&mut rng, target, *message); // Generate notarization for the target round to get the seed let proposal = sample_proposal(target.epoch(), target.view(), 14); let notarizes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| Notarize::sign(scheme, proposal.clone()).unwrap()) .collect(); let notarization = Notarization::from_notarizes(&schemes[0], ¬arizes, &Sequential).unwrap(); // Decrypt using the seed let seed = notarization.seed(); let decrypted = seed.decrypt(&ciphertext).unwrap(); assert_eq!(message, decrypted.as_ref()); let decrypted_verifier = seed.decrypt(&ciphertext_verifier).unwrap(); assert_eq!(message, decrypted_verifier.as_ref()); } #[test] fn test_encrypt_decrypt() { encrypt_decrypt::(); encrypt_decrypt::(); } fn verify_attestation_rejects_malleability() { let mut rng = test_rng(); let (schemes, _) = setup_signers::(4, 67); let proposal = sample_proposal(Epoch::new(0), View::new(27), 14); let attestation = schemes[0] .sign(Subject::Notarize { proposal: &proposal, }) .unwrap(); assert!(schemes[0].verify_attestation::<_, Sha256Digest>( &mut rng, Subject::Notarize { proposal: &proposal, }, &attestation, &Sequential, )); let random_scalar = Scalar::random(&mut rng); let delta = V::Signature::generator() * &random_scalar; let forged_attestation: Attestation> = Attestation { signer: attestation.signer, signature: Signature { vote_signature: attestation.signature.vote_signature - &delta, seed_signature: attestation.signature.seed_signature + &delta, }, }; let forged_sum = forged_attestation.signature.vote_signature + &forged_attestation.signature.seed_signature; let valid_sum = attestation.signature.vote_signature + &attestation.signature.seed_signature; assert_eq!(forged_sum, valid_sum, "signature sums should be equal"); assert!( !schemes[0].verify_attestation::<_, Sha256Digest>( &mut rng, Subject::Notarize { proposal: &proposal, }, &forged_attestation, &Sequential, ), "forged attestation should be rejected" ); } #[test] fn test_verify_attestation_rejects_malleability() { verify_attestation_rejects_malleability::(); verify_attestation_rejects_malleability::(); } fn verify_attestations_rejects_malleability() { let mut rng = test_rng(); let (schemes, _) = setup_signers::(4, 71); let proposal = sample_proposal(Epoch::new(0), View::new(29), 15); let attestation1 = schemes[0] .sign(Subject::Notarize { proposal: &proposal, }) .unwrap(); let attestation2 = schemes[1] .sign(Subject::Notarize { proposal: &proposal, }) .unwrap(); let verification = schemes[0].verify_attestations( &mut rng, Subject::Notarize { proposal: &proposal, }, vec![attestation1.clone(), attestation2.clone()], &Sequential, ); assert!(verification.invalid.is_empty()); assert_eq!(verification.verified.len(), 2); let random_scalar = Scalar::random(&mut rng); let delta = V::Signature::generator() * &random_scalar; let forged_attestation1: Attestation> = Attestation { signer: attestation1.signer, signature: Signature { vote_signature: attestation1.signature.vote_signature - &delta, seed_signature: attestation1.signature.seed_signature, }, }; let forged_attestation2: Attestation> = Attestation { signer: attestation2.signer, signature: Signature { vote_signature: attestation2.signature.vote_signature + &delta, seed_signature: attestation2.signature.seed_signature, }, }; let forged_vote_sum = forged_attestation1.signature.vote_signature + &forged_attestation2.signature.vote_signature; let valid_vote_sum = attestation1.signature.vote_signature + &attestation2.signature.vote_signature; assert_eq!( forged_vote_sum, valid_vote_sum, "vote signature sums should be equal" ); let verification = schemes[0].verify_attestations( &mut rng, Subject::Notarize { proposal: &proposal, }, vec![forged_attestation1, forged_attestation2], &Sequential, ); assert!( !verification.invalid.is_empty(), "forged attestations should be detected" ); } #[test] fn test_verify_attestations_rejects_malleability() { verify_attestations_rejects_malleability::(); verify_attestations_rejects_malleability::(); } fn verify_certificate_rejects_malleability() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(4, 73); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal = sample_proposal(Epoch::new(0), View::new(31), 16); let votes: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal, }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble::<_, N3f1>(votes, &Sequential) .expect("assemble certificate"); assert!(verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut rng, Subject::Notarize { proposal: &proposal, }, &certificate, &Sequential, )); let random_scalar = Scalar::random(&mut rng); let delta = V::Signature::generator() * &random_scalar; let forged_certificate = Signature { vote_signature: certificate.vote_signature - &delta, seed_signature: certificate.seed_signature + &delta, }; let forged_sum = forged_certificate.vote_signature + &forged_certificate.seed_signature; let valid_sum = certificate.vote_signature + &certificate.seed_signature; assert_eq!(forged_sum, valid_sum, "signature sums should be equal"); assert!( !verifier.verify_certificate::<_, Sha256Digest, N3f1>( &mut rng, Subject::Notarize { proposal: &proposal, }, &forged_certificate, &Sequential, ), "forged certificate should be rejected" ); } #[test] fn test_verify_certificate_rejects_malleability() { verify_certificate_rejects_malleability::(); verify_certificate_rejects_malleability::(); } fn verify_certificates_rejects_malleability() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(4, 79); let quorum = N3f1::quorum_from_slice(&schemes) as usize; let proposal1 = sample_proposal(Epoch::new(0), View::new(33), 17); let proposal2 = sample_proposal(Epoch::new(0), View::new(34), 18); let votes1: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal1, }) .unwrap() }) .collect(); let votes2: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign(Subject::Notarize { proposal: &proposal2, }) .unwrap() }) .collect(); let certificate1 = schemes[0] .assemble::<_, N3f1>(votes1, &Sequential) .expect("assemble certificate1"); let certificate2 = schemes[0] .assemble::<_, N3f1>(votes2, &Sequential) .expect("assemble certificate2"); assert!(verifier.verify_certificates::<_, Sha256Digest, _, N3f1>( &mut rng, [ ( Subject::Notarize { proposal: &proposal1, }, &certificate1 ), ( Subject::Notarize { proposal: &proposal2, }, &certificate2 ), ] .into_iter(), &Sequential, )); let random_scalar = Scalar::random(&mut rng); let delta = V::Signature::generator() * &random_scalar; let forged_certificate1 = Signature { vote_signature: certificate1.vote_signature - &delta, seed_signature: certificate1.seed_signature, }; let forged_certificate2 = Signature { vote_signature: certificate2.vote_signature + &delta, seed_signature: certificate2.seed_signature, }; let forged_vote_sum = forged_certificate1.vote_signature + &forged_certificate2.vote_signature; let valid_vote_sum = certificate1.vote_signature + &certificate2.vote_signature; assert_eq!( forged_vote_sum, valid_vote_sum, "vote signature sums should be equal" ); assert!( !verifier.verify_certificates::<_, Sha256Digest, _, N3f1>( &mut rng, [ ( Subject::Notarize { proposal: &proposal1, }, &forged_certificate1 ), ( Subject::Notarize { proposal: &proposal2, }, &forged_certificate2 ), ] .into_iter(), &Sequential, ), "forged certificates should be rejected" ); } #[test] fn test_verify_certificates_rejects_malleability() { verify_certificates_rejects_malleability::(); verify_certificates_rejects_malleability::(); } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use commonware_codec::conformance::CodecConformance; commonware_conformance::conformance_tests! { CodecConformance>, CodecConformance>, } } }