mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
@@ -53,7 +53,8 @@ fn certified(n: usize) -> (ValidatorSet, QuorumCert) {
|
||||
let sk = SecretKey::key_gen(&ikm, &[]).expect("key_gen");
|
||||
let id = node_id(i);
|
||||
let proof = pop::sign(&sk, &id, &sk.sk_to_pk().compress());
|
||||
set.insert(id, 100, &sk.sk_to_pk().compress(), &proof).expect("insert");
|
||||
set.insert(id, 100, &sk.sk_to_pk().compress(), &proof)
|
||||
.expect("insert");
|
||||
votes.push(Vote {
|
||||
node_id: id,
|
||||
accept: true,
|
||||
@@ -103,14 +104,7 @@ fn bench_assemble(c: &mut Criterion) {
|
||||
let (_, cert) = certified(21);
|
||||
let votes = cert.votes.clone();
|
||||
c.bench_function("cert_assemble_21", |b| {
|
||||
b.iter(|| {
|
||||
QuorumCert::assemble(
|
||||
Finality::Quasar,
|
||||
position(),
|
||||
21,
|
||||
black_box(&votes),
|
||||
)
|
||||
})
|
||||
b.iter(|| QuorumCert::assemble(Finality::Quasar, position(), 21, black_box(&votes)))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,7 +121,9 @@ fn bench_ballots(c: &mut Criterion) {
|
||||
let mut engine = QuasarEngine::new(config.clone());
|
||||
engine.start().unwrap();
|
||||
for i in 0..21 {
|
||||
engine.add_validator(lux_consensus::NodeID::from(node_id(i)), 1).unwrap();
|
||||
engine
|
||||
.add_validator(lux_consensus::NodeID::from(node_id(i)), 1)
|
||||
.unwrap();
|
||||
}
|
||||
let block = lux_consensus::Block::new(
|
||||
lux_consensus::ID::from([11u8; 32]),
|
||||
|
||||
@@ -115,7 +115,11 @@ pub fn canonical_vote_message(pos: &Position, accept: bool) -> Vec<u8> {
|
||||
// signed and the bytes finalized cannot drift apart. See
|
||||
// `Position::signed_identity`.
|
||||
let canonical = pos.signed_identity();
|
||||
let parent = if pos.parent_canonical_id == EMPTY { &pos.parent_id } else { &pos.parent_canonical_id };
|
||||
let parent = if pos.parent_canonical_id == EMPTY {
|
||||
&pos.parent_id
|
||||
} else {
|
||||
&pos.parent_canonical_id
|
||||
};
|
||||
buf.extend_from_slice(&canonical);
|
||||
buf.extend_from_slice(parent);
|
||||
|
||||
|
||||
+58
-54
@@ -62,7 +62,6 @@ use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
|
||||
// The finality standard: the bytes a validator signs and the thresholds that
|
||||
// decide. Held to the Go definitions by tests/conformance.rs.
|
||||
pub mod finality;
|
||||
@@ -96,20 +95,20 @@ pub fn sha256(input: &[u8]) -> [u8; 32] {
|
||||
}
|
||||
|
||||
// Re-export all public types
|
||||
pub use crate::engine::*;
|
||||
pub use crate::errors::*;
|
||||
pub use crate::finality::{
|
||||
canonical_vote_message, crash_tolerance, half_stake_floor, nova_beta, nova_quorum,
|
||||
nova_signer_floor, signer_floor, two_thirds_count, two_thirds_stake_floor, weighted_quasar,
|
||||
Finality, Position, QC_FINALITY, QUORUM_CERT_VERSION, VOTE_MESSAGE_LEN, VOTE_TAG,
|
||||
};
|
||||
pub use crate::types::*;
|
||||
pub use crate::errors::*;
|
||||
pub use crate::focus::*;
|
||||
pub use crate::fpc::*;
|
||||
pub use crate::photon::*;
|
||||
pub use crate::focus::*;
|
||||
pub use crate::wave::*;
|
||||
pub use crate::quasar::*;
|
||||
pub use crate::engine::*;
|
||||
pub use crate::types::*;
|
||||
pub use crate::vote::{SignedVote, Slot, Tally, VoteTransport, VOTE};
|
||||
pub use crate::wave::*;
|
||||
|
||||
// ============= TYPES MODULE =============
|
||||
|
||||
@@ -302,36 +301,33 @@ pub mod types {
|
||||
pub type Certificate = crate::cert::QuorumCert;
|
||||
|
||||
/// Security level for Corona post-quantum crypto
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SecurityLevel {
|
||||
Low = 2, // Corona Level 2
|
||||
Low = 2, // Corona Level 2
|
||||
#[default]
|
||||
Medium = 3, // Corona Level 3 - Default
|
||||
High = 5, // Corona Level 5
|
||||
High = 5, // Corona Level 5
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Quasar consensus configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QuasarConfig {
|
||||
// Wave parameters
|
||||
pub k: usize, // Sample/committee size
|
||||
pub alpha: f64, // Fixed threshold ratio (0.5-0.8)
|
||||
pub beta: u32, // Consecutive rounds for finality
|
||||
pub k: usize, // Sample/committee size
|
||||
pub alpha: f64, // Fixed threshold ratio (0.5-0.8)
|
||||
pub beta: u32, // Consecutive rounds for finality
|
||||
pub round_timeout: Duration, // Round timeout
|
||||
|
||||
// FPC parameters
|
||||
pub enable_fpc: bool, // Enable FPC adaptive thresholds
|
||||
pub theta_min: f64, // Minimum FPC threshold (0.5)
|
||||
pub theta_max: f64, // Maximum FPC threshold (0.8)
|
||||
pub fpc_seed: [u8; 32], // PRF seed for FPC
|
||||
pub enable_fpc: bool, // Enable FPC adaptive thresholds
|
||||
pub theta_min: f64, // Minimum FPC threshold (0.5)
|
||||
pub theta_max: f64, // Maximum FPC threshold (0.8)
|
||||
pub fpc_seed: [u8; 32], // PRF seed for FPC
|
||||
|
||||
// Photon parameters
|
||||
pub base_luminance: f64, // Base luminance in lux (100.0)
|
||||
pub max_luminance: f64, // Maximum luminance (1000.0)
|
||||
pub min_luminance: f64, // Minimum luminance (10.0)
|
||||
pub base_luminance: f64, // Base luminance in lux (100.0)
|
||||
pub max_luminance: f64, // Maximum luminance (1000.0)
|
||||
pub min_luminance: f64, // Minimum luminance (10.0)
|
||||
pub success_multiplier: f64, // Success brightens (1.1)
|
||||
pub failure_multiplier: f64, // Failure dims (0.9)
|
||||
|
||||
@@ -554,7 +550,6 @@ pub mod fpc {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// θ for a phase, from the PRF the network runs.
|
||||
///
|
||||
/// `θ(phase) = θ_min + sha256(seed ‖ be64(phase))[0..8] / (2⁶⁴−1) · (θ_max − θ_min)`
|
||||
@@ -573,8 +568,7 @@ pub mod fpc {
|
||||
|
||||
// Big-endian, first 8 bytes — the same window and order Go reads.
|
||||
let hash_u64 = u64::from_be_bytes([
|
||||
hash[0], hash[1], hash[2], hash[3],
|
||||
hash[4], hash[5], hash[6], hash[7],
|
||||
hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7],
|
||||
]);
|
||||
let normalized = (hash_u64 as f64) / (u64::MAX as f64);
|
||||
|
||||
@@ -713,7 +707,8 @@ pub mod photon {
|
||||
let k = k.min(self.peers.len());
|
||||
|
||||
// Calculate weights based on luminance
|
||||
let weights: Vec<f64> = self.peers
|
||||
let weights: Vec<f64> = self
|
||||
.peers
|
||||
.iter()
|
||||
.map(|p| self.luminance.brightness(p))
|
||||
.collect();
|
||||
@@ -786,8 +781,8 @@ pub mod focus {
|
||||
/// votes above the alpha threshold.
|
||||
#[derive(Debug)]
|
||||
pub struct Focus<ID: Eq + std::hash::Hash + Clone> {
|
||||
threshold: u32, // β - consecutive rounds needed
|
||||
alpha: f64, // Ratio threshold
|
||||
threshold: u32, // β - consecutive rounds needed
|
||||
alpha: f64, // Ratio threshold
|
||||
states: HashMap<ID, FocusState>,
|
||||
}
|
||||
|
||||
@@ -915,7 +910,9 @@ pub mod focus {
|
||||
|
||||
/// Get decision for an item
|
||||
pub fn decision(&self, id: &ID) -> Decision {
|
||||
self.states.get(id).map_or(Decision::Undecided, |s| s.decision)
|
||||
self.states
|
||||
.get(id)
|
||||
.map_or(Decision::Undecided, |s| s.decision)
|
||||
}
|
||||
|
||||
/// Get current confidence level
|
||||
@@ -1041,8 +1038,7 @@ pub mod wave {
|
||||
pub fn record_vote(&mut self, vote: Vote) -> bool {
|
||||
let block_id = vote.block_id.clone();
|
||||
|
||||
let state = self.states.entry(block_id.clone())
|
||||
.or_default();
|
||||
let state = self.states.entry(block_id.clone()).or_default();
|
||||
|
||||
if state.decided {
|
||||
return false;
|
||||
@@ -1146,7 +1142,9 @@ pub mod wave {
|
||||
|
||||
/// Get decision for a block
|
||||
pub fn decision(&self, block_id: &ID) -> Decision {
|
||||
self.states.get(block_id).map_or(Decision::Undecided, |s| s.decision)
|
||||
self.states
|
||||
.get(block_id)
|
||||
.map_or(Decision::Undecided, |s| s.decision)
|
||||
}
|
||||
|
||||
/// Reset state for a block
|
||||
@@ -1231,7 +1229,9 @@ pub mod quasar {
|
||||
bls_pubkey: &[u8],
|
||||
pop: &[u8],
|
||||
) -> Result<()> {
|
||||
Ok(self.validators.insert(*id.as_bytes(), weight, bls_pubkey, pop)?)
|
||||
Ok(self
|
||||
.validators
|
||||
.insert(*id.as_bytes(), weight, bls_pubkey, pop)?)
|
||||
}
|
||||
|
||||
/// Remove a validator
|
||||
@@ -1292,7 +1292,10 @@ pub mod quasar {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
if !self.validators.verify_vote(&id, &message, &v.signature, position.height) {
|
||||
if !self
|
||||
.validators
|
||||
.verify_vote(&id, &message, &v.signature, position.height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
accepted.push(CertVote {
|
||||
@@ -1372,7 +1375,8 @@ pub mod quasar {
|
||||
/// `StakeSource` impl on `ValidatorSet`). A set that reads a P-chain
|
||||
/// epoch must take that height from the caller.
|
||||
pub fn verify_certificate(&self, cert: &Certificate) -> bool {
|
||||
cert.verify_weighted(&self.validators, &self.validators, 0).is_ok()
|
||||
cert.verify_weighted(&self.validators, &self.validators, 0)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Whether the position with this signed identity has quantum finality.
|
||||
@@ -1628,13 +1632,17 @@ pub mod engine {
|
||||
}
|
||||
|
||||
fn is_accepted(&self, id: &ID) -> bool {
|
||||
self.status.read().unwrap()
|
||||
self.status
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(id)
|
||||
.is_some_and(|s| *s == Status::Accepted)
|
||||
}
|
||||
|
||||
fn get_status(&self, id: &ID) -> Status {
|
||||
self.status.read().unwrap()
|
||||
self.status
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(id)
|
||||
.copied()
|
||||
.unwrap_or(Status::Unknown)
|
||||
@@ -1667,7 +1675,6 @@ pub mod engine {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ============= CONVENIENCE FUNCTIONS =============
|
||||
@@ -1704,7 +1711,7 @@ pub fn generate_block_id() -> ID {
|
||||
state ^= state >> 7;
|
||||
state ^= state << 17;
|
||||
let chunk = state.to_le_bytes();
|
||||
bytes[i*8..(i+1)*8].copy_from_slice(&chunk);
|
||||
bytes[i * 8..(i + 1) * 8].copy_from_slice(&chunk);
|
||||
}
|
||||
|
||||
ID::new(bytes)
|
||||
@@ -1824,12 +1831,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Add a block
|
||||
let block = Block::new(
|
||||
ID::from([1u8; 32]),
|
||||
ID::zero(),
|
||||
1,
|
||||
b"test".to_vec(),
|
||||
);
|
||||
let block = Block::new(ID::from([1u8; 32]), ID::zero(), 1, b"test".to_vec());
|
||||
engine.add(block.clone()).unwrap();
|
||||
|
||||
// Record votes
|
||||
@@ -1861,15 +1863,17 @@ mod tests {
|
||||
}
|
||||
|
||||
// Create chain of blocks
|
||||
let blocks: Vec<Block> = (1..=3).map(|height| {
|
||||
let mut id = [0u8; 32];
|
||||
id[0] = height as u8;
|
||||
let mut parent_id = [0u8; 32];
|
||||
if height > 1 {
|
||||
parent_id[0] = (height - 1) as u8;
|
||||
}
|
||||
Block::new(ID::from(id), ID::from(parent_id), height, vec![])
|
||||
}).collect();
|
||||
let blocks: Vec<Block> = (1..=3)
|
||||
.map(|height| {
|
||||
let mut id = [0u8; 32];
|
||||
id[0] = height as u8;
|
||||
let mut parent_id = [0u8; 32];
|
||||
if height > 1 {
|
||||
parent_id[0] = (height - 1) as u8;
|
||||
}
|
||||
Block::new(ID::from(id), ID::from(parent_id), height, vec![])
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Add blocks
|
||||
for block in &blocks {
|
||||
|
||||
@@ -503,7 +503,10 @@ impl Tally {
|
||||
// and the rung's count clause reading it again is deliberate there: a
|
||||
// certificate from elsewhere has no issue to be consistent with, and a set
|
||||
// that moved under it is exactly what that clause refuses.
|
||||
let held = Held { set: stake, n: stake.signer_count(self.epoch_height) };
|
||||
let held = Held {
|
||||
set: stake,
|
||||
n: stake.signer_count(self.epoch_height),
|
||||
};
|
||||
let derived = signer_floor(self.tier, held.n);
|
||||
// The floor is a count of seats and the certificate states it in a `u32`.
|
||||
// A set claiming more signers than that can hold is not a set this tally
|
||||
|
||||
@@ -53,8 +53,8 @@ fn test_block_operations() {
|
||||
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
|
||||
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();
|
||||
@@ -67,21 +67,12 @@ fn test_consensus_flow() {
|
||||
}
|
||||
|
||||
// Create and add a block
|
||||
let block = Block::new(
|
||||
make_id(1),
|
||||
ID::zero(),
|
||||
1,
|
||||
b"Test Block".to_vec(),
|
||||
);
|
||||
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),
|
||||
);
|
||||
let vote = Vote::new(block.id.clone(), VoteType::Preference, make_node_id(i));
|
||||
chain.record_vote(vote).unwrap();
|
||||
}
|
||||
|
||||
@@ -140,11 +131,7 @@ fn test_error_handling() {
|
||||
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),
|
||||
);
|
||||
let vote = Vote::new(non_existent, VoteType::Commit, make_node_id(1));
|
||||
assert!(chain.record_vote(vote).is_err());
|
||||
|
||||
chain.stop().unwrap();
|
||||
@@ -162,14 +149,20 @@ fn test_full_quasar_flow() {
|
||||
}
|
||||
|
||||
// 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();
|
||||
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 {
|
||||
@@ -179,11 +172,7 @@ fn test_full_quasar_flow() {
|
||||
// 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),
|
||||
);
|
||||
let vote = Vote::new(block.id.clone(), VoteType::Preference, make_node_id(i));
|
||||
engine.record_vote(vote).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
+204
-55
@@ -76,7 +76,8 @@ fn committee(n: u8, weight: u64) -> (Vec<Signer>, ValidatorSet) {
|
||||
let signers: Vec<Signer> = (1..=n).map(|i| Signer::new(i, weight)).collect();
|
||||
let mut set = ValidatorSet::new();
|
||||
for s in &signers {
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop()).expect("insert");
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop())
|
||||
.expect("insert");
|
||||
}
|
||||
(signers, set)
|
||||
}
|
||||
@@ -192,7 +193,10 @@ fn votes_signed_over_another_position_are_refused() {
|
||||
elsewhere.height += 1;
|
||||
let other_message = canonical_vote_message(&elsewhere, true);
|
||||
|
||||
let votes: Vec<Vote> = signers[..4].iter().map(|s| s.vote(&other_message)).collect();
|
||||
let votes: Vec<Vote> = signers[..4]
|
||||
.iter()
|
||||
.map(|s| s.vote(&other_message))
|
||||
.collect();
|
||||
let cert = QuorumCert::assemble(Finality::Quasar, position(), 4, &votes).expect("assemble");
|
||||
|
||||
assert_eq!(cert.verify(&set, 0), Err(CertError::SigInvalid(0)));
|
||||
@@ -210,7 +214,7 @@ fn a_reject_signature_cannot_pass_as_an_accept() {
|
||||
.iter()
|
||||
.map(|s| Vote {
|
||||
node_id: s.id,
|
||||
accept: true, // claims accept
|
||||
accept: true, // claims accept
|
||||
signature: s.sign(&reject_message), // signed reject
|
||||
})
|
||||
.collect();
|
||||
@@ -228,7 +232,10 @@ fn a_duplicate_voter_is_refused() {
|
||||
let mut cert = valid_cert(&signers, 4, 4);
|
||||
cert.votes[1] = cert.votes[0].clone();
|
||||
|
||||
assert_eq!(cert.verify(&set, 0), Err(CertError::NotStrictlyIncreasing(1)));
|
||||
assert_eq!(
|
||||
cert.verify(&set, 0),
|
||||
Err(CertError::NotStrictlyIncreasing(1))
|
||||
);
|
||||
}
|
||||
|
||||
/// Order is part of the certificate. Without it the same votes have many
|
||||
@@ -239,7 +246,10 @@ fn a_reordered_certificate_is_refused() {
|
||||
let mut cert = valid_cert(&signers, 4, 4);
|
||||
cert.votes.reverse();
|
||||
|
||||
assert_eq!(cert.verify(&set, 0), Err(CertError::NotStrictlyIncreasing(1)));
|
||||
assert_eq!(
|
||||
cert.verify(&set, 0),
|
||||
Err(CertError::NotStrictlyIncreasing(1))
|
||||
);
|
||||
}
|
||||
|
||||
/// A finality certificate witnesses acceptance. A reject vote inside one is a
|
||||
@@ -292,7 +302,10 @@ fn a_wrong_version_or_type_is_refused() {
|
||||
|
||||
let mut cert = valid_cert(&signers, 4, 4);
|
||||
cert.qc_type = 9;
|
||||
assert_eq!(cert.verify(&set, 0), Err(CertError::Type { got: 9, want: 1 }));
|
||||
assert_eq!(
|
||||
cert.verify(&set, 0),
|
||||
Err(CertError::Type { got: 9, want: 1 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -319,7 +332,8 @@ fn a_count_quorum_without_the_stake_is_refused() {
|
||||
.collect();
|
||||
let mut set = ValidatorSet::new();
|
||||
for s in &signers {
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop()).expect("insert");
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop())
|
||||
.expect("insert");
|
||||
}
|
||||
|
||||
let cert = valid_cert(&signers, 4, 4);
|
||||
@@ -357,13 +371,18 @@ fn exactly_two_thirds_is_refused_and_one_more_passes() {
|
||||
.collect();
|
||||
let mut set = ValidatorSet::new();
|
||||
for s in &signers {
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop()).expect("insert");
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop())
|
||||
.expect("insert");
|
||||
}
|
||||
assert_eq!(set.signer_stake(0), 600);
|
||||
assert_eq!(two_thirds_stake_floor(600), 400);
|
||||
|
||||
let five = declaring(&signers, Finality::Quasar, 6, 5);
|
||||
assert_eq!(five.voter_count(), two_thirds_count(6), "the count floor is met");
|
||||
assert_eq!(
|
||||
five.voter_count(),
|
||||
two_thirds_count(6),
|
||||
"the count floor is met"
|
||||
);
|
||||
assert_eq!(
|
||||
five.verify_weighted(&set, &set, 0),
|
||||
Err(CertError::StakeBelowSupermajority {
|
||||
@@ -420,7 +439,8 @@ fn nova_needs_both_the_majority_and_the_signer_floor() {
|
||||
.collect();
|
||||
let mut set = ValidatorSet::new();
|
||||
for s in &signers {
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop()).expect("insert");
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop())
|
||||
.expect("insert");
|
||||
}
|
||||
|
||||
let message = canonical_vote_message(&position(), true);
|
||||
@@ -504,7 +524,10 @@ fn each_signature_is_checked_against_its_own_signer() {
|
||||
// One id repeated — the ordering clause catches it before any key is read.
|
||||
let mut dup = cert.clone();
|
||||
dup.votes[1].node_id = dup.votes[0].node_id;
|
||||
assert_eq!(dup.verify(&set, 0), Err(CertError::NotStrictlyIncreasing(1)));
|
||||
assert_eq!(
|
||||
dup.verify(&set, 0),
|
||||
Err(CertError::NotStrictlyIncreasing(1))
|
||||
);
|
||||
|
||||
// Zero, garbage, and the wrong length.
|
||||
for bad in [
|
||||
@@ -560,12 +583,24 @@ fn an_invalid_public_key_is_refused_at_registration() {
|
||||
let mut set = ValidatorSet::new();
|
||||
|
||||
// 48 bytes that are not a point, in the two shapes a wire decode produces.
|
||||
assert_eq!(set.insert(node(1), 100, &[0u8; 48], &s.pop()), Err(CertError::KeyEncoding));
|
||||
assert_eq!(set.insert(node(2), 100, &[0xABu8; 48], &s.pop()), Err(CertError::KeyEncoding));
|
||||
assert_eq!(
|
||||
set.insert(node(1), 100, &[0u8; 48], &s.pop()),
|
||||
Err(CertError::KeyEncoding)
|
||||
);
|
||||
assert_eq!(
|
||||
set.insert(node(2), 100, &[0xABu8; 48], &s.pop()),
|
||||
Err(CertError::KeyEncoding)
|
||||
);
|
||||
// A 96-byte signature offered where a 48-byte key belongs.
|
||||
assert_eq!(set.insert(node(4), 100, &[0u8; 96], &s.pop()), Err(CertError::KeyEncoding));
|
||||
assert_eq!(
|
||||
set.insert(node(4), 100, &[0u8; 96], &s.pop()),
|
||||
Err(CertError::KeyEncoding)
|
||||
);
|
||||
// No key at all is not a malformed key: it names the other door.
|
||||
assert_eq!(set.insert(node(3), 100, &[], &s.pop()), Err(CertError::NoKey));
|
||||
assert_eq!(
|
||||
set.insert(node(3), 100, &[], &s.pop()),
|
||||
Err(CertError::NoKey)
|
||||
);
|
||||
|
||||
assert!(set.is_empty(), "nothing refused left a member behind");
|
||||
}
|
||||
@@ -592,12 +627,17 @@ fn a_key_without_its_proof_is_refused() {
|
||||
("another validator's proof", other.pop()),
|
||||
// The right secret, the right key, the WRONG node — the proof binds the
|
||||
// identity, so it does not travel to a second one.
|
||||
("this key's proof, made for another node", pop::sign(&s.sk, &other.id, &s.public())),
|
||||
(
|
||||
"this key's proof, made for another node",
|
||||
pop::sign(&s.sk, &other.id, &s.public()),
|
||||
),
|
||||
// A signature by this secret over this preimage, in the VOTE domain.
|
||||
// Collapse the two tags and this passes.
|
||||
(
|
||||
"a vote-domain signature over the proof preimage",
|
||||
s.sk.sign(&pop::message(&s.id, &s.public()), DST, &[]).compress().to_vec(),
|
||||
s.sk.sign(&pop::message(&s.id, &s.public()), DST, &[])
|
||||
.compress()
|
||||
.to_vec(),
|
||||
),
|
||||
] {
|
||||
let mut set = ValidatorSet::new();
|
||||
@@ -626,13 +666,21 @@ fn one_key_belongs_to_one_node() {
|
||||
let second = node(9);
|
||||
|
||||
let mut set = ValidatorSet::new();
|
||||
set.insert(s.id, 100, &s.public(), &s.pop()).expect("first id");
|
||||
set.insert(s.id, 100, &s.public(), &s.pop())
|
||||
.expect("first id");
|
||||
|
||||
// A genuine, node-bound proof for the second id — possession is satisfied,
|
||||
// and uniqueness is what refuses it.
|
||||
let proof = pop::sign(&s.sk, &second, &s.public());
|
||||
assert_eq!(pop::verify(&second, &s.public(), &proof), Ok(()), "the proof is genuine");
|
||||
assert_eq!(set.insert(second, 100, &s.public(), &proof), Err(CertError::DuplicateKey));
|
||||
assert_eq!(
|
||||
pop::verify(&second, &s.public(), &proof),
|
||||
Ok(()),
|
||||
"the proof is genuine"
|
||||
);
|
||||
assert_eq!(
|
||||
set.insert(second, 100, &s.public(), &proof),
|
||||
Err(CertError::DuplicateKey)
|
||||
);
|
||||
|
||||
assert_eq!(set.len(), 1, "the second id never became a member");
|
||||
assert!(!set.contains(&second));
|
||||
@@ -647,36 +695,57 @@ fn one_node_holds_one_key() {
|
||||
let b = Signer::new(2, 100);
|
||||
|
||||
let mut set = ValidatorSet::new();
|
||||
set.insert(a.id, 100, &a.public(), &a.pop()).expect("admitted");
|
||||
set.insert(a.id, 100, &a.public(), &a.pop())
|
||||
.expect("admitted");
|
||||
|
||||
// The same node under a SECOND key it genuinely holds.
|
||||
let second = pop::sign(&b.sk, &a.id, &b.public());
|
||||
assert_eq!(pop::verify(&a.id, &b.public(), &second), Ok(()), "the proof is genuine");
|
||||
assert_eq!(set.insert(a.id, 100, &b.public(), &second), Err(CertError::DuplicateNode));
|
||||
assert_eq!(
|
||||
pop::verify(&a.id, &b.public(), &second),
|
||||
Ok(()),
|
||||
"the proof is genuine"
|
||||
);
|
||||
assert_eq!(
|
||||
set.insert(a.id, 100, &b.public(), &second),
|
||||
Err(CertError::DuplicateNode)
|
||||
);
|
||||
|
||||
// The identical registration offered twice fails on the KEY axis first,
|
||||
// which is the order Go iterates them in.
|
||||
assert_eq!(set.insert(a.id, 100, &a.public(), &a.pop()), Err(CertError::DuplicateKey));
|
||||
assert_eq!(
|
||||
set.insert(a.id, 100, &a.public(), &a.pop()),
|
||||
Err(CertError::DuplicateKey)
|
||||
);
|
||||
|
||||
// Neither door restates a member: an unkeyed admission of a keyed node is
|
||||
// refused, and so is the reverse — a re-admission cannot quietly de-key a
|
||||
// validator or change its weight.
|
||||
assert_eq!(set.insert_unkeyed(a.id, 5), Err(CertError::DuplicateNode));
|
||||
set.insert_unkeyed(b.id, 5).expect("a new node, no key");
|
||||
assert_eq!(set.insert(b.id, 100, &b.public(), &b.pop()), Err(CertError::DuplicateNode));
|
||||
assert_eq!(
|
||||
set.insert(b.id, 100, &b.public(), &b.pop()),
|
||||
Err(CertError::DuplicateNode)
|
||||
);
|
||||
|
||||
// Nothing moved.
|
||||
assert_eq!(set.len(), 2);
|
||||
assert_eq!(set.weight(&a.id, 0), 100);
|
||||
assert_eq!(set.public_key(&a.id).map(|k| k.compress()), Some(a.public()));
|
||||
assert_eq!(
|
||||
set.public_key(&a.id).map(|k| k.compress()),
|
||||
Some(a.public())
|
||||
);
|
||||
assert!(!set.can_verify(&b.id));
|
||||
|
||||
// Re-keying is a retraction and a fresh admission, and nothing else: the old
|
||||
// key is freed with the member, and the node comes back under the new one.
|
||||
set.remove(&a.id);
|
||||
assert!(!set.contains(&a.id));
|
||||
set.insert(a.id, 100, &b.public(), &second).expect("re-admitted under a new key");
|
||||
assert_eq!(set.public_key(&a.id).map(|k| k.compress()), Some(b.public()));
|
||||
set.insert(a.id, 100, &b.public(), &second)
|
||||
.expect("re-admitted under a new key");
|
||||
assert_eq!(
|
||||
set.public_key(&a.id).map(|k| k.compress()),
|
||||
Some(b.public())
|
||||
);
|
||||
}
|
||||
|
||||
/// A KEYED SIGNER WITH NO STAKE is a phantom: it raises the count of distinct
|
||||
@@ -688,21 +757,28 @@ fn a_zero_weight_signer_is_refused_at_registration() {
|
||||
let s = Signer::new(1, 100);
|
||||
let mut set = ValidatorSet::new();
|
||||
|
||||
assert_eq!(set.insert(s.id, 0, &s.public(), &s.pop()), Err(CertError::ZeroWeight));
|
||||
assert_eq!(
|
||||
set.insert(s.id, 0, &s.public(), &s.pop()),
|
||||
Err(CertError::ZeroWeight)
|
||||
);
|
||||
assert!(set.is_empty());
|
||||
|
||||
// The clause ORDER, pinned: a registration that is both weightless and
|
||||
// unproven is refused for its weight, because Go checks the O(1) clause
|
||||
// first and a port that reordered them would name a different reason — and
|
||||
// would spend a pairing on a registration that was inadmissible on its face.
|
||||
assert_eq!(set.insert(s.id, 0, &s.public(), &[]), Err(CertError::ZeroWeight));
|
||||
assert_eq!(
|
||||
set.insert(s.id, 0, &s.public(), &[]),
|
||||
Err(CertError::ZeroWeight)
|
||||
);
|
||||
// No key beats both, as it does in Go.
|
||||
assert_eq!(set.insert(s.id, 0, &[], &[]), Err(CertError::NoKey));
|
||||
|
||||
// A member with no KEY may hold no stake: it can never sign, so it is no
|
||||
// phantom signer — it only raises `n`, which is the direction that makes
|
||||
// every floor harder rather than easier. Go's flatten carries these too.
|
||||
set.insert_unkeyed(s.id, 0).expect("a keyless member may be weightless");
|
||||
set.insert_unkeyed(s.id, 0)
|
||||
.expect("a keyless member may be weightless");
|
||||
assert_eq!(set.len(), 1);
|
||||
assert!(!set.can_verify(&s.id));
|
||||
}
|
||||
@@ -810,7 +886,9 @@ fn the_engine_issues_certificates_only_from_signatures() {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let cert = q.create_certificate(pos.clone(), &signed).expect("certificate");
|
||||
let cert = q
|
||||
.create_certificate(pos.clone(), &signed)
|
||||
.expect("certificate");
|
||||
assert_eq!(cert.votes.len(), 4);
|
||||
assert_eq!(cert.tier, Finality::Quasar);
|
||||
assert!(q.verify_certificate(&cert));
|
||||
@@ -858,10 +936,20 @@ fn a_keyless_member_cannot_be_certified() {
|
||||
let mut q = QuasarConsensus::new(&config);
|
||||
|
||||
// Two keyed, two not.
|
||||
q.add_validator_with_key(NodeID::from(signers[0].id), 100, &signers[0].public(), &signers[0].pop())
|
||||
.unwrap();
|
||||
q.add_validator_with_key(NodeID::from(signers[1].id), 100, &signers[1].public(), &signers[1].pop())
|
||||
.unwrap();
|
||||
q.add_validator_with_key(
|
||||
NodeID::from(signers[0].id),
|
||||
100,
|
||||
&signers[0].public(),
|
||||
&signers[0].pop(),
|
||||
)
|
||||
.unwrap();
|
||||
q.add_validator_with_key(
|
||||
NodeID::from(signers[1].id),
|
||||
100,
|
||||
&signers[1].public(),
|
||||
&signers[1].pop(),
|
||||
)
|
||||
.unwrap();
|
||||
q.add_validator(NodeID::from(signers[2].id), 100).unwrap();
|
||||
q.add_validator(NodeID::from(signers[3].id), 100).unwrap();
|
||||
|
||||
@@ -890,7 +978,10 @@ fn a_keyless_member_cannot_be_certified() {
|
||||
// clause rather than reporting that something did not reach something.
|
||||
assert!(matches!(
|
||||
q.create_certificate(pos, &votes),
|
||||
Err(ConsensusError::Cert(CertError::MinCommittee { n: 2, need: 4 }))
|
||||
Err(ConsensusError::Cert(CertError::MinCommittee {
|
||||
n: 2,
|
||||
need: 4
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1040,7 +1131,8 @@ fn a_set_whose_weights_overflow_is_refused_at_admission() {
|
||||
#[test]
|
||||
fn an_unkeyed_member_cannot_overflow_the_total_either() {
|
||||
let mut set = ValidatorSet::new();
|
||||
set.insert_unkeyed(node(1), u64::MAX).expect("the first fits");
|
||||
set.insert_unkeyed(node(1), u64::MAX)
|
||||
.expect("the first fits");
|
||||
assert_eq!(
|
||||
set.insert_unkeyed(node(2), 1),
|
||||
Err(CertError::WeightOverflow)
|
||||
@@ -1066,11 +1158,15 @@ fn the_total_is_exact_at_the_boundary_and_after_a_retraction() {
|
||||
set.insert_unkeyed(node(2), 10).expect("insert");
|
||||
assert_eq!(set.carried(), u64::MAX);
|
||||
|
||||
assert_eq!(set.insert_unkeyed(node(3), 1), Err(CertError::WeightOverflow));
|
||||
assert_eq!(
|
||||
set.insert_unkeyed(node(3), 1),
|
||||
Err(CertError::WeightOverflow)
|
||||
);
|
||||
|
||||
set.remove(&node(2));
|
||||
assert_eq!(set.carried(), u64::MAX - 10);
|
||||
set.insert_unkeyed(node(3), 10).expect("the freed room is real");
|
||||
set.insert_unkeyed(node(3), 10)
|
||||
.expect("the freed room is real");
|
||||
assert_eq!(set.carried(), u64::MAX);
|
||||
}
|
||||
|
||||
@@ -1101,7 +1197,9 @@ fn a_clamping_stake_source_is_refused_rather_than_read() {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
fn signer_stake(&self, _epoch_height: u64) -> u64 {
|
||||
self.weights.iter().fold(0u64, |a, (_, w)| a.saturating_add(*w))
|
||||
self.weights
|
||||
.iter()
|
||||
.fold(0u64, |a, (_, w)| a.saturating_add(*w))
|
||||
}
|
||||
fn signer_count(&self, _epoch_height: u64) -> i64 {
|
||||
self.weights.len() as i64
|
||||
@@ -1184,7 +1282,10 @@ fn one_bad_registration_refuses_the_whole_set() {
|
||||
refused += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!((partial.len(), partial.signer_stake(0), refused), (3, 300, 1));
|
||||
assert_eq!(
|
||||
(partial.len(), partial.signer_stake(0), refused),
|
||||
(3, 300, 1)
|
||||
);
|
||||
|
||||
// Repaired, the set is admitted whole.
|
||||
registrations[3].proof = signers[3].pop();
|
||||
@@ -1255,7 +1356,8 @@ fn whale(n: u8, heavy: u64) -> (Vec<Signer>, ValidatorSet) {
|
||||
.collect();
|
||||
let mut set = ValidatorSet::new();
|
||||
for s in &signers {
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop()).expect("insert");
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop())
|
||||
.expect("insert");
|
||||
}
|
||||
(signers, set)
|
||||
}
|
||||
@@ -1310,7 +1412,11 @@ fn meeting_the_count_without_the_stake_is_refused_too() {
|
||||
|
||||
assert_eq!(
|
||||
cert.verify_weighted(&set, &set, 0),
|
||||
Err(CertError::StakeBelowSupermajority { voted: 4, signer: 104, need_above: 69 }),
|
||||
Err(CertError::StakeBelowSupermajority {
|
||||
voted: 4,
|
||||
signer: 104,
|
||||
need_above: 69
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1319,7 +1425,17 @@ fn meeting_the_count_without_the_stake_is_refused_too() {
|
||||
/// form. It is never above the set, so it is never a rung nothing can satisfy.
|
||||
#[test]
|
||||
fn the_export_floor_is_the_supermajority_in_seats() {
|
||||
for (n, want) in [(1, 1), (2, 2), (3, 3), (4, 3), (5, 4), (11, 8), (21, 15), (41, 28), (100, 67)] {
|
||||
for (n, want) in [
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 3),
|
||||
(5, 4),
|
||||
(11, 8),
|
||||
(21, 15),
|
||||
(41, 28),
|
||||
(100, 67),
|
||||
] {
|
||||
assert_eq!(two_thirds_count(n), want, "two_thirds_count({n})");
|
||||
}
|
||||
for n in 1i64..=1000 {
|
||||
@@ -1386,7 +1502,8 @@ fn keyless_stake_is_in_no_floor_and_export_still_reaches() {
|
||||
let (signers, mut set) = committee(6, 100);
|
||||
let mut spectator = [0u8; 20];
|
||||
spectator[0] = 200;
|
||||
set.insert_unkeyed(spectator, 300).expect("a keyless member");
|
||||
set.insert_unkeyed(spectator, 300)
|
||||
.expect("a keyless member");
|
||||
|
||||
// What the set carries, and what can actually sign.
|
||||
assert_eq!(set.carried(), 900, "the chain carries nine hundred");
|
||||
@@ -1510,7 +1627,10 @@ fn an_export_certificate_needs_a_byzantine_committee() {
|
||||
let cert = declaring(&signers, Finality::Quasar, i64::from(n), usize::from(n));
|
||||
|
||||
// Both quorum floors are MET, so neither can be what refuses it.
|
||||
assert!(cert.voter_count() >= two_thirds_count(set.signer_count(0)), "n={n}");
|
||||
assert!(
|
||||
cert.voter_count() >= two_thirds_count(set.signer_count(0)),
|
||||
"n={n}"
|
||||
);
|
||||
assert!(
|
||||
u64::from(n) * 100 > two_thirds_stake_floor(set.signer_stake(0)),
|
||||
"n={n}: unanimity does not clear the stake floor, so this case does not \
|
||||
@@ -1519,7 +1639,10 @@ fn an_export_certificate_needs_a_byzantine_committee() {
|
||||
|
||||
assert_eq!(
|
||||
cert.verify_weighted(&set, &set, 0),
|
||||
Err(CertError::MinCommittee { n: i64::from(n), need: 4 }),
|
||||
Err(CertError::MinCommittee {
|
||||
n: i64::from(n),
|
||||
need: 4
|
||||
}),
|
||||
"n={n}: a unanimous certificate over a set with no Byzantine fault budget \
|
||||
minted export finality"
|
||||
);
|
||||
@@ -1605,7 +1728,11 @@ fn a_certificate_may_not_declare_a_quorum_the_set_does_not_derive() {
|
||||
under.threshold = 1;
|
||||
assert_eq!(
|
||||
under.verify_weighted(&set, &set, 0),
|
||||
Err(CertError::ThresholdNotDerived { declared: 1, derived: 3, n: 4 }),
|
||||
Err(CertError::ThresholdNotDerived {
|
||||
declared: 1,
|
||||
derived: 3,
|
||||
n: 4
|
||||
}),
|
||||
);
|
||||
|
||||
// And an over-claim, for the same reason in the other direction: a certificate
|
||||
@@ -1616,7 +1743,11 @@ fn a_certificate_may_not_declare_a_quorum_the_set_does_not_derive() {
|
||||
over.threshold = 4;
|
||||
assert_eq!(
|
||||
over.verify_weighted(&set, &set, 0),
|
||||
Err(CertError::ThresholdNotDerived { declared: 4, derived: 3, n: 4 }),
|
||||
Err(CertError::ThresholdNotDerived {
|
||||
declared: 4,
|
||||
derived: 3,
|
||||
n: 4
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1633,7 +1764,11 @@ fn the_accept_rung_derives_its_quorum_the_same_way() {
|
||||
under.threshold = 1;
|
||||
assert_eq!(
|
||||
under.verify_weighted(&set, &set, 0),
|
||||
Err(CertError::ThresholdNotDerived { declared: 1, derived: 3, n: 5 }),
|
||||
Err(CertError::ThresholdNotDerived {
|
||||
declared: 1,
|
||||
derived: 3,
|
||||
n: 5
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1641,8 +1776,16 @@ fn the_accept_rung_derives_its_quorum_the_same_way() {
|
||||
#[test]
|
||||
fn the_signer_floor_is_the_rungs_own_arithmetic() {
|
||||
for n in 1..=64i64 {
|
||||
assert_eq!(signer_floor(Finality::Nova, n), nova_signer_floor(n), "n={n}");
|
||||
assert_eq!(signer_floor(Finality::Quasar, n), two_thirds_count(n), "n={n}");
|
||||
assert_eq!(
|
||||
signer_floor(Finality::Nova, n),
|
||||
nova_signer_floor(n),
|
||||
"n={n}"
|
||||
);
|
||||
assert_eq!(
|
||||
signer_floor(Finality::Quasar, n),
|
||||
two_thirds_count(n),
|
||||
"n={n}"
|
||||
);
|
||||
}
|
||||
for rung in [Finality::Photon, Finality::Wave, Finality::Horizon] {
|
||||
assert_eq!(signer_floor(rung, 41), 0, "{rung:?} is not an accept rung");
|
||||
@@ -1678,7 +1821,10 @@ fn an_unresolved_set_is_not_named_by_the_derived_clause() {
|
||||
// ------------------------------------------------ the issuer states no quorum
|
||||
|
||||
/// A committee of `n` keyed validators at equal weight, under `config`.
|
||||
fn issuer(config: &lux_consensus::QuasarConfig, n: u8) -> (Vec<Signer>, lux_consensus::QuasarConsensus) {
|
||||
fn issuer(
|
||||
config: &lux_consensus::QuasarConfig,
|
||||
n: u8,
|
||||
) -> (Vec<Signer>, lux_consensus::QuasarConsensus) {
|
||||
use lux_consensus::{NodeID, QuasarConsensus};
|
||||
let signers: Vec<Signer> = (1..=n).map(|i| Signer::new(i, 100)).collect();
|
||||
let mut q = QuasarConsensus::new(config);
|
||||
@@ -1738,7 +1884,10 @@ fn the_issuer_derives_its_certificates_quorum_from_the_set() {
|
||||
let cert = q
|
||||
.create_certificate(pos.clone(), &ballots(&signers, &pos, voters))
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("k={} n={n}: the issuer refused its own quorum: {e}", config.k)
|
||||
panic!(
|
||||
"k={} n={n}: the issuer refused its own quorum: {e}",
|
||||
config.k
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -64,7 +64,8 @@ fn committee(n: u8, weight: u64) -> (Vec<Signer>, ValidatorSet) {
|
||||
let signers: Vec<Signer> = (1..=n).map(|i| Signer::new(i, weight)).collect();
|
||||
let mut set = ValidatorSet::new();
|
||||
for s in &signers {
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop()).expect("insert");
|
||||
set.insert(s.id, s.weight, &s.public(), &s.pop())
|
||||
.expect("insert");
|
||||
}
|
||||
(signers, set)
|
||||
}
|
||||
@@ -199,7 +200,11 @@ fn retracting_an_unkeyed_member_leaves_the_signable_stake_alone() {
|
||||
|
||||
set.remove(&spectator);
|
||||
|
||||
assert_eq!(set.carried(), 400, "the carried total kept a departed member");
|
||||
assert_eq!(
|
||||
set.carried(),
|
||||
400,
|
||||
"the carried total kept a departed member"
|
||||
);
|
||||
assert_eq!(
|
||||
set.signer_stake(0),
|
||||
400,
|
||||
@@ -220,7 +225,11 @@ fn retracting_a_keyed_member_frees_both_the_stake_and_the_key() {
|
||||
|
||||
set.remove(&departing.id);
|
||||
|
||||
assert_eq!(set.carried(), 300, "the carried total kept a departed signer");
|
||||
assert_eq!(
|
||||
set.carried(),
|
||||
300,
|
||||
"the carried total kept a departed signer"
|
||||
);
|
||||
assert_eq!(
|
||||
set.signer_stake(0),
|
||||
300,
|
||||
@@ -364,7 +373,8 @@ fn a_real_shortfall_reports_the_stake_it_actually_had() {
|
||||
.collect();
|
||||
let mut set = ValidatorSet::new();
|
||||
for sgn in &signers {
|
||||
set.insert(sgn.id, sgn.weight, &sgn.public(), &sgn.pop()).expect("insert");
|
||||
set.insert(sgn.id, sgn.weight, &sgn.public(), &sgn.pop())
|
||||
.expect("insert");
|
||||
}
|
||||
|
||||
let text = cert(&signers[1..], 4, 5, Finality::Quasar)
|
||||
|
||||
@@ -56,7 +56,13 @@ fn an_undecided_round_resets_confidence() {
|
||||
/// round is itself evidence for the new preference.
|
||||
#[test]
|
||||
fn an_opposing_round_switches_preference_and_restarts_at_one() {
|
||||
let flip = [Some(true), Some(true), Some(false), Some(false), Some(false)];
|
||||
let flip = [
|
||||
Some(true),
|
||||
Some(true),
|
||||
Some(false),
|
||||
Some(false),
|
||||
Some(false),
|
||||
];
|
||||
assert_eq!(run(&flip, 3), Some((4, Decision::Reject)));
|
||||
|
||||
// Restarting at one, not zero: three NOs after the switch, the switch
|
||||
@@ -72,8 +78,7 @@ fn an_opposing_round_switches_preference_and_restarts_at_one() {
|
||||
/// keeps changing its mind about does not become final by attrition.
|
||||
#[test]
|
||||
fn alternating_rounds_never_decide() {
|
||||
let alternating: Vec<Option<bool>> =
|
||||
(0..1000).map(|i| Some(i % 2 == 0)).collect();
|
||||
let alternating: Vec<Option<bool>> = (0..1000).map(|i| Some(i % 2 == 0)).collect();
|
||||
assert_eq!(run(&alternating, 2), None);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,16 @@ use serde_json::Value;
|
||||
|
||||
fn corpus() -> Value {
|
||||
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../conformance/corpus.json");
|
||||
let raw = std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| panic!("read {path}: {e} — regenerate with `go test ./conformance -update`"));
|
||||
let raw = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
||||
panic!("read {path}: {e} — regenerate with `go test ./conformance -update`")
|
||||
});
|
||||
serde_json::from_str(&raw).expect("corpus.json does not parse")
|
||||
}
|
||||
|
||||
fn id(v: &Value, key: &str) -> Id {
|
||||
let s = v[key].as_str().unwrap_or_else(|| panic!("case has no {key}"));
|
||||
let s = v[key]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("case has no {key}"));
|
||||
let bytes = hex::decode(s).unwrap_or_else(|e| panic!("{key}: {e}"));
|
||||
let mut out = EMPTY;
|
||||
assert_eq!(bytes.len(), 32, "{key} is {} bytes, want 32", bytes.len());
|
||||
@@ -83,7 +86,10 @@ fn signed_messages_match_go() {
|
||||
let accept = case["accept"].as_bool().unwrap();
|
||||
let got = hex::encode(canonical_vote_message(&pos, accept));
|
||||
let want = case["message"].as_str().unwrap();
|
||||
assert_eq!(got, want, "vote case {name}: this crate signs different bytes than the network");
|
||||
assert_eq!(
|
||||
got, want,
|
||||
"vote case {name}: this crate signs different bytes than the network"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,13 +136,21 @@ fn count_thresholds_match_go() {
|
||||
|
||||
for row in c["threshold"]["count"].as_array().expect("no count rows") {
|
||||
let n = row["n"].as_i64().unwrap();
|
||||
assert_eq!(nova_quorum(n), row["novaQuorum"].as_i64().unwrap(), "novaQuorum n={n}");
|
||||
assert_eq!(
|
||||
nova_quorum(n),
|
||||
row["novaQuorum"].as_i64().unwrap(),
|
||||
"novaQuorum n={n}"
|
||||
);
|
||||
assert_eq!(
|
||||
nova_signer_floor(n),
|
||||
row["novaSignerFloor"].as_i64().unwrap(),
|
||||
"novaSignerFloor n={n}"
|
||||
);
|
||||
assert_eq!(nova_beta(n), row["novaBeta"].as_i64().unwrap(), "novaBeta n={n}");
|
||||
assert_eq!(
|
||||
nova_beta(n),
|
||||
row["novaBeta"].as_i64().unwrap(),
|
||||
"novaBeta n={n}"
|
||||
);
|
||||
assert_eq!(
|
||||
crash_tolerance(n),
|
||||
row["crashTolerance"].as_i64().unwrap(),
|
||||
@@ -149,7 +163,10 @@ fn count_thresholds_match_go() {
|
||||
);
|
||||
}
|
||||
|
||||
for row in c["threshold"]["weighted"].as_array().expect("no weighted rows") {
|
||||
for row in c["threshold"]["weighted"]
|
||||
.as_array()
|
||||
.expect("no weighted rows")
|
||||
{
|
||||
let weights: Vec<u64> = row["weights"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
@@ -180,7 +197,11 @@ fn ladder_matches_go() {
|
||||
Finality::Quasar,
|
||||
Finality::Horizon,
|
||||
];
|
||||
assert_eq!(rungs.len(), all.len(), "the ladder has a different number of rungs");
|
||||
assert_eq!(
|
||||
rungs.len(),
|
||||
all.len(),
|
||||
"the ladder has a different number of rungs"
|
||||
);
|
||||
|
||||
for (row, rung) in rungs.iter().zip(all.iter().copied()) {
|
||||
assert_eq!(row["name"].as_str().unwrap(), rung.name());
|
||||
@@ -207,7 +228,10 @@ fn ladder_matches_go() {
|
||||
|
||||
// Stated outright, not read from the corpus: nothing below Quasar leaves the
|
||||
// chain. If the corpus were re-blessed with this inverted, this still fails.
|
||||
assert!(!Finality::Nova.authorizes_export(), "Nova must never authorize export");
|
||||
assert!(
|
||||
!Finality::Nova.authorizes_export(),
|
||||
"Nova must never authorize export"
|
||||
);
|
||||
assert!(Finality::Nova.authorizes_local_execution());
|
||||
assert!(Finality::Quasar.authorizes_export());
|
||||
assert!(!Finality::Quasar.authorizes_irreversible_settlement());
|
||||
@@ -227,7 +251,10 @@ fn certificate_header_matches_go() {
|
||||
case["length"].as_u64().unwrap() as usize,
|
||||
"{name}: recorded length disagrees with the recorded bytes"
|
||||
);
|
||||
assert!(wire.len() > 4, "{name}: certificate is too short to carry a header");
|
||||
assert!(
|
||||
wire.len() > 4,
|
||||
"{name}: certificate is too short to carry a header"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
u16::from_be_bytes([wire[0], wire[1]]),
|
||||
|
||||
+74
-18
@@ -10,8 +10,8 @@
|
||||
//! started. Both are doors that failed open once.
|
||||
|
||||
use lux_consensus::{
|
||||
quick_start, ConsensusError, Engine, EventHorizon, NodeID, QuasarConfig,
|
||||
QuasarConsensus, QuasarEngine, Status, VoteType, ID,
|
||||
quick_start, ConsensusError, Engine, EventHorizon, NodeID, QuasarConfig, QuasarConsensus,
|
||||
QuasarEngine, Status, VoteType, ID,
|
||||
};
|
||||
|
||||
fn id(n: u8) -> ID {
|
||||
@@ -66,7 +66,10 @@ fn nothing_enters_an_engine_that_was_never_started() {
|
||||
|
||||
let vote = lux_consensus::new_vote(id(1), VoteType::Preference, voter(1));
|
||||
assert!(
|
||||
matches!(engine.record_vote(vote), Err(ConsensusError::NotInitialized)),
|
||||
matches!(
|
||||
engine.record_vote(vote),
|
||||
Err(ConsensusError::NotInitialized)
|
||||
),
|
||||
"a vote was counted by an unstarted engine"
|
||||
);
|
||||
}
|
||||
@@ -109,7 +112,11 @@ fn stopping_shuts_the_doors_again() {
|
||||
Err(ConsensusError::NotInitialized)
|
||||
));
|
||||
assert!(matches!(
|
||||
engine.record_vote(lux_consensus::new_vote(id(1), VoteType::Preference, voter(1))),
|
||||
engine.record_vote(lux_consensus::new_vote(
|
||||
id(1),
|
||||
VoteType::Preference,
|
||||
voter(1)
|
||||
)),
|
||||
Err(ConsensusError::NotInitialized)
|
||||
));
|
||||
}
|
||||
@@ -147,7 +154,11 @@ fn an_engine_with_no_validators_counts_nobody() {
|
||||
for i in 1..=9u8 {
|
||||
assert!(
|
||||
matches!(
|
||||
engine.record_vote(lux_consensus::new_vote(id(1), VoteType::Preference, voter(i))),
|
||||
engine.record_vote(lux_consensus::new_vote(
|
||||
id(1),
|
||||
VoteType::Preference,
|
||||
voter(i)
|
||||
)),
|
||||
Err(ConsensusError::NotValidator)
|
||||
),
|
||||
"voter {i} was counted by an engine that holds no validators"
|
||||
@@ -167,7 +178,11 @@ fn a_vote_for_an_unknown_block_is_refused() {
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
engine.record_vote(lux_consensus::new_vote(id(9), VoteType::Preference, voter(1))),
|
||||
engine.record_vote(lux_consensus::new_vote(
|
||||
id(9),
|
||||
VoteType::Preference,
|
||||
voter(1)
|
||||
)),
|
||||
Err(ConsensusError::BlockNotFound)
|
||||
),
|
||||
"a vote created a tally for a block nobody proposed"
|
||||
@@ -219,13 +234,21 @@ fn members_carry_a_block_to_accepted_and_the_height_follows() {
|
||||
|
||||
for i in 1..=3u8 {
|
||||
engine
|
||||
.record_vote(lux_consensus::new_vote(id(1), VoteType::Preference, voter(i)))
|
||||
.record_vote(lux_consensus::new_vote(
|
||||
id(1),
|
||||
VoteType::Preference,
|
||||
voter(i),
|
||||
))
|
||||
.expect("a member's vote was refused");
|
||||
}
|
||||
|
||||
assert!(engine.is_accepted(&id(1)), "three of three did not decide");
|
||||
assert_eq!(engine.get_status(&id(1)), Status::Accepted);
|
||||
assert_eq!(engine.height(), 7, "the accepted height did not follow the block");
|
||||
assert_eq!(
|
||||
engine.height(),
|
||||
7,
|
||||
"the accepted height did not follow the block"
|
||||
);
|
||||
}
|
||||
|
||||
/// The same committee voting to cancel rejects the block instead. Accept and
|
||||
@@ -275,7 +298,10 @@ fn there_is_no_quorum_until_the_set_reaches_it() {
|
||||
|
||||
assert!(!quasar.has_quorum(), "an empty set reported a quorum");
|
||||
quasar.add_validator(voter(1), 100).expect("add_validator");
|
||||
assert!(!quasar.has_quorum(), "one of a two-member quorum reported one");
|
||||
assert!(
|
||||
!quasar.has_quorum(),
|
||||
"one of a two-member quorum reported one"
|
||||
);
|
||||
quasar.add_validator(voter(2), 100).expect("add_validator");
|
||||
assert!(quasar.has_quorum());
|
||||
}
|
||||
@@ -290,11 +316,18 @@ fn unsigned_ballots_finalize_nothing() {
|
||||
engine.add(block(1, 7)).expect("add");
|
||||
for i in 1..=3u8 {
|
||||
engine
|
||||
.record_vote(lux_consensus::new_vote(id(1), VoteType::Preference, voter(i)))
|
||||
.record_vote(lux_consensus::new_vote(
|
||||
id(1),
|
||||
VoteType::Preference,
|
||||
voter(i),
|
||||
))
|
||||
.expect("record_vote");
|
||||
}
|
||||
|
||||
assert!(engine.is_accepted(&id(1)), "the preference did not converge");
|
||||
assert!(
|
||||
engine.is_accepted(&id(1)),
|
||||
"the preference did not converge"
|
||||
);
|
||||
|
||||
let quasar = QuasarConsensus::new(&quorum_of_three());
|
||||
assert!(
|
||||
@@ -314,7 +347,11 @@ fn the_horizon_ignores_a_block_from_an_unregistered_chain() {
|
||||
let mut horizon = EventHorizon::new(&quorum_of_three());
|
||||
|
||||
horizon.accept_block("zoo", id(1));
|
||||
assert_eq!(horizon.height(), 0, "an unregistered chain moved the height");
|
||||
assert_eq!(
|
||||
horizon.height(),
|
||||
0,
|
||||
"an unregistered chain moved the height"
|
||||
);
|
||||
|
||||
horizon.register_chain("zoo".to_string());
|
||||
horizon.accept_block("zoo", id(1));
|
||||
@@ -368,7 +405,10 @@ fn every_engine_refusal_names_itself() {
|
||||
for (err, expected) in cases {
|
||||
let text = err.to_string();
|
||||
assert_eq!(text, expected, "{err:?} does not name itself");
|
||||
assert!(!seen.contains(&text), "{text:?} names two different clauses");
|
||||
assert!(
|
||||
!seen.contains(&text),
|
||||
"{text:?} names two different clauses"
|
||||
);
|
||||
seen.push(text);
|
||||
}
|
||||
|
||||
@@ -381,7 +421,10 @@ fn every_engine_refusal_names_itself() {
|
||||
ConsensusError::NetworkError("peer gone".into()).to_string(),
|
||||
"Network error: peer gone"
|
||||
);
|
||||
assert_eq!(ConsensusError::Other("anything".into()).to_string(), "anything");
|
||||
assert_eq!(
|
||||
ConsensusError::Other("anything".into()).to_string(),
|
||||
"anything"
|
||||
);
|
||||
}
|
||||
|
||||
/// A vote is a preference for the block or against it, and the two commit types
|
||||
@@ -417,7 +460,11 @@ fn a_block_short_of_a_quorum_is_in_flight_and_not_rejected() {
|
||||
// Two of a committee of three: fewer than k, so no round has closed.
|
||||
for i in 1..=2u8 {
|
||||
engine
|
||||
.record_vote(lux_consensus::new_vote(id(1), VoteType::Preference, voter(i)))
|
||||
.record_vote(lux_consensus::new_vote(
|
||||
id(1),
|
||||
VoteType::Preference,
|
||||
voter(i),
|
||||
))
|
||||
.expect("record_vote");
|
||||
}
|
||||
|
||||
@@ -433,9 +480,18 @@ fn a_block_short_of_a_quorum_is_in_flight_and_not_rejected() {
|
||||
/// diff applied to the wrong field.
|
||||
#[test]
|
||||
fn an_engine_runs_the_configuration_it_was_given() {
|
||||
assert_eq!(QuasarEngine::testnet().config().k, QuasarConfig::testnet().k);
|
||||
assert_eq!(QuasarEngine::mainnet().config().k, QuasarConfig::mainnet().k);
|
||||
assert_eq!(QuasarEngine::default().config().k, QuasarConfig::default().k);
|
||||
assert_eq!(
|
||||
QuasarEngine::testnet().config().k,
|
||||
QuasarConfig::testnet().k
|
||||
);
|
||||
assert_eq!(
|
||||
QuasarEngine::mainnet().config().k,
|
||||
QuasarConfig::mainnet().k
|
||||
);
|
||||
assert_eq!(
|
||||
QuasarEngine::default().config().k,
|
||||
QuasarConfig::default().k
|
||||
);
|
||||
|
||||
// The three are genuinely different committees, so the presets are not one
|
||||
// config under three names.
|
||||
|
||||
@@ -117,7 +117,10 @@ fn each_phase_draws_independently() {
|
||||
|
||||
for (phase, _, _, _) in GO {
|
||||
let t = s.theta(phase);
|
||||
assert!((0.5..=0.8).contains(&t), "phase {phase}: θ={t} out of range");
|
||||
assert!(
|
||||
(0.5..=0.8).contains(&t),
|
||||
"phase {phase}: θ={t} out of range"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +134,8 @@ fn a_different_seed_is_a_different_schedule() {
|
||||
other[31] ^= 0x01;
|
||||
let b = FpcSelector::new(0.5, 0.8, other);
|
||||
|
||||
let differ = (0..16).filter(|&p| a.theta(p).to_bits() != b.theta(p).to_bits()).count();
|
||||
let differ = (0..16)
|
||||
.filter(|&p| a.theta(p).to_bits() != b.theta(p).to_bits())
|
||||
.count();
|
||||
assert_eq!(differ, 16, "one flipped seed bit left a phase unchanged");
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
//! counted twice on the way into evidence.
|
||||
|
||||
use blst::min_pk::SecretKey;
|
||||
use lux_consensus::pop;
|
||||
use lux_consensus::{
|
||||
canonical_vote_message, ConsensusError, Finality, NodeID, Position, QuasarConfig,
|
||||
QuasarConsensus, Vote, VoteType, ID,
|
||||
};
|
||||
use lux_consensus::pop;
|
||||
|
||||
fn node_id(n: u8) -> NodeID {
|
||||
NodeID([n; 20])
|
||||
@@ -56,7 +56,10 @@ fn the_block_id_pads_and_truncates_where_the_node_id_cannot() {
|
||||
// makes this constructor wrong for a validator.
|
||||
let a = ID::from_slice(&[7u8; 33]);
|
||||
let b = ID::from_slice(&[7u8; 34]);
|
||||
assert_eq!(a, b, "truncation is lossy, and that is the point being stated");
|
||||
assert_eq!(
|
||||
a, b,
|
||||
"truncation is lossy, and that is the point being stated"
|
||||
);
|
||||
|
||||
// Both `From` routes agree with the constructor they wrap.
|
||||
assert_eq!(ID::from([9u8; 32]), ID::new([9u8; 32]));
|
||||
@@ -190,7 +193,11 @@ fn the_set_handed_out_is_the_set_that_decides() {
|
||||
VoteType::Preference,
|
||||
NodeID::from(s.id),
|
||||
)
|
||||
.with_signature(s.sk.sign(&message, lux_consensus::cert::DST, &[]).compress().to_vec())
|
||||
.with_signature(
|
||||
s.sk.sign(&message, lux_consensus::cert::DST, &[])
|
||||
.compress()
|
||||
.to_vec(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -236,12 +243,20 @@ fn a_validator_contributes_one_vote_to_the_evidence() {
|
||||
VoteType::Preference,
|
||||
NodeID::from(s.id),
|
||||
)
|
||||
.with_signature(s.sk.sign(&message, lux_consensus::cert::DST, &[]).compress().to_vec())
|
||||
.with_signature(
|
||||
s.sk.sign(&message, lux_consensus::cert::DST, &[])
|
||||
.compress()
|
||||
.to_vec(),
|
||||
)
|
||||
};
|
||||
|
||||
// Two validators, one of them shouting: three ballots, two signers, and the
|
||||
// threshold is three.
|
||||
let shouted = vec![ballot(&signers[0]), ballot(&signers[0]), ballot(&signers[1])];
|
||||
let shouted = vec![
|
||||
ballot(&signers[0]),
|
||||
ballot(&signers[0]),
|
||||
ballot(&signers[1]),
|
||||
];
|
||||
assert!(
|
||||
matches!(
|
||||
quasar.create_certificate(pos.clone(), &shouted),
|
||||
@@ -255,7 +270,11 @@ fn a_validator_contributes_one_vote_to_the_evidence() {
|
||||
let cert = quasar
|
||||
.create_certificate(pos, &distinct)
|
||||
.expect("three distinct signers is a quorum");
|
||||
assert_eq!(cert.votes.len(), 3, "the evidence carries one vote per signer");
|
||||
assert_eq!(
|
||||
cert.votes.len(),
|
||||
3,
|
||||
"the evidence carries one vote per signer"
|
||||
);
|
||||
}
|
||||
|
||||
/// A ballot against the block is not evidence for it. `create_certificate`
|
||||
@@ -277,8 +296,11 @@ fn a_signed_cancel_is_not_evidence_of_acceptance() {
|
||||
let cancels: Vec<Vote> = signers
|
||||
.iter()
|
||||
.map(|s| {
|
||||
Vote::new(ID::from(pos.block_id), VoteType::Cancel, NodeID::from(s.id))
|
||||
.with_signature(s.sk.sign(&message, lux_consensus::cert::DST, &[]).compress().to_vec())
|
||||
Vote::new(ID::from(pos.block_id), VoteType::Cancel, NodeID::from(s.id)).with_signature(
|
||||
s.sk.sign(&message, lux_consensus::cert::DST, &[])
|
||||
.compress()
|
||||
.to_vec(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -306,7 +328,9 @@ fn a_signed_cancel_is_not_evidence_of_acceptance() {
|
||||
/// this code, and a test of it would pass or fail by machine.
|
||||
#[test]
|
||||
fn a_generated_id_fills_every_lane() {
|
||||
let draws: Vec<ID> = (0..16).map(|_| lux_consensus::generate_block_id()).collect();
|
||||
let draws: Vec<ID> = (0..16)
|
||||
.map(|_| lux_consensus::generate_block_id())
|
||||
.collect();
|
||||
|
||||
for lane in 0..4 {
|
||||
let range = lane * 8..(lane + 1) * 8;
|
||||
|
||||
+16
-11
@@ -31,9 +31,7 @@ use blst::{
|
||||
blst_p1_from_affine, blst_p1_to_affine, blst_p1_uncompress, BLST_ERROR,
|
||||
};
|
||||
|
||||
use lux_consensus::cert::{
|
||||
CertError, NodeId, QuorumCert, ValidatorSet, Vote, VoteVerifier, DST,
|
||||
};
|
||||
use lux_consensus::cert::{CertError, NodeId, QuorumCert, ValidatorSet, Vote, VoteVerifier, DST};
|
||||
use lux_consensus::finality::{canonical_vote_message, Finality, Position};
|
||||
use lux_consensus::pop;
|
||||
use lux_consensus::quasar::QuasarConsensus;
|
||||
@@ -261,7 +259,8 @@ fn the_rogue_key_cannot_register_without_possession() {
|
||||
|
||||
let mut set = ValidatorSet::new();
|
||||
for h in &honest {
|
||||
set.insert(h.id, 100, &h.pk(), &h.pop()).expect("honest registers");
|
||||
set.insert(h.id, 100, &h.pk(), &h.pop())
|
||||
.expect("honest registers");
|
||||
}
|
||||
|
||||
// The best proof the attacker can make — by the one secret it holds, whose
|
||||
@@ -272,7 +271,10 @@ fn the_rogue_key_cannot_register_without_possession() {
|
||||
);
|
||||
// No proof at all, and a well-formed proof over the wrong (someone else's)
|
||||
// key, are refused just the same.
|
||||
assert_eq!(set.insert(rogue.id, 100, &rogue.pk, &[]), Err(CertError::PopInvalid));
|
||||
assert_eq!(
|
||||
set.insert(rogue.id, 100, &rogue.pk, &[]),
|
||||
Err(CertError::PopInvalid)
|
||||
);
|
||||
assert_eq!(
|
||||
set.insert(rogue.id, 100, &rogue.pk, &honest[0].pop()),
|
||||
Err(CertError::PopInvalid),
|
||||
@@ -485,11 +487,10 @@ fn a_proof_for_one_node_does_not_register_the_key_under_another() {
|
||||
#[test]
|
||||
fn a_vote_dst_signature_is_not_a_proof_of_possession() {
|
||||
let h = Honest::new(1);
|
||||
let vote_dst = h
|
||||
.sk
|
||||
.sign(&pop::message(&h.id, &h.pk()), DST, &[])
|
||||
.compress()
|
||||
.to_vec();
|
||||
let vote_dst =
|
||||
h.sk.sign(&pop::message(&h.id, &h.pk()), DST, &[])
|
||||
.compress()
|
||||
.to_vec();
|
||||
|
||||
let mut set = ValidatorSet::new();
|
||||
assert_eq!(
|
||||
@@ -522,7 +523,11 @@ fn the_identity_key_is_refused_before_the_proof() {
|
||||
#[test]
|
||||
fn signed_identity_uses_the_transport_id_only_in_the_degrade() {
|
||||
let mut p = position(); // canonical_id = [12; 32], block_id = [11; 32]
|
||||
assert_eq!(p.signed_identity(), [12u8; 32], "non-degrade: the canonical id");
|
||||
assert_eq!(
|
||||
p.signed_identity(),
|
||||
[12u8; 32],
|
||||
"non-degrade: the canonical id"
|
||||
);
|
||||
p.canonical_id = [0u8; 32]; // EMPTY
|
||||
assert_eq!(p.signed_identity(), [11u8; 32], "degrade: the transport id");
|
||||
}
|
||||
|
||||
+44
-10
@@ -61,7 +61,11 @@ fn a_validator_votes_once_per_block() {
|
||||
}
|
||||
|
||||
let state = wave.state(&id(1)).expect("state");
|
||||
assert_eq!(state.yes_count, 1, "one voter was counted {} times", state.yes_count);
|
||||
assert_eq!(
|
||||
state.yes_count, 1,
|
||||
"one voter was counted {} times",
|
||||
state.yes_count
|
||||
);
|
||||
assert_eq!(state.votes.len(), 1);
|
||||
assert!(!wave.is_decided(&id(1)));
|
||||
}
|
||||
@@ -81,7 +85,10 @@ fn fewer_than_k_answers_is_not_a_round() {
|
||||
"confidence accumulated before k answers were in"
|
||||
);
|
||||
|
||||
assert!(wave.record_vote(yes(1, 3)), "k answers did not close the round");
|
||||
assert!(
|
||||
wave.record_vote(yes(1, 3)),
|
||||
"k answers did not close the round"
|
||||
);
|
||||
assert!(wave.is_decided(&id(1)));
|
||||
assert_eq!(wave.decision(&id(1)), Decision::Accept);
|
||||
}
|
||||
@@ -163,13 +170,24 @@ fn asking_for_the_threshold_does_not_advance_the_phase() {
|
||||
let phase = wave.phase();
|
||||
let first = wave.threshold();
|
||||
for _ in 0..20 {
|
||||
assert_eq!(wave.threshold(), first, "the threshold moved while being read");
|
||||
assert_eq!(
|
||||
wave.threshold(),
|
||||
first,
|
||||
"the threshold moved while being read"
|
||||
);
|
||||
}
|
||||
assert_eq!(wave.phase(), phase, "reading the threshold advanced the phase");
|
||||
assert_eq!(
|
||||
wave.phase(),
|
||||
phase,
|
||||
"reading the threshold advanced the phase"
|
||||
);
|
||||
|
||||
// A vote is what advances it — the round, not the question.
|
||||
wave.record_vote(yes(1, 1));
|
||||
assert!(wave.phase() > phase, "a recorded vote did not advance the phase");
|
||||
assert!(
|
||||
wave.phase() > phase,
|
||||
"a recorded vote did not advance the phase"
|
||||
);
|
||||
}
|
||||
|
||||
/// With FPC off the threshold is the configured count and the phase never moves,
|
||||
@@ -202,7 +220,10 @@ fn an_unheard_of_block_is_undecided_and_holds_no_state() {
|
||||
|
||||
// Opening a tally for it is explicit, and starts empty.
|
||||
let state = wave.get_or_create_state(&id(9));
|
||||
assert_eq!((state.yes_count, state.no_count, state.confidence), (0, 0, 0));
|
||||
assert_eq!(
|
||||
(state.yes_count, state.no_count, state.confidence),
|
||||
(0, 0, 0)
|
||||
);
|
||||
assert!(!state.decided);
|
||||
assert!(wave.state(&id(9)).is_some());
|
||||
}
|
||||
@@ -262,13 +283,20 @@ fn beta_consecutive_ratios_decide_and_a_middling_round_resets() {
|
||||
|
||||
// 2 of 4 is 0.5, which is neither above alpha nor below 1-alpha: no quorum.
|
||||
assert!(!focus.update(1, 2, 4));
|
||||
assert_eq!(focus.confidence(&1), 0, "a middling round did not reset the run");
|
||||
assert_eq!(
|
||||
focus.confidence(&1),
|
||||
0,
|
||||
"a middling round did not reset the run"
|
||||
);
|
||||
assert!(!focus.is_decided(&1));
|
||||
|
||||
for _ in 0..2 {
|
||||
assert!(!focus.update(1, 4, 4));
|
||||
}
|
||||
assert!(focus.update(1, 4, 4), "three consecutive rounds did not decide");
|
||||
assert!(
|
||||
focus.update(1, 4, 4),
|
||||
"three consecutive rounds did not decide"
|
||||
);
|
||||
assert!(focus.is_decided(&1));
|
||||
assert_eq!(focus.decision(&1), Decision::Accept);
|
||||
}
|
||||
@@ -295,7 +323,10 @@ fn a_decided_item_does_not_reopen() {
|
||||
let settled = focus.decision(&1);
|
||||
|
||||
for _ in 0..5 {
|
||||
assert!(!focus.update(1, 0, 4), "a decided item reported a new decision");
|
||||
assert!(
|
||||
!focus.update(1, 0, 4),
|
||||
"a decided item reported a new decision"
|
||||
);
|
||||
}
|
||||
assert_eq!(focus.decision(&1), settled);
|
||||
assert_eq!(focus.state(&1).expect("state").last_ratio, 1.0);
|
||||
@@ -337,7 +368,10 @@ fn a_run_inside_the_window_accumulates_normally() {
|
||||
|
||||
assert!(!focus.update(1, 4, 4));
|
||||
assert!(!focus.update(1, 4, 4));
|
||||
assert!(focus.update(1, 4, 4), "three rounds inside the window did not decide");
|
||||
assert!(
|
||||
focus.update(1, 4, 4),
|
||||
"three rounds inside the window did not decide"
|
||||
);
|
||||
assert!(focus.is_decided(&1));
|
||||
assert_eq!(focus.decision(&1), Decision::Accept);
|
||||
}
|
||||
|
||||
@@ -186,7 +186,11 @@ fn a_sample_is_distinct_and_never_larger_than_the_set() {
|
||||
let mut distinct = drawn.clone();
|
||||
distinct.sort();
|
||||
distinct.dedup();
|
||||
assert_eq!(distinct.len(), drawn.len(), "a peer was sampled twice at k={k}");
|
||||
assert_eq!(
|
||||
distinct.len(),
|
||||
drawn.len(),
|
||||
"a peer was sampled twice at k={k}"
|
||||
);
|
||||
|
||||
for d in &drawn {
|
||||
assert!(peers.contains(d), "a peer nobody registered was sampled");
|
||||
@@ -217,7 +221,11 @@ fn the_same_state_draws_the_same_committee() {
|
||||
|
||||
let first = sampler.sample(4);
|
||||
for _ in 0..8 {
|
||||
assert_eq!(sampler.sample(4), first, "two draws from one state disagreed");
|
||||
assert_eq!(
|
||||
sampler.sample(4),
|
||||
first,
|
||||
"two draws from one state disagreed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -377,7 +377,12 @@ fn a_keyed_seat_with_no_stake_is_refused_at_either_door() {
|
||||
.as_array()
|
||||
.unwrap_or_else(|| panic!("{name}: no weights"))
|
||||
.iter()
|
||||
.map(|w| w.as_str().expect("weight is not a decimal").parse().unwrap())
|
||||
.map(|w| {
|
||||
w.as_str()
|
||||
.expect("weight is not a decimal")
|
||||
.parse()
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
weights.contains(&0),
|
||||
|
||||
Reference in New Issue
Block a user