Skip to content

Commit 1500d7a

Browse files
apollo_consensus: update consensus sim test to simulate network msg drops
1 parent 32a5ce3 commit 1500d7a

File tree

1 file changed

+39
-24
lines changed

1 file changed

+39
-24
lines changed

crates/apollo_consensus/src/simulation_test.rs

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ use rand::rngs::StdRng;
1515
use rand::{Rng, SeedableRng};
1616
use starknet_api::block::BlockNumber;
1717
use starknet_types_core::felt::Felt;
18+
use test_case::test_case;
1819

1920
use crate::single_height_consensus::SingleHeightConsensus;
2021
use crate::state_machine::{SMRequest, StateMachineEvent, Step};
2122
use crate::types::{Decision, ProposalCommitment, Round, ValidatorId};
2223
use crate::votes_threshold::QuorumType;
2324

2425
const HEIGHT_0: BlockNumber = BlockNumber(0);
25-
const PROPOSAL_COMMITMENT: ProposalCommitment = ProposalCommitment(Felt::ONE);
2626
const TOTAL_NODES: usize = 100;
2727
const THRESHOLD: usize = (2 * TOTAL_NODES / 3) + 1;
2828
const DEADLINE_TICKS: u64 = 200;
@@ -74,6 +74,12 @@ impl Ord for TimedEvent {
7474
}
7575
}
7676

77+
/// Generates a deterministic commitment for the given round.
78+
/// Each round gets a unique commitment based on the round number.
79+
fn proposal_commitment_for_round(round: Round) -> ProposalCommitment {
80+
ProposalCommitment(Felt::from(u64::from(round)))
81+
}
82+
7783
/// Discrete event simulation for consensus protocol.
7884
///
7985
/// Uses a timeline-based approach where events are scheduled at specific
@@ -100,10 +106,12 @@ struct DiscreteEventSimulation {
100106
build_finish_info: HashMap<Round, (u64, Option<ProposalCommitment>)>,
101107
/// Tracks what the node actually voted for in each round.
102108
node_votes: HashMap<Round, Vote>,
109+
/// The keep ratio for the network (probability that messages are not dropped).
110+
keep_ratio: f64,
103111
}
104112

105113
impl DiscreteEventSimulation {
106-
fn new(total_nodes: usize, seed: u64) -> Self {
114+
fn new(total_nodes: usize, seed: u64, keep_ratio: f64) -> Self {
107115
let rng = StdRng::seed_from_u64(seed);
108116
let validators: Vec<ValidatorId> =
109117
(0..total_nodes).map(|i| ValidatorId::from(u64::try_from(i).unwrap())).collect();
@@ -128,6 +136,7 @@ impl DiscreteEventSimulation {
128136
processed_history: Vec::new(),
129137
build_finish_info: HashMap::new(),
130138
node_votes: HashMap::new(),
139+
keep_ratio,
131140
}
132141
}
133142

@@ -152,11 +161,17 @@ impl DiscreteEventSimulation {
152161
}
153162

154163
/// Schedules an event to occur after the specified delay.
164+
/// Internal events are always scheduled.
165+
/// Other events are scheduled with probability keep_ratio.
155166
fn schedule(&mut self, delay: u64, event: InputEvent) {
156-
self.pending_events.push(TimedEvent { tick: self.current_tick + delay, event });
167+
let should_enqueue =
168+
matches!(event, InputEvent::Internal(_)) || self.rng.gen_bool(self.keep_ratio);
169+
if should_enqueue {
170+
self.pending_events.push(TimedEvent { tick: self.current_tick + delay, event });
171+
}
157172
}
158173

159-
/// Generates traffic for a specific round with only honest nodes.
174+
/// Generates traffic for a specific round with a given keep ratio.
160175
///
161176
/// - Proposer sends: Proposal -> Prevote -> Precommit (in order)
162177
/// - Other validators send: Prevote -> Precommit (in order)
@@ -165,8 +180,8 @@ impl DiscreteEventSimulation {
165180
/// but each node's messages maintain correct ordering.
166181
fn generate_round_traffic(&mut self, round: Round) {
167182
let leader_id = Self::get_leader(self.seed, round);
183+
let proposal_commitment = Some(proposal_commitment_for_round(round));
168184

169-
// 1. Proposal from leader (if not self)
170185
if leader_id != *NODE_0 {
171186
let delay = self.rng.gen_range(2..20);
172187
self.schedule(
@@ -180,11 +195,8 @@ impl DiscreteEventSimulation {
180195
);
181196
}
182197

183-
// 2. Votes from other honest validators
184-
// Skip index 0 (self) - our votes are handled by the state machine
185198
for i in 1..self.validators.len() {
186199
let voter = self.validators[i];
187-
let commitment = Some(PROPOSAL_COMMITMENT);
188200

189201
// Random delays to simulate network jitter
190202
// TODO(Asmaa): currently ignore the proposal commitment value, since we are in the
@@ -194,26 +206,23 @@ impl DiscreteEventSimulation {
194206
let prevote_delay = base + self.rng.gen_range(2..20);
195207
let precommit_delta = self.rng.gen_range(5..20);
196208

197-
// Schedule prevote
198209
self.schedule(
199210
prevote_delay,
200211
InputEvent::Vote(Vote {
201212
vote_type: VoteType::Prevote,
202213
height: HEIGHT_0,
203214
round,
204-
proposal_commitment: commitment,
215+
proposal_commitment,
205216
voter,
206217
}),
207218
);
208-
209-
// Schedule precommit (after prevote)
210219
self.schedule(
211220
prevote_delay + precommit_delta,
212221
InputEvent::Vote(Vote {
213222
vote_type: VoteType::Precommit,
214223
height: HEIGHT_0,
215224
round,
216-
proposal_commitment: commitment,
225+
proposal_commitment,
217226
voter,
218227
}),
219228
);
@@ -271,23 +280,25 @@ impl DiscreteEventSimulation {
271280
///
272281
/// This simulates the manager's role in handling consensus requests,
273282
/// such as validation results, proposal building, and timeouts.
283+
/// Also tracks BroadcastVote requests to know what the node actually voted for.
274284
fn handle_requests(&mut self, reqs: VecDeque<SMRequest>) -> Option<Decision> {
275285
for req in reqs {
276286
match req {
277287
SMRequest::StartValidateProposal(init) => {
278288
let delay = self.rng.gen_range(15..30);
289+
let proposal_commitment = Some(proposal_commitment_for_round(init.round));
279290
let result = StateMachineEvent::FinishedValidation(
280-
Some(PROPOSAL_COMMITMENT),
291+
proposal_commitment,
281292
init.round,
282293
None,
283294
);
284295
self.schedule(delay, InputEvent::Internal(result));
285296
}
286297
SMRequest::StartBuildProposal(round) => {
287298
let delay = self.rng.gen_range(15..30);
288-
let result =
289-
StateMachineEvent::FinishedBuilding(Some(PROPOSAL_COMMITMENT), round);
290-
self.build_finish_info.insert(round, (delay, Some(PROPOSAL_COMMITMENT)));
299+
let proposal_commitment = Some(proposal_commitment_for_round(round));
300+
let result = StateMachineEvent::FinishedBuilding(proposal_commitment, round);
301+
self.build_finish_info.insert(round, (delay, proposal_commitment));
291302
self.schedule(delay, InputEvent::Internal(result));
292303
}
293304
SMRequest::ScheduleTimeout(step, round) => {
@@ -338,7 +349,7 @@ fn verify_result(sim: &DiscreteEventSimulation, result: Option<&Decision>) {
338349
if v.vote_type == VoteType::Precommit {
339350
if let Some(proposal_commitment) = v.proposal_commitment {
340351
let entry = stats.entry(v.round).or_insert_with(|| RoundStats {
341-
expected_commitment: PROPOSAL_COMMITMENT,
352+
expected_commitment: proposal_commitment_for_round(v.round),
342353
..Default::default()
343354
});
344355
if proposal_commitment == entry.expected_commitment {
@@ -352,7 +363,7 @@ fn verify_result(sim: &DiscreteEventSimulation, result: Option<&Decision>) {
352363
| InputEvent::Internal(StateMachineEvent::FinishedBuilding(c, r)) => {
353364
if let Some(proposal_commitment) = *c {
354365
let entry = stats.entry(*r).or_insert_with(|| RoundStats {
355-
expected_commitment: PROPOSAL_COMMITMENT,
366+
expected_commitment: proposal_commitment_for_round(*r),
356367
..Default::default()
357368
});
358369
if proposal_commitment == entry.expected_commitment {
@@ -373,7 +384,7 @@ fn verify_result(sim: &DiscreteEventSimulation, result: Option<&Decision>) {
373384
if let Some(s) = stats.get(&r) {
374385
// Check what the node actually voted for in this round
375386
// If the node voted precommit for the valid commitment, count it
376-
let expected_commitment = PROPOSAL_COMMITMENT;
387+
let expected_commitment = proposal_commitment_for_round(r);
377388
let self_vote = sim.node_votes.get(&r).map_or(0, |v| {
378389
if v.vote_type == VoteType::Precommit
379390
&& v.proposal_commitment == Some(expected_commitment)
@@ -475,12 +486,16 @@ fn verify_result(sim: &DiscreteEventSimulation, result: Option<&Decision>) {
475486
}
476487
}
477488

478-
#[test]
479-
fn test_honest_nodes_only() {
489+
#[test_case(1.0; "keep_all")]
490+
#[test_case(0.7; "keep_70%")]
491+
fn test_honest_nodes_only(keep_ratio: f64) {
480492
let seed = rand::thread_rng().gen();
481-
println!("Running consensus simulation with total nodes {TOTAL_NODES} and seed: {seed}");
493+
println!(
494+
"Running consensus simulation with total nodes {TOTAL_NODES}, keep ratio {keep_ratio} and \
495+
seed: {seed}"
496+
);
482497

483-
let mut sim = DiscreteEventSimulation::new(TOTAL_NODES, seed);
498+
let mut sim = DiscreteEventSimulation::new(TOTAL_NODES, seed, keep_ratio);
484499

485500
let result = sim.run(DEADLINE_TICKS);
486501

0 commit comments

Comments
 (0)