test(fuzz): add `approve_new_view` & `receive_safe_block_with_aggregated_qc` transition (#208)

This commit is contained in:
Youngjoon Lee 2023-06-26 18:37:35 +09:00 committed by GitHub
parent 8fad13b0cc
commit deeb3eeba0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 21 deletions

View File

@ -1,6 +1,6 @@
use std::collections::{BTreeMap, HashSet}; 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::prelude::*;
use proptest::strategy::BoxedStrategy; use proptest::strategy::BoxedStrategy;
use proptest_state_machine::ReferenceStateMachine; use proptest_state_machine::ReferenceStateMachine;
@ -71,6 +71,8 @@ impl ReferenceStateMachine for RefState {
state.transition_local_timeout(), state.transition_local_timeout(),
state.transition_receive_timeout_qc_for_current_view(), state.transition_receive_timeout_qc_for_current_view(),
state.transition_receive_timeout_qc_for_old_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() .boxed()
} }
@ -99,6 +101,10 @@ impl ReferenceStateMachine for RefState {
Transition::ReceiveTimeoutQcForOldView(timeout_qc) => { Transition::ReceiveTimeoutQcForOldView(timeout_qc) => {
timeout_qc.view < state.current_view() 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(_) => { Transition::ReceiveTimeoutQcForOldView(_) => {
// Nothing to do because we expect the state doesn't change. // 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 state
@ -228,23 +239,12 @@ impl RefState {
// Generate a Transition::ReceiveTimeoutQcForCurrentView // Generate a Transition::ReceiveTimeoutQcForCurrentView
fn transition_receive_timeout_qc_for_current_view(&self) -> BoxedStrategy<Transition> { fn transition_receive_timeout_qc_for_current_view(&self) -> BoxedStrategy<Transition> {
let view = self.current_view(); Just(Transition::ReceiveTimeoutQcForCurrentView(TimeoutQc {
let high_qc = self view: self.current_view(),
.chain high_qc: self.high_qc(),
.iter() sender: SENDER,
.rev() }))
.find_map(|(_, entry)| entry.high_qc()); .boxed()
if let Some(high_qc) = high_qc {
Just(Transition::ReceiveTimeoutQcForCurrentView(TimeoutQc {
view,
high_qc,
sender: SENDER,
}))
.boxed()
} else {
Just(Transition::Nop).boxed()
}
} }
// Generate a Transition::ReceiveTimeoutQcForOldView // 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<Transition> {
let latest_timeout_qcs: Vec<TimeoutQc> = 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<Transition> {
//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 { pub fn highest_voted_view(&self) -> View {
self.highest_voted_view self.highest_voted_view
} }
@ -289,6 +327,31 @@ impl RefState {
timeout_qc.view + 1 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<TimeoutQc> {
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::<Vec<TimeoutQc>>(),
None => vec![],
}
}
fn consecutive_block(parent: &Block) -> Block { fn consecutive_block(parent: &Block) -> Block {
Block { Block {
// use rand because we don't want this to be shrinked by proptest // use rand because we don't want this to be shrinked by proptest
@ -304,6 +367,10 @@ impl RefState {
} }
impl ViewEntry { impl ViewEntry {
fn new() -> ViewEntry {
Default::default()
}
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.blocks.is_empty() && self.timeout_qcs.is_empty() self.blocks.is_empty() && self.timeout_qcs.is_empty()
} }

View File

@ -109,6 +109,15 @@ impl StateMachineTest for ConsensusEngineTest {
// Check that the engine state didn't change. // Check that the engine state didn't change.
assert_eq!(engine, prev_engine); 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 } ConsensusEngineTest { engine }
} }
} }

View File

@ -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 // State transtitions that will be picked randomly
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -11,6 +13,6 @@ pub enum Transition {
LocalTimeout, LocalTimeout,
ReceiveTimeoutQcForCurrentView(TimeoutQc), ReceiveTimeoutQcForCurrentView(TimeoutQc),
ReceiveTimeoutQcForOldView(TimeoutQc), ReceiveTimeoutQcForOldView(TimeoutQc),
//TODO: add more invalid transitions that must be rejected by consensus-engine ApproveNewViewWithLatestTimeoutQc(TimeoutQc, HashSet<NewView>),
//TODO: add more transitions //TODO: add more corner transitions
} }