feat(consensus): weighted rotation, skip tracking, auto-unban
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ca388b4bc2
commit
5e00c6f090
3 changed files with 78 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue