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