From 5e00c6f090e739712ad0c74c021bf219c7151bf48486b46657c02cf6218e268b Mon Sep 17 00:00:00 2001 From: Mukan Erkin Date: Fri, 24 Apr 2026 21:47:40 +0300 Subject: [PATCH] feat(consensus): weighted rotation, skip tracking, auto-unban Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 +++++ crates/nu-consensus/src/validator_set.rs | 28 ++++++++++---- src/block_loop.rs | 48 ++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c2be7..1094528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/nu-consensus/src/validator_set.rs b/crates/nu-consensus/src/validator_set.rs index e2a0f96..fa280db 100644 --- a/crates/nu-consensus/src/validator_set.rs +++ b/crates/nu-consensus/src/validator_set.rs @@ -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); } } diff --git a/src/block_loop.rs b/src/block_loop.rs index da62f46..2d07661 100644 --- a/src/block_loop.rs +++ b/src/block_loop.rs @@ -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 = 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::(&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 = db.scan_prefix("validator:");