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 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<Transition> {
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<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 {
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<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 {
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()
}

View File

@ -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 }
}
}

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
#[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<NewView>),
//TODO: add more corner transitions
}