use {
agave_votor_messages::{
certificate::{Certificate, CertificateType},
consensus_message::VoteMessage,
fraction::Fraction,
sig_verified_messages::VoteAggregate,
},
bitvec::vec::BitVec,
solana_bls_signatures::{
BlsError, Signature as BLSSignature, SignatureCompressed as BLSSignatureCompressed,
SignatureProjective,
},
solana_signer_store::{EncodeError, encode_base2, encode_base3},
std::num::NonZero,
thiserror::Error,
};
#[derive(Debug, PartialEq, Eq, Error)]
pub enum AggregateAccumulatorError {
#[error("Signature aggregation failed with {0}")]
SignatureAggregationFailed(BlsError),
#[error("encoding failed with {0:?}")]
EncodingFailed(EncodeError),
#[error("duplicate aggregate")]
Duplicate,
}
fn default_bitvec(max_validators: usize) -> BitVec<u8> {
BitVec::repeat(false, max_validators)
}
#[derive(Debug, Clone)]
pub struct AggregateAccumulator {
ranks: BitVec<u8>,
signature: SignatureProjective,
stake: u64,
}
impl AggregateAccumulator {
pub fn new(max_validators: usize) -> Self {
Self {
ranks: default_bitvec(max_validators),
signature: SignatureProjective::identity(),
stake: 0,
}
}
pub fn add_aggregate(
&mut self,
aggregate: &VoteAggregate,
) -> Result<u64, AggregateAccumulatorError> {
self.signature
.aggregate_with(std::iter::once(aggregate.signature()))
.map_err(AggregateAccumulatorError::SignatureAggregationFailed)?;
self.ranks |= aggregate.ranks();
self.stake = self.stake.saturating_add(aggregate.stake().get());
Ok(self.stake)
}
pub fn add_own_vote_message(
&mut self,
msg: &VoteMessage,
) -> Result<u64, AggregateAccumulatorError> {
let mut signature = self.signature;
signature
.aggregate_with(std::iter::once(&msg.signature))
.map_err(AggregateAccumulatorError::SignatureAggregationFailed)?;
if self.ranks.replace(msg.rank as usize, true) {
return Err(AggregateAccumulatorError::Duplicate);
}
self.signature = signature;
self.stake = self.stake.saturating_add(msg.stake.get());
Ok(self.stake)
}
pub fn try_build_base2_cert(
&self,
cert_type: CertificateType,
total_stake: NonZero<u64>,
) -> Result<Option<Certificate>, AggregateAccumulatorError> {
let observed_fraction = Fraction::new(self.stake, total_stake);
if observed_fraction < cert_type.threshold() {
return Ok(None);
}
let mut ranks = self.ranks.clone();
let new_len = ranks.last_one().map_or(0, |i| i.saturating_add(1));
ranks.resize(new_len, false);
let bitmap = encode_base2(&ranks).map_err(AggregateAccumulatorError::EncodingFailed)?;
let signature = BLSSignature::from(self.signature);
Ok(Some(Certificate {
cert_type,
signature,
bitmap,
}))
}
pub fn try_build_base3_cert(
cert_type: CertificateType,
total_stake: NonZero<u64>,
primary: Option<&AggregateAccumulator>,
fallback: &AggregateAccumulator,
) -> Result<Option<Certificate>, AggregateAccumulatorError> {
let primary = primary.filter(|accumulator| !accumulator.is_identity());
if fallback.is_identity() {
return match primary {
Some(primary) => primary.try_build_base2_cert(cert_type, total_stake),
None => Ok(None),
};
}
let observed_fraction = Fraction::new(
fallback
.stake()
.saturating_add(primary.map(|p| p.stake).unwrap_or(0)),
total_stake,
);
if observed_fraction < cert_type.threshold() {
return Ok(None);
}
let (primary_ranks, fallback_ranks, signature) = match primary {
None => {
let mut fallback_ranks = fallback.ranks.clone();
let fallback_len = fallback_ranks.last_one().map_or(0, |i| i.saturating_add(1));
fallback_ranks.resize(fallback_len, false);
let primary = BitVec::repeat(false, fallback_len);
(primary, fallback_ranks, fallback.signature)
}
Some(primary) => {
let mut signature = primary.signature;
let mut primary_ranks = primary.ranks.clone();
let mut fallback_ranks = fallback.ranks.clone();
let last_one_0 = primary_ranks.last_one().map_or(0, |i| i.saturating_add(1));
let last_one_1 = fallback_ranks.last_one().map_or(0, |i| i.saturating_add(1));
let new_length = last_one_0.max(last_one_1);
primary_ranks.resize(new_length, false);
fallback_ranks.resize(new_length, false);
signature
.aggregate_with(std::iter::once(&fallback.signature))
.map_err(AggregateAccumulatorError::SignatureAggregationFailed)?;
(primary_ranks, fallback_ranks, signature)
}
};
let bitmap = encode_base3(&primary_ranks, &fallback_ranks)
.map_err(AggregateAccumulatorError::EncodingFailed)?;
Ok(Some(Certificate {
cert_type,
signature: signature.into(),
bitmap,
}))
}
pub fn into_sig_and_ranks(self) -> Result<(BLSSignatureCompressed, Vec<u8>), EncodeError> {
let mut ranks = self.ranks;
let new_len = ranks.last_one().map_or(0, |i| i.saturating_add(1));
ranks.resize(new_len, false);
let ranks = encode_base2(&ranks)?;
let signature = BLSSignature::from(self.signature).try_into().unwrap();
Ok((signature, ranks))
}
pub fn stake(&self) -> u64 {
self.stake
}
fn is_identity(&self) -> bool {
self.signature == SignatureProjective::identity()
}
}
#[cfg(test)]
mod tests {
use {
super::*,
solana_bls_signatures::Keypair as BlsKeypair,
solana_signer_store::{Decoded, decode},
};
const MAX_VALIDATORS: usize = 4;
fn new_accumulator(
ranks: &[usize],
stake: u64,
signature: SignatureProjective,
) -> AggregateAccumulator {
let mut accumulator = AggregateAccumulator::new(MAX_VALIDATORS);
for &rank in ranks {
accumulator.ranks.set(rank, true);
}
accumulator.signature = signature;
accumulator.stake = stake;
accumulator
}
#[test]
fn test_base3_identity_fallback_uses_primary_base2() {
let cert_type = CertificateType::Skip(1);
let primary_signature = BlsKeypair::new().sign(b"primary");
let primary = new_accumulator(&[0, 1], 60, primary_signature);
let fallback = new_accumulator(&[2, 3], 2, SignatureProjective::identity());
let cert = AggregateAccumulator::try_build_base3_cert(
cert_type,
NonZero::new(100).unwrap(),
Some(&primary),
&fallback,
)
.unwrap()
.unwrap();
assert_eq!(cert.signature, BLSSignature::from(primary_signature));
let Decoded::Base2(ranks) = decode(&cert.bitmap, MAX_VALIDATORS).unwrap() else {
panic!("expected base2 encoding");
};
assert_eq!(ranks.count_ones(), 2);
assert!(ranks[0]);
assert!(ranks[1]);
}
#[test]
fn test_base3_identity_fallback_stake_is_ignored() {
let primary = new_accumulator(&[0, 1], 58, BlsKeypair::new().sign(b"primary"));
let fallback = new_accumulator(&[2, 3], 2, SignatureProjective::identity());
let cert = AggregateAccumulator::try_build_base3_cert(
CertificateType::Skip(1),
NonZero::new(100).unwrap(),
Some(&primary),
&fallback,
)
.unwrap();
assert!(cert.is_none());
}
#[test]
fn test_base3_identity_primary_uses_fallback_only() {
let fallback_signature = BlsKeypair::new().sign(b"fallback");
let primary = new_accumulator(&[0, 1], 2, SignatureProjective::identity());
let fallback = new_accumulator(&[2, 3], 60, fallback_signature);
let cert = AggregateAccumulator::try_build_base3_cert(
CertificateType::Skip(1),
NonZero::new(100).unwrap(),
Some(&primary),
&fallback,
)
.unwrap()
.unwrap();
assert_eq!(cert.signature, BLSSignature::from(fallback_signature));
let Decoded::Base3(primary_ranks, fallback_ranks) =
decode(&cert.bitmap, MAX_VALIDATORS).unwrap()
else {
panic!("expected base3 encoding");
};
assert_eq!(primary_ranks.count_ones(), 0);
assert_eq!(fallback_ranks.count_ones(), 2);
assert!(fallback_ranks[2]);
assert!(fallback_ranks[3]);
}
#[test]
fn test_base3_identity_primary_stake_is_ignored() {
let primary = new_accumulator(&[0, 1], 2, SignatureProjective::identity());
let fallback = new_accumulator(&[2, 3], 58, BlsKeypair::new().sign(b"fallback"));
let cert = AggregateAccumulator::try_build_base3_cert(
CertificateType::Skip(1),
NonZero::new(100).unwrap(),
Some(&primary),
&fallback,
)
.unwrap();
assert!(cert.is_none());
}
}