diff --git a/crates/apollo_consensus/src/simulation_test.rs b/crates/apollo_consensus/src/simulation_test.rs index 51bcc020cd2..98450c41a61 100644 --- a/crates/apollo_consensus/src/simulation_test.rs +++ b/crates/apollo_consensus/src/simulation_test.rs @@ -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; @@ -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 @@ -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, - /// 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, /// Current simulation tick. @@ -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(), @@ -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. @@ -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. @@ -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, @@ -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] }; @@ -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 @@ -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 ); @@ -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);