mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
193 lines
5.5 KiB
Rust
193 lines
5.5 KiB
Rust
// 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<Block> = (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();
|
|
}
|