lux-consensus 1.22.7

Lux Consensus SDK — pure-Rust Quasar post-quantum probabilistic consensus (Wave / FPC / Photon / Focus / Quasar PQ finality via hybrid BLS12-381 + Pulsar = P3Q)
Documentation
// 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();
}