Skip to main content

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}