Skip to content

Commit 677c3f5

Browse files
committed
feat: wall clock timestamps, committee BFT, emission fix, duplicate node fix
Emission fix (lazy_rewards.rs, node.rs): - Add sentinel guard in calculate_years_since_genesis: genesis_timestamp==0 returns 0 Prevents 56-year miscalculation causing 14 halvings and 3 QNC instead of 251k QNC - Add update_genesis_timestamp() method to PhaseAwareRewardManager - Call update_genesis_timestamp at 4 critical points: genesis creation, network sync, P2P receive, and storage load on restart Committee-based BFT (block.rs, node.rs): - Add consensus_committee field Option<Vec<String>> to ConsensusData struct with serde default - Implement select_consensus_committee() using SHA3 VRF seeded from MacroBlock N-2 randomness_beacon - CONSENSUS_COMMITTEE_SIZE=100, COMMITTEE_THRESHOLD=120 - trigger_macroblock_consensus: subsample committee when total validators exceed threshold - start_macroblock_consensus_listener: non-committee validators skip BFT and receive MB via sync - Emergency macroblocks bypass committee (consensus_committee=None) - Scales MacroBlock BFT from O(n^2) to O(1) for networks up to 1000+ validators Wall clock timestamps (node.rs): - Replace slot-based timestamps (genesis_ts + height * 1s) with SystemTime::now() Old approach accumulated 20min drift per 8h, compounding to days and months over time - MicroBlock: timestamp = max(now(), parent.timestamp + 1) guarantees strict monotonicity - Emergency MacroBlock: timestamp = now() with defensive genesis existence guard - Consensus MacroBlock: timestamp = now() - Validation updated to Ethereum-style rules only: future (<=now+15s) and monotonicity (>parent.ts) - Remove slot-based past check to ensure rolling update compatibility across mixed-version networks - TIMESTAMP_FUTURE_TOLERANCE raised from 5s to 15s matching Ethereum standard - Explorer will now show correct relative times for all new transactions Duplicate node registration fix (node.rs): - Prevent same wallet address from registering a Light or Super node more than once on-chain Made-with: Cursor
1 parent 2acc8c1 commit 677c3f5

3 files changed

Lines changed: 227 additions & 172 deletions

File tree

core/qnet-consensus/src/lazy_rewards.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,14 +270,21 @@ impl PhaseAwareRewardManager {
270270
.duration_since(UNIX_EPOCH)
271271
.unwrap_or_default()
272272
.as_secs();
273-
273+
274+
// CRITICAL: genesis_timestamp=0 means genesis block not yet received (sentinel).
275+
// In that case return 0 (year 0 = no halving = full emission).
276+
// Without this guard, unix_epoch(0) as genesis gives ~56 years → 14 halvings → 3 QNC instead of 251k QNC.
277+
if self.genesis_timestamp == 0 {
278+
return 0;
279+
}
280+
274281
if now > self.genesis_timestamp {
275282
(now - self.genesis_timestamp) / (365 * 24 * 60 * 60)
276283
} else {
277284
0
278285
}
279286
}
280-
287+
281288
/// Calculate dynamic Pool 1 base emission with sharp drop halving
282289
fn calculate_pool1_base_emission(&self) -> u64 {
283290
let years_since_genesis = self.calculate_years_since_genesis();
@@ -576,6 +583,16 @@ impl PhaseAwareRewardManager {
576583
pub fn get_genesis_timestamp(&self) -> u64 {
577584
self.genesis_timestamp
578585
}
586+
587+
/// Update genesis timestamp (called when genesis block is received from network)
588+
/// CRITICAL: Must be called whenever GLOBAL_GENESIS_TIMESTAMP is set to non-zero value,
589+
/// otherwise calculate_pool1_base_emission() uses unix_epoch as genesis → years=56 → wrong halving
590+
pub fn update_genesis_timestamp(&mut self, ts: u64) {
591+
if ts > 0 && ts != self.genesis_timestamp {
592+
println!("[INFO][REWARDS] genesis_ts_updated old={} new={}", self.genesis_timestamp, ts);
593+
self.genesis_timestamp = ts;
594+
}
595+
}
579596

580597
/// Get Pool #2 transaction fees accumulated
581598
pub fn get_pool2_fees(&self) -> u64 {

core/qnet-state/src/block.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ pub struct ConsensusData {
219219
#[serde(default)]
220220
pub pool2_total_fees: Option<u64>,
221221

222+
// ═══════════════════════════════════════════════════════════════════════════
223+
// COMMITTEE-BASED BFT (v3.36)
224+
// VRF-subsampled committee for scalable MacroBlock consensus (up to 1000+ validators)
225+
// ═══════════════════════════════════════════════════════════════════════════
226+
227+
/// Committee members selected for this MacroBlock's BFT consensus
228+
/// When total validators > COMMITTEE_THRESHOLD, a VRF-subsampled committee
229+
/// of COMMITTEE_SIZE nodes handles commit-reveal. Other nodes accept the result.
230+
/// Format: sorted Vec<String> of node_ids
231+
#[serde(default)]
232+
pub consensus_committee: Option<Vec<String>>,
233+
222234
/// Pool 3: Total activation QNC collected in this emission window (Phase 2 only)
223235
/// Recorded ONLY in EMISSION MacroBlocks when Phase 2 is active
224236
/// Distribution: Equal share to ALL eligible nodes (Light + Full + Super)

0 commit comments

Comments
 (0)