refactor(fuzz): modularize fuzz testing (#211)
This commit is contained in:
parent
f4194bc728
commit
ec3fb62baf
|
@ -416,7 +416,7 @@ mod test {
|
||||||
let mut next_id = block.id;
|
let mut next_id = block.id;
|
||||||
next_id[0] += 1;
|
next_id[0] += 1;
|
||||||
|
|
||||||
return Block {
|
Block {
|
||||||
view: block.view + 1,
|
view: block.view + 1,
|
||||||
id: next_id,
|
id: next_id,
|
||||||
parent_qc: Qc::Standard(StandardQc {
|
parent_qc: Qc::Standard(StandardQc {
|
||||||
|
@ -424,7 +424,7 @@ mod test {
|
||||||
id: block.id,
|
id: block.id,
|
||||||
}),
|
}),
|
||||||
leader_proof: LeaderProof::LeaderId { leader_id: [0; 32] },
|
leader_proof: LeaderProof::LeaderId { leader_id: [0; 32] },
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -463,8 +463,8 @@ mod test {
|
||||||
|
|
||||||
let mut block2 = next_block(&block1);
|
let mut block2 = next_block(&block1);
|
||||||
block2.id = block1.id;
|
block2.id = block1.id;
|
||||||
engine = engine.receive_block(block2.clone()).unwrap();
|
engine = engine.receive_block(block2).unwrap();
|
||||||
assert_eq!(engine.blocks_in_view(1), vec![block1.clone()]);
|
assert_eq!(engine.blocks_in_view(1), vec![block1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -484,7 +484,7 @@ mod test {
|
||||||
leader_proof: LeaderProof::LeaderId { leader_id: [0; 32] },
|
leader_proof: LeaderProof::LeaderId { leader_id: [0; 32] },
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = engine.receive_block(block.clone());
|
let _ = engine.receive_block(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -518,6 +518,7 @@ mod test {
|
||||||
let block2 = next_block(&block1);
|
let block2 = next_block(&block1);
|
||||||
engine = engine.receive_block(block2.clone()).unwrap();
|
engine = engine.receive_block(block2.clone()).unwrap();
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_clone)]
|
||||||
let mut block3 = block2.clone();
|
let mut block3 = block2.clone();
|
||||||
block3.id = [3; 32]; // use a new ID, so that this block isn't ignored
|
block3.id = [3; 32]; // use a new ID, so that this block isn't ignored
|
||||||
engine = engine.receive_block(block3.clone()).unwrap();
|
engine = engine.receive_block(block3.clone()).unwrap();
|
||||||
|
@ -554,7 +555,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
let block4 = next_block(&block3);
|
let block4 = next_block(&block3);
|
||||||
engine = engine.receive_block(block4.clone()).unwrap();
|
engine = engine.receive_block(block4).unwrap();
|
||||||
assert_eq!(engine.latest_committed_block(), block2);
|
assert_eq!(engine.latest_committed_block(), block2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
engine.committed_blocks(),
|
engine.committed_blocks(),
|
||||||
|
@ -589,7 +590,7 @@ mod test {
|
||||||
let engine = init_from_genesis();
|
let engine = init_from_genesis();
|
||||||
|
|
||||||
let block = next_block(&engine.genesis_block());
|
let block = next_block(&engine.genesis_block());
|
||||||
let _ = engine.approve_block(block.clone());
|
let _ = engine.approve_block(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -611,7 +612,7 @@ mod test {
|
||||||
fn local_timeout() {
|
fn local_timeout() {
|
||||||
let mut engine = init_from_genesis();
|
let mut engine = init_from_genesis();
|
||||||
let block = next_block(&engine.genesis_block());
|
let block = next_block(&engine.genesis_block());
|
||||||
engine = engine.receive_block(block.clone()).unwrap(); // received but not approved yet
|
engine = engine.receive_block(block).unwrap(); // received but not approved yet
|
||||||
|
|
||||||
let (engine, send) = engine.local_timeout();
|
let (engine, send) = engine.local_timeout();
|
||||||
assert_eq!(engine.highest_voted_view, 1); // updated from 0 (genesis) to 1 (current_view)
|
assert_eq!(engine.highest_voted_view, 1); // updated from 0 (genesis) to 1 (current_view)
|
||||||
|
@ -637,7 +638,7 @@ mod test {
|
||||||
fn receive_timeout_qc_after_local_timeout() {
|
fn receive_timeout_qc_after_local_timeout() {
|
||||||
let mut engine = init_from_genesis();
|
let mut engine = init_from_genesis();
|
||||||
let block = next_block(&engine.genesis_block());
|
let block = next_block(&engine.genesis_block());
|
||||||
engine = engine.receive_block(block.clone()).unwrap(); // received but not approved yet
|
engine = engine.receive_block(block).unwrap(); // received but not approved yet
|
||||||
|
|
||||||
let (mut engine, _) = engine.local_timeout();
|
let (mut engine, _) = engine.local_timeout();
|
||||||
|
|
||||||
|
@ -652,7 +653,7 @@ mod test {
|
||||||
};
|
};
|
||||||
engine = engine.receive_timeout_qc(timeout_qc.clone());
|
engine = engine.receive_timeout_qc(timeout_qc.clone());
|
||||||
assert_eq!(engine.local_high_qc, timeout_qc.high_qc);
|
assert_eq!(engine.local_high_qc, timeout_qc.high_qc);
|
||||||
assert_eq!(engine.last_view_timeout_qc, Some(timeout_qc.clone()));
|
assert_eq!(engine.last_view_timeout_qc, Some(timeout_qc));
|
||||||
assert_eq!(engine.current_view(), 2);
|
assert_eq!(engine.current_view(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -661,7 +662,7 @@ mod test {
|
||||||
fn receive_timeout_qc_before_local_timeout() {
|
fn receive_timeout_qc_before_local_timeout() {
|
||||||
let mut engine = init_from_genesis();
|
let mut engine = init_from_genesis();
|
||||||
let block = next_block(&engine.genesis_block());
|
let block = next_block(&engine.genesis_block());
|
||||||
engine = engine.receive_block(block.clone()).unwrap(); // received but not approved yet
|
engine = engine.receive_block(block).unwrap(); // received but not approved yet
|
||||||
|
|
||||||
// before local_timeout occurs
|
// before local_timeout occurs
|
||||||
|
|
||||||
|
@ -676,7 +677,7 @@ mod test {
|
||||||
};
|
};
|
||||||
engine = engine.receive_timeout_qc(timeout_qc.clone());
|
engine = engine.receive_timeout_qc(timeout_qc.clone());
|
||||||
assert_eq!(engine.local_high_qc, timeout_qc.high_qc);
|
assert_eq!(engine.local_high_qc, timeout_qc.high_qc);
|
||||||
assert_eq!(engine.last_view_timeout_qc, Some(timeout_qc.clone()));
|
assert_eq!(engine.last_view_timeout_qc, Some(timeout_qc));
|
||||||
assert_eq!(engine.current_view(), 2);
|
assert_eq!(engine.current_view(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -685,7 +686,7 @@ mod test {
|
||||||
fn approve_new_view() {
|
fn approve_new_view() {
|
||||||
let mut engine = init_from_genesis();
|
let mut engine = init_from_genesis();
|
||||||
let block = next_block(&engine.genesis_block());
|
let block = next_block(&engine.genesis_block());
|
||||||
engine = engine.receive_block(block.clone()).unwrap(); // received but not approved yet
|
engine = engine.receive_block(block).unwrap(); // received but not approved yet
|
||||||
|
|
||||||
assert_eq!(engine.current_view(), 1); // still waiting for a QC(view=1)
|
assert_eq!(engine.current_view(), 1); // still waiting for a QC(view=1)
|
||||||
let timeout_qc = TimeoutQc {
|
let timeout_qc = TimeoutQc {
|
||||||
|
@ -707,12 +708,12 @@ mod test {
|
||||||
assert_eq!(engine.current_view(), 2); // not changed
|
assert_eq!(engine.current_view(), 2); // not changed
|
||||||
assert_eq!(engine.highest_voted_view, 2);
|
assert_eq!(engine.highest_voted_view, 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
send.clone().payload,
|
send.payload,
|
||||||
Payload::NewView(NewView {
|
Payload::NewView(NewView {
|
||||||
view: 2,
|
view: 2,
|
||||||
sender: [0; 32],
|
sender: [0; 32],
|
||||||
timeout_qc: timeout_qc.clone(),
|
timeout_qc: timeout_qc.clone(),
|
||||||
high_qc: timeout_qc.clone().high_qc,
|
high_qc: timeout_qc.high_qc,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -722,7 +723,7 @@ mod test {
|
||||||
fn approve_new_view_not_bigger_than_timeout_qc() {
|
fn approve_new_view_not_bigger_than_timeout_qc() {
|
||||||
let mut engine = init_from_genesis();
|
let mut engine = init_from_genesis();
|
||||||
let block = next_block(&engine.genesis_block());
|
let block = next_block(&engine.genesis_block());
|
||||||
engine = engine.receive_block(block.clone()).unwrap(); // received but not approved yet
|
engine = engine.receive_block(block).unwrap(); // received but not approved yet
|
||||||
|
|
||||||
assert_eq!(engine.current_view(), 1);
|
assert_eq!(engine.current_view(), 1);
|
||||||
let timeout_qc1 = TimeoutQc {
|
let timeout_qc1 = TimeoutQc {
|
||||||
|
@ -746,9 +747,9 @@ mod test {
|
||||||
sender: [0; 32],
|
sender: [0; 32],
|
||||||
};
|
};
|
||||||
engine = engine.receive_timeout_qc(timeout_qc2.clone());
|
engine = engine.receive_timeout_qc(timeout_qc2.clone());
|
||||||
assert_eq!(engine.last_view_timeout_qc, Some(timeout_qc2.clone()));
|
assert_eq!(engine.last_view_timeout_qc, Some(timeout_qc2));
|
||||||
|
|
||||||
// we expect new_view(timeout_qc2), but...
|
// we expect new_view(timeout_qc2), but...
|
||||||
let _ = engine.approve_new_view(timeout_qc1.clone(), HashSet::new());
|
let _ = engine.approve_new_view(timeout_qc1, HashSet::new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod ref_state;
|
||||||
|
pub mod sut;
|
||||||
|
mod transition;
|
|
@ -0,0 +1,319 @@
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
use consensus_engine::{Block, LeaderProof, NodeId, Qc, StandardQc, TimeoutQc, View};
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use proptest::strategy::BoxedStrategy;
|
||||||
|
use proptest_state_machine::ReferenceStateMachine;
|
||||||
|
|
||||||
|
use crate::fuzz::transition::Transition;
|
||||||
|
|
||||||
|
// A reference state machine (RefState) is used to generated state transitions.
|
||||||
|
// To generate some kinds of transition, we may need to keep historical blocks in RefState.
|
||||||
|
// Also, RefState can be used to check invariants of the real state machine in some cases.
|
||||||
|
//
|
||||||
|
// We should try to design this reference state as simple/intuitive as possible,
|
||||||
|
// so that we don't need to replicate the logic implemented in consensus-engine.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RefState {
|
||||||
|
chain: BTreeMap<View, ViewEntry>,
|
||||||
|
highest_voted_view: View,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
struct ViewEntry {
|
||||||
|
blocks: HashSet<Block>,
|
||||||
|
timeout_qcs: HashSet<TimeoutQc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEADER_PROOF: LeaderProof = LeaderProof::LeaderId { leader_id: [0; 32] };
|
||||||
|
const INITIAL_HIGHEST_VOTED_VIEW: View = -1;
|
||||||
|
const SENDER: NodeId = [0; 32];
|
||||||
|
|
||||||
|
impl ReferenceStateMachine for RefState {
|
||||||
|
type State = Self;
|
||||||
|
|
||||||
|
type Transition = Transition;
|
||||||
|
|
||||||
|
// Initialize the reference state machine
|
||||||
|
fn init_state() -> BoxedStrategy<Self::State> {
|
||||||
|
let genesis_block = Block {
|
||||||
|
view: 0,
|
||||||
|
id: [0; 32],
|
||||||
|
parent_qc: Qc::Standard(StandardQc::genesis()),
|
||||||
|
leader_proof: LEADER_PROOF.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Just(RefState {
|
||||||
|
chain: BTreeMap::from([(
|
||||||
|
genesis_block.view,
|
||||||
|
ViewEntry {
|
||||||
|
blocks: HashSet::from([genesis_block]),
|
||||||
|
timeout_qcs: Default::default(),
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
highest_voted_view: INITIAL_HIGHEST_VOTED_VIEW,
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate transitions based on the current reference state machine
|
||||||
|
fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition> {
|
||||||
|
// Instead of using verbose `if` statements here to filter out the types of transitions
|
||||||
|
// which cannot be created based on the current reference state,
|
||||||
|
// each `state.transition_*` function returns a Nop transition
|
||||||
|
// if it cannot generate the promised transition for the current reference state.
|
||||||
|
// Both reference and real state machine do nothing for Nop transitions.
|
||||||
|
prop_oneof![
|
||||||
|
state.transition_receive_safe_block(),
|
||||||
|
state.transition_receive_unsafe_block(),
|
||||||
|
state.transition_approve_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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the transition is valid for a given reference state, before applying the transition
|
||||||
|
// If invalid, the transition will be ignored and a new transition will be generated.
|
||||||
|
//
|
||||||
|
// Also, preconditions are used for shrinking in failure cases.
|
||||||
|
// Preconditions check if the transition is still valid after some shrinking is applied.
|
||||||
|
// If the transition became invalid for the shrinked state, the shrinking is stopped or
|
||||||
|
// is continued to other directions.
|
||||||
|
fn preconditions(state: &Self::State, transition: &Self::Transition) -> bool {
|
||||||
|
// In most cases, we need to check the same conditions again used to create transitions.
|
||||||
|
// This is redundant for success cases, but is necessary for shrinking in failure cases,
|
||||||
|
// because some transitions may no longer be valid after some shrinking is applied.
|
||||||
|
match transition {
|
||||||
|
Transition::Nop => true,
|
||||||
|
Transition::ReceiveSafeBlock(block) => block.view >= state.current_view(),
|
||||||
|
Transition::ReceiveUnsafeBlock(block) => block.view < state.current_view(),
|
||||||
|
Transition::ApproveBlock(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the given transition on the reference state machine,
|
||||||
|
// so that it can be used to generate next transitions.
|
||||||
|
fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State {
|
||||||
|
match transition {
|
||||||
|
Transition::Nop => {}
|
||||||
|
Transition::ReceiveSafeBlock(block) => {
|
||||||
|
state
|
||||||
|
.chain
|
||||||
|
.entry(block.view)
|
||||||
|
.or_default()
|
||||||
|
.blocks
|
||||||
|
.insert(block.clone());
|
||||||
|
}
|
||||||
|
Transition::ReceiveUnsafeBlock(_) => {
|
||||||
|
// Nothing to do because we expect the state doesn't change.
|
||||||
|
}
|
||||||
|
Transition::ApproveBlock(block) => {
|
||||||
|
state.highest_voted_view = block.view;
|
||||||
|
}
|
||||||
|
Transition::ApprovePastBlock(_) => {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RefState {
|
||||||
|
// Generate a Transition::ReceiveSafeBlock.
|
||||||
|
fn transition_receive_safe_block(&self) -> BoxedStrategy<Transition> {
|
||||||
|
let recent_parents = self
|
||||||
|
.chain
|
||||||
|
.range(self.current_view() - 1..)
|
||||||
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
|
if recent_parents.is_empty() {
|
||||||
|
Just(Transition::Nop).boxed()
|
||||||
|
} else {
|
||||||
|
// proptest::sample::select panics if the input is empty
|
||||||
|
proptest::sample::select(recent_parents)
|
||||||
|
.prop_map(move |parent| -> Transition {
|
||||||
|
Transition::ReceiveSafeBlock(Self::consecutive_block(&parent))
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a Transition::ReceiveUnsafeBlock.
|
||||||
|
fn transition_receive_unsafe_block(&self) -> BoxedStrategy<Transition> {
|
||||||
|
let old_parents = self
|
||||||
|
.chain
|
||||||
|
.range(..self.current_view() - 1)
|
||||||
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
|
if old_parents.is_empty() {
|
||||||
|
Just(Transition::Nop).boxed()
|
||||||
|
} else {
|
||||||
|
// proptest::sample::select panics if the input is empty
|
||||||
|
proptest::sample::select(old_parents)
|
||||||
|
.prop_map(move |parent| -> Transition {
|
||||||
|
Transition::ReceiveUnsafeBlock(Self::consecutive_block(&parent))
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a Transition::ApproveBlock.
|
||||||
|
fn transition_approve_block(&self) -> BoxedStrategy<Transition> {
|
||||||
|
let blocks_not_voted = self
|
||||||
|
.chain
|
||||||
|
.range(self.highest_voted_view + 1..)
|
||||||
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
|
if blocks_not_voted.is_empty() {
|
||||||
|
Just(Transition::Nop).boxed()
|
||||||
|
} else {
|
||||||
|
// proptest::sample::select panics if the input is empty
|
||||||
|
proptest::sample::select(blocks_not_voted)
|
||||||
|
.prop_map(Transition::ApproveBlock)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a Transition::ApprovePastBlock.
|
||||||
|
fn transition_approve_past_block(&self) -> BoxedStrategy<Transition> {
|
||||||
|
let past_blocks = self
|
||||||
|
.chain
|
||||||
|
.range(INITIAL_HIGHEST_VOTED_VIEW..self.highest_voted_view)
|
||||||
|
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
||||||
|
.collect::<Vec<Block>>();
|
||||||
|
|
||||||
|
if past_blocks.is_empty() {
|
||||||
|
Just(Transition::Nop).boxed()
|
||||||
|
} else {
|
||||||
|
// proptest::sample::select panics if the input is empty
|
||||||
|
proptest::sample::select(past_blocks)
|
||||||
|
.prop_map(Transition::ApprovePastBlock)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
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,
|
||||||
|
high_qc: entry.high_qc().unwrap(),
|
||||||
|
sender: SENDER,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highest_voted_view(&self) -> View {
|
||||||
|
self.highest_voted_view
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_view(&self) -> View {
|
||||||
|
let (&last_view, last_entry) = self.chain.last_key_value().unwrap();
|
||||||
|
if last_entry.timeout_qcs.is_empty() {
|
||||||
|
last_view
|
||||||
|
} else {
|
||||||
|
let timeout_qc = last_entry.timeout_qcs.iter().next().unwrap();
|
||||||
|
RefState::new_view_from(timeout_qc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_view_from(timeout_qc: &TimeoutQc) -> View {
|
||||||
|
timeout_qc.view + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consecutive_block(parent: &Block) -> Block {
|
||||||
|
Block {
|
||||||
|
// use rand because we don't want this to be shrinked by proptest
|
||||||
|
id: rand::thread_rng().gen(),
|
||||||
|
view: parent.view + 1,
|
||||||
|
parent_qc: Qc::Standard(StandardQc {
|
||||||
|
view: parent.view,
|
||||||
|
id: parent.id,
|
||||||
|
}),
|
||||||
|
leader_proof: LEADER_PROOF.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
use consensus_engine::{
|
||||||
|
overlay::{FlatOverlay, RoundRobin, Settings},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use proptest_state_machine::{ReferenceStateMachine, StateMachineTest};
|
||||||
|
|
||||||
|
use crate::fuzz::ref_state::RefState;
|
||||||
|
use crate::fuzz::transition::Transition;
|
||||||
|
|
||||||
|
// ConsensusEngineTest defines a state that we want to test.
|
||||||
|
// This is called as SUT (System Under Test).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ConsensusEngineTest {
|
||||||
|
pub engine: Carnot<FlatOverlay<RoundRobin>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsensusEngineTest {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let engine = Carnot::from_genesis(
|
||||||
|
[0; 32],
|
||||||
|
Block {
|
||||||
|
view: 0,
|
||||||
|
id: [0; 32],
|
||||||
|
parent_qc: Qc::Standard(StandardQc::genesis()),
|
||||||
|
leader_proof: LeaderProof::LeaderId { leader_id: [0; 32] },
|
||||||
|
},
|
||||||
|
FlatOverlay::new(Settings {
|
||||||
|
nodes: vec![[0; 32]],
|
||||||
|
leader: RoundRobin::default(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
ConsensusEngineTest { engine }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateMachineTest defines how transitions are applied to the real state machine
|
||||||
|
// and what checks should be performed.
|
||||||
|
impl StateMachineTest for ConsensusEngineTest {
|
||||||
|
// SUT is the real state machine that we want to test.
|
||||||
|
type SystemUnderTest = Self;
|
||||||
|
|
||||||
|
// A reference state machine that should be compared against the SUT.
|
||||||
|
type Reference = RefState;
|
||||||
|
|
||||||
|
// Initialize the SUT state
|
||||||
|
fn init_test(
|
||||||
|
_ref_state: &<Self::Reference as proptest_state_machine::ReferenceStateMachine>::State,
|
||||||
|
) -> Self::SystemUnderTest {
|
||||||
|
ConsensusEngineTest::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the transition on the SUT state and check post-conditions
|
||||||
|
fn apply(
|
||||||
|
state: Self::SystemUnderTest,
|
||||||
|
_ref_state: &<Self::Reference as proptest_state_machine::ReferenceStateMachine>::State,
|
||||||
|
transition: <Self::Reference as proptest_state_machine::ReferenceStateMachine>::Transition,
|
||||||
|
) -> Self::SystemUnderTest {
|
||||||
|
println!("{transition:?}");
|
||||||
|
|
||||||
|
match transition {
|
||||||
|
Transition::Nop => state,
|
||||||
|
Transition::ReceiveSafeBlock(block) => {
|
||||||
|
// Because we generate/apply transitions sequentially,
|
||||||
|
// this transition will always be applied to the same state that it was generated against.
|
||||||
|
// In other words, we can assume that the `block` is always safe for the current state.
|
||||||
|
let engine = state.engine.receive_block(block.clone()).unwrap();
|
||||||
|
assert!(engine.blocks_in_view(block.view).contains(&block));
|
||||||
|
|
||||||
|
ConsensusEngineTest { engine }
|
||||||
|
}
|
||||||
|
Transition::ReceiveUnsafeBlock(block) => {
|
||||||
|
let result = panic::catch_unwind(|| state.engine.receive_block(block));
|
||||||
|
assert!(result.is_err() || result.unwrap().is_err());
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
Transition::ApproveBlock(block) => {
|
||||||
|
let (engine, _) = state.engine.approve_block(block.clone());
|
||||||
|
assert_eq!(engine.highest_voted_view(), block.view);
|
||||||
|
|
||||||
|
ConsensusEngineTest { engine }
|
||||||
|
}
|
||||||
|
Transition::ApprovePastBlock(block) => {
|
||||||
|
let result = panic::catch_unwind(|| state.engine.approve_block(block));
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
#[allow(clippy::redundant_clone)]
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check invariants after every transition.
|
||||||
|
// We have the option to use RefState for comparison in some cases.
|
||||||
|
fn check_invariants(
|
||||||
|
state: &Self::SystemUnderTest,
|
||||||
|
ref_state: &<Self::Reference as ReferenceStateMachine>::State,
|
||||||
|
) {
|
||||||
|
assert_eq!(state.engine.current_view(), ref_state.current_view());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
state.engine.highest_voted_view(),
|
||||||
|
ref_state.highest_voted_view()
|
||||||
|
);
|
||||||
|
|
||||||
|
//TODO: add more invariants with more public functions of Carnot
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
use consensus_engine::{Block, TimeoutQc};
|
||||||
|
|
||||||
|
// State transtitions that will be picked randomly
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Transition {
|
||||||
|
Nop,
|
||||||
|
ReceiveSafeBlock(Block),
|
||||||
|
ReceiveUnsafeBlock(Block),
|
||||||
|
ApproveBlock(Block),
|
||||||
|
ApprovePastBlock(Block),
|
||||||
|
LocalTimeout,
|
||||||
|
ReceiveTimeoutQcForCurrentView(TimeoutQc),
|
||||||
|
ReceiveTimeoutQcForOldView(TimeoutQc),
|
||||||
|
//TODO: add more invalid transitions that must be rejected by consensus-engine
|
||||||
|
//TODO: add more transitions
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
use std::{
|
mod fuzz;
|
||||||
collections::{BTreeMap, HashSet},
|
|
||||||
panic,
|
use std::panic;
|
||||||
};
|
|
||||||
|
|
||||||
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};
|
||||||
use system_under_test::ConsensusEngineTest;
|
|
||||||
|
use fuzz::sut::ConsensusEngineTest;
|
||||||
|
|
||||||
prop_state_machine! {
|
prop_state_machine! {
|
||||||
#![proptest_config(Config {
|
#![proptest_config(Config {
|
||||||
|
@ -19,456 +18,5 @@ prop_state_machine! {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// run 50 state transitions per test case
|
// run 50 state transitions per test case
|
||||||
fn happy_path(sequential 1..50 => ConsensusEngineTest);
|
fn consensus_engine_test(sequential 1..50 => ConsensusEngineTest);
|
||||||
}
|
|
||||||
|
|
||||||
// A reference state machine (RefState) is used to generated state transitions.
|
|
||||||
// To generate some kinds of transition, we may need to keep historical blocks in RefState.
|
|
||||||
// Also, RefState can be used to check invariants of the real state machine in some cases.
|
|
||||||
//
|
|
||||||
// We should try to design this reference state as simple/intuitive as possible,
|
|
||||||
// so that we don't need to replicate the logic implemented in consensus-engine.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct RefState {
|
|
||||||
chain: BTreeMap<View, ViewEntry>,
|
|
||||||
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
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum Transition {
|
|
||||||
Nop,
|
|
||||||
ReceiveSafeBlock(Block),
|
|
||||||
ReceiveUnsafeBlock(Block),
|
|
||||||
ApproveBlock(Block),
|
|
||||||
ApprovePastBlock(Block),
|
|
||||||
LocalTimeout,
|
|
||||||
ReceiveTimeoutQcForCurrentView(TimeoutQc),
|
|
||||||
ReceiveTimeoutQcForOldView(TimeoutQc),
|
|
||||||
//TODO: add more invalid transitions that must be rejected by consensus-engine
|
|
||||||
//TODO: add more transitions
|
|
||||||
}
|
|
||||||
|
|
||||||
const LEADER_PROOF: LeaderProof = LeaderProof::LeaderId { leader_id: [0; 32] };
|
|
||||||
const INITIAL_HIGHEST_VOTED_VIEW: View = -1;
|
|
||||||
const SENDER: NodeId = [0; 32];
|
|
||||||
|
|
||||||
impl ReferenceStateMachine for RefState {
|
|
||||||
type State = Self;
|
|
||||||
|
|
||||||
type Transition = Transition;
|
|
||||||
|
|
||||||
// Initialize the reference state machine
|
|
||||||
fn init_state() -> BoxedStrategy<Self::State> {
|
|
||||||
let genesis_block = Block {
|
|
||||||
view: 0,
|
|
||||||
id: [0; 32],
|
|
||||||
parent_qc: Qc::Standard(StandardQc::genesis()),
|
|
||||||
leader_proof: LEADER_PROOF.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Just(RefState {
|
|
||||||
chain: BTreeMap::from([(
|
|
||||||
genesis_block.view,
|
|
||||||
ViewEntry {
|
|
||||||
blocks: HashSet::from([genesis_block]),
|
|
||||||
timeout_qcs: Default::default(),
|
|
||||||
},
|
|
||||||
)]),
|
|
||||||
highest_voted_view: INITIAL_HIGHEST_VOTED_VIEW,
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate transitions based on the current reference state machine
|
|
||||||
fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition> {
|
|
||||||
// Instead of using verbose `if` statements here to filter out the types of transitions
|
|
||||||
// which cannot be created based on the current reference state,
|
|
||||||
// each `state.transition_*` function returns a Nop transition
|
|
||||||
// if it cannot generate the promised transition for the current reference state.
|
|
||||||
// Both reference and real state machine do nothing for Nop transitions.
|
|
||||||
prop_oneof![
|
|
||||||
state.transition_receive_safe_block(),
|
|
||||||
state.transition_receive_unsafe_block(),
|
|
||||||
state.transition_approve_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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the transition is valid for a given reference state, before applying the transition
|
|
||||||
// If invalid, the transition will be ignored and a new transition will be generated.
|
|
||||||
//
|
|
||||||
// Also, preconditions are used for shrinking in failure cases.
|
|
||||||
// Preconditions check if the transition is still valid after some shrinking is applied.
|
|
||||||
// If the transition became invalid for the shrinked state, the shrinking is stopped or
|
|
||||||
// is continued to other directions.
|
|
||||||
fn preconditions(state: &Self::State, transition: &Self::Transition) -> bool {
|
|
||||||
// In most cases, we need to check the same conditions again used to create transitions.
|
|
||||||
// This is redundant for success cases, but is necessary for shrinking in failure cases,
|
|
||||||
// because some transitions may no longer be valid after some shrinking is applied.
|
|
||||||
match transition {
|
|
||||||
Transition::Nop => true,
|
|
||||||
Transition::ReceiveSafeBlock(block) => block.view >= state.current_view(),
|
|
||||||
Transition::ReceiveUnsafeBlock(block) => block.view < state.current_view(),
|
|
||||||
Transition::ApproveBlock(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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the given transition on the reference state machine,
|
|
||||||
// so that it can be used to generate next transitions.
|
|
||||||
fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State {
|
|
||||||
match transition {
|
|
||||||
Transition::Nop => {}
|
|
||||||
Transition::ReceiveSafeBlock(block) => {
|
|
||||||
state
|
|
||||||
.chain
|
|
||||||
.entry(block.view)
|
|
||||||
.or_default()
|
|
||||||
.blocks
|
|
||||||
.insert(block.clone());
|
|
||||||
}
|
|
||||||
Transition::ReceiveUnsafeBlock(_) => {
|
|
||||||
// Nothing to do because we expect the state doesn't change.
|
|
||||||
}
|
|
||||||
Transition::ApproveBlock(block) => {
|
|
||||||
state.highest_voted_view = block.view;
|
|
||||||
}
|
|
||||||
Transition::ApprovePastBlock(_) => {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RefState {
|
|
||||||
// Generate a Transition::ReceiveSafeBlock.
|
|
||||||
fn transition_receive_safe_block(&self) -> BoxedStrategy<Transition> {
|
|
||||||
let recent_parents = self
|
|
||||||
.chain
|
|
||||||
.range(self.current_view() - 1..)
|
|
||||||
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
|
||||||
.collect::<Vec<Block>>();
|
|
||||||
|
|
||||||
if recent_parents.is_empty() {
|
|
||||||
Just(Transition::Nop).boxed()
|
|
||||||
} else {
|
|
||||||
// proptest::sample::select panics if the input is empty
|
|
||||||
proptest::sample::select(recent_parents)
|
|
||||||
.prop_map(move |parent| -> Transition {
|
|
||||||
Transition::ReceiveSafeBlock(Self::consecutive_block(&parent))
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a Transition::ReceiveUnsafeBlock.
|
|
||||||
fn transition_receive_unsafe_block(&self) -> BoxedStrategy<Transition> {
|
|
||||||
let old_parents = self
|
|
||||||
.chain
|
|
||||||
.range(..self.current_view() - 1)
|
|
||||||
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
|
||||||
.collect::<Vec<Block>>();
|
|
||||||
|
|
||||||
if old_parents.is_empty() {
|
|
||||||
Just(Transition::Nop).boxed()
|
|
||||||
} else {
|
|
||||||
// proptest::sample::select panics if the input is empty
|
|
||||||
proptest::sample::select(old_parents)
|
|
||||||
.prop_map(move |parent| -> Transition {
|
|
||||||
Transition::ReceiveUnsafeBlock(Self::consecutive_block(&parent))
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a Transition::ApproveBlock.
|
|
||||||
fn transition_approve_block(&self) -> BoxedStrategy<Transition> {
|
|
||||||
let blocks_not_voted = self
|
|
||||||
.chain
|
|
||||||
.range(self.highest_voted_view + 1..)
|
|
||||||
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
|
||||||
.collect::<Vec<Block>>();
|
|
||||||
|
|
||||||
if blocks_not_voted.is_empty() {
|
|
||||||
Just(Transition::Nop).boxed()
|
|
||||||
} else {
|
|
||||||
// proptest::sample::select panics if the input is empty
|
|
||||||
proptest::sample::select(blocks_not_voted)
|
|
||||||
.prop_map(Transition::ApproveBlock)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a Transition::ApprovePastBlock.
|
|
||||||
fn transition_approve_past_block(&self) -> BoxedStrategy<Transition> {
|
|
||||||
let past_blocks = self
|
|
||||||
.chain
|
|
||||||
.range(INITIAL_HIGHEST_VOTED_VIEW..self.highest_voted_view)
|
|
||||||
.flat_map(|(_view, entry)| entry.blocks.iter().cloned())
|
|
||||||
.collect::<Vec<Block>>();
|
|
||||||
|
|
||||||
if past_blocks.is_empty() {
|
|
||||||
Just(Transition::Nop).boxed()
|
|
||||||
} else {
|
|
||||||
// proptest::sample::select panics if the input is empty
|
|
||||||
proptest::sample::select(past_blocks)
|
|
||||||
.prop_map(Transition::ApprovePastBlock)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
let (&last_view, last_entry) = self.chain.last_key_value().unwrap();
|
|
||||||
if last_entry.timeout_qcs.is_empty() {
|
|
||||||
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 {
|
|
||||||
Block {
|
|
||||||
// use rand because we don't want this to be shrinked by proptest
|
|
||||||
id: rand::thread_rng().gen(),
|
|
||||||
view: parent.view + 1,
|
|
||||||
parent_qc: Qc::Standard(StandardQc {
|
|
||||||
view: parent.view,
|
|
||||||
id: parent.id,
|
|
||||||
}),
|
|
||||||
leader_proof: LEADER_PROOF.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateMachineTest defines how transitions are applied to the real state machine
|
|
||||||
// and what checks should be performed.
|
|
||||||
impl StateMachineTest for ConsensusEngineTest {
|
|
||||||
// SUT is the real state machine that we want to test.
|
|
||||||
type SystemUnderTest = Self;
|
|
||||||
|
|
||||||
// A reference state machine that should be compared against the SUT.
|
|
||||||
type Reference = RefState;
|
|
||||||
|
|
||||||
// Initialize the SUT state
|
|
||||||
fn init_test(
|
|
||||||
_ref_state: &<Self::Reference as proptest_state_machine::ReferenceStateMachine>::State,
|
|
||||||
) -> Self::SystemUnderTest {
|
|
||||||
ConsensusEngineTest::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the transition on the SUT state and check post-conditions
|
|
||||||
fn apply(
|
|
||||||
state: Self::SystemUnderTest,
|
|
||||||
_ref_state: &<Self::Reference as proptest_state_machine::ReferenceStateMachine>::State,
|
|
||||||
transition: <Self::Reference as proptest_state_machine::ReferenceStateMachine>::Transition,
|
|
||||||
) -> Self::SystemUnderTest {
|
|
||||||
println!("{transition:?}");
|
|
||||||
|
|
||||||
match transition {
|
|
||||||
Transition::Nop => state,
|
|
||||||
Transition::ReceiveSafeBlock(block) => {
|
|
||||||
// Because we generate/apply transitions sequentially,
|
|
||||||
// this transition will always be applied to the same state that it was generated against.
|
|
||||||
// In other words, we can assume that the `block` is always safe for the current state.
|
|
||||||
let engine = state.engine.receive_block(block.clone()).unwrap();
|
|
||||||
assert!(engine.blocks_in_view(block.view).contains(&block));
|
|
||||||
|
|
||||||
ConsensusEngineTest { engine }
|
|
||||||
}
|
|
||||||
Transition::ReceiveUnsafeBlock(block) => {
|
|
||||||
let result = panic::catch_unwind(|| state.engine.receive_block(block));
|
|
||||||
assert!(result.is_err() || result.unwrap().is_err());
|
|
||||||
|
|
||||||
state
|
|
||||||
}
|
|
||||||
Transition::ApproveBlock(block) => {
|
|
||||||
let (engine, _) = state.engine.approve_block(block.clone());
|
|
||||||
assert_eq!(engine.highest_voted_view(), block.view);
|
|
||||||
|
|
||||||
ConsensusEngineTest { engine }
|
|
||||||
}
|
|
||||||
Transition::ApprovePastBlock(block) => {
|
|
||||||
let result = panic::catch_unwind(|| state.engine.approve_block(block));
|
|
||||||
assert!(result.is_err());
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check invariants after every transition.
|
|
||||||
// We have the option to use RefState for comparison in some cases.
|
|
||||||
fn check_invariants(
|
|
||||||
state: &Self::SystemUnderTest,
|
|
||||||
ref_state: &<Self::Reference as ReferenceStateMachine>::State,
|
|
||||||
) {
|
|
||||||
assert_eq!(state.engine.current_view(), ref_state.current_view());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
state.engine.highest_voted_view(),
|
|
||||||
ref_state.highest_voted_view
|
|
||||||
);
|
|
||||||
|
|
||||||
//TODO: add more invariants with more public functions of Carnot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SUT is a system (state) that we want to test.
|
|
||||||
mod system_under_test {
|
|
||||||
use consensus_engine::{
|
|
||||||
overlay::{FlatOverlay, RoundRobin, Settings},
|
|
||||||
*,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::LEADER_PROOF;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ConsensusEngineTest {
|
|
||||||
pub engine: Carnot<FlatOverlay<RoundRobin>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConsensusEngineTest {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let engine = Carnot::from_genesis(
|
|
||||||
[0; 32],
|
|
||||||
Block {
|
|
||||||
view: 0,
|
|
||||||
id: [0; 32],
|
|
||||||
parent_qc: Qc::Standard(StandardQc::genesis()),
|
|
||||||
leader_proof: LEADER_PROOF.clone(),
|
|
||||||
},
|
|
||||||
FlatOverlay::new(Settings {
|
|
||||||
nodes: vec![[0; 32]],
|
|
||||||
leader: RoundRobin::default(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
ConsensusEngineTest { engine }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue