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 CAPABILITY_BASELINE_BPS: u128 = 10000;
pub const CAPABILITY_MAX_BPS: u128 = 15000;
pub const CAPABILITY_TEE_SPAN_BPS: u128 = 1000;
pub fn capability_multiplier_bps(
hardware: &tenzro_types::hardware::HardwareCapabilities,
tee_attested: bool,
) -> u128 {
let class_span = CAPABILITY_MAX_BPS - CAPABILITY_BASELINE_BPS - CAPABILITY_TEE_SPAN_BPS;
let w = hardware.class().advertised_weight();
let class_frac = ((w - 0.2) / 0.8).clamp(0.0, 1.0);
let class_bonus = (class_frac * class_span as f64) as u128;
let tee_bonus = if tee_attested {
CAPABILITY_TEE_SPAN_BPS
} else {
0
};
(CAPABILITY_BASELINE_BPS + class_bonus + tee_bonus).min(CAPABILITY_MAX_BPS)
}
#[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>,
capability_scale_bps: u128,
}
impl LeaderReputation {
pub fn new(n: usize) -> Self {
Self::with_capability_scale(n, CAPABILITY_BASELINE_BPS)
}
pub fn with_capability_scale(n: usize, capability_scale_bps: u128) -> Self {
Self {
proposer_history: RwLock::new(ProposerHistory::new(n)),
voter_history: RwLock::new(VoterHistory::new(n)),
capability_scale_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 capability =
capability_multiplier_bps(&v.capability, v.has_valid_tee_attestation());
let excess = capability.saturating_sub(CAPABILITY_BASELINE_BPS);
let multiplier = CAPABILITY_BASELINE_BPS
+ excess.saturating_mul(self.capability_scale_bps) / CAPABILITY_BASELINE_BPS;
let scaled = raw.saturating_mul(multiplier) / CAPABILITY_BASELINE_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();
let mut v = ValidatorInfo::new(
address,
keypair.public_key().clone(),
pq.verifying_key_bytes().to_vec(),
bls.public_key().to_bytes().to_vec(),
stake,
);
v.capability.detected = true;
v.capability.vram_gb = 0; v
}
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
);
}
fn caps(vram_gb: u32) -> tenzro_types::hardware::HardwareCapabilities {
tenzro_types::hardware::HardwareCapabilities {
vram_gb,
detected: true,
..Default::default()
}
}
fn caps_multi_accelerator(vram_gb: u32) -> tenzro_types::hardware::HardwareCapabilities {
use tenzro_types::hardware::{GpuDevice, GpuVendor};
let gpu = |name: &str| GpuDevice {
vendor: GpuVendor::Nvidia,
name: name.to_string(),
vram_gb: vram_gb / 2,
compute_capability: "9.0".to_string(),
fp8: true,
fp4: false,
};
tenzro_types::hardware::HardwareCapabilities {
vram_gb,
detected: true,
gpus: vec![gpu("acc-0"), gpu("acc-1")],
..Default::default()
}
}
#[test]
fn capability_multiplier_spans_baseline_to_ceiling() {
use tenzro_types::hardware::HardwareCapabilities;
assert_eq!(
capability_multiplier_bps(&caps(0), false),
CAPABILITY_BASELINE_BPS
);
assert_eq!(
capability_multiplier_bps(&caps_multi_accelerator(200), true),
CAPABILITY_MAX_BPS
);
assert_eq!(
capability_multiplier_bps(&caps(0), true),
CAPABILITY_BASELINE_BPS + CAPABILITY_TEE_SPAN_BPS
);
assert_eq!(
capability_multiplier_bps(&caps_multi_accelerator(200), false),
CAPABILITY_MAX_BPS - CAPABILITY_TEE_SPAN_BPS
);
let consumer = capability_multiplier_bps(
&HardwareCapabilities {
vram_gb: 16,
detected: true,
..Default::default()
},
false,
);
let datacenter = capability_multiplier_bps(
&HardwareCapabilities {
vram_gb: 80,
detected: true,
..Default::default()
},
false,
);
let multi = capability_multiplier_bps(&caps_multi_accelerator(200), false);
assert!(consumer < datacenter);
assert!(datacenter < multi);
}
#[test]
fn capability_weight_lifts_stronger_validator() {
let v1 = validator(1000)
.with_capability(caps_multi_accelerator(200))
.with_tee_attestation(
tenzro_types::tee::AttestationReport::default(),
tenzro_types::tee::AttestationResult::success(
tenzro_types::tee::TeeVendor::IntelTdx,
vec![0u8; 32],
),
);
let v2 = validator(1000).with_capability(caps(0));
let set = vset(vec![v1.clone(), v2.clone()]);
let lr = LeaderReputation::new(2);
let weights = lr.compute_weights(5, &set);
let v1_w = weights.weights[&v1.address];
let v2_w = weights.weights[&v2.address];
assert_eq!(
v1_w,
INACTIVE_WEIGHT * 1000 * CAPABILITY_MAX_BPS / CAPABILITY_BASELINE_BPS
);
assert_eq!(v2_w, INACTIVE_WEIGHT * 1000);
assert_eq!(v1_w, v2_w * 3 / 2);
}
#[test]
fn capability_scale_zero_disables_the_bias() {
let v1 = validator(1000)
.with_capability(caps_multi_accelerator(200))
.with_tee_attestation(
tenzro_types::tee::AttestationReport::default(),
tenzro_types::tee::AttestationResult::success(
tenzro_types::tee::TeeVendor::IntelTdx,
vec![0u8; 32],
),
);
let v2 = validator(1000).with_capability(caps(0));
let set = vset(vec![v1.clone(), v2.clone()]);
let lr = LeaderReputation::with_capability_scale(2, 0);
let weights = lr.compute_weights(5, &set);
assert_eq!(weights.weights[&v1.address], weights.weights[&v2.address]);
}
}