test(fuzz): add local_timeout
& receive_timeout_qc
transitions (#207)
This commit is contained in:
parent
371ba17922
commit
f4194bc728
@ -5,7 +5,7 @@ mod types;
|
|||||||
pub use overlay::Overlay;
|
pub use overlay::Overlay;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Carnot<O: Overlay> {
|
pub struct Carnot<O: Overlay> {
|
||||||
id: NodeId,
|
id: NodeId,
|
||||||
current_view: View,
|
current_view: View,
|
||||||
|
@ -2,7 +2,7 @@ use super::LeaderSelection;
|
|||||||
use crate::{Committee, NodeId, Overlay};
|
use crate::{Committee, NodeId, Overlay};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
/// Flat overlay with a single committee and round robin leader selection.
|
/// Flat overlay with a single committee and round robin leader selection.
|
||||||
pub struct FlatOverlay<L: LeaderSelection> {
|
pub struct FlatOverlay<L: LeaderSelection> {
|
||||||
nodes: Vec<NodeId>,
|
nodes: Vec<NodeId>,
|
||||||
@ -86,7 +86,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct RoundRobin {
|
pub struct RoundRobin {
|
||||||
cur: usize,
|
cur: usize,
|
||||||
|
@ -3,7 +3,7 @@ use std::{
|
|||||||
panic,
|
panic,
|
||||||
};
|
};
|
||||||
|
|
||||||
use consensus_engine::{Block, LeaderProof, Qc, StandardQc, View};
|
use consensus_engine::{Block, LeaderProof, NodeId, Qc, StandardQc, TimeoutQc, View};
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
use proptest::test_runner::Config;
|
use proptest::test_runner::Config;
|
||||||
use proptest_state_machine::{prop_state_machine, ReferenceStateMachine, StateMachineTest};
|
use proptest_state_machine::{prop_state_machine, ReferenceStateMachine, StateMachineTest};
|
||||||
@ -30,10 +30,31 @@ prop_state_machine! {
|
|||||||
// so that we don't need to replicate the logic implemented in consensus-engine.
|
// so that we don't need to replicate the logic implemented in consensus-engine.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RefState {
|
pub struct RefState {
|
||||||
chain: BTreeMap<View, HashSet<Block>>,
|
chain: BTreeMap<View, ViewEntry>,
|
||||||
highest_voted_view: View,
|
highest_voted_view: View,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
struct ViewEntry {
|
||||||
|
blocks: HashSet<Block>,
|
||||||
|
timeout_qcs: HashSet<TimeoutQc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewEntry {
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.blocks.is_empty() && self.timeout_qcs.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn high_qc(&self) -> Option<StandardQc> {
|
||||||
|
let iter1 = self.blocks.iter().map(|block| block.parent_qc.high_qc());
|
||||||
|
let iter2 = self
|
||||||
|
.timeout_qcs
|
||||||
|
.iter()
|
||||||
|
.map(|timeout_qc| timeout_qc.high_qc.clone());
|
||||||
|
iter1.chain(iter2).max_by_key(|qc| qc.view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// State transtitions that will be picked randomly
|
// State transtitions that will be picked randomly
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Transition {
|
pub enum Transition {
|
||||||
@ -42,11 +63,16 @@ pub enum Transition {
|
|||||||
ReceiveUnsafeBlock(Block),
|
ReceiveUnsafeBlock(Block),
|
||||||
ApproveBlock(Block),
|
ApproveBlock(Block),
|
||||||
ApprovePastBlock(Block),
|
ApprovePastBlock(Block),
|
||||||
|
LocalTimeout,
|
||||||
|
ReceiveTimeoutQcForCurrentView(TimeoutQc),
|
||||||
|
ReceiveTimeoutQcForOldView(TimeoutQc),
|
||||||
|
//TODO: add more invalid transitions that must be rejected by consensus-engine
|
||||||
//TODO: add more transitions
|
//TODO: add more transitions
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEADER_PROOF: LeaderProof = LeaderProof::LeaderId { leader_id: [0; 32] };
|
const LEADER_PROOF: LeaderProof = LeaderProof::LeaderId { leader_id: [0; 32] };
|
||||||
const INITIAL_HIGHEST_VOTED_VIEW: View = -1;
|
const INITIAL_HIGHEST_VOTED_VIEW: View = -1;
|
||||||
|
const SENDER: NodeId = [0; 32];
|
||||||
|
|
||||||
impl ReferenceStateMachine for RefState {
|
impl ReferenceStateMachine for RefState {
|
||||||
type State = Self;
|
type State = Self;
|
||||||
@ -63,7 +89,13 @@ impl ReferenceStateMachine for RefState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Just(RefState {
|
Just(RefState {
|
||||||
chain: BTreeMap::from([(genesis_block.view, HashSet::from([genesis_block.clone()]))]),
|
chain: BTreeMap::from([(
|
||||||
|
genesis_block.view,
|
||||||
|
ViewEntry {
|
||||||
|
blocks: HashSet::from([genesis_block]),
|
||||||
|
timeout_qcs: Default::default(),
|
||||||
|
},
|
||||||
|
)]),
|
||||||
highest_voted_view: INITIAL_HIGHEST_VOTED_VIEW,
|
highest_voted_view: INITIAL_HIGHEST_VOTED_VIEW,
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
@ -77,10 +109,13 @@ impl ReferenceStateMachine for RefState {
|
|||||||
// if it cannot generate the promised transition for the current reference state.
|
// if it cannot generate the promised transition for the current reference state.
|
||||||
// Both reference and real state machine do nothing for Nop transitions.
|
// Both reference and real state machine do nothing for Nop transitions.
|
||||||
prop_oneof![
|
prop_oneof![
|
||||||
3 => state.transition_receive_safe_block(),
|
state.transition_receive_safe_block(),
|
||||||
2 => state.transition_receive_unsafe_block(),
|
state.transition_receive_unsafe_block(),
|
||||||
2 => state.transition_approve_block(),
|
state.transition_approve_block(),
|
||||||
1 => state.transition_approve_past_block(),
|
state.transition_approve_past_block(),
|
||||||
|
state.transition_local_timeout(),
|
||||||
|
state.transition_receive_timeout_qc_for_current_view(),
|
||||||
|
state.transition_receive_timeout_qc_for_old_view(),
|
||||||
]
|
]
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
@ -102,6 +137,13 @@ impl ReferenceStateMachine for RefState {
|
|||||||
Transition::ReceiveUnsafeBlock(block) => block.view < state.current_view(),
|
Transition::ReceiveUnsafeBlock(block) => block.view < state.current_view(),
|
||||||
Transition::ApproveBlock(block) => state.highest_voted_view < block.view,
|
Transition::ApproveBlock(block) => state.highest_voted_view < block.view,
|
||||||
Transition::ApprovePastBlock(block) => state.highest_voted_view >= block.view,
|
Transition::ApprovePastBlock(block) => state.highest_voted_view >= block.view,
|
||||||
|
Transition::LocalTimeout => true,
|
||||||
|
Transition::ReceiveTimeoutQcForCurrentView(timeout_qc) => {
|
||||||
|
timeout_qc.view == state.current_view()
|
||||||
|
}
|
||||||
|
Transition::ReceiveTimeoutQcForOldView(timeout_qc) => {
|
||||||
|
timeout_qc.view < state.current_view()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +157,7 @@ impl ReferenceStateMachine for RefState {
|
|||||||
.chain
|
.chain
|
||||||
.entry(block.view)
|
.entry(block.view)
|
||||||
.or_default()
|
.or_default()
|
||||||
|
.blocks
|
||||||
.insert(block.clone());
|
.insert(block.clone());
|
||||||
}
|
}
|
||||||
Transition::ReceiveUnsafeBlock(_) => {
|
Transition::ReceiveUnsafeBlock(_) => {
|
||||||
@ -126,6 +169,20 @@ impl ReferenceStateMachine for RefState {
|
|||||||
Transition::ApprovePastBlock(_) => {
|
Transition::ApprovePastBlock(_) => {
|
||||||
// Nothing to do because we expect the state doesn't change.
|
// Nothing to do because we expect the state doesn't change.
|
||||||
}
|
}
|
||||||
|
Transition::LocalTimeout => {
|
||||||
|
state.highest_voted_view = state.current_view();
|
||||||
|
}
|
||||||
|
Transition::ReceiveTimeoutQcForCurrentView(timeout_qc) => {
|
||||||
|
state
|
||||||
|
.chain
|
||||||
|
.entry(timeout_qc.view)
|
||||||
|
.or_default()
|
||||||
|
.timeout_qcs
|
||||||
|
.insert(timeout_qc.clone());
|
||||||
|
}
|
||||||
|
Transition::ReceiveTimeoutQcForOldView(_) => {
|
||||||
|
// Nothing to do because we expect the state doesn't change.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
@ -138,7 +195,7 @@ impl RefState {
|
|||||||
let recent_parents = self
|
let recent_parents = self
|
||||||
.chain
|
.chain
|
||||||
.range(self.current_view() - 1..)
|
.range(self.current_view() - 1..)
|
||||||
.flat_map(|(_view, blocks)| blocks.iter().cloned())
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
.collect::<Vec<Block>>();
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
if recent_parents.is_empty() {
|
if recent_parents.is_empty() {
|
||||||
@ -158,7 +215,7 @@ impl RefState {
|
|||||||
let old_parents = self
|
let old_parents = self
|
||||||
.chain
|
.chain
|
||||||
.range(..self.current_view() - 1)
|
.range(..self.current_view() - 1)
|
||||||
.flat_map(|(_view, blocks)| blocks.iter().cloned())
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
.collect::<Vec<Block>>();
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
if old_parents.is_empty() {
|
if old_parents.is_empty() {
|
||||||
@ -178,7 +235,7 @@ impl RefState {
|
|||||||
let blocks_not_voted = self
|
let blocks_not_voted = self
|
||||||
.chain
|
.chain
|
||||||
.range(self.highest_voted_view + 1..)
|
.range(self.highest_voted_view + 1..)
|
||||||
.flat_map(|(_view, blocks)| blocks.iter().cloned())
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
.collect::<Vec<Block>>();
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
if blocks_not_voted.is_empty() {
|
if blocks_not_voted.is_empty() {
|
||||||
@ -196,7 +253,7 @@ impl RefState {
|
|||||||
let past_blocks = self
|
let past_blocks = self
|
||||||
.chain
|
.chain
|
||||||
.range(INITIAL_HIGHEST_VOTED_VIEW..self.highest_voted_view)
|
.range(INITIAL_HIGHEST_VOTED_VIEW..self.highest_voted_view)
|
||||||
.flat_map(|(_view, blocks)| blocks.iter().cloned())
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
.collect::<Vec<Block>>();
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
if past_blocks.is_empty() {
|
if past_blocks.is_empty() {
|
||||||
@ -209,10 +266,68 @@ impl RefState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a Transition::LocalTimeout.
|
||||||
|
fn transition_local_timeout(&self) -> BoxedStrategy<Transition> {
|
||||||
|
Just(Transition::LocalTimeout).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: view.clone(),
|
||||||
|
high_qc,
|
||||||
|
sender: SENDER,
|
||||||
|
}))
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
Just(Transition::Nop).boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a Transition::ReceiveTimeoutQcForOldView
|
||||||
|
fn transition_receive_timeout_qc_for_old_view(&self) -> BoxedStrategy<Transition> {
|
||||||
|
let old_view_entries: Vec<(View, ViewEntry)> = self
|
||||||
|
.chain
|
||||||
|
.range(..self.current_view())
|
||||||
|
.filter(|(_, entry)| !entry.is_empty())
|
||||||
|
.map(|(&view, entry)| (view, entry.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if old_view_entries.is_empty() {
|
||||||
|
Just(Transition::Nop).boxed()
|
||||||
|
} else {
|
||||||
|
proptest::sample::select(old_view_entries)
|
||||||
|
.prop_map(move |(view, entry)| {
|
||||||
|
Transition::ReceiveTimeoutQcForOldView(TimeoutQc {
|
||||||
|
view: view.clone(),
|
||||||
|
high_qc: entry.high_qc().unwrap(),
|
||||||
|
sender: SENDER,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn current_view(&self) -> View {
|
fn current_view(&self) -> View {
|
||||||
let (&last_view, _) = self.chain.last_key_value().unwrap();
|
let (&last_view, last_entry) = self.chain.last_key_value().unwrap();
|
||||||
// TODO: this logic must cover other cases for unhappy path
|
if last_entry.timeout_qcs.is_empty() {
|
||||||
last_view
|
last_view
|
||||||
|
} else {
|
||||||
|
let timeout_qc = last_entry.timeout_qcs.iter().next().unwrap();
|
||||||
|
RefState::new_view_from(&timeout_qc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_view_from(timeout_qc: &TimeoutQc) -> View {
|
||||||
|
timeout_qc.view + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn consecutive_block(parent: &Block) -> Block {
|
fn consecutive_block(parent: &Block) -> Block {
|
||||||
@ -282,6 +397,27 @@ impl StateMachineTest for ConsensusEngineTest {
|
|||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
Transition::LocalTimeout => {
|
||||||
|
let (engine, _) = state.engine.local_timeout();
|
||||||
|
assert_eq!(engine.highest_voted_view(), engine.current_view());
|
||||||
|
|
||||||
|
ConsensusEngineTest { engine }
|
||||||
|
}
|
||||||
|
Transition::ReceiveTimeoutQcForCurrentView(timeout_qc) => {
|
||||||
|
let engine = state.engine.receive_timeout_qc(timeout_qc.clone());
|
||||||
|
assert_eq!(engine.current_view(), RefState::new_view_from(&timeout_qc));
|
||||||
|
|
||||||
|
ConsensusEngineTest { engine }
|
||||||
|
}
|
||||||
|
Transition::ReceiveTimeoutQcForOldView(timeout_qc) => {
|
||||||
|
let prev_engine = state.engine.clone();
|
||||||
|
let engine = state.engine.receive_timeout_qc(timeout_qc);
|
||||||
|
|
||||||
|
// Check that the engine state didn't change.
|
||||||
|
assert_eq!(engine, prev_engine);
|
||||||
|
|
||||||
|
ConsensusEngineTest { engine }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +427,6 @@ impl StateMachineTest for ConsensusEngineTest {
|
|||||||
state: &Self::SystemUnderTest,
|
state: &Self::SystemUnderTest,
|
||||||
ref_state: &<Self::Reference as ReferenceStateMachine>::State,
|
ref_state: &<Self::Reference as ReferenceStateMachine>::State,
|
||||||
) {
|
) {
|
||||||
//TODO: this may be false in unhappy path
|
|
||||||
assert_eq!(state.engine.current_view(), ref_state.current_view());
|
assert_eq!(state.engine.current_view(), ref_state.current_view());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user