diff --git a/consensus-engine/tests/fuzz/ref_state.rs b/consensus-engine/tests/fuzz/ref_state.rs index aa207bfc..f38ffefe 100644 --- a/consensus-engine/tests/fuzz/ref_state.rs +++ b/consensus-engine/tests/fuzz/ref_state.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashSet}; -use consensus_engine::{Block, LeaderProof, NodeId, Qc, StandardQc, TimeoutQc, View}; +use consensus_engine::{AggregateQc, Block, LeaderProof, NodeId, Qc, StandardQc, TimeoutQc, View}; use proptest::prelude::*; use proptest::strategy::BoxedStrategy; use proptest_state_machine::ReferenceStateMachine; @@ -71,6 +71,8 @@ impl ReferenceStateMachine for RefState { state.transition_local_timeout(), state.transition_receive_timeout_qc_for_current_view(), state.transition_receive_timeout_qc_for_old_view(), + state.transition_approve_new_view_with_latest_timeout_qc(), + state.transition_receive_safe_block_with_aggregated_qc(), ] .boxed() } @@ -99,6 +101,10 @@ impl ReferenceStateMachine for RefState { Transition::ReceiveTimeoutQcForOldView(timeout_qc) => { timeout_qc.view < state.current_view() } + Transition::ApproveNewViewWithLatestTimeoutQc(timeout_qc, _) => { + state.latest_timeout_qcs().contains(timeout_qc) + && state.highest_voted_view < RefState::new_view_from(timeout_qc) + } } } @@ -138,6 +144,11 @@ impl ReferenceStateMachine for RefState { Transition::ReceiveTimeoutQcForOldView(_) => { // Nothing to do because we expect the state doesn't change. } + Transition::ApproveNewViewWithLatestTimeoutQc(timeout_qc, _) => { + let new_view = RefState::new_view_from(timeout_qc); + state.chain.entry(new_view).or_insert(ViewEntry::new()); + state.highest_voted_view = new_view; + } } state @@ -228,23 +239,12 @@ impl RefState { // Generate a Transition::ReceiveTimeoutQcForCurrentView fn transition_receive_timeout_qc_for_current_view(&self) -> BoxedStrategy { - let view = self.current_view(); - let high_qc = self - .chain - .iter() - .rev() - .find_map(|(_, entry)| entry.high_qc()); - - if let Some(high_qc) = high_qc { - Just(Transition::ReceiveTimeoutQcForCurrentView(TimeoutQc { - view, - high_qc, - sender: SENDER, - })) - .boxed() - } else { - Just(Transition::Nop).boxed() - } + Just(Transition::ReceiveTimeoutQcForCurrentView(TimeoutQc { + view: self.current_view(), + high_qc: self.high_qc(), + sender: SENDER, + })) + .boxed() } // Generate a Transition::ReceiveTimeoutQcForOldView @@ -271,6 +271,44 @@ impl RefState { } } + // Generate a Transition::ApproveNewViewWithLatestTimeoutQc. + fn transition_approve_new_view_with_latest_timeout_qc(&self) -> BoxedStrategy { + let latest_timeout_qcs: Vec = self + .latest_timeout_qcs() + .iter() + .filter(|timeout_qc| self.highest_voted_view < RefState::new_view_from(timeout_qc)) + .cloned() + .collect(); + + if latest_timeout_qcs.is_empty() { + Just(Transition::Nop).boxed() + } else { + proptest::sample::select(latest_timeout_qcs) + .prop_map(move |timeout_qc| { + //TODO: set new_views + Transition::ApproveNewViewWithLatestTimeoutQc(timeout_qc, HashSet::new()) + }) + .boxed() + } + } + + // Generate a Transition::ReceiveSafeBlock, but with AggregatedQc. + fn transition_receive_safe_block_with_aggregated_qc(&self) -> BoxedStrategy { + //TODO: more randomness + let current_view = self.current_view(); + + Just(Transition::ReceiveSafeBlock(Block { + id: rand::thread_rng().gen(), + view: current_view + 1, + parent_qc: Qc::Aggregated(AggregateQc { + high_qc: self.high_qc(), + view: current_view, + }), + leader_proof: LEADER_PROOF.clone(), + })) + .boxed() + } + pub fn highest_voted_view(&self) -> View { self.highest_voted_view } @@ -289,6 +327,31 @@ impl RefState { timeout_qc.view + 1 } + fn high_qc(&self) -> StandardQc { + self.chain + .iter() + .rev() + .find_map(|(_, entry)| entry.high_qc()) + .unwrap() // doesn't fail because self.chain always contains at least a genesis block + } + + fn latest_timeout_qcs(&self) -> Vec { + let latest_timeout_qc_view_entry = self + .chain + .iter() + .rev() + .find(|(_, entry)| !entry.timeout_qcs.is_empty()); + + match latest_timeout_qc_view_entry { + Some((_, entry)) => entry + .timeout_qcs + .iter() + .cloned() + .collect::>(), + None => vec![], + } + } + fn consecutive_block(parent: &Block) -> Block { Block { // use rand because we don't want this to be shrinked by proptest @@ -304,6 +367,10 @@ impl RefState { } impl ViewEntry { + fn new() -> ViewEntry { + Default::default() + } + fn is_empty(&self) -> bool { self.blocks.is_empty() && self.timeout_qcs.is_empty() } diff --git a/consensus-engine/tests/fuzz/sut.rs b/consensus-engine/tests/fuzz/sut.rs index 6cae6a87..476b0bdc 100644 --- a/consensus-engine/tests/fuzz/sut.rs +++ b/consensus-engine/tests/fuzz/sut.rs @@ -109,6 +109,15 @@ impl StateMachineTest for ConsensusEngineTest { // Check that the engine state didn't change. assert_eq!(engine, prev_engine); + ConsensusEngineTest { engine } + } + Transition::ApproveNewViewWithLatestTimeoutQc(timeout_qc, new_views) => { + let (engine, _) = state.engine.approve_new_view(timeout_qc.clone(), new_views); + assert_eq!( + engine.highest_voted_view(), + RefState::new_view_from(&timeout_qc) + ); + ConsensusEngineTest { engine } } } diff --git a/consensus-engine/tests/fuzz/transition.rs b/consensus-engine/tests/fuzz/transition.rs index 19bbaca3..221834eb 100644 --- a/consensus-engine/tests/fuzz/transition.rs +++ b/consensus-engine/tests/fuzz/transition.rs @@ -1,4 +1,6 @@ -use consensus_engine::{Block, TimeoutQc}; +use std::collections::HashSet; + +use consensus_engine::{Block, NewView, TimeoutQc}; // State transtitions that will be picked randomly #[derive(Clone, Debug)] @@ -11,6 +13,6 @@ pub enum Transition { LocalTimeout, ReceiveTimeoutQcForCurrentView(TimeoutQc), ReceiveTimeoutQcForOldView(TimeoutQc), - //TODO: add more invalid transitions that must be rejected by consensus-engine - //TODO: add more transitions + ApproveNewViewWithLatestTimeoutQc(TimeoutQc, HashSet), + //TODO: add more corner transitions }