use crate::error::{ConsensusError, Result};
use crate::validator::ValidatorSet;
use dashmap::DashMap;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
use tenzro_crypto::composite::{
CompositePublicKey, CompositeSignature, HybridVerifier, StandardHybridVerifier,
};
use tenzro_types::primitives::Address;
pub const TIMEOUT_MSG_FORMAT_VERSION: u8 = 3;
const TIMEOUT_SIGNING_DOMAIN: &[u8] = b"TENZRO_TIMEOUT:";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TimeoutMsg {
pub format_version: u8,
pub view: u64,
pub high_qc_view: u64,
pub finalized_height: u64,
pub voter: Address,
pub signature: CompositeSignature,
pub public_key: CompositePublicKey,
}
impl TimeoutMsg {
pub fn new(
view: u64,
high_qc_view: u64,
finalized_height: u64,
voter: Address,
signature: CompositeSignature,
public_key: CompositePublicKey,
) -> Self {
Self {
format_version: TIMEOUT_MSG_FORMAT_VERSION,
view,
high_qc_view,
finalized_height,
voter,
signature,
public_key,
}
}
pub fn signing_payload(&self) -> Vec<u8> {
let mut payload = Vec::with_capacity(15 + 1 + 8 + 8 + 8 + 32);
payload.extend_from_slice(TIMEOUT_SIGNING_DOMAIN);
payload.push(self.format_version);
payload.extend_from_slice(&self.view.to_le_bytes());
payload.extend_from_slice(&self.high_qc_view.to_le_bytes());
payload.extend_from_slice(&self.finalized_height.to_le_bytes());
payload.extend_from_slice(self.voter.as_bytes());
payload
}
pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
if self.format_version != TIMEOUT_MSG_FORMAT_VERSION {
return Err(ConsensusError::InvalidSignature(format!(
"timeout message rejected: unsupported format_version {} (expected {})",
self.format_version, TIMEOUT_MSG_FORMAT_VERSION
)));
}
if self.high_qc_view >= self.view {
return Err(ConsensusError::InvalidSignature(format!(
"timeout message rejected: high_qc_view {} >= view {} (impossible)",
self.high_qc_view, self.view
)));
}
let validator = validator_set.get_by_address(&self.voter).ok_or_else(|| {
ConsensusError::NonValidator(format!("Address: {}", self.voter))
})?;
if !validator.is_active() {
return Err(ConsensusError::NonValidator(format!(
"Validator {} is not active",
self.voter
)));
}
if self.public_key.classical != validator.public_key {
return Err(ConsensusError::InvalidSignature(format!(
"timeout classical public key does not match registered validator key for {}",
self.voter
)));
}
if self.public_key.pq != validator.pq_public_key {
return Err(ConsensusError::InvalidSignature(format!(
"timeout PQ public key does not match registered validator key for {}",
self.voter
)));
}
let payload = self.signing_payload();
let verifier = StandardHybridVerifier::new(validator.composite_public_key());
verifier.verify(&payload, &self.signature).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"hybrid timeout signature verification failed for {}: {}",
self.voter, e
))
})?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TcSigner {
pub voter: Address,
pub high_qc_view: u64,
pub finalized_height: u64,
pub signature: CompositeSignature,
pub public_key: CompositePublicKey,
}
pub const TIMEOUT_CERTIFICATE_FORMAT_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TimeoutCertificate {
pub format_version: u8,
pub view: u64,
pub signers: Vec<TcSigner>,
}
impl TimeoutCertificate {
pub fn new(view: u64, signers: Vec<TcSigner>) -> Self {
Self {
format_version: TIMEOUT_CERTIFICATE_FORMAT_VERSION,
view,
signers,
}
}
pub fn max_high_qc_view(&self) -> u64 {
self.signers
.iter()
.map(|s| s.high_qc_view)
.max()
.unwrap_or(0)
}
pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
if self.format_version != TIMEOUT_CERTIFICATE_FORMAT_VERSION {
return Err(ConsensusError::InvalidSignature(format!(
"TC rejected: unsupported format_version {} (expected {})",
self.format_version, TIMEOUT_CERTIFICATE_FORMAT_VERSION
)));
}
let quorum = validator_set.quorum_threshold();
if self.signers.len() < quorum {
return Err(ConsensusError::InvalidSignature(format!(
"TC has {} signers, below 2f+1 quorum threshold {}",
self.signers.len(),
quorum
)));
}
let mut seen: HashSet<Address> = HashSet::with_capacity(self.signers.len());
for signer in &self.signers {
if !seen.insert(signer.voter) {
return Err(ConsensusError::InvalidSignature(format!(
"TC contains duplicate signer {}",
signer.voter
)));
}
if signer.high_qc_view >= self.view {
return Err(ConsensusError::InvalidSignature(format!(
"TC signer {} has high_qc_view {} >= tc.view {}",
signer.voter, signer.high_qc_view, self.view
)));
}
let validator = validator_set.get_by_address(&signer.voter).ok_or_else(|| {
ConsensusError::NonValidator(format!("TC signer {}", signer.voter))
})?;
if !validator.is_active() {
return Err(ConsensusError::NonValidator(format!(
"TC signer {} is not active",
signer.voter
)));
}
if signer.public_key.classical != validator.public_key {
return Err(ConsensusError::InvalidSignature(format!(
"TC signer {}: classical key mismatch with registered validator",
signer.voter
)));
}
if signer.public_key.pq != validator.pq_public_key {
return Err(ConsensusError::InvalidSignature(format!(
"TC signer {}: PQ key mismatch with registered validator",
signer.voter
)));
}
let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
let unsigned = TimeoutMsg::new(
self.view,
signer.high_qc_view,
signer.finalized_height,
signer.voter,
placeholder,
signer.public_key.clone(),
);
let payload = unsigned.signing_payload();
let verifier = StandardHybridVerifier::new(validator.composite_public_key());
verifier.verify(&payload, &signer.signature).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"TC signer {}: hybrid signature verification failed: {}",
signer.voter, e
))
})?;
}
Ok(())
}
}
struct PerViewCollector {
by_voter: std::collections::HashMap<Address, TcSigner>,
bracha_fired: bool,
tc_formed: bool,
}
impl PerViewCollector {
fn new() -> Self {
Self {
by_voter: std::collections::HashMap::new(),
bracha_fired: false,
tc_formed: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CollectOutcome {
Added,
BrachaBoost,
CertificateFormed(TimeoutCertificate),
Duplicate,
}
pub struct TimeoutCollector {
views: DashMap<u64, Arc<Mutex<PerViewCollector>>>,
quorum_threshold: usize,
bracha_threshold: usize,
}
impl TimeoutCollector {
pub fn new(validator_set: Arc<ValidatorSet>) -> Self {
let n = validator_set.len();
let f = (n.saturating_sub(1)) / 3;
let quorum_threshold = 2 * f + 1;
let bracha_threshold = f + 1;
Self {
views: DashMap::new(),
quorum_threshold,
bracha_threshold,
}
}
pub fn add(&self, msg: &TimeoutMsg) -> CollectOutcome {
let view = msg.view;
let entry = self
.views
.entry(view)
.or_insert_with(|| Arc::new(Mutex::new(PerViewCollector::new())))
.clone();
let mut state = entry.lock();
if state.tc_formed {
return match state.by_voter.get(&msg.voter) {
Some(_) => CollectOutcome::Duplicate,
None => {
state.by_voter.insert(
msg.voter,
TcSigner {
voter: msg.voter,
high_qc_view: msg.high_qc_view,
finalized_height: msg.finalized_height,
signature: msg.signature.clone(),
public_key: msg.public_key.clone(),
},
);
CollectOutcome::Added
}
};
}
if state.by_voter.contains_key(&msg.voter) {
return CollectOutcome::Duplicate;
}
state.by_voter.insert(
msg.voter,
TcSigner {
voter: msg.voter,
high_qc_view: msg.high_qc_view,
finalized_height: msg.finalized_height,
signature: msg.signature.clone(),
public_key: msg.public_key.clone(),
},
);
let count = state.by_voter.len();
if count >= self.quorum_threshold {
state.tc_formed = true;
state.bracha_fired = true;
let signers: Vec<TcSigner> = state.by_voter.values().cloned().collect();
let tc = TimeoutCertificate::new(view, signers);
return CollectOutcome::CertificateFormed(tc);
}
if !state.bracha_fired && count >= self.bracha_threshold {
state.bracha_fired = true;
return CollectOutcome::BrachaBoost;
}
CollectOutcome::Added
}
pub fn cleanup_below(&self, min_view: u64) {
self.views.retain(|view, _| *view >= min_view);
}
pub fn quorum_threshold(&self) -> usize {
self.quorum_threshold
}
pub fn bracha_threshold(&self) -> usize {
self.bracha_threshold
}
}
pub const NO_ENDORSEMENT_MSG_FORMAT_VERSION: u8 = 1;
pub const NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION: u8 = 1;
const NO_ENDORSEMENT_SIGNING_DOMAIN: &[u8] = b"TENZRO_NO_ENDORSEMENT:";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoEndorsementMsg {
pub format_version: u8,
pub view: u64,
pub voter: Address,
pub signature: CompositeSignature,
pub public_key: CompositePublicKey,
}
impl NoEndorsementMsg {
pub fn new(
view: u64,
voter: Address,
signature: CompositeSignature,
public_key: CompositePublicKey,
) -> Self {
Self {
format_version: NO_ENDORSEMENT_MSG_FORMAT_VERSION,
view,
voter,
signature,
public_key,
}
}
pub fn signing_payload(&self) -> Vec<u8> {
let mut payload = Vec::with_capacity(22 + 1 + 8 + 32);
payload.extend_from_slice(NO_ENDORSEMENT_SIGNING_DOMAIN);
payload.push(self.format_version);
payload.extend_from_slice(&self.view.to_le_bytes());
payload.extend_from_slice(self.voter.as_bytes());
payload
}
pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
if self.format_version != NO_ENDORSEMENT_MSG_FORMAT_VERSION {
return Err(ConsensusError::InvalidSignature(format!(
"no-endorsement message rejected: unsupported format_version {} (expected {})",
self.format_version, NO_ENDORSEMENT_MSG_FORMAT_VERSION
)));
}
if self.view == 0 {
return Err(ConsensusError::InvalidSignature(
"no-endorsement message rejected: view 0 has no predecessor".to_string(),
));
}
let validator = validator_set.get_by_address(&self.voter).ok_or_else(|| {
ConsensusError::NonValidator(format!("Address: {}", self.voter))
})?;
if !validator.is_active() {
return Err(ConsensusError::NonValidator(format!(
"Validator {} is not active",
self.voter
)));
}
if self.public_key.classical != validator.public_key {
return Err(ConsensusError::InvalidSignature(format!(
"no-endorsement classical public key does not match registered validator key for {}",
self.voter
)));
}
if self.public_key.pq != validator.pq_public_key {
return Err(ConsensusError::InvalidSignature(format!(
"no-endorsement PQ public key does not match registered validator key for {}",
self.voter
)));
}
let payload = self.signing_payload();
let verifier = StandardHybridVerifier::new(validator.composite_public_key());
verifier.verify(&payload, &self.signature).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"hybrid no-endorsement signature verification failed for {}: {}",
self.voter, e
))
})?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NecSigner {
pub voter: Address,
pub signature: CompositeSignature,
pub public_key: CompositePublicKey,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoEndorsementCertificate {
pub format_version: u8,
pub view: u64,
pub signers: Vec<NecSigner>,
}
impl NoEndorsementCertificate {
pub fn new(view: u64, signers: Vec<NecSigner>) -> Self {
Self {
format_version: NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION,
view,
signers,
}
}
pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
if self.format_version != NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION {
return Err(ConsensusError::InvalidSignature(format!(
"NEC rejected: unsupported format_version {} (expected {})",
self.format_version, NO_ENDORSEMENT_CERTIFICATE_FORMAT_VERSION
)));
}
if self.view == 0 {
return Err(ConsensusError::InvalidSignature(
"NEC rejected: view 0 has no predecessor".to_string(),
));
}
let n = validator_set.len();
let f = n.saturating_sub(1) / 3;
let bracha = f + 1;
if self.signers.len() < bracha {
return Err(ConsensusError::InvalidSignature(format!(
"NEC has {} signers, below f+1 threshold {}",
self.signers.len(),
bracha
)));
}
let mut seen: HashSet<Address> = HashSet::with_capacity(self.signers.len());
for signer in &self.signers {
if !seen.insert(signer.voter) {
return Err(ConsensusError::InvalidSignature(format!(
"NEC contains duplicate signer {}",
signer.voter
)));
}
let validator = validator_set.get_by_address(&signer.voter).ok_or_else(|| {
ConsensusError::NonValidator(format!("NEC signer {}", signer.voter))
})?;
if !validator.is_active() {
return Err(ConsensusError::NonValidator(format!(
"NEC signer {} is not active",
signer.voter
)));
}
if signer.public_key.classical != validator.public_key {
return Err(ConsensusError::InvalidSignature(format!(
"NEC signer {}: classical key mismatch with registered validator",
signer.voter
)));
}
if signer.public_key.pq != validator.pq_public_key {
return Err(ConsensusError::InvalidSignature(format!(
"NEC signer {}: PQ key mismatch with registered validator",
signer.voter
)));
}
let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
let unsigned = NoEndorsementMsg::new(
self.view,
signer.voter,
placeholder,
signer.public_key.clone(),
);
let payload = unsigned.signing_payload();
let verifier = StandardHybridVerifier::new(validator.composite_public_key());
verifier.verify(&payload, &signer.signature).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"NEC signer {}: hybrid signature verification failed: {}",
signer.voter, e
))
})?;
}
Ok(())
}
}
struct PerViewNecCollector {
by_voter: std::collections::HashMap<Address, NecSigner>,
nec_formed: bool,
}
impl PerViewNecCollector {
fn new() -> Self {
Self {
by_voter: std::collections::HashMap::new(),
nec_formed: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NecCollectOutcome {
Added,
CertificateFormed(NoEndorsementCertificate),
Duplicate,
}
pub struct NoEndorsementCollector {
views: DashMap<u64, Arc<Mutex<PerViewNecCollector>>>,
bracha_threshold: usize,
}
impl NoEndorsementCollector {
pub fn new(validator_set: Arc<ValidatorSet>) -> Self {
let n = validator_set.len();
let f = n.saturating_sub(1) / 3;
let bracha_threshold = f + 1;
Self {
views: DashMap::new(),
bracha_threshold,
}
}
pub fn add(&self, msg: &NoEndorsementMsg) -> NecCollectOutcome {
let view = msg.view;
let entry = self
.views
.entry(view)
.or_insert_with(|| Arc::new(Mutex::new(PerViewNecCollector::new())))
.clone();
let mut state = entry.lock();
if state.nec_formed {
return match state.by_voter.get(&msg.voter) {
Some(_) => NecCollectOutcome::Duplicate,
None => {
state.by_voter.insert(
msg.voter,
NecSigner {
voter: msg.voter,
signature: msg.signature.clone(),
public_key: msg.public_key.clone(),
},
);
NecCollectOutcome::Added
}
};
}
if state.by_voter.contains_key(&msg.voter) {
return NecCollectOutcome::Duplicate;
}
state.by_voter.insert(
msg.voter,
NecSigner {
voter: msg.voter,
signature: msg.signature.clone(),
public_key: msg.public_key.clone(),
},
);
let count = state.by_voter.len();
if count >= self.bracha_threshold {
state.nec_formed = true;
let signers: Vec<NecSigner> = state.by_voter.values().cloned().collect();
let nec = NoEndorsementCertificate::new(view, signers);
return NecCollectOutcome::CertificateFormed(nec);
}
NecCollectOutcome::Added
}
pub fn cleanup_below(&self, min_view: u64) {
self.views.retain(|view, _| *view >= min_view);
}
pub fn bracha_threshold(&self) -> usize {
self.bracha_threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validator::ValidatorInfo;
use std::sync::Arc;
use tenzro_crypto::bls::BlsKeyPair;
use tenzro_crypto::composite::{HybridSigner, InMemoryHybridSigner};
use tenzro_crypto::pq::MlDsaSigningKey;
use tenzro_crypto::signatures::Ed25519SignerImpl;
use tenzro_crypto::{KeyPair, KeyType};
fn build_validator(stake: u128) -> (KeyPair, MlDsaSigningKey, Address, ValidatorInfo) {
let keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
let pq = MlDsaSigningKey::generate();
let bls = BlsKeyPair::generate().unwrap();
let crypto_addr = keypair.address();
let mut addr_bytes = [0u8; 32];
addr_bytes[..20].copy_from_slice(crypto_addr.as_bytes());
let address = Address::new(addr_bytes);
let info = ValidatorInfo::new(
address,
keypair.public_key().clone(),
pq.verifying_key_bytes().to_vec(),
bls.public_key().to_bytes().to_vec(),
stake,
);
(keypair, pq, address, info)
}
const TEST_FINALIZED_HEIGHT: u64 = 7;
fn sign_timeout(
view: u64,
high_qc_view: u64,
address: Address,
keypair: &KeyPair,
pq: &MlDsaSigningKey,
) -> TimeoutMsg {
let composite_pk = CompositePublicKey::new(
keypair.public_key().clone(),
pq.verifying_key_bytes().to_vec(),
);
let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
let unsigned = TimeoutMsg::new(
view,
high_qc_view,
TEST_FINALIZED_HEIGHT,
address,
placeholder,
composite_pk.clone(),
);
let payload = unsigned.signing_payload();
let kp_bytes = keypair.to_bytes();
let kp_copy = KeyPair::from_bytes(keypair.key_type(), &kp_bytes).unwrap();
let classical = Ed25519SignerImpl::new(kp_copy).unwrap();
let pq_copy = MlDsaSigningKey::from_seed(pq.seed_bytes()).unwrap();
let signer = InMemoryHybridSigner::new(Box::new(classical), pq_copy);
let signature = signer.sign(&payload).unwrap();
TimeoutMsg::new(
view,
high_qc_view,
TEST_FINALIZED_HEIGHT,
address,
signature,
composite_pk,
)
}
fn vset(infos: Vec<ValidatorInfo>) -> Arc<ValidatorSet> {
Arc::new(ValidatorSet::new(0, infos).unwrap())
}
#[test]
fn timeout_msg_round_trips_signature() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let msg = sign_timeout(42, 41, addr, &kp, &pq);
msg.verify(&validator_set).expect("signature verifies");
}
#[test]
fn timeout_msg_rejects_unknown_voter() {
let (_kp, _pq, _addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let (kp_other, pq_other, addr_other, _) = build_validator(1000);
let msg = sign_timeout(42, 41, addr_other, &kp_other, &pq_other);
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::NonValidator(_)), "got {err:?}");
}
#[test]
fn timeout_msg_rejects_format_version_mismatch() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
msg.format_version = 99;
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn timeout_msg_rejects_older_formats() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
for old_version in [1u8, 2u8] {
let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
msg.format_version = old_version;
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
}
#[test]
fn timeout_msg_rejects_wrong_view_after_signing() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
msg.view = 99;
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn timeout_msg_rejects_tampered_high_qc_view() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let mut msg = sign_timeout(42, 10, addr, &kp, &pq);
msg.high_qc_view = 41;
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn timeout_msg_rejects_tampered_finalized_height() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let mut msg = sign_timeout(42, 41, addr, &kp, &pq);
msg.finalized_height = TEST_FINALIZED_HEIGHT + 1_000_000;
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn timeout_msg_rejects_high_qc_view_ge_view() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let msg = sign_timeout(42, 42, addr, &kp, &pq);
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
fn build_n_signed_timeouts(
n: usize,
view: u64,
high_qc_views: &[u64],
) -> (
Arc<ValidatorSet>,
Vec<(KeyPair, MlDsaSigningKey, Address)>,
Vec<TimeoutMsg>,
) {
assert_eq!(high_qc_views.len(), n);
let mut keys = Vec::with_capacity(n);
let mut infos = Vec::with_capacity(n);
for _ in 0..n {
let (kp, pq, addr, info) = build_validator(1000);
keys.push((kp, pq, addr));
infos.push(info);
}
let validator_set = vset(infos);
let timeouts: Vec<TimeoutMsg> = keys
.iter()
.zip(high_qc_views.iter())
.map(|((kp, pq, addr), &hqv)| sign_timeout(view, hqv, *addr, kp, pq))
.collect();
(validator_set, keys, timeouts)
}
fn tc_from_timeouts(view: u64, timeouts: &[TimeoutMsg]) -> TimeoutCertificate {
let signers = timeouts
.iter()
.map(|t| TcSigner {
voter: t.voter,
high_qc_view: t.high_qc_view,
finalized_height: t.finalized_height,
signature: t.signature.clone(),
public_key: t.public_key.clone(),
})
.collect();
TimeoutCertificate::new(view, signers)
}
#[test]
fn tc_verifies_with_2f_plus_1_signers() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let tc = tc_from_timeouts(100, &timeouts[..3]);
tc.verify(&vset).expect("TC verifies");
assert_eq!(tc.max_high_qc_view(), 95);
}
#[test]
fn tc_rejects_below_quorum() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let tc = tc_from_timeouts(100, &timeouts[..2]);
let err = tc.verify(&vset).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn tc_rejects_duplicate_signers() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let mut signers: Vec<TcSigner> = timeouts[..3]
.iter()
.map(|t| TcSigner {
voter: t.voter,
high_qc_view: t.high_qc_view,
finalized_height: t.finalized_height,
signature: t.signature.clone(),
public_key: t.public_key.clone(),
})
.collect();
signers.push(signers[0].clone());
let tc = TimeoutCertificate::new(100, signers);
let err = tc.verify(&vset).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn tc_rejects_tampered_high_qc_view() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let mut tc = tc_from_timeouts(100, &timeouts[..3]);
tc.signers[0].high_qc_view = 60;
let err = tc.verify(&vset).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn tc_rejects_tampered_finalized_height() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let mut tc = tc_from_timeouts(100, &timeouts[..3]);
tc.signers[0].finalized_height = TEST_FINALIZED_HEIGHT + 1_000_000;
let err = tc.verify(&vset).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn tc_rejects_tampered_view() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let mut tc = tc_from_timeouts(100, &timeouts[..3]);
tc.view = 200;
let err = tc.verify(&vset).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn tc_rejects_unregistered_signer() {
let (vset, _keys, mut timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let (kp_outsider, pq_outsider, addr_outsider, _) = build_validator(1000);
let outsider_msg = sign_timeout(100, 50, addr_outsider, &kp_outsider, &pq_outsider);
timeouts.push(outsider_msg);
let mut t3 = timeouts[..2].to_vec();
t3.push(timeouts[4].clone());
let tc = tc_from_timeouts(100, &t3);
let err = tc.verify(&vset).unwrap_err();
assert!(matches!(err, ConsensusError::NonValidator(_)), "got {err:?}");
}
#[test]
fn tc_format_version_mismatch_rejected() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let mut tc = tc_from_timeouts(100, &timeouts[..3]);
tc.format_version = 99;
let err = tc.verify(&vset).unwrap_err();
assert!(matches!(err, ConsensusError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn tc_max_high_qc_view_is_max() {
let (_vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[10, 99, 50, 30]);
let tc = tc_from_timeouts(100, &timeouts);
assert_eq!(tc.max_high_qc_view(), 99);
}
#[test]
fn collector_emits_added_for_first_timeout() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let collector = TimeoutCollector::new(vset);
assert_eq!(collector.bracha_threshold(), 2);
assert_eq!(collector.quorum_threshold(), 3);
let outcome = collector.add(&timeouts[0]);
assert_eq!(outcome, CollectOutcome::Added);
}
#[test]
fn collector_emits_bracha_boost_at_f_plus_1() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let collector = TimeoutCollector::new(vset);
assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
let outcome = collector.add(&timeouts[1]);
assert_eq!(outcome, CollectOutcome::BrachaBoost);
}
#[test]
fn collector_bracha_boost_is_idempotent() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(7, 100, &[10, 20, 30, 40, 50, 60, 70]);
let collector = TimeoutCollector::new(vset);
assert_eq!(collector.bracha_threshold(), 3);
assert_eq!(collector.quorum_threshold(), 5);
assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
assert_eq!(collector.add(&timeouts[1]), CollectOutcome::Added);
assert_eq!(collector.add(&timeouts[2]), CollectOutcome::BrachaBoost);
assert_eq!(collector.add(&timeouts[3]), CollectOutcome::Added);
}
#[test]
fn collector_emits_certificate_at_quorum() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let collector = TimeoutCollector::new(vset.clone());
assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
assert_eq!(collector.add(&timeouts[1]), CollectOutcome::BrachaBoost);
let outcome = collector.add(&timeouts[2]);
match outcome {
CollectOutcome::CertificateFormed(tc) => {
tc.verify(&vset).expect("emitted TC verifies");
assert_eq!(tc.view, 100);
assert_eq!(tc.signers.len(), 3);
assert_eq!(tc.max_high_qc_view(), 95);
}
other => panic!("expected CertificateFormed, got {other:?}"),
}
}
#[test]
fn collector_certificate_is_idempotent() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let collector = TimeoutCollector::new(vset);
let _ = collector.add(&timeouts[0]);
let _ = collector.add(&timeouts[1]);
let _ = collector.add(&timeouts[2]); let outcome = collector.add(&timeouts[3]);
assert_eq!(outcome, CollectOutcome::Added);
}
#[test]
fn collector_dedupes_same_voter() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let collector = TimeoutCollector::new(vset);
assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Added);
assert_eq!(collector.add(&timeouts[0]), CollectOutcome::Duplicate);
assert_eq!(collector.add(&timeouts[1]), CollectOutcome::BrachaBoost);
}
#[test]
fn collector_separates_views() {
let (vset, _keys, timeouts_v100) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let collector = TimeoutCollector::new(vset);
assert_eq!(collector.add(&timeouts_v100[0]), CollectOutcome::Added);
assert_eq!(collector.add(&timeouts_v100[1]), CollectOutcome::BrachaBoost);
}
#[test]
fn collector_cleanup_below_drops_old_views() {
let (vset, _keys, timeouts) =
build_n_signed_timeouts(4, 100, &[80, 90, 95, 99]);
let collector = TimeoutCollector::new(vset);
let _ = collector.add(&timeouts[0]);
assert_eq!(collector.views.len(), 1);
collector.cleanup_below(101);
assert_eq!(collector.views.len(), 0);
}
#[test]
fn collector_quorum_in_one_shot_when_n_eq_1() {
let (vset, _keys, timeouts) = build_n_signed_timeouts(1, 50, &[10]);
let collector = TimeoutCollector::new(vset.clone());
assert_eq!(collector.bracha_threshold(), 1);
assert_eq!(collector.quorum_threshold(), 1);
let outcome = collector.add(&timeouts[0]);
match outcome {
CollectOutcome::CertificateFormed(tc) => {
tc.verify(&vset).expect("single-validator TC verifies");
assert_eq!(tc.signers.len(), 1);
}
other => panic!("expected CertificateFormed, got {other:?}"),
}
}
fn sign_no_endorsement(
view: u64,
address: Address,
keypair: &KeyPair,
pq: &MlDsaSigningKey,
) -> NoEndorsementMsg {
let composite_pk = CompositePublicKey::new(
keypair.public_key().clone(),
pq.verifying_key_bytes().to_vec(),
);
let placeholder = CompositeSignature::new(Vec::new(), Vec::new());
let unsigned = NoEndorsementMsg::new(view, address, placeholder, composite_pk.clone());
let payload = unsigned.signing_payload();
let kp_bytes = keypair.to_bytes();
let kp_copy = KeyPair::from_bytes(keypair.key_type(), &kp_bytes).unwrap();
let classical = Ed25519SignerImpl::new(kp_copy).unwrap();
let pq_copy = MlDsaSigningKey::from_seed(pq.seed_bytes()).unwrap();
let signer = InMemoryHybridSigner::new(Box::new(classical), pq_copy);
let signature = signer.sign(&payload).unwrap();
NoEndorsementMsg::new(view, address, signature, composite_pk)
}
fn build_n_signed_no_endorsements(
n: usize,
view: u64,
) -> (
Arc<ValidatorSet>,
Vec<(KeyPair, MlDsaSigningKey, Address)>,
Vec<NoEndorsementMsg>,
) {
let mut keys = Vec::with_capacity(n);
let mut infos = Vec::with_capacity(n);
for _ in 0..n {
let (kp, pq, addr, info) = build_validator(1000);
keys.push((kp, pq, addr));
infos.push(info);
}
let validator_set = vset(infos);
let msgs: Vec<NoEndorsementMsg> = keys
.iter()
.map(|(kp, pq, addr)| sign_no_endorsement(view, *addr, kp, pq))
.collect();
(validator_set, keys, msgs)
}
fn nec_from_msgs(view: u64, msgs: &[NoEndorsementMsg]) -> NoEndorsementCertificate {
let signers = msgs
.iter()
.map(|m| NecSigner {
voter: m.voter,
signature: m.signature.clone(),
public_key: m.public_key.clone(),
})
.collect();
NoEndorsementCertificate::new(view, signers)
}
#[test]
fn nec_msg_round_trips_signature() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let msg = sign_no_endorsement(42, addr, &kp, &pq);
msg.verify(&validator_set).expect("signature verifies");
}
#[test]
fn nec_msg_rejects_view_zero() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let msg = sign_no_endorsement(0, addr, &kp, &pq);
let err = msg.verify(&validator_set).unwrap_err();
assert!(
matches!(err, ConsensusError::InvalidSignature(_)),
"got {err:?}"
);
}
#[test]
fn nec_msg_rejects_unknown_voter() {
let (_kp, _pq, _addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let (kp_other, pq_other, addr_other, _) = build_validator(1000);
let msg = sign_no_endorsement(42, addr_other, &kp_other, &pq_other);
let err = msg.verify(&validator_set).unwrap_err();
assert!(matches!(err, ConsensusError::NonValidator(_)), "got {err:?}");
}
#[test]
fn nec_msg_rejects_format_version_mismatch() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let mut msg = sign_no_endorsement(42, addr, &kp, &pq);
msg.format_version = 99;
let err = msg.verify(&validator_set).unwrap_err();
assert!(
matches!(err, ConsensusError::InvalidSignature(_)),
"got {err:?}"
);
}
#[test]
fn nec_msg_rejects_tampered_view() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let mut msg = sign_no_endorsement(42, addr, &kp, &pq);
msg.view = 99;
let err = msg.verify(&validator_set).unwrap_err();
assert!(
matches!(err, ConsensusError::InvalidSignature(_)),
"got {err:?}"
);
}
#[test]
fn nec_payload_distinct_from_timeout_payload() {
let (kp, pq, addr, info) = build_validator(1000);
let validator_set = vset(vec![info]);
let timeout = sign_timeout(42, 41, addr, &kp, &pq);
let nec = NoEndorsementMsg::new(
42,
addr,
timeout.signature.clone(),
timeout.public_key.clone(),
);
let err = nec.verify(&validator_set).unwrap_err();
assert!(
matches!(err, ConsensusError::InvalidSignature(_)),
"got {err:?}"
);
}
#[test]
fn nec_verifies_with_f_plus_1_signers() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let nec = nec_from_msgs(100, &msgs[..2]);
nec.verify(&vset).expect("NEC verifies at f+1");
}
#[test]
fn nec_rejects_below_f_plus_1() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let nec = nec_from_msgs(100, &msgs[..1]);
let err = nec.verify(&vset).unwrap_err();
assert!(
matches!(err, ConsensusError::InvalidSignature(_)),
"got {err:?}"
);
}
#[test]
fn nec_rejects_duplicate_signers() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let mut signers: Vec<NecSigner> = msgs[..2]
.iter()
.map(|m| NecSigner {
voter: m.voter,
signature: m.signature.clone(),
public_key: m.public_key.clone(),
})
.collect();
signers.push(signers[0].clone());
let nec = NoEndorsementCertificate::new(100, signers);
let err = nec.verify(&vset).unwrap_err();
assert!(
matches!(err, ConsensusError::InvalidSignature(_)),
"got {err:?}"
);
}
#[test]
fn nec_rejects_tampered_view() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let mut nec = nec_from_msgs(100, &msgs[..2]);
nec.view = 200;
let err = nec.verify(&vset).unwrap_err();
assert!(
matches!(err, ConsensusError::InvalidSignature(_)),
"got {err:?}"
);
}
#[test]
fn nec_collector_emits_added_for_first_msg() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let collector = NoEndorsementCollector::new(vset);
assert_eq!(collector.bracha_threshold(), 2);
let outcome = collector.add(&msgs[0]);
assert_eq!(outcome, NecCollectOutcome::Added);
}
#[test]
fn nec_collector_emits_certificate_at_f_plus_1() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let collector = NoEndorsementCollector::new(vset.clone());
assert_eq!(collector.add(&msgs[0]), NecCollectOutcome::Added);
let outcome = collector.add(&msgs[1]);
match outcome {
NecCollectOutcome::CertificateFormed(nec) => {
nec.verify(&vset).expect("emitted NEC verifies");
assert_eq!(nec.view, 100);
assert_eq!(nec.signers.len(), 2);
}
other => panic!("expected CertificateFormed, got {other:?}"),
}
}
#[test]
fn nec_collector_certificate_is_idempotent() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let collector = NoEndorsementCollector::new(vset);
let _ = collector.add(&msgs[0]);
let _ = collector.add(&msgs[1]); let outcome = collector.add(&msgs[2]);
assert_eq!(outcome, NecCollectOutcome::Added);
}
#[test]
fn nec_collector_dedupes_same_voter() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let collector = NoEndorsementCollector::new(vset);
assert_eq!(collector.add(&msgs[0]), NecCollectOutcome::Added);
assert_eq!(collector.add(&msgs[0]), NecCollectOutcome::Duplicate);
}
#[test]
fn nec_collector_cleanup_below_drops_old_views() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(4, 100);
let collector = NoEndorsementCollector::new(vset);
let _ = collector.add(&msgs[0]);
assert_eq!(collector.views.len(), 1);
collector.cleanup_below(101);
assert_eq!(collector.views.len(), 0);
}
#[test]
fn nec_collector_certificate_at_n_eq_1() {
let (vset, _keys, msgs) = build_n_signed_no_endorsements(1, 50);
let collector = NoEndorsementCollector::new(vset.clone());
assert_eq!(collector.bracha_threshold(), 1);
let outcome = collector.add(&msgs[0]);
match outcome {
NecCollectOutcome::CertificateFormed(nec) => {
nec.verify(&vset).expect("single-validator NEC verifies");
assert_eq!(nec.signers.len(), 1);
}
other => panic!("expected CertificateFormed, got {other:?}"),
}
}
}