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]
|
## [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
|
## [0.9.0] — 2026-04-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -70,14 +70,28 @@ impl ValidatorSet {
|
||||||
h.finalize().into()
|
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]) {
|
fn weighted_shuffle(candidates: &mut Vec<&ValidatorRecord>, seed: &[u8; 32]) {
|
||||||
for i in (1..candidates.len()).rev() {
|
let mut scores: Vec<(usize, f64)> = candidates
|
||||||
let key = u64::from_le_bytes(seed[..8].try_into().unwrap())
|
.iter()
|
||||||
.wrapping_add(i as u64)
|
.enumerate()
|
||||||
.wrapping_mul(candidates[i].pon_score.to_bits());
|
.map(|(i, v)| {
|
||||||
let j = (key as usize) % (i + 1);
|
let mut h = Sha256::new();
|
||||||
candidates.swap(i, j);
|
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::{
|
use nu_consensus::{
|
||||||
slot::current_slot,
|
slot::current_slot,
|
||||||
|
slashing::record_skip,
|
||||||
types::ValidatorRecord,
|
types::ValidatorRecord,
|
||||||
validator_set::ValidatorSet,
|
validator_set::ValidatorSet,
|
||||||
};
|
};
|
||||||
|
|
@ -58,12 +59,23 @@ pub async fn run(
|
||||||
load_validator_set(&db_guard)
|
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
|
// Rotation check: in non-dev mode skip if not our slot
|
||||||
if !config.dev_mode {
|
if !config.dev_mode {
|
||||||
let expected = validator_set.slot_producer(slot, &prev_hash);
|
let expected = validator_set.slot_producer(slot, &prev_hash);
|
||||||
match &expected {
|
match &expected {
|
||||||
Some(addr) if addr != &config.validator_addr => {
|
Some(addr) if addr != &config.validator_addr => {
|
||||||
tracing::debug!(slot, expected = %addr, "not our slot — skipping");
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
None => {
|
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 {
|
fn load_validator_set(db: &StateDb) -> ValidatorSet {
|
||||||
let mut set = ValidatorSet::new();
|
let mut set = ValidatorSet::new();
|
||||||
let records: Vec<ValidatorState> = db.scan_prefix("validator:");
|
let records: Vec<ValidatorState> = db.scan_prefix("validator:");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue