feat(consensus): weighted rotation, skip tracking, auto-unban

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-24 21:47:40 +03:00
parent ca388b4bc2
commit 5e00c6f090
3 changed files with 78 additions and 7 deletions

View file

@ -7,6 +7,15 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
## [Unreleased]
## [0.10.0] — 2026-04-24
### Added
- `block_loop`: her slot başında süresi dolan ban'lar kaldırılır (`unban_expired_validators`)
- `block_loop`: beklenen validator blok üretmezse `record_validator_skip` ile skip_count artar; 10 art arda skip → deactivation
### Changed
- `validator_set::weighted_shuffle`: deterministik ağırlıklı sıralama — her adaya `hash(seed||i)^(1/pon_score²)` skoru atanır; yüksek PoN skoru schedule'da öne geçer
## [0.9.0] — 2026-04-24
### Added

View file

@ -70,14 +70,28 @@ impl ValidatorSet {
h.finalize().into()
}
/// Weighted sort: each candidate gets score = hash(seed||i)^(1/pon_score²).
/// Higher PoN score → smaller exponent → larger value → earlier in schedule.
fn weighted_shuffle(candidates: &mut Vec<&ValidatorRecord>, seed: &[u8; 32]) {
for i in (1..candidates.len()).rev() {
let key = u64::from_le_bytes(seed[..8].try_into().unwrap())
.wrapping_add(i as u64)
.wrapping_mul(candidates[i].pon_score.to_bits());
let j = (key as usize) % (i + 1);
candidates.swap(i, j);
}
let mut scores: Vec<(usize, f64)> = candidates
.iter()
.enumerate()
.map(|(i, v)| {
let mut h = Sha256::new();
h.update(seed);
h.update((i as u64).to_le_bytes());
let hash_val = u64::from_le_bytes(h.finalize()[..8].try_into().unwrap());
let u = (hash_val as f64) / (u64::MAX as f64 + 1.0);
let weighted = u.powf(1.0 / (v.pon_score * v.pon_score));
(i, weighted)
})
.collect();
scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let ordered: Vec<&ValidatorRecord> = scores.iter().map(|(i, _)| candidates[*i]).collect();
candidates.clear();
candidates.extend(ordered);
}
}

View file

@ -9,6 +9,7 @@ use nu_block::{
};
use nu_consensus::{
slot::current_slot,
slashing::record_skip,
types::ValidatorRecord,
validator_set::ValidatorSet,
};
@ -58,12 +59,23 @@ pub async fn run(
load_validator_set(&db_guard)
};
// Try unban any validators whose ban period has expired
{
let db_guard = db.lock().await;
unban_expired_validators(&db_guard, slot);
}
// Rotation check: in non-dev mode skip if not our slot
if !config.dev_mode {
let expected = validator_set.slot_producer(slot, &prev_hash);
match &expected {
Some(addr) if addr != &config.validator_addr => {
tracing::debug!(slot, expected = %addr, "not our slot — skipping");
// Record a skip for the expected producer who didn't produce
{
let db_guard = db.lock().await;
record_validator_skip(&db_guard, addr);
}
continue;
}
None => {
@ -156,6 +168,42 @@ pub async fn run(
}
}
fn unban_expired_validators(db: &StateDb, current_slot: u32) {
let records: Vec<ValidatorState> = db.scan_prefix("validator:");
for mut vs in records {
if vs.ban_until_slot > 0 && current_slot >= vs.ban_until_slot {
vs.is_active = true;
vs.ban_until_slot = 0;
let _ = db.put(&format!("validator:{}", vs.address), &vs);
tracing::info!(address = %vs.address, slot = current_slot, "validator unbanned");
}
}
}
fn record_validator_skip(db: &StateDb, address: &str) {
if let Ok(Some(mut vs)) = db.get::<ValidatorState>(&format!("validator:{address}")) {
let mut record = ValidatorRecord {
address: vs.address.clone(),
stake: vs.stake,
pon_score: vs.pon_score,
is_active: vs.is_active,
last_block: vs.last_block,
slash_count: vs.slash_count,
skip_count: vs.skip_count,
consecutive_blocks: vs.consecutive_blocks,
ban_until_slot: vs.ban_until_slot,
};
let deactivated = record_skip(&mut record);
vs.skip_count = record.skip_count;
vs.consecutive_blocks = record.consecutive_blocks;
vs.is_active = record.is_active;
let _ = db.put(&format!("validator:{address}"), &vs);
if deactivated {
tracing::warn!(address, "validator deactivated after 10 consecutive skips");
}
}
}
fn load_validator_set(db: &StateDb) -> ValidatorSet {
let mut set = ValidatorSet::new();
let records: Vec<ValidatorState> = db.scan_prefix("validator:");