commonware_consensus/simplex/signing_scheme/mod.rs
1//! Signing scheme implementations for `simplex`.
2//!
3//! # Attributable Schemes and Liveness/Fault Evidence
4//!
5//! Signing schemes differ in whether per-validator activities can be used as evidence of either
6//! liveness or of committing a fault:
7//!
8//! - **Attributable Schemes** ([`ed25519`], [`bls12381_multisig`]): Individual signatures can be presented
9//! to some third party as evidence of either liveness or of committing a fault. Certificates contain signer
10//! indices alongside individual signatures, enabling secure per-validator activity tracking and
11//! conflict detection.
12//!
13//! - **Non-Attributable schemes** ([`bls12381_threshold`]): Individual signatures cannot be presented
14//! to some third party as evidence of either liveness or of committing a fault because they can be forged
15//! by other players (often after some quorum of partial signatures are collected). With [`bls12381_threshold`],
16//! possession of any `t` valid partial signatures can be used to forge a partial signature for any other player.
17//! Because peer connections are authenticated, evidence can be used locally (as it must be sent by said participant)
18//! but can't be used by an external observer.
19//!
20//! The [`Scheme::is_attributable()`] method signals whether evidence can be safely
21//! exposed. For applications only interested in collecting evidence for liveness/faults, use [`reporter::AttributableReporter`]
22//! which automatically handles filtering and verification based on scheme (hiding votes/proofs that are not attributable). If
23//! full observability is desired, process all messages passed through the [`crate::Reporter`] interface.
24
25pub mod bls12381_multisig;
26pub mod bls12381_threshold;
27pub mod ed25519;
28pub mod utils;
29
30cfg_if::cfg_if! {
31 if #[cfg(not(target_arch = "wasm32"))] {
32 pub mod reporter;
33 }
34}
35
36use crate::{
37 simplex::types::{Vote, VoteContext, VoteVerification},
38 types::Round,
39};
40use commonware_codec::{Codec, CodecFixed, Encode, Read};
41use commonware_cryptography::{Digest, PublicKey};
42use commonware_utils::{set::Ordered, union};
43use rand::{CryptoRng, Rng};
44use std::{collections::BTreeSet, fmt::Debug, hash::Hash};
45
46/// Cryptographic surface required by `simplex`.
47///
48/// A `Scheme` produces validator votes, validates them (individually or in batches), assembles
49/// quorum certificates, checks recovered certificates and, when available, derives a randomness
50/// seed for leader rotation. Implementations may override the provided defaults to take advantage
51/// of scheme-specific batching strategies.
52///
53/// # Identity Keys vs Consensus Keys
54///
55/// A participant may supply both an identity key and a consensus key. The identity key
56/// is used for assigning a unique order to the committee and authenticating connections whereas the consensus key
57/// is used for actually signing and verifying votes/certificates.
58///
59/// This flexibility is supported because some cryptographic schemes are only performant when used in batch verification
60/// (like [bls12381_multisig]) and/or are refreshed frequently (like [bls12381_threshold]). Refer to [ed25519]
61/// for an example of a scheme that uses the same key for both purposes.
62pub trait Scheme: Clone + Debug + Send + Sync + 'static {
63 /// Public key type for participant identity used to order and index the committee.
64 type PublicKey: PublicKey;
65 /// Vote signature emitted by individual validators.
66 type Signature: Clone + Debug + PartialEq + Eq + Hash + Send + Sync + CodecFixed<Cfg = ()>;
67 /// Quorum certificate recovered from a set of votes.
68 type Certificate: Clone + Debug + PartialEq + Eq + Hash + Send + Sync + Codec;
69 /// Randomness seed derived from a certificate, if the scheme supports it.
70 type Seed: Clone + Encode + Send;
71
72 /// Returns the index of "self" in the participant set, if available.
73 /// Returns `None` if the scheme is a verifier-only instance.
74 fn me(&self) -> Option<u32>;
75
76 /// Returns the ordered set of participant public identity keys managed by the scheme.
77 fn participants(&self) -> &Ordered<Self::PublicKey>;
78
79 /// Signs a vote for the given context using the supplied namespace for domain separation.
80 /// Returns `None` if the scheme cannot sign (e.g. it's a verifier-only instance).
81 fn sign_vote<D: Digest>(
82 &self,
83 namespace: &[u8],
84 context: VoteContext<'_, D>,
85 ) -> Option<Vote<Self>>;
86
87 /// Verifies a single vote against the participant material managed by the scheme.
88 fn verify_vote<D: Digest>(
89 &self,
90 namespace: &[u8],
91 context: VoteContext<'_, D>,
92 vote: &Vote<Self>,
93 ) -> bool;
94
95 /// Batch-verifies votes and separates valid messages from the voter indices that failed
96 /// verification.
97 ///
98 /// Callers must not include duplicate votes from the same signer.
99 fn verify_votes<R, D, I>(
100 &self,
101 _rng: &mut R,
102 namespace: &[u8],
103 context: VoteContext<'_, D>,
104 votes: I,
105 ) -> VoteVerification<Self>
106 where
107 R: Rng + CryptoRng,
108 D: Digest,
109 I: IntoIterator<Item = Vote<Self>>,
110 {
111 let mut invalid = BTreeSet::new();
112
113 let verified = votes.into_iter().filter_map(|vote| {
114 if self.verify_vote(namespace, context, &vote) {
115 Some(vote)
116 } else {
117 invalid.insert(vote.signer);
118 None
119 }
120 });
121
122 VoteVerification::new(verified.collect(), invalid.into_iter().collect())
123 }
124
125 /// Aggregates a quorum of votes into a certificate, returning `None` if the quorum is not met.
126 ///
127 /// Callers must not include duplicate votes from the same signer.
128 fn assemble_certificate<I>(&self, votes: I) -> Option<Self::Certificate>
129 where
130 I: IntoIterator<Item = Vote<Self>>;
131
132 /// Verifies a certificate that was recovered or received from the network.
133 fn verify_certificate<R: Rng + CryptoRng, D: Digest>(
134 &self,
135 rng: &mut R,
136 namespace: &[u8],
137 context: VoteContext<'_, D>,
138 certificate: &Self::Certificate,
139 ) -> bool;
140
141 /// Verifies a stream of certificates, returning `false` at the first failure.
142 fn verify_certificates<'a, R, D, I>(
143 &self,
144 rng: &mut R,
145 namespace: &[u8],
146 certificates: I,
147 ) -> bool
148 where
149 R: Rng + CryptoRng,
150 D: Digest,
151 I: Iterator<Item = (VoteContext<'a, D>, &'a Self::Certificate)>,
152 {
153 for (context, certificate) in certificates {
154 if !self.verify_certificate(rng, namespace, context, certificate) {
155 return false;
156 }
157 }
158
159 true
160 }
161
162 /// Extracts randomness seed, if provided by the scheme, derived from the certificate
163 /// for the given round.
164 fn seed(&self, round: Round, certificate: &Self::Certificate) -> Option<Self::Seed>;
165
166 /// Returns whether per-validator fault evidence can be safely exposed.
167 ///
168 /// Schemes where individual signatures can be safely reported as fault evidence should
169 /// return `true`.
170 ///
171 /// This is used by [`reporter::AttributableReporter`] to safely expose consensus
172 /// activities.
173 fn is_attributable(&self) -> bool;
174
175 /// Encoding configuration for bounded-size certificate decoding used in network payloads.
176 fn certificate_codec_config(&self) -> <Self::Certificate as Read>::Cfg;
177
178 /// Encoding configuration that allows unbounded certificate decoding.
179 ///
180 /// Only use this when decoding data from trusted local storage, it must not be exposed to
181 /// adversarial inputs or network payloads.
182 fn certificate_codec_config_unbounded() -> <Self::Certificate as Read>::Cfg;
183}
184
185// Constants for domain separation in signature verification
186// These are used to prevent cross-protocol attacks and message-type confusion
187const SEED_SUFFIX: &[u8] = b"_SEED";
188const NOTARIZE_SUFFIX: &[u8] = b"_NOTARIZE";
189const NULLIFY_SUFFIX: &[u8] = b"_NULLIFY";
190const FINALIZE_SUFFIX: &[u8] = b"_FINALIZE";
191
192/// Creates a namespace for seed messages by appending the SEED_SUFFIX
193/// The seed is used for leader election and randomness generation
194#[inline]
195pub(crate) fn seed_namespace(namespace: &[u8]) -> Vec<u8> {
196 union(namespace, SEED_SUFFIX)
197}
198
199/// Creates a namespace for notarize messages by appending the NOTARIZE_SUFFIX
200/// Domain separation prevents cross-protocol attacks
201#[inline]
202pub(crate) fn notarize_namespace(namespace: &[u8]) -> Vec<u8> {
203 union(namespace, NOTARIZE_SUFFIX)
204}
205
206/// Creates a namespace for nullify messages by appending the NULLIFY_SUFFIX
207/// Domain separation prevents cross-protocol attacks
208#[inline]
209pub(crate) fn nullify_namespace(namespace: &[u8]) -> Vec<u8> {
210 union(namespace, NULLIFY_SUFFIX)
211}
212
213/// Creates a namespace for finalize messages by appending the FINALIZE_SUFFIX
214/// Domain separation prevents cross-protocol attacks
215#[inline]
216pub(crate) fn finalize_namespace(namespace: &[u8]) -> Vec<u8> {
217 union(namespace, FINALIZE_SUFFIX)
218}
219
220/// Produces the vote namespace and message bytes for a given vote context.
221///
222/// Returns the final namespace (with the context-specific suffix) and the
223/// serialized message to sign or verify.
224#[inline]
225pub(crate) fn vote_namespace_and_message<D: Digest>(
226 namespace: &[u8],
227 context: VoteContext<'_, D>,
228) -> (Vec<u8>, Vec<u8>) {
229 match context {
230 VoteContext::Notarize { proposal } => {
231 (notarize_namespace(namespace), proposal.encode().to_vec())
232 }
233 VoteContext::Nullify { round } => (nullify_namespace(namespace), round.encode().to_vec()),
234 VoteContext::Finalize { proposal } => {
235 (finalize_namespace(namespace), proposal.encode().to_vec())
236 }
237 }
238}
239
240/// Produces the seed namespace and message bytes for a given vote context.
241///
242/// Returns the final namespace (with the seed suffix) and the serialized
243/// message to sign or verify.
244#[inline]
245pub(crate) fn seed_namespace_and_message<D: Digest>(
246 namespace: &[u8],
247 context: VoteContext<'_, D>,
248) -> (Vec<u8>, Vec<u8>) {
249 (
250 seed_namespace(namespace),
251 match context {
252 VoteContext::Notarize { proposal } | VoteContext::Finalize { proposal } => {
253 proposal.round.encode().to_vec()
254 }
255 VoteContext::Nullify { round } => round.encode().to_vec(),
256 },
257 )
258}