use crate::error::{ConsensusError, Result};
use crate::validator::{ValidatorInfo, ValidatorSet};
use parking_lot::RwLock;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use tenzro_types::primitives::{Address, Hash};
const REPUTATION_SEED_DOMAIN: &[u8] = b"TENZRO_LEADER_REPUTATION:";
pub const FAILED_WEIGHT: u128 = 1;
pub const INACTIVE_WEIGHT: u128 = 10;
pub const ACTIVE_WEIGHT: u128 = 1000;
pub const FAILURE_THRESHOLD_PERCENT: u32 = 10;
pub const TEE_MULTIPLIER_BPS: u128 = 15000;
pub const NO_TEE_MULTIPLIER_BPS: u128 = 10000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProposerRecord {
pub round: u64,
pub proposer: Address,
pub success: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VoterRecord {
pub round: u64,
pub voters: Vec<Address>,
}
#[derive(Debug)]
pub struct ProposerHistory {
records: VecDeque<ProposerRecord>,
capacity: usize,
}
impl ProposerHistory {
pub fn new(n: usize) -> Self {
let capacity = (10 * n + 40).max(64);
Self {
records: VecDeque::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, record: ProposerRecord) {
if let Some(existing) = self.records.iter_mut().find(|r| r.round == record.round) {
*existing = record;
return;
}
if self.records.len() >= self.capacity {
self.records.pop_front();
}
self.records.push_back(record);
}
pub fn range(&self, lo: u64, hi: u64) -> impl Iterator<Item = &ProposerRecord> {
self.records
.iter()
.filter(move |r| r.round >= lo && r.round < hi)
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
}
#[derive(Debug)]
pub struct VoterHistory {
records: VecDeque<VoterRecord>,
capacity: usize,
}
impl VoterHistory {
pub fn new(n: usize) -> Self {
let capacity = (10 * n + 40).max(64);
Self {
records: VecDeque::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, record: VoterRecord) {
if let Some(existing) = self.records.iter_mut().find(|r| r.round == record.round) {
*existing = record;
return;
}
if self.records.len() >= self.capacity {
self.records.pop_front();
}
self.records.push_back(record);
}
pub fn range(&self, lo: u64, hi: u64) -> impl Iterator<Item = &VoterRecord> {
self.records
.iter()
.filter(move |r| r.round >= lo && r.round < hi)
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
}
pub const TRAILING_BUFFER_ROUNDS: u64 = 20;
pub fn proposer_window(round: u64, n: usize) -> Option<(u64, u64)> {
let span = 10u64.saturating_mul(n as u64);
let buffer = TRAILING_BUFFER_ROUNDS;
if round < span + buffer {
return None;
}
let lo = round - span - buffer;
let hi = round - buffer;
Some((lo, hi))
}
pub fn voter_window(round: u64, n: usize) -> Option<(u64, u64)> {
let span = 10u64.saturating_mul(n as u64);
let buffer = TRAILING_BUFFER_ROUNDS;
let voter_hi_offset = 9u64.saturating_mul(n as u64).saturating_add(buffer);
if round < span + buffer || round < voter_hi_offset {
return None;
}
let lo = round - span - buffer;
let hi = round - voter_hi_offset;
if lo >= hi {
return None;
}
Some((lo, hi))
}
pub fn reputation_seed(epoch: u64, round: u64, prev_block_id: &Hash) -> [u8; 32] {
let mut buf = Vec::with_capacity(REPUTATION_SEED_DOMAIN.len() + 8 + 8 + 32);
buf.extend_from_slice(REPUTATION_SEED_DOMAIN);
buf.extend_from_slice(&epoch.to_be_bytes());
buf.extend_from_slice(&round.to_be_bytes());
buf.extend_from_slice(prev_block_id.as_bytes());
tenzro_crypto::hash::sha256(&buf).to_bytes()
}
fn seed_to_index(seed: &[u8; 32], total: u128) -> u128 {
debug_assert!(total > 0);
let mut buf = [0u8; 16];
buf.copy_from_slice(&seed[..16]);
let val = u128::from_be_bytes(buf);
val % total
}
#[derive(Debug, Clone)]
pub struct ValidatorWeights {
pub weights: HashMap<Address, u128>,
pub total: u128,
}
pub struct LeaderReputation {
proposer_history: RwLock<ProposerHistory>,
voter_history: RwLock<VoterHistory>,
tee_multiplier_bps: u128,
}
impl LeaderReputation {
pub fn new(n: usize) -> Self {
Self::with_tee_multiplier(n, TEE_MULTIPLIER_BPS)
}
pub fn with_tee_multiplier(n: usize, tee_multiplier_bps: u128) -> Self {
Self {
proposer_history: RwLock::new(ProposerHistory::new(n)),
voter_history: RwLock::new(VoterHistory::new(n)),
tee_multiplier_bps,
}
}
pub fn record_round_outcome(&self, round: u64, proposer: Address, success: bool) {
self.proposer_history.write().push(ProposerRecord {
round,
proposer,
success,
});
}
pub fn record_round_voters(&self, round: u64, voters: Vec<Address>) {
self.voter_history
.write()
.push(VoterRecord { round, voters });
}
pub fn proposer_history_len(&self) -> usize {
self.proposer_history.read().len()
}
pub fn voter_history_len(&self) -> usize {
self.voter_history.read().len()
}
pub fn compute_weights(
&self,
round: u64,
validator_set: &ValidatorSet,
) -> ValidatorWeights {
let n = validator_set.len();
let p_window = proposer_window(round, n);
let v_window = voter_window(round, n);
let proposer_counts: HashMap<Address, (u64, u64)> = match p_window {
Some((lo, hi)) => {
let history = self.proposer_history.read();
let mut counts: HashMap<Address, (u64, u64)> = HashMap::new();
for record in history.range(lo, hi) {
let entry = counts.entry(record.proposer).or_insert((0, 0));
entry.0 += 1; if !record.success {
entry.1 += 1; }
}
counts
}
None => HashMap::new(),
};
let voted: HashSet<Address> = match v_window {
Some((lo, hi)) => {
let history = self.voter_history.read();
let mut set = HashSet::new();
for record in history.range(lo, hi) {
for voter in &record.voters {
set.insert(*voter);
}
}
set
}
None => HashSet::new(),
};
let bootstrap = p_window.is_none() && v_window.is_none();
let mut weights = HashMap::with_capacity(n);
let mut total: u128 = 0;
for v in validator_set.iter() {
if !v.is_active() {
continue;
}
let behavioural = if bootstrap {
INACTIVE_WEIGHT
} else {
tier_weight(&proposer_counts, &voted, &v.address)
};
let stake = v.stake.max(1); let raw = behavioural.saturating_mul(stake);
let multiplier = if v.has_valid_tee_attestation() {
self.tee_multiplier_bps
} else {
NO_TEE_MULTIPLIER_BPS
};
let scaled = raw.saturating_mul(multiplier) / NO_TEE_MULTIPLIER_BPS;
let final_weight = scaled.max(1);
weights.insert(v.address, final_weight);
total = total.saturating_add(final_weight);
}
ValidatorWeights { weights, total }
}
pub fn select_leader<'a>(
&self,
round: u64,
epoch: u64,
prev_block_id: &Hash,
validator_set: &'a ValidatorSet,
) -> Result<&'a ValidatorInfo> {
let weights = self.compute_weights(round, validator_set);
if weights.total == 0 {
return Err(ConsensusError::InvalidValidatorSet(
"leader reputation: total weight is zero".to_string(),
));
}
let seed = reputation_seed(epoch, round, prev_block_id);
let target = seed_to_index(&seed, weights.total);
let mut cumulative: u128 = 0;
for v in validator_set.iter() {
if !v.is_active() {
continue;
}
let w = weights.weights.get(&v.address).copied().unwrap_or(0);
cumulative = cumulative.saturating_add(w);
if target < cumulative {
return Ok(v);
}
}
validator_set
.iter()
.rev()
.find(|v| v.is_active())
.ok_or_else(|| {
ConsensusError::InvalidValidatorSet(
"leader reputation: no active validators".to_string(),
)
})
}
}
fn tier_weight(
proposer_counts: &HashMap<Address, (u64, u64)>,
voted: &HashSet<Address>,
address: &Address,
) -> u128 {
if let Some(&(proposed, failed)) = proposer_counts.get(address)
&& proposed > 0
{
let lhs = failed.saturating_mul(100);
let rhs = proposed.saturating_mul(FAILURE_THRESHOLD_PERCENT as u64);
if lhs < rhs {
return ACTIVE_WEIGHT;
} else {
return FAILED_WEIGHT;
}
}
if voted.contains(address) {
INACTIVE_WEIGHT
} else {
FAILED_WEIGHT
}
}
pub type SharedLeaderReputation = Arc<LeaderReputation>;
#[cfg(test)]
mod tests {
use super::*;
use tenzro_crypto::bls::BlsKeyPair;
use tenzro_crypto::pq::MlDsaSigningKey;
use tenzro_crypto::{KeyPair, KeyType};
fn convert_address(crypto_addr: tenzro_crypto::Address) -> Address {
let mut addr_bytes = [0u8; 32];
addr_bytes[..20].copy_from_slice(crypto_addr.as_bytes());
Address::new(addr_bytes)
}
fn validator(stake: u128) -> ValidatorInfo {
let keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
let address = convert_address(keypair.address());
let pq = MlDsaSigningKey::generate();
let bls = BlsKeyPair::generate().unwrap();
ValidatorInfo::new(
address,
keypair.public_key().clone(),
pq.verifying_key_bytes().to_vec(),
bls.public_key().to_bytes().to_vec(),
stake,
)
}
fn vset(validators: Vec<ValidatorInfo>) -> ValidatorSet {
ValidatorSet::new(0, validators).unwrap()
}
#[test]
fn proposer_window_genesis_returns_none() {
assert_eq!(proposer_window(0, 4), None);
assert_eq!(proposer_window(59, 4), None);
assert_eq!(proposer_window(60, 4), Some((0, 40)));
}
#[test]
fn voter_window_genesis_returns_none() {
assert_eq!(voter_window(0, 4), None);
assert_eq!(voter_window(59, 4), None);
assert_eq!(voter_window(60, 4), Some((0, 4)));
}
#[test]
fn proposer_window_at_n_4_round_100() {
assert_eq!(proposer_window(100, 4), Some((40, 80)));
}
#[test]
fn voter_window_at_n_4_round_100() {
assert_eq!(voter_window(100, 4), Some((40, 44)));
}
#[test]
fn proposer_history_dedupes_same_round() {
let mut h = ProposerHistory::new(4);
let addr = convert_address(KeyPair::generate(KeyType::Ed25519).unwrap().address());
h.push(ProposerRecord {
round: 5,
proposer: addr,
success: false,
});
h.push(ProposerRecord {
round: 5,
proposer: addr,
success: true,
});
assert_eq!(h.len(), 1);
let only = h.range(0, 100).next().unwrap();
assert!(only.success);
}
#[test]
fn proposer_history_evicts_oldest_at_capacity() {
let mut h = ProposerHistory::new(1); let addr = convert_address(KeyPair::generate(KeyType::Ed25519).unwrap().address());
for r in 0..70u64 {
h.push(ProposerRecord {
round: r,
proposer: addr,
success: true,
});
}
assert_eq!(h.len(), 64);
let earliest = h.range(0, 100).next().unwrap();
assert_eq!(earliest.round, 6);
}
#[test]
fn reputation_seed_is_deterministic() {
let h = Hash::new([7u8; 32]);
let s1 = reputation_seed(3, 100, &h);
let s2 = reputation_seed(3, 100, &h);
assert_eq!(s1, s2);
}
#[test]
fn reputation_seed_diverges_on_round() {
let h = Hash::new([7u8; 32]);
let s1 = reputation_seed(3, 100, &h);
let s2 = reputation_seed(3, 101, &h);
assert_ne!(s1, s2);
}
#[test]
fn reputation_seed_diverges_on_epoch() {
let h = Hash::new([7u8; 32]);
let s1 = reputation_seed(3, 100, &h);
let s2 = reputation_seed(4, 100, &h);
assert_ne!(s1, s2);
}
#[test]
fn reputation_seed_diverges_on_prev_block() {
let h1 = Hash::new([7u8; 32]);
let h2 = Hash::new([8u8; 32]);
let s1 = reputation_seed(3, 100, &h1);
let s2 = reputation_seed(3, 100, &h2);
assert_ne!(s1, s2);
}
#[test]
fn bootstrap_window_falls_back_to_inactive_weight() {
let v1 = validator(1000);
let v2 = validator(2000);
let set = vset(vec![v1.clone(), v2.clone()]);
let lr = LeaderReputation::new(2);
let weights = lr.compute_weights(5, &set); assert_eq!(weights.weights[&v1.address], INACTIVE_WEIGHT * 1000);
assert_eq!(weights.weights[&v2.address], INACTIVE_WEIGHT * 2000);
assert_eq!(weights.total, INACTIVE_WEIGHT * 3000);
}
#[test]
fn active_proposer_gets_active_weight() {
let v1 = validator(1000);
let v2 = validator(1000);
let set = vset(vec![v1.clone(), v2.clone()]);
let lr = LeaderReputation::new(2);
for r in 60..70u64 {
lr.record_round_outcome(r, v1.address, true);
}
for r in 60..62u64 {
lr.record_round_voters(r, vec![v2.address]);
}
let weights = lr.compute_weights(100, &set);
assert_eq!(weights.weights[&v1.address], ACTIVE_WEIGHT * 1000);
assert_eq!(weights.weights[&v2.address], INACTIVE_WEIGHT * 1000);
}
#[test]
fn high_failure_rate_proposer_drops_to_failed_weight() {
let v1 = validator(1000);
let set = vset(vec![v1.clone()]);
let lr = LeaderReputation::new(1);
for r in 70..75u64 {
lr.record_round_outcome(r, v1.address, true);
}
for r in 75..80u64 {
lr.record_round_outcome(r, v1.address, false);
}
let weights = lr.compute_weights(100, &set);
assert_eq!(weights.weights[&v1.address], FAILED_WEIGHT * 1000);
}
#[test]
fn select_leader_is_deterministic_across_replicas() {
let v1 = validator(1000);
let v2 = validator(1000);
let v3 = validator(1000);
let v4 = validator(1000);
let set = vset(vec![v1.clone(), v2.clone(), v3.clone(), v4.clone()]);
let lr_a = LeaderReputation::new(4);
let lr_b = LeaderReputation::new(4);
let prev = Hash::new([42u8; 32]);
for r in 100..120u64 {
let l_a = lr_a.select_leader(r, 0, &prev, &set).unwrap();
let l_b = lr_b.select_leader(r, 0, &prev, &set).unwrap();
assert_eq!(l_a.address, l_b.address);
}
}
#[test]
fn select_leader_skips_flaky_validator() {
let v1 = validator(1000);
let v2 = validator(1000);
let v3 = validator(1000);
let v4 = validator(1000);
let set = vset(vec![v1.clone(), v2.clone(), v3.clone(), v4.clone()]);
let lr = LeaderReputation::new(4);
for r in 40..80u64 {
let chosen = match r % 4 {
0 => v1.address,
1 => v2.address,
2 => v3.address,
_ => v4.address,
};
let success = chosen != v4.address;
lr.record_round_outcome(r, chosen, success);
}
for r in 40..44u64 {
lr.record_round_voters(r, vec![v1.address, v2.address, v3.address, v4.address]);
}
let weights = lr.compute_weights(100, &set);
assert_eq!(weights.weights[&v1.address], ACTIVE_WEIGHT * 1000);
assert_eq!(weights.weights[&v2.address], ACTIVE_WEIGHT * 1000);
assert_eq!(weights.weights[&v3.address], ACTIVE_WEIGHT * 1000);
assert_eq!(weights.weights[&v4.address], FAILED_WEIGHT * 1000);
let prev = Hash::new([1u8; 32]);
let mut v4_count = 0usize;
for epoch in 0..1000u64 {
let chosen = lr.select_leader(100, epoch, &prev, &set).unwrap();
if chosen.address == v4.address {
v4_count += 1;
}
}
assert!(v4_count <= 5, "v4 was selected {} times out of 1000", v4_count);
}
#[test]
fn tee_multiplier_lifts_attested_validator() {
let v1 = validator(1000);
let v2 = validator(1000);
let mut info1 = v1.clone();
info1 = info1.with_tee_attestation(
tenzro_types::tee::AttestationReport::default(),
tenzro_types::tee::AttestationResult::success(
tenzro_types::tee::TeeVendor::IntelTdx,
vec![0u8; 32],
),
);
let set = vset(vec![info1.clone(), v2.clone()]);
let lr = LeaderReputation::new(2);
let weights = lr.compute_weights(5, &set);
let v1_w = weights.weights[&info1.address];
let v2_w = weights.weights[&v2.address];
assert_eq!(v1_w, INACTIVE_WEIGHT * 1000 * 15000 / 10000);
assert_eq!(v2_w, INACTIVE_WEIGHT * 1000);
assert_eq!(v1_w, v2_w * 3 / 2);
}
}