use std::collections::HashMap;
use blst::min_pk::{PublicKey, Signature};
use blst::BLST_ERROR;
use crate::finality::{
canonical_vote_message, half_stake_floor, nova_signer_floor, two_thirds_count,
two_thirds_stake_floor, Finality, Position, MIN_BFT_COMMITTEE, QC_FINALITY,
QUORUM_CERT_VERSION,
};
use crate::pop::{self, PopError};
pub use crate::pop::NodeId;
pub const DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";
pub const PUBLIC_KEY_LEN: usize = 48;
pub const SIGNATURE_LEN: usize = 96;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Vote {
pub node_id: NodeId,
pub accept: bool,
pub signature: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CertError {
Version { got: u16, want: u16 },
Type { got: u8, want: u8 },
UnknownTier(Finality),
ThresholdZero,
NoVotes,
NotStrictlyIncreasing(usize),
VoteNotAccept(usize),
SigInvalid(usize),
BelowThreshold { have: u32, need: u32 },
UnresolvedSet { n: i64 },
MinCommittee { n: i64, need: i64 },
SignerFloor { have: i64, need: i64, n: i64 },
StakeZero { epoch_height: u64 },
StakeBelowMajority { voted: u64, signer: u64, need_above: u64 },
StakeBelowSupermajority { voted: u64, signer: u64, need_above: u64 },
KeyEncoding,
NoKey,
ZeroWeight,
DuplicateKey,
DuplicateNode,
PopInvalid,
WeightOverflow,
}
impl std::fmt::Display for CertError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CertError::Version { got, want } => write!(f, "cert version: got {got} want {want}"),
CertError::Type { got, want } => write!(f, "cert type: got {got} want {want}"),
CertError::UnknownTier(t) => write!(f, "cert tier: {} is not an accept tier", t.name()),
CertError::ThresholdZero => write!(f, "cert threshold is zero"),
CertError::NoVotes => write!(f, "cert carries no votes"),
CertError::NotStrictlyIncreasing(i) => {
write!(f, "cert votes not strictly increasing at vote {i}")
}
CertError::VoteNotAccept(i) => write!(f, "cert vote {i} is not an accept"),
CertError::SigInvalid(i) => write!(f, "cert vote {i} signature does not verify"),
CertError::BelowThreshold { have, need } => {
write!(f, "cert below threshold: have {have} need {need}")
}
CertError::UnresolvedSet { n } => {
write!(f, "cert over an unresolved validator set (n={n})")
}
CertError::MinCommittee { n, need } => write!(
f,
"quasar over {n} signers, need at least {need} — below the minimum Byzantine \
committee f=(n-1)/3 is 0 and a two-thirds supermajority tolerates no fault"
),
CertError::SignerFloor { have, need, n } => {
write!(f, "cert has {have} distinct voters, need {need} of {n}")
}
CertError::StakeZero { epoch_height } => {
write!(f, "total stake is zero at epoch height {epoch_height}")
}
CertError::StakeBelowMajority { voted, signer, need_above } => {
write!(f, "nova voted={voted} signer={signer}, need > {need_above}")
}
CertError::StakeBelowSupermajority { voted, signer, need_above } => {
write!(f, "quasar voted={voted} signer={signer}, need > {need_above}")
}
CertError::KeyEncoding => write!(f, "public key does not decode to a valid point"),
CertError::NoKey => write!(f, "registration carries no public key"),
CertError::ZeroWeight => write!(f, "validator has zero weight"),
CertError::DuplicateKey => write!(f, "public key is registered to more than one node"),
CertError::DuplicateNode => write!(f, "node is registered more than once"),
CertError::PopInvalid => write!(f, "proof of possession does not verify for this key"),
CertError::WeightOverflow => write!(f, "weight overflowed"),
}
}
}
impl std::error::Error for CertError {}
pub trait VoteVerifier {
fn verify_vote(
&self,
node: &NodeId,
message: &[u8],
signature: &[u8],
epoch_height: u64,
) -> bool;
}
pub trait StakeSource {
fn weight(&self, node: &NodeId, epoch_height: u64) -> u64;
fn signer_stake(&self, epoch_height: u64) -> u64;
fn signer_count(&self, epoch_height: u64) -> i64;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QuorumCert {
pub version: u16,
pub qc_type: u8,
pub tier: Finality,
pub position: Position,
pub threshold: u32,
pub votes: Vec<Vote>,
}
impl QuorumCert {
pub fn assemble(
tier: Finality,
position: Position,
threshold: u32,
votes: &[Vote],
) -> Result<Self, CertError> {
if threshold == 0 {
return Err(CertError::ThresholdZero);
}
let mut sorted: Vec<Vote> = votes.iter().filter(|v| v.accept).cloned().collect();
sorted.sort_by_key(|a| a.node_id);
sorted.dedup_by(|a, b| a.node_id == b.node_id);
if (sorted.len() as u64) < threshold as u64 {
return Err(CertError::BelowThreshold {
have: sorted.len() as u32,
need: threshold,
});
}
Ok(QuorumCert {
version: QUORUM_CERT_VERSION,
qc_type: QC_FINALITY,
tier,
position,
threshold,
votes: sorted,
})
}
pub fn voter_count(&self) -> i64 {
self.votes.len() as i64
}
pub fn message(&self) -> Vec<u8> {
canonical_vote_message(&self.position, true)
}
pub fn verify(
&self,
verifier: &dyn VoteVerifier,
epoch_height: u64,
) -> Result<(), CertError> {
if self.version != QUORUM_CERT_VERSION {
return Err(CertError::Version {
got: self.version,
want: QUORUM_CERT_VERSION,
});
}
if self.qc_type != QC_FINALITY {
return Err(CertError::Type {
got: self.qc_type,
want: QC_FINALITY,
});
}
if self.tier != Finality::Nova && self.tier != Finality::Quasar {
return Err(CertError::UnknownTier(self.tier));
}
if self.threshold == 0 {
return Err(CertError::ThresholdZero);
}
if self.votes.is_empty() {
return Err(CertError::NoVotes);
}
let message = self.message();
let mut count: u32 = 0;
let mut prev: Option<&NodeId> = None;
for (i, v) in self.votes.iter().enumerate() {
if let Some(p) = prev {
if p >= &v.node_id {
return Err(CertError::NotStrictlyIncreasing(i));
}
}
prev = Some(&v.node_id);
if !v.accept {
return Err(CertError::VoteNotAccept(i));
}
if !verifier.verify_vote(&v.node_id, &message, &v.signature, epoch_height) {
return Err(CertError::SigInvalid(i));
}
count += 1;
}
if count < self.threshold {
return Err(CertError::BelowThreshold {
have: count,
need: self.threshold,
});
}
Ok(())
}
pub fn verify_weighted(
&self,
verifier: &dyn VoteVerifier,
stake: &dyn StakeSource,
epoch_height: u64,
) -> Result<(), CertError> {
self.verify(verifier, epoch_height)?;
match self.tier {
Finality::Nova => self.verify_nova_majority(stake, epoch_height),
Finality::Quasar => self.verify_quasar_supermajority(stake, epoch_height),
other => Err(CertError::UnknownTier(other)),
}
}
fn verify_nova_majority(
&self,
stake: &dyn StakeSource,
epoch_height: u64,
) -> Result<(), CertError> {
let n = stake.signer_count(epoch_height);
if n < 1 {
return Err(CertError::UnresolvedSet { n });
}
let floor = nova_signer_floor(n);
if self.voter_count() < floor {
return Err(CertError::SignerFloor {
have: self.voter_count(),
need: floor,
n,
});
}
let signer = stake.signer_stake(epoch_height);
if signer == 0 {
return Err(CertError::StakeZero { epoch_height });
}
let voted = self.voted_stake(stake, epoch_height)?;
let half = half_stake_floor(signer);
if voted <= half {
return Err(CertError::StakeBelowMajority {
voted,
signer,
need_above: half,
});
}
Ok(())
}
fn verify_quasar_supermajority(
&self,
stake: &dyn StakeSource,
epoch_height: u64,
) -> Result<(), CertError> {
let signer = stake.signer_stake(epoch_height);
if signer == 0 {
return Err(CertError::StakeZero { epoch_height });
}
let voted = self.voted_stake(stake, epoch_height)?;
let floor = two_thirds_stake_floor(signer);
if voted <= floor {
return Err(CertError::StakeBelowSupermajority {
voted,
signer,
need_above: floor,
});
}
let n = stake.signer_count(epoch_height);
if n < 1 {
return Err(CertError::UnresolvedSet { n });
}
if n < MIN_BFT_COMMITTEE {
return Err(CertError::MinCommittee {
n,
need: MIN_BFT_COMMITTEE,
});
}
let need = two_thirds_count(n);
if self.voter_count() < need {
return Err(CertError::SignerFloor {
have: self.voter_count(),
need,
n,
});
}
Ok(())
}
fn voted_stake(&self, stake: &dyn StakeSource, epoch_height: u64) -> Result<u64, CertError> {
self.votes.iter().try_fold(0u64, |acc, v| {
acc.checked_add(stake.weight(&v.node_id, epoch_height))
.ok_or(CertError::WeightOverflow)
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Registration {
pub node: NodeId,
pub public_key: Vec<u8>,
pub proof: Vec<u8>,
pub weight: u64,
}
#[derive(Clone, Debug, Default)]
pub struct ValidatorSet {
keys: HashMap<NodeId, PublicKey>,
weights: HashMap<NodeId, u64>,
owner: HashMap<[u8; PUBLIC_KEY_LEN], NodeId>,
total: u64,
signable: u64,
}
impl ValidatorSet {
pub fn new() -> Self {
Self::default()
}
pub fn register(mut registrations: Vec<Registration>) -> Result<Self, CertError> {
registrations.sort_by_key(|r| r.node);
let mut set = Self::new();
for r in ®istrations {
set.insert(r.node, r.weight, &r.public_key, &r.proof)?;
}
Ok(set)
}
pub fn insert(
&mut self,
node: NodeId,
weight: u64,
public_key: &[u8],
proof: &[u8],
) -> Result<(), CertError> {
if public_key.is_empty() {
return Err(CertError::NoKey);
}
if weight == 0 {
return Err(CertError::ZeroWeight);
}
pop::verify(&node, public_key, proof).map_err(|e| match e {
PopError::Key => CertError::KeyEncoding,
PopError::Proof | PopError::Possession => CertError::PopInvalid,
})?;
let pk = PublicKey::key_validate(public_key).map_err(|_| CertError::KeyEncoding)?;
let canonical = pk.compress();
if self.owner.contains_key(&canonical) {
return Err(CertError::DuplicateKey);
}
if self.weights.contains_key(&node) {
return Err(CertError::DuplicateNode);
}
let total = self
.total
.checked_add(weight)
.ok_or(CertError::WeightOverflow)?;
let signable = self.signable + weight;
self.keys.insert(node, pk);
self.weights.insert(node, weight);
self.owner.insert(canonical, node);
self.total = total;
self.signable = signable;
Ok(())
}
pub fn insert_unkeyed(&mut self, node: NodeId, weight: u64) -> Result<(), CertError> {
if self.weights.contains_key(&node) {
return Err(CertError::DuplicateNode);
}
let total = self
.total
.checked_add(weight)
.ok_or(CertError::WeightOverflow)?;
self.weights.insert(node, weight);
self.total = total;
Ok(())
}
pub fn remove(&mut self, node: &NodeId) {
let was_keyed = self.keys.contains_key(node);
if let Some(old) = self.keys.remove(node) {
self.owner.remove(&old.compress());
}
if let Some(weight) = self.weights.remove(node) {
self.total -= weight;
if was_keyed {
self.signable -= weight;
}
}
}
pub fn len(&self) -> usize {
self.weights.len()
}
pub fn carried(&self) -> u64 {
self.total
}
pub fn is_empty(&self) -> bool {
self.weights.is_empty()
}
pub fn contains(&self, node: &NodeId) -> bool {
self.weights.contains_key(node)
}
pub fn can_verify(&self, node: &NodeId) -> bool {
self.keys.contains_key(node)
}
pub fn public_key(&self, node: &NodeId) -> Option<&PublicKey> {
self.keys.get(node)
}
}
impl VoteVerifier for ValidatorSet {
fn verify_vote(
&self,
node: &NodeId,
message: &[u8],
signature: &[u8],
_epoch_height: u64,
) -> bool {
if signature.len() != SIGNATURE_LEN {
return false;
}
let pk = match self.keys.get(node) {
Some(pk) => pk,
None => return false,
};
let sig = match Signature::uncompress(signature) {
Ok(s) => s,
Err(_) => return false,
};
sig.verify(true, message, DST, &[], pk, true) == BLST_ERROR::BLST_SUCCESS
}
}
impl StakeSource for ValidatorSet {
fn weight(&self, node: &NodeId, _epoch_height: u64) -> u64 {
if !self.keys.contains_key(node) {
return 0;
}
self.weights.get(node).copied().unwrap_or(0)
}
fn signer_stake(&self, _epoch_height: u64) -> u64 {
self.signable
}
fn signer_count(&self, _epoch_height: u64) -> i64 {
self.keys.len() as i64
}
}