Skip to content

Commit

Permalink
refactor(consensus): add a test wrapper to avoid boilerplate with res…
Browse files Browse the repository at this point in the history
…ponse events (#347)
  • Loading branch information
matan-starkware authored Aug 8, 2024
1 parent ccb220f commit a3cd2d0
Showing 1 changed file with 137 additions and 144 deletions.
281 changes: 137 additions & 144 deletions crates/sequencing/papyrus_consensus/src/state_machine_test.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::VecDeque;

use lazy_static::lazy_static;
use starknet_api::block::BlockHash;
use starknet_types_core::felt::Felt;
Expand All @@ -15,207 +17,198 @@ lazy_static! {
const BLOCK_HASH: Option<BlockHash> = Some(BlockHash(Felt::ONE));
const ROUND: Round = 0;

struct TestWrapper<LeaderFn: Fn(Round) -> ValidatorId> {
state_machine: StateMachine,
leader_fn: LeaderFn,
events: VecDeque<StateMachineEvent>,
}

impl<LeaderFn: Fn(Round) -> ValidatorId> TestWrapper<LeaderFn> {
pub fn new(id: ValidatorId, total_weight: u32, leader_fn: LeaderFn) -> Self {
Self {
state_machine: StateMachine::new(id, total_weight),
leader_fn,
events: VecDeque::new(),
}
}

pub fn next_event(&mut self) -> Option<StateMachineEvent> {
self.events.pop_front()
}

pub fn start(&mut self) {
self.events.append(&mut self.state_machine.start(&self.leader_fn))
}

pub fn send_get_proposal(&mut self, block_hash: Option<BlockHash>, round: Round) {
self.send_event(StateMachineEvent::GetProposal(block_hash, round))
}

pub fn send_proposal(&mut self, block_hash: Option<BlockHash>, round: Round) {
self.send_event(StateMachineEvent::Proposal(block_hash, round))
}

pub fn send_prevote(&mut self, block_hash: Option<BlockHash>, round: Round) {
self.send_event(StateMachineEvent::Prevote(block_hash, round))
}

pub fn send_precommit(&mut self, block_hash: Option<BlockHash>, round: Round) {
self.send_event(StateMachineEvent::Precommit(block_hash, round))
}

fn send_event(&mut self, event: StateMachineEvent) {
self.events.append(&mut self.state_machine.handle_event(event, &self.leader_fn));
}
}

#[test_case(true; "proposer")]
#[test_case(false; "validator")]
fn events_arrive_in_ideal_order(is_proposer: bool) {
let id = if is_proposer { *PROPOSER_ID } else { *VALIDATOR_ID };
let mut state_machine = StateMachine::new(id, 4);
let leader_fn = |_: Round| *PROPOSER_ID;
let mut events = state_machine.start(&leader_fn);
let mut wrapper = TestWrapper::new(id, 4, |_: Round| *PROPOSER_ID);

wrapper.start();
if is_proposer {
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::GetProposal(None, ROUND));
events = state_machine
.handle_event(StateMachineEvent::GetProposal(BLOCK_HASH, ROUND), &leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Proposal(BLOCK_HASH, ROUND));
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::GetProposal(None, ROUND));
wrapper.send_get_proposal(BLOCK_HASH, ROUND);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Proposal(BLOCK_HASH, ROUND));
} else {
assert!(events.is_empty(), "{:?}", events);
events =
state_machine.handle_event(StateMachineEvent::Proposal(BLOCK_HASH, ROUND), &leader_fn);
assert!(wrapper.next_event().is_none());
wrapper.send_proposal(BLOCK_HASH, ROUND);
}
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert!(events.is_empty(), "{:?}", events);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert!(wrapper.next_event().is_none());

events = state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn);
assert!(events.is_empty(), "{:?}", events);
wrapper.send_prevote(BLOCK_HASH, ROUND);
assert!(wrapper.next_event().is_none());

events = state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
assert!(events.is_empty(), "{:?}", events);
wrapper.send_prevote(BLOCK_HASH, ROUND);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
assert!(wrapper.next_event().is_none());

events =
state_machine.handle_event(StateMachineEvent::Precommit(BLOCK_HASH, ROUND), &leader_fn);
assert!(events.is_empty(), "{:?}", events);
wrapper.send_precommit(BLOCK_HASH, ROUND);
assert!(wrapper.next_event().is_none());

events =
state_machine.handle_event(StateMachineEvent::Precommit(BLOCK_HASH, ROUND), &leader_fn);
wrapper.send_precommit(BLOCK_HASH, ROUND);
assert_eq!(
events.pop_front().unwrap(),
wrapper.next_event().unwrap(),
StateMachineEvent::Decision(BLOCK_HASH.unwrap(), ROUND)
);
assert!(events.is_empty(), "{:?}", events);
assert!(wrapper.next_event().is_none());
}

#[test]
fn validator_receives_votes_first() {
let mut state_machine = StateMachine::new(*VALIDATOR_ID, 4);
let mut wrapper = TestWrapper::new(*VALIDATOR_ID, 4, |_: Round| *PROPOSER_ID);

let leader_fn = |_: Round| *PROPOSER_ID;
let mut events = state_machine.start(&leader_fn);
assert!(events.is_empty(), "{:?}", events);
wrapper.start();
assert!(wrapper.next_event().is_none());

// Receives votes from all the other nodes first (more than minimum for a quorum).
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine
.handle_event(StateMachineEvent::Precommit(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine
.handle_event(StateMachineEvent::Precommit(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine
.handle_event(StateMachineEvent::Precommit(BLOCK_HASH, ROUND), &leader_fn),
);
assert!(events.is_empty(), "{:?}", events);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_precommit(BLOCK_HASH, ROUND);
wrapper.send_precommit(BLOCK_HASH, ROUND);
wrapper.send_precommit(BLOCK_HASH, ROUND);
assert!(wrapper.next_event().is_none());

// Finally the proposal arrives.
events = state_machine.handle_event(StateMachineEvent::Proposal(BLOCK_HASH, ROUND), &leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
wrapper.send_proposal(BLOCK_HASH, ROUND);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
assert_eq!(
events.pop_front().unwrap(),
wrapper.next_event().unwrap(),
StateMachineEvent::Decision(BLOCK_HASH.unwrap(), ROUND)
);
assert!(events.is_empty(), "{:?}", events);
assert!(wrapper.next_event().is_none());
}

#[test_case(BLOCK_HASH ; "valid_proposal")]
#[test_case(None ; "invalid_proposal")]
fn buffer_events_during_get_proposal(vote: Option<BlockHash>) {
let mut state_machine = StateMachine::new(*PROPOSER_ID, 4);
let leader_fn = |_: Round| *PROPOSER_ID;
let mut events = state_machine.start(&leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::GetProposal(None, 0));
assert!(events.is_empty(), "{:?}", events);

events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(vote, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(vote, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(vote, ROUND), &leader_fn),
);
assert!(events.is_empty(), "{:?}", events);
let mut wrapper = TestWrapper::new(*PROPOSER_ID, 4, |_: Round| *PROPOSER_ID);

wrapper.start();
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::GetProposal(None, 0));
assert!(wrapper.next_event().is_none());

wrapper.send_prevote(vote, ROUND);
wrapper.send_prevote(vote, ROUND);
wrapper.send_prevote(vote, ROUND);
assert!(wrapper.next_event().is_none());

// Node finishes building the proposal.
events =
state_machine.handle_event(StateMachineEvent::GetProposal(BLOCK_HASH, ROUND), &leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Proposal(BLOCK_HASH, ROUND));
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Precommit(vote, ROUND));
assert!(events.is_empty(), "{:?}", events);
wrapper.send_get_proposal(BLOCK_HASH, ROUND);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Proposal(BLOCK_HASH, ROUND));
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Precommit(vote, ROUND));
assert!(wrapper.next_event().is_none());
}

#[test]
fn only_send_precommit_with_prevote_quorum_and_proposal() {
let mut state_machine = StateMachine::new(*VALIDATOR_ID, 4);
let leader_fn = |_: Round| *PROPOSER_ID;
let mut events = state_machine.start(&leader_fn);
assert!(events.is_empty(), "{:?}", events);
let mut wrapper = TestWrapper::new(*VALIDATOR_ID, 4, |_: Round| *PROPOSER_ID);

wrapper.start();
assert!(wrapper.next_event().is_none());

// Receives votes from all the other nodes first (more than minimum for a quorum).
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
assert!(events.is_empty(), "{:?}", events);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_prevote(BLOCK_HASH, ROUND);
assert!(wrapper.next_event().is_none());

// Finally the proposal arrives.
events = state_machine.handle_event(StateMachineEvent::Proposal(BLOCK_HASH, ROUND), &leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
assert!(events.is_empty(), "{:?}", events);
wrapper.send_proposal(BLOCK_HASH, ROUND);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
assert!(wrapper.next_event().is_none());
}

#[test]
fn only_decide_with_prcommit_quorum_and_proposal() {
let mut state_machine = StateMachine::new(*VALIDATOR_ID, 4);
let leader_fn = |_: Round| *PROPOSER_ID;
let mut events = state_machine.start(&leader_fn);
assert!(events.is_empty(), "{:?}", events);
let mut wrapper = TestWrapper::new(*VALIDATOR_ID, 4, |_: Round| *PROPOSER_ID);

wrapper.start();
assert!(wrapper.next_event().is_none());

// Receives votes from all the other nodes first (more than minimum for a quorum).
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Prevote(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine
.handle_event(StateMachineEvent::Precommit(BLOCK_HASH, ROUND), &leader_fn),
);
events.append(
&mut state_machine
.handle_event(StateMachineEvent::Precommit(BLOCK_HASH, ROUND), &leader_fn),
);
assert!(events.is_empty(), "{:?}", events);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_prevote(BLOCK_HASH, ROUND);
wrapper.send_precommit(BLOCK_HASH, ROUND);
wrapper.send_precommit(BLOCK_HASH, ROUND);
assert!(wrapper.next_event().is_none());

// Finally the proposal arrives.
events = state_machine.handle_event(StateMachineEvent::Proposal(BLOCK_HASH, ROUND), &leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
wrapper.send_proposal(BLOCK_HASH, ROUND);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Precommit(BLOCK_HASH, ROUND));
assert_eq!(
events.pop_front().unwrap(),
wrapper.next_event().unwrap(),
StateMachineEvent::Decision(BLOCK_HASH.unwrap(), ROUND)
);
assert!(events.is_empty(), "{:?}", events);
assert!(wrapper.next_event().is_none());
}

#[test]
fn advance_to_the_next_round() {
let mut state_machine = StateMachine::new(*VALIDATOR_ID, 4);
let mut wrapper = TestWrapper::new(*VALIDATOR_ID, 4, |_: Round| *PROPOSER_ID);

let leader_fn = |_: Round| *PROPOSER_ID;
let mut events = state_machine.start(&leader_fn);
assert!(events.is_empty(), "{:?}", events);
wrapper.start();
assert!(wrapper.next_event().is_none());

events = state_machine.handle_event(StateMachineEvent::Proposal(BLOCK_HASH, ROUND), &leader_fn);
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
events.append(
&mut state_machine.handle_event(StateMachineEvent::Precommit(None, ROUND), &leader_fn),
);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Precommit(None, ROUND), &leader_fn),
);
assert_eq!(state_machine.round, ROUND);
events.append(
&mut state_machine
.handle_event(StateMachineEvent::Proposal(BLOCK_HASH, ROUND + 1), &leader_fn),
);
assert!(events.is_empty(), "{:?}", events);
events.append(
&mut state_machine.handle_event(StateMachineEvent::Precommit(None, ROUND), &leader_fn),
);
wrapper.send_proposal(BLOCK_HASH, ROUND);
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND));
wrapper.send_precommit(None, ROUND);
wrapper.send_precommit(None, ROUND);
assert!(wrapper.next_event().is_none());

wrapper.send_proposal(BLOCK_HASH, ROUND + 1);
assert!(wrapper.next_event().is_none());

wrapper.send_precommit(None, ROUND);
// The Node sends Prevote after advancing to the next round.
assert_eq!(events.pop_front().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND + 1));
assert_eq!(wrapper.next_event().unwrap(), StateMachineEvent::Prevote(BLOCK_HASH, ROUND + 1));
}

0 comments on commit a3cd2d0

Please sign in to comment.