// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. use lux_consensus::*; /// Helper to create a 32-byte ID from a single byte fn make_id(b: u8) -> ID { let mut arr = [0u8; 32]; arr[0] = b; ID::from(arr) } /// Helper to create a 20-byte NodeID from a single byte — a validator identity /// is 20 bytes, the width Go names a node by, and never a block id. fn make_node_id(b: u8) -> NodeID { let mut arr = [0u8; 20]; arr[0] = b; NodeID::from(arr) } #[test] fn test_initialization() { let config = QuasarConfig::testnet(); let mut chain = QuasarEngine::new(config); assert!(chain.start().is_ok()); chain.stop().unwrap(); } #[test] fn test_block_operations() { let config = QuasarConfig::testnet(); let mut chain = QuasarEngine::new(config); chain.start().unwrap(); // Create and add a block let block = Block::new( make_id(1), ID::zero(), // Genesis 1, b"Test Block".to_vec(), ); assert!(chain.add(block.clone()).is_ok()); // Check status let status = chain.get_status(&block.id); assert_eq!(status, Status::Processing); chain.stop().unwrap(); } #[test] fn test_consensus_flow() { let mut config = QuasarConfig::testnet(); config.k = 3; config.alpha = 0.67; // Need ~2 votes for acceptance config.beta = 1; // Just 1 round for quick test let mut chain = QuasarEngine::new(config); chain.start().unwrap(); // The committee whose ballots count. Without this the votes below are from // nobody, and an engine that counts them is sampling the network rather // than the validator set. for i in 0..3 { chain.add_validator(make_node_id(i), 1).unwrap(); } // Create and add a block let block = Block::new(make_id(1), ID::zero(), 1, b"Test Block".to_vec()); chain.add(block.clone()).unwrap(); // Record votes for i in 0..3 { let vote = Vote::new(block.id.clone(), VoteType::Preference, make_node_id(i)); chain.record_vote(vote).unwrap(); } // Check status (should be at least processing) let status = chain.get_status(&block.id); assert!(status == Status::Processing || status == Status::Accepted); chain.stop().unwrap(); } /// A ballot from outside the validator set is refused, and refused whether or /// not the set is empty. This is the hole that let nine unregistered node ids /// drive a block to Accepted. #[test] fn votes_from_outside_the_committee_are_refused() { let mut config = QuasarConfig::testnet(); config.k = 3; config.beta = 1; let mut chain = QuasarEngine::new(config); chain.start().unwrap(); let block = Block::new(make_id(1), ID::zero(), 1, b"Test Block".to_vec()); chain.add(block.clone()).unwrap(); // No validators registered: nobody may vote. for i in 0..9 { let vote = Vote::new(block.id.clone(), VoteType::Preference, make_node_id(i)); assert!(chain.record_vote(vote).is_err(), "voter {i} was counted"); } assert!(!chain.is_accepted(&block.id)); // With a committee, its members are counted and a stranger still is not. for i in 0..3 { chain.add_validator(make_node_id(i), 1).unwrap(); } for i in 0..3 { let vote = Vote::new(block.id.clone(), VoteType::Preference, make_node_id(i)); chain.record_vote(vote).unwrap(); } let stranger = Vote::new(block.id.clone(), VoteType::Preference, make_node_id(200)); assert!(chain.record_vote(stranger).is_err()); chain.stop().unwrap(); } #[test] fn test_error_handling() { let config = QuasarConfig::testnet(); let mut chain = QuasarEngine::new(config); chain.start().unwrap(); // Try to get non-existent block status (should return Unknown) let non_existent = make_id(255); let status = chain.get_status(&non_existent); assert_eq!(status, Status::Unknown); // Try to vote for non-existent block (should error) let vote = Vote::new(non_existent, VoteType::Commit, make_node_id(1)); assert!(chain.record_vote(vote).is_err()); chain.stop().unwrap(); } #[test] fn test_full_quasar_flow() { let config = QuasarConfig::testnet(); let mut engine = QuasarEngine::new(config.clone()); engine.start().unwrap(); // Add validators for i in 0..10 { engine.add_validator(make_node_id(i), 1).unwrap(); } // Create chain of blocks let blocks: Vec = (1..=3) .map(|height| { Block::new( make_id(height as u8), if height > 1 { make_id((height - 1) as u8) } else { ID::zero() }, height, format!("Block {}", height).into_bytes(), ) }) .collect(); // Add blocks for block in &blocks { engine.add(block.clone()).unwrap(); } // Vote on each block (alpha=5 for testnet) for block in &blocks { for i in 0..5 { let vote = Vote::new(block.id.clone(), VoteType::Preference, make_node_id(i)); engine.record_vote(vote).unwrap(); } } // All blocks should be accepted or processing for block in &blocks { let status = engine.get_status(&block.id); assert!( status == Status::Accepted || status == Status::Processing, "Block {} has unexpected status {:?}", block.height, status ); } engine.stop().unwrap(); }