lux_consensus/lib.rs
1// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
2// See the file LICENSE for licensing terms.
3
4//! # Lux Consensus Rust SDK
5//!
6//! Complete Quasar consensus implementation with Wave, FPC, Photon, Focus protocols.
7//! Full post-quantum support via Pulsar (Module-LWE threshold) hybrid signatures.
8//!
9//! ## Features
10//!
11//! - **Wave**: Threshold voting with FPC-based adaptive thresholds
12//! - **FPC**: Fast Probabilistic Consensus via PRF-derived thresholds
13//! - **Photon**: Light-based validator sampling with luminance tracking
14//! - **Focus**: Confidence accumulation through β consecutive rounds
15//! - **Quasar**: Post-quantum finality with hybrid BLS + Pulsar signatures
16//! - **Vote**: A signed vote on the wire, and the tally that turns arriving
17//! votes into a certificate the whole `cert` predicate has already passed
18//! - **Zap**: The transport frame those votes travel under — the same wire Go
19//! and C++ speak
20//!
21//! ## Example
22//!
23//! ```rust,no_run
24//! use lux_consensus::*;
25//!
26//! fn main() {
27//! // Create Quasar consensus engine (full protocol stack)
28//! let config = QuasarConfig::mainnet();
29//! let mut engine = QuasarEngine::new(config);
30//! engine.start().unwrap();
31//!
32//! // The committee. Only its members' ballots are counted.
33//! for i in 0..20 {
34//! engine.add_validator(NodeID::from([i; 20]), 1).unwrap();
35//! }
36//!
37//! // Add a block
38//! let block = Block::new(
39//! ID::from([1u8; 32]),
40//! ID::from([0u8; 32]),
41//! 1,
42//! b"Hello, Lux!".to_vec(),
43//! );
44//! engine.add(block.clone()).unwrap();
45//!
46//! // Record votes (20 for mainnet quorum)
47//! for i in 0..20 {
48//! let vote = Vote::new(
49//! block.id.clone(),
50//! VoteType::Preference,
51//! NodeID::from([i; 20]),
52//! );
53//! engine.record_vote(vote).unwrap();
54//! }
55//!
56//! assert!(engine.is_accepted(&block.id));
57//! engine.stop().unwrap();
58//! }
59//! ```
60
61use std::collections::HashMap;
62use std::sync::{Arc, RwLock};
63use std::time::{Duration, Instant, SystemTime};
64
65// The finality standard: the bytes a validator signs and the thresholds that
66// decide. Held to the Go definitions by tests/conformance.rs.
67pub mod finality;
68
69// The quorum certificate: the predicate that decides whether a block is
70// accepted. Held to `engine/chain/cert.go` by tests/cert.rs.
71pub mod cert;
72pub mod pop;
73
74// The vote plane: what one validator puts on the wire, and the tally that turns
75// what arrives into a certificate. It states no accept rule of its own — the
76// rule is `cert`'s, and `vote::Tally::cert` is the only door out of a tally.
77pub mod vote;
78
79// The ZAP transport frame the vote plane travels under — the same wire Go's
80// `luxfi/api/zap` writes and `luxcpp/zap` reads. Layer A only: nothing in it
81// knows what a vote is.
82pub mod zap;
83
84/// SHA-256. The crate's one hash, from the BLS library already linked here.
85///
86/// Every derivation the network agrees on runs through this — there is no
87/// second hash and no "close enough" mixing function. Go computes the same
88/// bytes with `crypto/sha256`.
89pub fn sha256(input: &[u8]) -> [u8; 32] {
90 let mut out = [0u8; 32];
91 // SAFETY: blst_sha256 writes exactly 32 bytes to `out` and reads `len`
92 // bytes from `input`; both are sized here from the slices themselves.
93 unsafe { blst::blst_sha256(out.as_mut_ptr(), input.as_ptr(), input.len()) };
94 out
95}
96
97// Re-export all public types
98pub use crate::engine::*;
99pub use crate::errors::*;
100pub use crate::finality::{
101 canonical_vote_message, crash_tolerance, half_stake_floor, nova_beta, nova_quorum,
102 nova_signer_floor, signer_floor, two_thirds_count, two_thirds_stake_floor, weighted_quasar,
103 Finality, Position, QC_FINALITY, QUORUM_CERT_VERSION, VOTE_MESSAGE_LEN, VOTE_TAG,
104};
105pub use crate::focus::*;
106pub use crate::fpc::*;
107pub use crate::photon::*;
108pub use crate::quasar::*;
109pub use crate::types::*;
110pub use crate::vote::{SignedVote, Slot, Tally, VoteTransport, VOTE};
111pub use crate::wave::*;
112
113// ============= TYPES MODULE =============
114
115pub mod types {
116 use std::fmt;
117 use std::time::{Duration, SystemTime};
118
119 /// 32-byte identifier type
120 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
121 pub struct ID(pub [u8; 32]);
122
123 impl ID {
124 pub fn new(data: [u8; 32]) -> Self {
125 ID(data)
126 }
127
128 pub fn zero() -> Self {
129 ID([0u8; 32])
130 }
131
132 pub fn from_slice(data: &[u8]) -> Self {
133 let mut arr = [0u8; 32];
134 let len = data.len().min(32);
135 arr[..len].copy_from_slice(&data[..len]);
136 ID(arr)
137 }
138
139 pub fn to_vec(&self) -> Vec<u8> {
140 self.0.to_vec()
141 }
142
143 pub fn as_bytes(&self) -> &[u8; 32] {
144 &self.0
145 }
146 }
147
148 impl From<[u8; 32]> for ID {
149 fn from(data: [u8; 32]) -> Self {
150 ID(data)
151 }
152 }
153
154 impl From<Vec<u8>> for ID {
155 fn from(data: Vec<u8>) -> Self {
156 ID::from_slice(&data)
157 }
158 }
159
160 impl fmt::Display for ID {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 write!(f, "{}", hex::encode(self.0))
163 }
164 }
165
166 /// A validator, by the 20-byte NodeID the network names it by.
167 ///
168 /// It is a type of its own and not an alias of [`ID`], because a node is not
169 /// a block. Go's `ids.NodeID` is 20 bytes and the proof of possession a
170 /// validator registers with signs `node ‖ key` over exactly those 20 — so a
171 /// set that named validators by the 32-byte block id would compute a
172 /// different preimage from the same registrant and could not check the
173 /// network's proofs at all. Being a distinct type, the two cannot be passed
174 /// for one another by accident.
175 ///
176 /// There is deliberately no lossy constructor. `ID::from_slice` pads and
177 /// truncates, which is convenient for a hash and is exactly the hazard here:
178 /// truncating 32 bytes into 20 maps distinct nodes onto one identity, and
179 /// one identity is one signer slot and one share of the weight. The only way
180 /// in is 20 bytes that are already 20 bytes.
181 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
182 pub struct NodeID(pub crate::pop::NodeId);
183
184 impl NodeID {
185 pub fn as_bytes(&self) -> &crate::pop::NodeId {
186 &self.0
187 }
188 }
189
190 impl From<crate::pop::NodeId> for NodeID {
191 fn from(data: crate::pop::NodeId) -> Self {
192 NodeID(data)
193 }
194 }
195
196 impl fmt::Display for NodeID {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(f, "{}", hex::encode(self.0))
199 }
200 }
201
202 pub type Hash = ID;
203
204 /// Block status in consensus
205 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
206 pub enum Status {
207 Unknown,
208 Processing,
209 Rejected,
210 Accepted,
211 }
212
213 /// Consensus decision result
214 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
215 pub enum Decision {
216 Undecided,
217 Accept,
218 Reject,
219 }
220
221 /// Vote type for consensus
222 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
223 pub enum VoteType {
224 Preference, // Initial preference vote
225 Commit, // Final commit vote
226 Cancel, // Reject/cancel vote
227 }
228
229 /// Block in the blockchain
230 #[derive(Debug, Clone)]
231 pub struct Block {
232 pub id: ID,
233 pub parent_id: ID,
234 pub height: u64,
235 pub payload: Vec<u8>,
236 pub timestamp: SystemTime,
237 }
238
239 impl Block {
240 pub fn new(id: ID, parent_id: ID, height: u64, payload: Vec<u8>) -> Self {
241 Block {
242 id,
243 parent_id,
244 height,
245 payload,
246 timestamp: SystemTime::now(),
247 }
248 }
249
250 pub fn genesis() -> Self {
251 Block {
252 id: ID::zero(),
253 parent_id: ID::zero(),
254 height: 0,
255 payload: Vec::new(),
256 timestamp: SystemTime::UNIX_EPOCH,
257 }
258 }
259 }
260
261 /// Vote on a block
262 #[derive(Debug, Clone)]
263 pub struct Vote {
264 pub block_id: ID,
265 pub vote_type: VoteType,
266 pub voter: NodeID,
267 pub signature: Vec<u8>,
268 pub timestamp: SystemTime,
269 }
270
271 impl Vote {
272 pub fn new(block_id: ID, vote_type: VoteType, voter: NodeID) -> Self {
273 Vote {
274 block_id,
275 vote_type,
276 voter,
277 signature: Vec::new(),
278 timestamp: SystemTime::now(),
279 }
280 }
281
282 pub fn with_signature(mut self, signature: Vec<u8>) -> Self {
283 self.signature = signature;
284 self
285 }
286
287 pub fn prefer(&self) -> bool {
288 matches!(self.vote_type, VoteType::Preference | VoteType::Commit)
289 }
290 }
291
292 /// A finality certificate is a [`crate::cert::QuorumCert`] — one position and
293 /// the distinct signed accepts that carry it.
294 ///
295 /// There is one certificate type in this crate and it is the Go one. The
296 /// type this name used to denote carried an aggregate signature and a pair
297 /// of header fields, `block_id` and `height`, that no signature covered; it
298 /// was forgeable by a registered rogue key and re-labellable to any block.
299 /// Both faults were properties of its shape, so the shape is gone: what a
300 /// certificate claims is its `position`, and every claim is signed.
301 pub type Certificate = crate::cert::QuorumCert;
302
303 /// Security level for Corona post-quantum crypto
304 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
305 pub enum SecurityLevel {
306 Low = 2, // Corona Level 2
307 #[default]
308 Medium = 3, // Corona Level 3 - Default
309 High = 5, // Corona Level 5
310 }
311
312 /// Quasar consensus configuration
313 #[derive(Debug, Clone)]
314 pub struct QuasarConfig {
315 // Wave parameters
316 pub k: usize, // Sample/committee size
317 pub alpha: f64, // Fixed threshold ratio (0.5-0.8)
318 pub beta: u32, // Consecutive rounds for finality
319 pub round_timeout: Duration, // Round timeout
320
321 // FPC parameters
322 pub enable_fpc: bool, // Enable FPC adaptive thresholds
323 pub theta_min: f64, // Minimum FPC threshold (0.5)
324 pub theta_max: f64, // Maximum FPC threshold (0.8)
325 pub fpc_seed: [u8; 32], // PRF seed for FPC
326
327 // Photon parameters
328 pub base_luminance: f64, // Base luminance in lux (100.0)
329 pub max_luminance: f64, // Maximum luminance (1000.0)
330 pub min_luminance: f64, // Minimum luminance (10.0)
331 pub success_multiplier: f64, // Success brightens (1.1)
332 pub failure_multiplier: f64, // Failure dims (0.9)
333
334 // Network parameters
335 pub network_timeout: Duration,
336 pub max_message_size: usize,
337 pub max_outstanding: usize,
338
339 // Security parameters
340 pub security_level: SecurityLevel,
341 pub quantum_resistant: bool,
342 pub gpu_acceleration: bool,
343 }
344
345 /// The FPC seed a node uses when no epoch seed has been derived yet.
346 ///
347 /// It is the seed the Go selector defaults to, byte for byte, so a default
348 /// Rust node and a default Go node draw the same θ schedule. In production
349 /// the seed comes from the epoch instead — `sha256(epoch ‖ chain ‖ last
350 /// finalized block)` — which is unpredictable until the previous epoch
351 /// finalizes.
352 pub const DEFAULT_FPC_SEED: [u8; 32] = *b"lux-fpc-default-seed-00000000000";
353
354 /// Testnet's standing seed, until epoch derivation is wired.
355 pub const TESTNET_FPC_SEED: [u8; 32] = *b"lux-testnet-fpc-seed-00000000000";
356
357 /// Mainnet's standing seed, until epoch derivation is wired.
358 pub const MAINNET_FPC_SEED: [u8; 32] = *b"lux-mainnet-fpc-secure-seed-2025";
359
360 /// The balanced configuration. The two network presets below are stated as
361 /// what they change about it, so a field that ought to be shared cannot
362 /// quietly drift in one of three copies.
363 impl Default for QuasarConfig {
364 fn default() -> Self {
365 QuasarConfig {
366 // Wave
367 k: 20,
368 alpha: 0.69, // 69% quorum - 2% above standard 67%
369 beta: 20,
370 round_timeout: Duration::from_millis(100),
371
372 // FPC
373 enable_fpc: true,
374 theta_min: 0.5,
375 theta_max: 0.8,
376 fpc_seed: DEFAULT_FPC_SEED,
377
378 // Photon
379 base_luminance: 100.0,
380 max_luminance: 1000.0,
381 min_luminance: 10.0,
382 success_multiplier: 1.1,
383 failure_multiplier: 0.9,
384
385 // Network
386 network_timeout: Duration::from_secs(5),
387 max_message_size: 2 * 1024 * 1024, // 2MB
388 max_outstanding: 10,
389
390 // Security
391 security_level: SecurityLevel::Medium,
392 quantum_resistant: true,
393 gpu_acceleration: true,
394 }
395 }
396 }
397
398 impl QuasarConfig {
399 /// Testnet: a small committee, short rounds, fixed thresholds.
400 pub fn testnet() -> Self {
401 QuasarConfig {
402 k: 5,
403 alpha: 0.6,
404 beta: 5,
405 round_timeout: Duration::from_millis(50),
406 enable_fpc: false,
407 theta_max: 0.7,
408 fpc_seed: TESTNET_FPC_SEED,
409 max_luminance: 500.0,
410 min_luminance: 20.0,
411 success_multiplier: 1.05,
412 failure_multiplier: 0.95,
413 network_timeout: Duration::from_secs(10),
414 max_message_size: 1024 * 1024,
415 max_outstanding: 5,
416 security_level: SecurityLevel::Low,
417 quantum_resistant: false,
418 gpu_acceleration: false,
419 ..QuasarConfig::default()
420 }
421 }
422
423 /// Mainnet: an odd committee of 21 for tie-breaking, at the highest
424 /// security level. Everything else is the balanced default.
425 pub fn mainnet() -> Self {
426 QuasarConfig {
427 k: 21,
428 fpc_seed: MAINNET_FPC_SEED,
429 security_level: SecurityLevel::High,
430 ..QuasarConfig::default()
431 }
432 }
433
434 /// Calculate alpha threshold as integer count
435 pub fn alpha_count(&self) -> usize {
436 (self.alpha * self.k as f64).ceil() as usize
437 }
438 }
439}
440
441// ============= ERRORS MODULE =============
442
443pub mod errors {
444 use std::error::Error;
445 use std::fmt;
446
447 use crate::cert::CertError;
448
449 /// Consensus error type.
450 ///
451 /// `#[non_exhaustive]`: a refusal is a fact about the protocol, and the
452 /// protocol acquires facts. Matching one arm at a time is what a caller wants;
453 /// being broken by a clause that did not exist when the match was written is
454 /// not. A caller that must handle every case writes a wildcard and keeps
455 /// compiling.
456 #[derive(Debug)]
457 #[non_exhaustive]
458 pub enum ConsensusError {
459 BlockNotFound,
460 InvalidBlock,
461 InvalidVote,
462 InvalidSignature,
463 NoQuorum,
464 AlreadyVoted,
465 NotValidator,
466 Timeout,
467 NotInitialized,
468 AlreadyStarted,
469 /// A certificate clause refused, carried whole rather than flattened.
470 ///
471 /// The floors a certificate is held to each name a different fact — a
472 /// quorum the set does not derive, a stake majority the voters do not
473 /// hold, a signing set too small for a supermajority to absorb a fault —
474 /// and answering all of them with `NoQuorum` tells an operator only that
475 /// something did not reach something. Assembly says "not yet"; a floor
476 /// says which floor.
477 Cert(CertError),
478 CryptoError(String),
479 NetworkError(String),
480 Other(String),
481 }
482
483 impl From<CertError> for ConsensusError {
484 fn from(e: CertError) -> Self {
485 ConsensusError::Cert(e)
486 }
487 }
488
489 impl fmt::Display for ConsensusError {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 match self {
492 ConsensusError::BlockNotFound => write!(f, "Block not found"),
493 ConsensusError::InvalidBlock => write!(f, "Invalid block"),
494 ConsensusError::InvalidVote => write!(f, "Invalid vote"),
495 ConsensusError::InvalidSignature => write!(f, "Invalid signature"),
496 ConsensusError::NoQuorum => write!(f, "No quorum reached"),
497 ConsensusError::AlreadyVoted => write!(f, "Already voted"),
498 ConsensusError::NotValidator => write!(f, "Not a validator"),
499 ConsensusError::Timeout => write!(f, "Operation timeout"),
500 ConsensusError::NotInitialized => write!(f, "Engine not initialized"),
501 ConsensusError::AlreadyStarted => write!(f, "Engine already started"),
502 ConsensusError::Cert(e) => write!(f, "Certificate refused: {e}"),
503 ConsensusError::CryptoError(msg) => write!(f, "Crypto error: {}", msg),
504 ConsensusError::NetworkError(msg) => write!(f, "Network error: {}", msg),
505 ConsensusError::Other(msg) => write!(f, "{}", msg),
506 }
507 }
508 }
509
510 impl Error for ConsensusError {}
511
512 /// Result type alias
513 pub type Result<T> = std::result::Result<T, ConsensusError>;
514}
515
516// ============= FPC MODULE - Fast Probabilistic Consensus =============
517
518pub mod fpc {
519 use super::*;
520
521 /// FPC threshold selector using PRF for deterministic phase-dependent thresholds
522 ///
523 /// Formula: α(phase, k) = ⌈θ(phase) · k⌉
524 /// Where θ(phase) = θ_min + PRF(seed, phase) * (θ_max - θ_min)
525 #[derive(Debug, Clone)]
526 pub struct FpcSelector {
527 theta_min: f64,
528 theta_max: f64,
529 seed: [u8; 32],
530 }
531
532 impl FpcSelector {
533 /// Create a new FPC selector with custom parameters
534 pub fn new(theta_min: f64, theta_max: f64, seed: [u8; 32]) -> Self {
535 let theta_min = if theta_min > 0.0 && theta_min < 1.0 {
536 theta_min
537 } else {
538 0.5
539 };
540 let theta_max = if theta_max > theta_min && theta_max <= 1.0 {
541 theta_max
542 } else {
543 0.8
544 };
545
546 FpcSelector {
547 theta_min,
548 theta_max,
549 seed,
550 }
551 }
552
553 /// θ for a phase, from the PRF the network runs.
554 ///
555 /// `θ(phase) = θ_min + sha256(seed ‖ be64(phase))[0..8] / (2⁶⁴−1) · (θ_max − θ_min)`
556 ///
557 /// This must be SHA-256 and nothing else. Go computes the quorum
558 /// threshold this way (`protocol/wave/fpc.computeTheta`), and two nodes
559 /// that derive different θ for one phase require different majorities of
560 /// the same committee — a fork by arithmetic, with no bad actor in it.
561 /// `tests/fpc_conformance.rs` holds this to the Go values.
562 fn compute_theta(&self, phase: u64) -> f64 {
563 let mut input = [0u8; 40];
564 input[..32].copy_from_slice(&self.seed);
565 input[32..40].copy_from_slice(&phase.to_be_bytes());
566
567 let hash = sha256(&input);
568
569 // Big-endian, first 8 bytes — the same window and order Go reads.
570 let hash_u64 = u64::from_be_bytes([
571 hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7],
572 ]);
573 let normalized = (hash_u64 as f64) / (u64::MAX as f64);
574
575 self.theta_min + normalized * (self.theta_max - self.theta_min)
576 }
577
578 /// Select threshold α for given phase and committee size k
579 pub fn select_threshold(&self, phase: u64, k: usize) -> usize {
580 let theta = self.compute_theta(phase);
581 (theta * k as f64).ceil() as usize
582 }
583
584 /// Get raw theta value for a phase (for debugging/testing)
585 pub fn theta(&self, phase: u64) -> f64 {
586 self.compute_theta(phase)
587 }
588
589 /// Get configured range
590 pub fn range(&self) -> (f64, f64) {
591 (self.theta_min, self.theta_max)
592 }
593 }
594
595 /// The default selector reads the default configuration, so the θ range and
596 /// the seed have one home apiece.
597 impl Default for FpcSelector {
598 fn default() -> Self {
599 let c = QuasarConfig::default();
600 FpcSelector::new(c.theta_min, c.theta_max, c.fpc_seed)
601 }
602 }
603}
604
605// ============= PHOTON MODULE - Light-Based Validator Sampling =============
606
607pub mod photon {
608 use super::*;
609
610 /// Luminance tracks node brightness based on consensus participation
611 ///
612 /// Successful votes increase brightness, failures decrease it.
613 /// Based on real-world lighting levels:
614 /// - 100 lux: Base (office lighting)
615 /// - 1000 lux: Maximum (daylight)
616 /// - 10 lux: Minimum (twilight)
617 #[derive(Debug, Clone)]
618 pub struct Luminance {
619 lux: HashMap<NodeID, f64>,
620 base: f64,
621 max: f64,
622 min: f64,
623 success_mult: f64,
624 failure_mult: f64,
625 }
626
627 impl Luminance {
628 /// Create new luminance tracker with config
629 pub fn new(config: &QuasarConfig) -> Self {
630 Luminance {
631 lux: HashMap::new(),
632 base: config.base_luminance,
633 max: config.max_luminance,
634 min: config.min_luminance,
635 success_mult: config.success_multiplier,
636 failure_mult: config.failure_multiplier,
637 }
638 }
639
640 /// Update brightness based on vote success/failure
641 pub fn illuminate(&mut self, id: &NodeID, success: bool) {
642 let current = self.lux.entry(*id).or_insert(self.base);
643
644 if success {
645 *current *= self.success_mult;
646 if *current > self.max {
647 *current = self.max;
648 }
649 } else {
650 *current *= self.failure_mult;
651 if *current < self.min {
652 *current = self.min;
653 }
654 }
655 }
656
657 /// Get normalized brightness (0.1 to 10.0)
658 pub fn brightness(&self, id: &NodeID) -> f64 {
659 self.lux.get(id).copied().unwrap_or(self.base) / self.base
660 }
661
662 /// Get raw lux value
663 pub fn lux(&self, id: &NodeID) -> f64 {
664 self.lux.get(id).copied().unwrap_or(self.base)
665 }
666
667 /// Get total luminance across all nodes
668 pub fn total_luminance(&self) -> f64 {
669 self.lux.values().sum()
670 }
671
672 /// Get number of tracked nodes
673 pub fn node_count(&self) -> usize {
674 self.lux.len()
675 }
676 }
677
678 /// The default tracker reads the default configuration rather than
679 /// restating its five numbers, which is how the two used to disagree.
680 impl Default for Luminance {
681 fn default() -> Self {
682 Luminance::new(&QuasarConfig::default())
683 }
684 }
685
686 /// Photon sampler for peer selection
687 pub struct PhotonSampler {
688 peers: Vec<NodeID>,
689 luminance: Luminance,
690 }
691
692 impl PhotonSampler {
693 /// Create new photon sampler
694 pub fn new(peers: Vec<NodeID>, config: &QuasarConfig) -> Self {
695 PhotonSampler {
696 peers,
697 luminance: Luminance::new(config),
698 }
699 }
700
701 /// Sample k peers weighted by luminance
702 pub fn sample(&self, k: usize) -> Vec<NodeID> {
703 if self.peers.is_empty() {
704 return Vec::new();
705 }
706
707 let k = k.min(self.peers.len());
708
709 // Calculate weights based on luminance
710 let weights: Vec<f64> = self
711 .peers
712 .iter()
713 .map(|p| self.luminance.brightness(p))
714 .collect();
715
716 let total_weight: f64 = weights.iter().sum();
717 if total_weight == 0.0 {
718 // Fallback to uniform sampling
719 return self.peers.iter().take(k).cloned().collect();
720 }
721
722 // Simple deterministic weighted selection
723 let mut selected = Vec::with_capacity(k);
724 let mut used = vec![false; self.peers.len()];
725
726 for i in 0..k {
727 let mut best_idx = 0;
728 let mut best_score = f64::MIN;
729
730 for (idx, &weight) in weights.iter().enumerate() {
731 if used[idx] {
732 continue;
733 }
734 // Score = weight * deterministic factor based on position
735 let score = weight * ((idx + i + 1) as f64 / self.peers.len() as f64);
736 if score > best_score {
737 best_score = score;
738 best_idx = idx;
739 }
740 }
741
742 used[best_idx] = true;
743 selected.push(self.peers[best_idx]);
744 }
745
746 selected
747 }
748
749 /// Update luminance after vote result
750 pub fn update_luminance(&mut self, id: &NodeID, success: bool) {
751 self.luminance.illuminate(id, success);
752 }
753
754 /// Add a peer
755 pub fn add_peer(&mut self, peer: NodeID) {
756 if !self.peers.contains(&peer) {
757 self.peers.push(peer);
758 }
759 }
760
761 /// Remove a peer
762 pub fn remove_peer(&mut self, peer: &NodeID) {
763 self.peers.retain(|p| p != peer);
764 }
765
766 /// Get luminance reference
767 pub fn luminance(&self) -> &Luminance {
768 &self.luminance
769 }
770 }
771}
772
773// ============= FOCUS MODULE - Confidence Accumulation =============
774
775pub mod focus {
776 use super::*;
777
778 /// Focus tracks confidence building for consensus through consecutive rounds
779 ///
780 /// A block achieves finality when it receives β consecutive rounds of
781 /// votes above the alpha threshold.
782 #[derive(Debug)]
783 pub struct Focus<ID: Eq + std::hash::Hash + Clone> {
784 threshold: u32, // β - consecutive rounds needed
785 alpha: f64, // Ratio threshold
786 states: HashMap<ID, FocusState>,
787 }
788
789 /// Internal state for a single item
790 #[derive(Debug, Clone)]
791 pub struct FocusState {
792 pub confidence: u32, // Consecutive rounds count
793 pub preference: bool, // Current preference (yes/no)
794 pub decided: bool, // Has reached finality
795 pub decision: Decision, // Final decision
796 pub last_ratio: f64, // Last vote ratio
797 }
798
799 impl Default for FocusState {
800 fn default() -> Self {
801 FocusState {
802 confidence: 0,
803 preference: false,
804 decided: false,
805 decision: Decision::Undecided,
806 last_ratio: 0.0,
807 }
808 }
809 }
810
811 /// What one round said: a quorum for, a quorum against, or neither.
812 ///
813 /// A round produces this; confidence is a fold over a sequence of them.
814 /// Keeping the two apart is what lets the tally be counted in whatever unit
815 /// suits the caller — Wave counts votes, Focus takes a ratio — while the
816 /// rule about consecutive agreement stays in exactly one place.
817 pub type Verdict = Option<bool>;
818
819 /// Fold one verdict into a running confidence, and report a decision when
820 /// `beta` consecutive rounds have agreed.
821 ///
822 /// A verdict that agrees with the standing preference deepens confidence; a
823 /// verdict that opposes it replaces the preference and starts again at one;
824 /// no quorum at all resets to zero. That reset is the point of the
825 /// mechanism — β must be β *consecutive* rounds, or a block could
826 /// accumulate agreement across rounds that disagreed in between.
827 pub fn accumulate(
828 preference: &mut bool,
829 confidence: &mut u32,
830 verdict: Verdict,
831 beta: u32,
832 ) -> Option<Decision> {
833 match verdict {
834 Some(v) if *preference == v => *confidence += 1,
835 Some(v) => {
836 *preference = v;
837 *confidence = 1;
838 }
839 None => *confidence = 0,
840 }
841
842 if *confidence >= beta {
843 Some(if *preference {
844 Decision::Accept
845 } else {
846 Decision::Reject
847 })
848 } else {
849 None
850 }
851 }
852
853 impl<ID: Eq + std::hash::Hash + Clone> Focus<ID> {
854 /// Create new focus tracker
855 pub fn new(threshold: u32, alpha: f64) -> Self {
856 Focus {
857 threshold,
858 alpha,
859 states: HashMap::new(),
860 }
861 }
862
863 /// Update confidence based on vote ratio
864 ///
865 /// Returns true if decision was just reached
866 pub fn update(&mut self, id: ID, yes_votes: usize, total_votes: usize) -> bool {
867 if total_votes == 0 {
868 return false;
869 }
870
871 let ratio = yes_votes as f64 / total_votes as f64;
872 let beta = self.threshold;
873 let alpha = self.alpha;
874 let state = self.states.entry(id).or_default();
875
876 if state.decided {
877 return false;
878 }
879
880 state.last_ratio = ratio;
881
882 // This round's verdict, read as a ratio against alpha.
883 let verdict = if ratio >= alpha {
884 Some(true)
885 } else if ratio <= 1.0 - alpha {
886 Some(false)
887 } else {
888 None
889 };
890
891 match accumulate(&mut state.preference, &mut state.confidence, verdict, beta) {
892 Some(decision) => {
893 state.decided = true;
894 state.decision = decision;
895 true
896 }
897 None => false,
898 }
899 }
900
901 /// Get state for an item
902 pub fn state(&self, id: &ID) -> Option<&FocusState> {
903 self.states.get(id)
904 }
905
906 /// Check if item has reached finality
907 pub fn is_decided(&self, id: &ID) -> bool {
908 self.states.get(id).is_some_and(|s| s.decided)
909 }
910
911 /// Get decision for an item
912 pub fn decision(&self, id: &ID) -> Decision {
913 self.states
914 .get(id)
915 .map_or(Decision::Undecided, |s| s.decision)
916 }
917
918 /// Get current confidence level
919 pub fn confidence(&self, id: &ID) -> u32 {
920 self.states.get(id).map_or(0, |s| s.confidence)
921 }
922
923 /// Reset state for an item
924 pub fn reset(&mut self, id: &ID) {
925 self.states.remove(id);
926 }
927 }
928
929 /// Windowed confidence tracker with time-based expiry
930 pub struct WindowedFocus<ID: Eq + std::hash::Hash + Clone> {
931 inner: Focus<ID>,
932 window: Duration,
933 last_update: HashMap<ID, Instant>,
934 }
935
936 impl<ID: Eq + std::hash::Hash + Clone> WindowedFocus<ID> {
937 pub fn new(threshold: u32, alpha: f64, window: Duration) -> Self {
938 WindowedFocus {
939 inner: Focus::new(threshold, alpha),
940 window,
941 last_update: HashMap::new(),
942 }
943 }
944
945 /// Update with window expiry check
946 pub fn update(&mut self, id: ID, yes_votes: usize, total_votes: usize) -> bool {
947 let now = Instant::now();
948
949 // Check for window expiry
950 if let Some(&last) = self.last_update.get(&id) {
951 if now.duration_since(last) > self.window {
952 self.inner.reset(&id);
953 }
954 }
955
956 self.last_update.insert(id.clone(), now);
957 self.inner.update(id, yes_votes, total_votes)
958 }
959
960 pub fn is_decided(&self, id: &ID) -> bool {
961 self.inner.is_decided(id)
962 }
963
964 pub fn decision(&self, id: &ID) -> Decision {
965 self.inner.decision(id)
966 }
967 }
968}
969
970// ============= WAVE MODULE - Threshold Voting Protocol =============
971
972pub mod wave {
973 use super::*;
974
975 /// Wave state for a single block
976 #[derive(Debug, Clone)]
977 pub struct WaveState {
978 pub votes: Vec<Vote>,
979 pub yes_count: usize,
980 pub no_count: usize,
981 pub preference: bool,
982 pub confidence: u32,
983 pub decided: bool,
984 pub decision: Decision,
985 }
986
987 impl Default for WaveState {
988 fn default() -> Self {
989 WaveState {
990 votes: Vec::new(),
991 yes_count: 0,
992 no_count: 0,
993 preference: false,
994 confidence: 0,
995 decided: false,
996 decision: Decision::Undecided,
997 }
998 }
999 }
1000
1001 /// Wave consensus engine with FPC support
1002 pub struct Wave {
1003 config: QuasarConfig,
1004 fpc: Option<FpcSelector>,
1005 phase: u64,
1006 states: HashMap<ID, WaveState>,
1007 }
1008
1009 impl Wave {
1010 /// Create new Wave consensus
1011 pub fn new(config: QuasarConfig) -> Self {
1012 let fpc = if config.enable_fpc {
1013 Some(FpcSelector::new(
1014 config.theta_min,
1015 config.theta_max,
1016 config.fpc_seed,
1017 ))
1018 } else {
1019 None
1020 };
1021
1022 Wave {
1023 config,
1024 fpc,
1025 phase: 0,
1026 states: HashMap::new(),
1027 }
1028 }
1029
1030 /// Get or create state for a block
1031 pub fn get_or_create_state(&mut self, block_id: &ID) -> &mut WaveState {
1032 self.states.entry(block_id.clone()).or_default()
1033 }
1034
1035 /// Record a vote and check for consensus
1036 ///
1037 /// Returns true if decision was just reached
1038 pub fn record_vote(&mut self, vote: Vote) -> bool {
1039 let block_id = vote.block_id.clone();
1040
1041 let state = self.states.entry(block_id.clone()).or_default();
1042
1043 if state.decided {
1044 return false;
1045 }
1046
1047 // Check for duplicate voter
1048 if state.votes.iter().any(|v| v.voter == vote.voter) {
1049 return false;
1050 }
1051
1052 // Count vote
1053 if vote.prefer() {
1054 state.yes_count += 1;
1055 } else {
1056 state.no_count += 1;
1057 }
1058
1059 state.votes.push(vote);
1060
1061 // Check if we have enough votes for a decision
1062 self.check_consensus(&block_id)
1063 }
1064
1065 /// Check for consensus on a block
1066 fn check_consensus(&mut self, block_id: &ID) -> bool {
1067 self.advance_phase();
1068 let threshold = self.threshold();
1069 let (k, beta) = (self.config.k, self.config.beta);
1070
1071 let state = match self.states.get_mut(block_id) {
1072 Some(s) => s,
1073 None => return false,
1074 };
1075
1076 if state.decided {
1077 return false;
1078 }
1079
1080 // Need at least k votes
1081 if state.yes_count + state.no_count < k {
1082 return false;
1083 }
1084
1085 // This round's verdict, read as counts against the threshold.
1086 let verdict = if state.yes_count >= threshold {
1087 Some(true)
1088 } else if state.no_count >= threshold {
1089 Some(false)
1090 } else {
1091 None
1092 };
1093
1094 match crate::focus::accumulate(
1095 &mut state.preference,
1096 &mut state.confidence,
1097 verdict,
1098 beta,
1099 ) {
1100 Some(decision) => {
1101 state.decided = true;
1102 state.decision = decision;
1103 true
1104 }
1105 None => false,
1106 }
1107 }
1108
1109 /// The votes a block needs in the current phase.
1110 ///
1111 /// Pure: asking does not move the phase on. It used to, which meant the
1112 /// FPC schedule advanced once per vote rather than once per round.
1113 pub fn threshold(&self) -> usize {
1114 match self.fpc {
1115 Some(ref fpc) => fpc.select_threshold(self.phase, self.config.k),
1116 None => self.config.alpha_count(),
1117 }
1118 }
1119
1120 /// Move to the next FPC phase.
1121 ///
1122 /// This engine has no round boundary — `check_consensus` runs on every
1123 /// vote — so a phase is currently a vote. That is the gap: FPC draws a
1124 /// threshold per ROUND, and a round is a fresh sample of k validators.
1125 /// Until a round loop exists, the advance is at least explicit and in
1126 /// one place rather than hidden inside a getter.
1127 fn advance_phase(&mut self) {
1128 if self.fpc.is_some() {
1129 self.phase += 1;
1130 }
1131 }
1132
1133 /// Get state for a block
1134 pub fn state(&self, block_id: &ID) -> Option<&WaveState> {
1135 self.states.get(block_id)
1136 }
1137
1138 /// Check if block is decided
1139 pub fn is_decided(&self, block_id: &ID) -> bool {
1140 self.states.get(block_id).is_some_and(|s| s.decided)
1141 }
1142
1143 /// Get decision for a block
1144 pub fn decision(&self, block_id: &ID) -> Decision {
1145 self.states
1146 .get(block_id)
1147 .map_or(Decision::Undecided, |s| s.decision)
1148 }
1149
1150 /// Reset state for a block
1151 pub fn reset(&mut self, block_id: &ID) {
1152 self.states.remove(block_id);
1153 }
1154
1155 /// Get current phase
1156 pub fn phase(&self) -> u64 {
1157 self.phase
1158 }
1159 }
1160}
1161
1162// ============= QUASAR MODULE - Post-Quantum Finality =============
1163
1164pub mod quasar {
1165 use super::*;
1166 use crate::cert::{CertError, StakeSource, ValidatorSet, Vote as CertVote, VoteVerifier};
1167
1168 /// This set does not vary with a P-chain epoch — its membership and weights are
1169 /// what they are (see the `StakeSource` impl on `ValidatorSet`) — so every clause
1170 /// here reads it at the same height and that height is zero. A set that DOES read
1171 /// an epoch takes the height from its caller.
1172 const EPOCH: u64 = 0;
1173
1174 /// Quasar finality: the certificate side of the engine.
1175 ///
1176 /// It holds the one validator set — membership, stake and signing keys —
1177 /// and it will only issue or accept a certificate whose signatures verify
1178 /// under it. A vote from a member with no registered key contributes
1179 /// nothing here, which is the fail-closed direction.
1180 pub struct QuasarConsensus {
1181 validators: ValidatorSet,
1182 /// The configured accept quorum, and the ONLY thing it is: how many members
1183 /// this node was told to expect before it is worth asking the set anything.
1184 /// It is a readiness number, not a floor — no certificate is issued to it and
1185 /// none is checked against it. A certificate's quorum is `signer_floor` over
1186 /// the set, derived at the moment of issue, and the two must never be spelled
1187 /// as one: `ceil(alpha * k)` is 3 on the testnet's committee of five, where
1188 /// the set of five derives 4.
1189 alpha: usize,
1190 /// Certificates this node issued, keyed by the signed identity of the
1191 /// position (`Position::signed_identity`) — the value every vote
1192 /// committed to, never the unsigned transport id. A certificate cannot
1193 /// be filed under a block it never attested.
1194 finalized: HashMap<ID, Certificate>,
1195 }
1196
1197 impl QuasarConsensus {
1198 /// Create new Quasar consensus
1199 pub fn new(config: &QuasarConfig) -> Self {
1200 QuasarConsensus {
1201 validators: ValidatorSet::new(),
1202 alpha: config.alpha_count(),
1203 finalized: HashMap::new(),
1204 }
1205 }
1206
1207 /// Register a validator this node has no signing key for.
1208 ///
1209 /// It is a member and it holds stake, so its ballots count toward
1210 /// preference — but it cannot contribute to a certificate until its key
1211 /// is known. A node already in the set is refused rather than restated:
1212 /// one node, one admission, on this door as on the keyed one.
1213 pub fn add_validator(&mut self, id: NodeID, weight: u64) -> Result<()> {
1214 Ok(self.validators.insert_unkeyed(*id.as_bytes(), weight)?)
1215 }
1216
1217 /// Register a validator with the BLS key it signs with, and its proof
1218 /// that it holds the matching secret.
1219 ///
1220 /// An invalid key, a key already claimed by another validator, or a
1221 /// proof of possession that does not verify is refused rather than
1222 /// stored — so a registered key is one a signature can be checked
1223 /// against AND one the registrant demonstrably controls. The proof
1224 /// travels with the validator declaration; this node does not mint it.
1225 pub fn add_validator_with_key(
1226 &mut self,
1227 id: NodeID,
1228 weight: u64,
1229 bls_pubkey: &[u8],
1230 pop: &[u8],
1231 ) -> Result<()> {
1232 Ok(self
1233 .validators
1234 .insert(*id.as_bytes(), weight, bls_pubkey, pop)?)
1235 }
1236
1237 /// Remove a validator
1238 pub fn remove_validator(&mut self, id: &NodeID) {
1239 self.validators.remove(id.as_bytes());
1240 }
1241
1242 /// Get validator count
1243 pub fn validator_count(&self) -> usize {
1244 self.validators.len()
1245 }
1246
1247 /// Whether this node may count `id`'s ballot.
1248 pub fn is_validator(&self, id: &NodeID) -> bool {
1249 self.validators.contains(id.as_bytes())
1250 }
1251
1252 /// The validator set, for verifying certificates against.
1253 pub fn validators(&self) -> &ValidatorSet {
1254 &self.validators
1255 }
1256
1257 /// Whether the set has grown to the committee this node was configured for.
1258 ///
1259 /// A readiness question about MEMBERSHIP, and deliberately not a statement
1260 /// about any certificate: what a certificate must carry is derived from the
1261 /// set when it is issued, and this number has no part in it.
1262 pub fn has_quorum(&self) -> bool {
1263 self.validators.len() >= self.alpha
1264 }
1265
1266 /// Issue a certificate for `position` from the votes that actually
1267 /// support it.
1268 ///
1269 /// A vote counts only if its signature verifies under the voter's
1270 /// registered key over `canonical_vote_message(position, true)`. So a
1271 /// certificate is issued from evidence or not at all — unsigned ballots
1272 /// produce `NoQuorum`, not an empty certificate that later verifies.
1273 ///
1274 /// The signatures are kept, one per voter, exactly as Go keeps them.
1275 /// They are not summed: per-signature is the form Go reads, and it is
1276 /// the interoperable choice. Registration now supplies a proof of
1277 /// possession, so an aggregate would be sound too — but it would not be
1278 /// a form Go can read, so the certificate stays per-signature.
1279 pub fn create_certificate(
1280 &mut self,
1281 position: Position,
1282 votes: &[Vote],
1283 ) -> Result<Certificate> {
1284 let message = canonical_vote_message(&position, true);
1285
1286 let mut accepted: Vec<CertVote> = Vec::new();
1287 let mut seen: std::collections::HashSet<crate::pop::NodeId> =
1288 std::collections::HashSet::new();
1289
1290 for v in votes.iter().filter(|v| v.prefer()) {
1291 let id = *v.voter.as_bytes();
1292 if !seen.insert(id) {
1293 continue;
1294 }
1295 if !self
1296 .validators
1297 .verify_vote(&id, &message, &v.signature, position.height)
1298 {
1299 continue;
1300 }
1301 accepted.push(CertVote {
1302 node_id: id,
1303 accept: true,
1304 signature: v.signature.clone(),
1305 });
1306 }
1307
1308 // DERIVED, HERE AND NOW. The quorum this certificate carries is
1309 // `signer_floor` for its rung over the set as it stands — the same number
1310 // every party checking it computes for itself, which is what makes the two
1311 // sides agree by construction rather than by luck.
1312 //
1313 // It was `ceil(alpha * k)`, the CONFIGURED accept quorum, and that number
1314 // is about a committee while the floor is about a set. They part at every
1315 // n != k, and the testnet parts at its own k: five validators, alpha 0.6,
1316 // a stamped 3 against a derived 4 — so this engine issued a certificate
1317 // its own `verify_certificate` refuses, and the refusal came back as
1318 // `NoQuorum` over a set that held a unanimous one.
1319 let n = self.validators.signer_count(EPOCH);
1320 let derived = signer_floor(Finality::Quasar, n);
1321 // The floor is a count of seats and a certificate states it in a `u32`. A
1322 // set claiming more signers than that can hold is not a set this node
1323 // holds a quorum of, and saying so in the clause that names seats is truer
1324 // than truncating the number the certificate would carry.
1325 let threshold = u32::try_from(derived).map_err(|_| {
1326 ConsensusError::Cert(CertError::SignerFloor {
1327 have: accepted.len() as i64,
1328 need: derived,
1329 n,
1330 })
1331 })?;
1332 // Not yet: the votes in hand do not reach the set's floor. This is the
1333 // liveness answer and it reads the DERIVED number, so it can never stop
1334 // short of a quorum the predicate below would have admitted.
1335 if (accepted.len() as i64) < derived {
1336 return Err(ConsensusError::NoQuorum);
1337 }
1338
1339 // Key finality on the identity the votes actually signed, never on
1340 // the transport block id, which is unsigned: a verified certificate
1341 // must not be relabelable to a block it never attested. See
1342 // `Position::signed_identity`.
1343 let key = ID::from(position.signed_identity());
1344 // Assembly sorts and dedups, so the certificate satisfies the
1345 // ordering clause by construction.
1346 let cert = Certificate::assemble(Finality::Quasar, position, threshold, &accepted)?;
1347
1348 // Finalize only what the accept rule accepts. The count above is the
1349 // liveness answer; the authority is this predicate — the derived clause,
1350 // the stake floor and the signer floor, all recomputed from the set — so
1351 // `is_finalized` can never report a certificate `verify_certificate` would
1352 // reject. Its refusal is carried whole: which floor was not met is the
1353 // answer, and `NoQuorum` is not that answer.
1354 cert.verify_weighted(&self.validators, &self.validators, EPOCH)?;
1355
1356 self.finalized.insert(key, cert.clone());
1357 Ok(cert)
1358 }
1359
1360 /// Whether `cert` is evidence that its position was accepted.
1361 ///
1362 /// The whole rule, and only the rule: [`crate::cert::QuorumCert::
1363 /// verify_weighted`] — version, type, tier, strictly increasing distinct
1364 /// voters, every vote an ACCEPT, every signature checked against its own
1365 /// signer's key over the message rebuilt from the certificate's own
1366 /// position, and the tier's stake floor recomputed from this set.
1367 ///
1368 /// A certificate makes exactly one claim, its position, and every part
1369 /// of that claim is signed. There is no header field beside it to
1370 /// disagree with it, and no aggregate to check in place of the
1371 /// signatures.
1372 ///
1373 /// The epoch height is zero because this set is epoch-independent: its
1374 /// weights and membership do not vary with the argument (see the
1375 /// `StakeSource` impl on `ValidatorSet`). A set that reads a P-chain
1376 /// epoch must take that height from the caller.
1377 pub fn verify_certificate(&self, cert: &Certificate) -> bool {
1378 cert.verify_weighted(&self.validators, &self.validators, 0)
1379 .is_ok()
1380 }
1381
1382 /// Whether the position with this signed identity has quantum finality.
1383 /// The argument is `Position::signed_identity` — the value the votes
1384 /// committed to, not the transport block id, which finality is never
1385 /// keyed on.
1386 pub fn is_finalized(&self, signed_identity: &ID) -> bool {
1387 self.finalized.contains_key(signed_identity)
1388 }
1389
1390 /// The certificate for a signed identity (`Position::signed_identity`),
1391 /// if one is finalized.
1392 pub fn get_certificate(&self, signed_identity: &ID) -> Option<&Certificate> {
1393 self.finalized.get(signed_identity)
1394 }
1395 }
1396
1397 /// Event Horizon - Multi-chain block aggregation
1398 pub struct EventHorizon {
1399 quasar: QuasarConsensus,
1400 chains: HashMap<String, Vec<ID>>,
1401 height: u64,
1402 }
1403
1404 impl EventHorizon {
1405 pub fn new(config: &QuasarConfig) -> Self {
1406 EventHorizon {
1407 quasar: QuasarConsensus::new(config),
1408 chains: HashMap::new(),
1409 height: 0,
1410 }
1411 }
1412
1413 /// Register a chain
1414 pub fn register_chain(&mut self, chain_id: String) {
1415 self.chains.entry(chain_id).or_default();
1416 }
1417
1418 /// Accept a block from a chain
1419 pub fn accept_block(&mut self, chain_id: &str, block_id: ID) {
1420 if let Some(blocks) = self.chains.get_mut(chain_id) {
1421 blocks.push(block_id);
1422 self.height += 1;
1423 }
1424 }
1425
1426 /// Get current height
1427 pub fn height(&self) -> u64 {
1428 self.height
1429 }
1430
1431 /// Get quasar consensus reference
1432 pub fn quasar(&self) -> &QuasarConsensus {
1433 &self.quasar
1434 }
1435
1436 /// Get mutable quasar consensus reference
1437 pub fn quasar_mut(&mut self) -> &mut QuasarConsensus {
1438 &mut self.quasar
1439 }
1440 }
1441}
1442
1443// ============= ENGINE MODULE - Complete Consensus Engine =============
1444
1445pub mod engine {
1446 use super::*;
1447
1448 /// Consensus engine trait
1449 pub trait Engine {
1450 fn add(&mut self, block: Block) -> Result<()>;
1451 fn record_vote(&mut self, vote: Vote) -> Result<()>;
1452 fn record_votes_batch(&mut self, votes: Vec<Vote>) -> usize;
1453 fn is_accepted(&self, id: &ID) -> bool;
1454 fn get_status(&self, id: &ID) -> Status;
1455 fn start(&mut self) -> Result<()>;
1456 fn stop(&mut self) -> Result<()>;
1457 }
1458
1459 /// Complete Quasar consensus engine
1460 ///
1461 /// Integrates Wave voting, FPC thresholds, Photon sampling,
1462 /// Focus confidence, and Quasar post-quantum finality.
1463 pub struct QuasarEngine {
1464 config: QuasarConfig,
1465 wave: Wave,
1466 quasar: QuasarConsensus,
1467 blocks: Arc<RwLock<HashMap<ID, Block>>>,
1468 status: Arc<RwLock<HashMap<ID, Status>>>,
1469 started: Arc<RwLock<bool>>,
1470 height: Arc<RwLock<u64>>,
1471 }
1472
1473 impl QuasarEngine {
1474 /// Create new Quasar engine with configuration
1475 pub fn new(config: QuasarConfig) -> Self {
1476 let wave = Wave::new(config.clone());
1477 let quasar = QuasarConsensus::new(&config);
1478
1479 QuasarEngine {
1480 config,
1481 wave,
1482 quasar,
1483 blocks: Arc::new(RwLock::new(HashMap::new())),
1484 status: Arc::new(RwLock::new(HashMap::new())),
1485 started: Arc::new(RwLock::new(false)),
1486 height: Arc::new(RwLock::new(0)),
1487 }
1488 }
1489
1490 /// Create testnet engine
1491 pub fn testnet() -> Self {
1492 QuasarEngine::new(QuasarConfig::testnet())
1493 }
1494
1495 /// Create mainnet engine
1496 pub fn mainnet() -> Self {
1497 QuasarEngine::new(QuasarConfig::mainnet())
1498 }
1499
1500 /// Add a validator this engine has no signing key for. Refused if it is
1501 /// already a member — see `QuasarConsensus::add_validator`.
1502 pub fn add_validator(&mut self, id: NodeID, weight: u64) -> Result<()> {
1503 self.quasar.add_validator(id, weight)
1504 }
1505
1506 /// Get configuration
1507 pub fn config(&self) -> &QuasarConfig {
1508 &self.config
1509 }
1510
1511 /// Get current height
1512 pub fn height(&self) -> u64 {
1513 *self.height.read().unwrap()
1514 }
1515
1516 /// Accept a block (internal)
1517 fn accept_block(&mut self, block_id: &ID) {
1518 let mut status = self.status.write().unwrap();
1519 status.insert(block_id.clone(), Status::Accepted);
1520
1521 let blocks = self.blocks.read().unwrap();
1522 if let Some(block) = blocks.get(block_id) {
1523 let mut height = self.height.write().unwrap();
1524 if block.height > *height {
1525 *height = block.height;
1526 }
1527 }
1528
1529 // Issue a certificate if the votes carry signatures that support
1530 // one. They often will not — this engine's ballots are unsigned —
1531 // and then there is no certificate, which is the honest outcome.
1532 let position = {
1533 let blocks = self.blocks.read().unwrap();
1534 blocks.get(block_id).map(|block| Position {
1535 height: block.height,
1536 block_id: *block_id.as_bytes(),
1537 parent_id: *block.parent_id.as_bytes(),
1538 ..Position::default()
1539 })
1540 };
1541 if let (Some(position), Some(votes)) =
1542 (position, self.wave.state(block_id).map(|s| s.votes.clone()))
1543 {
1544 let _ = self.quasar.create_certificate(position, &votes);
1545 }
1546 }
1547 }
1548
1549 /// The balanced configuration, as a trait impl rather than an inherent
1550 /// method that shadows it — one `default`, and it is the standard one.
1551 impl Default for QuasarEngine {
1552 fn default() -> Self {
1553 QuasarEngine::new(QuasarConfig::default())
1554 }
1555 }
1556
1557 impl Engine for QuasarEngine {
1558 fn add(&mut self, block: Block) -> Result<()> {
1559 if !*self.started.read().unwrap() {
1560 return Err(ConsensusError::NotInitialized);
1561 }
1562
1563 let id = block.id.clone();
1564
1565 {
1566 let mut blocks = self.blocks.write().unwrap();
1567 blocks.insert(id.clone(), block);
1568 }
1569
1570 {
1571 let mut status = self.status.write().unwrap();
1572 status.insert(id.clone(), Status::Processing);
1573 }
1574
1575 // Initialize wave state
1576 self.wave.get_or_create_state(&id);
1577
1578 Ok(())
1579 }
1580
1581 fn record_vote(&mut self, vote: Vote) -> Result<()> {
1582 if !*self.started.read().unwrap() {
1583 return Err(ConsensusError::NotInitialized);
1584 }
1585
1586 // Check block exists
1587 {
1588 let blocks = self.blocks.read().unwrap();
1589 if !blocks.contains_key(&vote.block_id) {
1590 return Err(ConsensusError::BlockNotFound);
1591 }
1592 }
1593
1594 // Only members are counted. Without this the sample is whoever
1595 // happened to send a message: nine unregistered node ids used to
1596 // carry a block to Accepted against an engine holding no validators
1597 // at all. An empty set now counts nobody, which is the direction to
1598 // fail in.
1599 if !self.quasar.is_validator(&vote.voter) {
1600 return Err(ConsensusError::NotValidator);
1601 }
1602
1603 let block_id = vote.block_id.clone();
1604
1605 // Record vote in Wave
1606 let decided = self.wave.record_vote(vote);
1607
1608 // If decided, update status
1609 if decided {
1610 let decision = self.wave.decision(&block_id);
1611 match decision {
1612 Decision::Accept => self.accept_block(&block_id),
1613 Decision::Reject => {
1614 let mut status = self.status.write().unwrap();
1615 status.insert(block_id, Status::Rejected);
1616 }
1617 Decision::Undecided => {}
1618 }
1619 }
1620
1621 Ok(())
1622 }
1623
1624 fn record_votes_batch(&mut self, votes: Vec<Vote>) -> usize {
1625 let mut success_count = 0;
1626 for vote in votes {
1627 if self.record_vote(vote).is_ok() {
1628 success_count += 1;
1629 }
1630 }
1631 success_count
1632 }
1633
1634 fn is_accepted(&self, id: &ID) -> bool {
1635 self.status
1636 .read()
1637 .unwrap()
1638 .get(id)
1639 .is_some_and(|s| *s == Status::Accepted)
1640 }
1641
1642 fn get_status(&self, id: &ID) -> Status {
1643 self.status
1644 .read()
1645 .unwrap()
1646 .get(id)
1647 .copied()
1648 .unwrap_or(Status::Unknown)
1649 }
1650
1651 fn start(&mut self) -> Result<()> {
1652 let mut started = self.started.write().unwrap();
1653 if *started {
1654 return Err(ConsensusError::AlreadyStarted);
1655 }
1656
1657 // Initialize genesis block
1658 let genesis = Block::genesis();
1659 {
1660 let mut blocks = self.blocks.write().unwrap();
1661 blocks.insert(genesis.id.clone(), genesis.clone());
1662 }
1663 {
1664 let mut status = self.status.write().unwrap();
1665 status.insert(genesis.id, Status::Accepted);
1666 }
1667
1668 *started = true;
1669 Ok(())
1670 }
1671
1672 fn stop(&mut self) -> Result<()> {
1673 let mut started = self.started.write().unwrap();
1674 *started = false;
1675 Ok(())
1676 }
1677 }
1678}
1679
1680// ============= CONVENIENCE FUNCTIONS =============
1681
1682/// Quick start a consensus engine
1683pub fn quick_start() -> Result<QuasarEngine> {
1684 let mut engine = QuasarEngine::default();
1685 engine.start()?;
1686 Ok(engine)
1687}
1688
1689/// Create a new block helper
1690pub fn new_block(id: ID, parent_id: ID, height: u64, payload: Vec<u8>) -> Block {
1691 Block::new(id, parent_id, height, payload)
1692}
1693
1694/// Create a new vote helper
1695pub fn new_vote(block_id: ID, vote_type: VoteType, voter: NodeID) -> Vote {
1696 Vote::new(block_id, vote_type, voter)
1697}
1698
1699/// Generate a random block ID
1700pub fn generate_block_id() -> ID {
1701 // Simple PRNG based on system time
1702 let now = SystemTime::now()
1703 .duration_since(SystemTime::UNIX_EPOCH)
1704 .unwrap_or_default();
1705 let seed = now.as_nanos() as u64;
1706
1707 let mut state = seed;
1708 let mut bytes = [0u8; 32];
1709 for i in 0..4 {
1710 state ^= state << 13;
1711 state ^= state >> 7;
1712 state ^= state << 17;
1713 let chunk = state.to_le_bytes();
1714 bytes[i * 8..(i + 1) * 8].copy_from_slice(&chunk);
1715 }
1716
1717 ID::new(bytes)
1718}
1719
1720/// Get SDK version
1721pub fn version() -> &'static str {
1722 env!("CARGO_PKG_VERSION")
1723}
1724
1725// ============= TESTS =============
1726
1727#[cfg(test)]
1728mod tests {
1729 use super::*;
1730
1731 #[test]
1732 fn test_fpc_selector() {
1733 let fpc = FpcSelector::default();
1734
1735 // Test determinism - same phase should give same theta
1736 let theta1 = fpc.theta(100);
1737 let theta2 = fpc.theta(100);
1738 assert_eq!(theta1, theta2);
1739
1740 // Different phases should give different thetas
1741 let theta3 = fpc.theta(101);
1742 assert_ne!(theta1, theta3);
1743
1744 // Theta should be in range
1745 for phase in 0..1000 {
1746 let theta = fpc.theta(phase);
1747 assert!((0.5..=0.8).contains(&theta), "theta {} out of range", theta);
1748 }
1749 }
1750
1751 #[test]
1752 fn test_fpc_threshold() {
1753 let fpc = FpcSelector::new(0.5, 0.8, *b"test-seed-0000000000000000000000");
1754 let k = 20;
1755
1756 let threshold = fpc.select_threshold(0, k);
1757 // Should be between ceil(0.5 * 20) = 10 and ceil(0.8 * 20) = 16
1758 assert!((10..=16).contains(&threshold));
1759 }
1760
1761 #[test]
1762 fn test_luminance() {
1763 let config = QuasarConfig::testnet();
1764 let mut luminance = photon::Luminance::new(&config);
1765
1766 let node = NodeID::from([1u8; 20]);
1767
1768 // Initial brightness
1769 assert_eq!(luminance.brightness(&node), 1.0);
1770
1771 // Success increases brightness
1772 luminance.illuminate(&node, true);
1773 assert!(luminance.brightness(&node) > 1.0);
1774
1775 // Failure decreases brightness
1776 let bright_before = luminance.brightness(&node);
1777 luminance.illuminate(&node, false);
1778 assert!(luminance.brightness(&node) < bright_before);
1779 }
1780
1781 #[test]
1782 fn test_focus_confidence() {
1783 let mut focus: focus::Focus<ID> = focus::Focus::new(5, 0.6);
1784 let block_id = ID::from([1u8; 32]);
1785
1786 // Not decided initially
1787 assert!(!focus.is_decided(&block_id));
1788
1789 // 5 consecutive rounds above 60% should finalize
1790 for _ in 0..5 {
1791 focus.update(block_id.clone(), 7, 10); // 70%
1792 }
1793
1794 assert!(focus.is_decided(&block_id));
1795 assert_eq!(focus.decision(&block_id), Decision::Accept);
1796 }
1797
1798 #[test]
1799 fn test_wave_voting() {
1800 let config = QuasarConfig::testnet(); // alpha=5, k=5, beta=5
1801 let mut wave = wave::Wave::new(config);
1802
1803 let block_id = ID::from([1u8; 32]);
1804
1805 // Record 5 preference votes
1806 for i in 0..5 {
1807 let vote = Vote::new(
1808 block_id.clone(),
1809 VoteType::Preference,
1810 NodeID::from([i; 20]),
1811 );
1812 wave.record_vote(vote);
1813 }
1814
1815 // Should have positive preference
1816 let state = wave.state(&block_id).unwrap();
1817 assert_eq!(state.yes_count, 5);
1818 }
1819
1820 #[test]
1821 fn test_quasar_engine() {
1822 let config = QuasarConfig::testnet();
1823 let mut engine = QuasarEngine::new(config);
1824
1825 // Start engine
1826 engine.start().unwrap();
1827
1828 // Add validators
1829 for i in 0..5 {
1830 engine.add_validator(NodeID::from([i; 20]), 1).unwrap();
1831 }
1832
1833 // Add a block
1834 let block = Block::new(ID::from([1u8; 32]), ID::zero(), 1, b"test".to_vec());
1835 engine.add(block.clone()).unwrap();
1836
1837 // Record votes
1838 for i in 0..5 {
1839 let vote = Vote::new(
1840 block.id.clone(),
1841 VoteType::Preference,
1842 NodeID::from([i; 20]),
1843 );
1844 engine.record_vote(vote).unwrap();
1845 }
1846
1847 // Should be processing or accepted
1848 let status = engine.get_status(&block.id);
1849 assert!(status == Status::Processing || status == Status::Accepted);
1850
1851 engine.stop().unwrap();
1852 }
1853
1854 #[test]
1855 fn test_full_consensus_flow() {
1856 let config = QuasarConfig::testnet();
1857 let mut engine = QuasarEngine::new(config.clone());
1858 engine.start().unwrap();
1859
1860 // Add validators
1861 for i in 0..10 {
1862 engine.add_validator(NodeID::from([i; 20]), 1).unwrap();
1863 }
1864
1865 // Create chain of blocks
1866 let blocks: Vec<Block> = (1..=3)
1867 .map(|height| {
1868 let mut id = [0u8; 32];
1869 id[0] = height as u8;
1870 let mut parent_id = [0u8; 32];
1871 if height > 1 {
1872 parent_id[0] = (height - 1) as u8;
1873 }
1874 Block::new(ID::from(id), ID::from(parent_id), height, vec![])
1875 })
1876 .collect();
1877
1878 // Add blocks
1879 for block in &blocks {
1880 engine.add(block.clone()).unwrap();
1881 }
1882
1883 // Vote on each block (alpha=5 for testnet)
1884 for block in &blocks {
1885 for i in 0..5 {
1886 let vote = Vote::new(
1887 block.id.clone(),
1888 VoteType::Preference,
1889 NodeID::from([i; 20]),
1890 );
1891 engine.record_vote(vote).unwrap();
1892 }
1893 }
1894
1895 // All blocks should be accepted or processing
1896 for block in &blocks {
1897 let status = engine.get_status(&block.id);
1898 assert!(
1899 status == Status::Accepted || status == Status::Processing,
1900 "Block {} has unexpected status {:?}",
1901 block.height,
1902 status
1903 );
1904 }
1905
1906 engine.stop().unwrap();
1907 }
1908
1909 #[test]
1910 fn test_configs() {
1911 let default = QuasarConfig::default();
1912 assert_eq!(default.alpha, 0.69);
1913 assert_eq!(default.k, 20);
1914 assert_eq!(default.beta, 20);
1915 assert!(default.quantum_resistant);
1916
1917 let testnet = QuasarConfig::testnet();
1918 assert_eq!(testnet.alpha, 0.6);
1919 assert_eq!(testnet.k, 5);
1920 assert!(!testnet.quantum_resistant);
1921
1922 let mainnet = QuasarConfig::mainnet();
1923 assert_eq!(mainnet.alpha, 0.69);
1924 assert_eq!(mainnet.k, 21);
1925 assert!(mainnet.quantum_resistant);
1926 }
1927}