Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 40 additions & 32 deletions crates/apollo_consensus/src/simulation_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ use crate::types::{Decision, ProposalCommitment, Round, ValidatorId};
use crate::votes_threshold::QuorumType;

const HEIGHT_0: BlockNumber = BlockNumber(0);
const TOTAL_NODES: usize = 100;
const THRESHOLD: usize = (2 * TOTAL_NODES / 3) + 1;
const NODE_0_LEADER_PROBABILITY: f64 = 0.1;
const NODE_UNDER_TEST: usize = 0;

Expand Down Expand Up @@ -128,6 +126,25 @@ fn proposal_commitment_for_round(round: Round, is_fake: bool) -> ProposalCommitm
ProposalCommitment(Felt::from(u64::from(round) + offset))
}

/// Probabilistically selects a leader index for the given round.
/// Node 0 (the one under test) has probability NODE_0_LEADER_PROBABILITY of being selected.
/// Other nodes share the remaining probability (1 - NODE_0_LEADER_PROBABILITY) uniformly.
/// The selection is deterministic per round - the same round will always return the same
/// leader index.
fn get_leader_index(seed: u64, total_nodes: usize, round: Round) -> usize {
let round_u64 = u64::from(round);
let round_seed = seed.wrapping_mul(31).wrapping_add(round_u64);
let mut round_rng = StdRng::seed_from_u64(round_seed);

let random_value: f64 = round_rng.gen();

if random_value < NODE_0_LEADER_PROBABILITY {
NODE_UNDER_TEST
} else {
round_rng.gen_range(1..total_nodes)
}
}

/// Discrete event simulation for consensus protocol.
///
/// Uses a timeline-based approach where events are scheduled at specific
Expand All @@ -137,12 +154,16 @@ struct DiscreteEventSimulation {
rng: StdRng,
/// The seed used to initialize the simulation.
seed: u64,
/// Total number of nodes in the network.
total_nodes: usize,
/// Number of honest nodes (the rest are faulty).
honest_nodes: usize,
/// Quorum threshold for reaching consensus (2/3 + 1 of total nodes).
quorum_threshold: usize,
/// The single height consensus instance.
shc: SingleHeightConsensus,
/// All validators in the network.
validators: Vec<ValidatorId>,
/// Number of honest nodes (the rest are faulty).
honest_nodes: usize,
/// Priority queue of pending events that have yet to be processed (min-heap by tick).
pending_events: BinaryHeap<TimedEvent>,
/// Current simulation tick.
Expand Down Expand Up @@ -187,12 +208,16 @@ impl DiscreteEventSimulation {
TimeoutsConfig::default(),
);

let quorum_threshold = (2 * total_nodes / 3) + 1;

Self {
rng,
seed,
total_nodes,
honest_nodes,
quorum_threshold,
shc,
validators,
honest_nodes,
pending_events: BinaryHeap::new(),
current_tick: 0,
processed_history: Vec::new(),
Expand Down Expand Up @@ -223,25 +248,6 @@ impl DiscreteEventSimulation {
*fault_types.choose(&mut fault_rng).unwrap()
}

/// Probabilistically selects a leader index for the given round.
/// Node 0 (the one under test) has probability NODE_0_LEADER_PROBABILITY of being selected.
/// Other nodes share the remaining probability (1 - NODE_0_LEADER_PROBABILITY) uniformly.
/// The selection is deterministic per round - the same round will always return the same
/// leader index.
fn get_leader_index(seed: u64, round: Round) -> usize {
let round_u64 = u64::from(round);
let round_seed = seed.wrapping_mul(31).wrapping_add(round_u64);
let mut round_rng = StdRng::seed_from_u64(round_seed);

let random_value: f64 = round_rng.gen();

if random_value < NODE_0_LEADER_PROBABILITY {
NODE_UNDER_TEST
} else {
round_rng.gen_range(1..TOTAL_NODES)
}
}

/// Schedules an event to occur at the specified absolute tick.
/// Internal events are always scheduled.
/// Other events are scheduled with probability keep_ratio.
Expand Down Expand Up @@ -300,7 +306,7 @@ impl DiscreteEventSimulation {
fn pre_generate_all_rounds(&mut self) {
for round_idx in 0..self.num_rounds {
let round = Round::from(u32::try_from(round_idx).unwrap());
let leader_idx = Self::get_leader_index(self.seed, round);
let leader_idx = get_leader_index(self.seed, self.total_nodes, round);
let leader_id = self.validators[leader_idx];
// Track rounds where NODE_0 is the proposer.
// We will schedule peer votes for these rounds after the build finish event.
Expand Down Expand Up @@ -407,7 +413,7 @@ impl DiscreteEventSimulation {
}
FaultType::NonValidator => {
// Send votes with a voter ID that is outside the validator set
let non_validator_id = ValidatorId::from(u64::try_from(TOTAL_NODES).unwrap());
let non_validator_id = ValidatorId::from(u64::try_from(self.total_nodes).unwrap());
self.schedule_prevote_and_precommit(
non_validator_id,
round,
Expand Down Expand Up @@ -460,8 +466,9 @@ impl DiscreteEventSimulation {

let validators = self.validators.clone();
let seed = self.seed;
let total_nodes = self.total_nodes;
let leader_fn = move |r: Round| {
let idx = Self::get_leader_index(seed, r);
let idx = get_leader_index(seed, total_nodes, r);
validators[idx]
};

Expand Down Expand Up @@ -603,7 +610,7 @@ fn verify_result(sim: &DiscreteEventSimulation, result: Option<&Decision>) {
.count();
let total_precommits = peer_precommits + self_vote;

if total_precommits >= THRESHOLD {
if total_precommits >= sim.quorum_threshold {
Some((*r, *commitment, precommits.clone()))
} else {
None
Expand Down Expand Up @@ -657,10 +664,10 @@ fn verify_result(sim: &DiscreteEventSimulation, result: Option<&Decision>) {

// Verify quorum threshold is met
assert!(
actual.precommits.len() >= THRESHOLD,
actual.precommits.len() >= sim.quorum_threshold,
"Insufficient precommits in decision: {}/{}. Decision: {:?}, History: {:?}",
actual.precommits.len(),
THRESHOLD,
sim.quorum_threshold,
actual,
sim.processed_history
);
Expand All @@ -686,13 +693,14 @@ fn verify_result(sim: &DiscreteEventSimulation, result: Option<&Decision>) {
fn test_consensus_simulation(keep_ratio: f64, honest_nodes: usize) {
let seed = rand::thread_rng().gen();
let num_rounds = 5; // Number of rounds to pre-generate
let total_nodes = 100;
println!(
"Running consensus simulation with total nodes {TOTAL_NODES}, {num_rounds} rounds, keep \
"Running consensus simulation with total nodes {total_nodes}, {num_rounds} rounds, keep \
ratio {keep_ratio}, honest nodes {honest_nodes} and seed: {seed}"
);

let mut sim =
DiscreteEventSimulation::new(TOTAL_NODES, honest_nodes, seed, num_rounds, keep_ratio);
DiscreteEventSimulation::new(total_nodes, honest_nodes, seed, num_rounds, keep_ratio);

let deadline_ticks = u64::try_from(num_rounds).unwrap() * ROUND_DURATION;
let result = sim.run(deadline_ticks);
Expand Down
Loading