mirror of https://github.com/poanetwork/hbbft.git
350 lines
11 KiB
Rust
350 lines
11 KiB
Rust
//! Proptest helpers and strategies.
|
|
//!
|
|
//! This module houses strategies to generate (and reduce/expand) various `hbbft` and `net` related
|
|
//! structures.
|
|
|
|
use std::{cell, fmt};
|
|
|
|
use hbbft::messaging::DistAlgorithm;
|
|
use net::adversary::{self, Adversary};
|
|
use proptest::prelude::{any, Rng};
|
|
use proptest::strategy::{BoxedStrategy, LazyJust, Strategy, ValueTree};
|
|
use proptest::test_runner::{Reason, TestRunner};
|
|
|
|
/// Node network dimension.
|
|
///
|
|
/// A `NetworkDimension` describes the number of correct and faulty nodes in a network. It can also
|
|
/// be checked, "averaged" (using the `average_higher` function) and generated using
|
|
/// `NetworkDimensionTree`.
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub struct NetworkDimension {
|
|
/// Total number of nodes in network.
|
|
pub size: usize,
|
|
/// Number of faulty nodes in a network.
|
|
pub faulty: usize,
|
|
}
|
|
|
|
impl NetworkDimension {
|
|
/// Creates a new `NetworkDimension` with the supplied parameters.
|
|
///
|
|
/// Dimensions that do not satisfy BFT conditions (see `is_bft`) can be created using this
|
|
/// function.
|
|
pub fn new(size: usize, faulty: usize) -> Self {
|
|
NetworkDimension { size, faulty }
|
|
}
|
|
|
|
/// Checks whether the network dimension satisfies the `3 * faulty + 1 <= size` condition.
|
|
pub fn is_bft(&self) -> bool {
|
|
self.faulty * 3 < self.size
|
|
}
|
|
|
|
/// Creates a new dimension of average complexity.
|
|
///
|
|
/// The new dimension is approximately half way in the interval of `[self, high]` and will
|
|
/// conform to the constraint checked by `is_bft()`.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// `high` must be have a higher or equal size and faulty node count.
|
|
pub fn average_higher(&self, high: NetworkDimension) -> NetworkDimension {
|
|
assert!(high.size >= self.size);
|
|
assert!(high.faulty >= self.faulty);
|
|
|
|
// We try halving both values, rounding down. If `size` is at the minimum, `faulty` will
|
|
// shrink afterwards.
|
|
let mut half = NetworkDimension {
|
|
size: self.size + (high.size - self.size) / 2,
|
|
faulty: self.faulty + (high.faulty - self.faulty) / 2,
|
|
};
|
|
|
|
// Reduce the number of faulty nodes, if we are outside our limits.
|
|
if half.faulty * 3 > half.size {
|
|
half.faulty -= 1;
|
|
}
|
|
|
|
// This assert just checks for bugs.
|
|
assert!(half.is_bft());
|
|
|
|
half
|
|
}
|
|
|
|
/// Creates a proptest strategy to create network dimensions within a certain range.
|
|
pub fn range(min_size: usize, max_size: usize) -> NetworkDimensionStrategy {
|
|
NetworkDimensionStrategy { min_size, max_size }
|
|
}
|
|
}
|
|
|
|
/// Network dimension tree for proptest generation.
|
|
///
|
|
/// See `proptest::strategy::ValueTree` for a more thorough description.
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct NetworkDimensionTree {
|
|
/// The upper bound for any generated dimension.
|
|
high: NetworkDimension,
|
|
/// The currently generated network dimension.
|
|
current: NetworkDimension,
|
|
/// The lower bound for any generated dimension value (changes during generation or shrinking).
|
|
low: NetworkDimension,
|
|
}
|
|
|
|
impl NetworkDimensionTree {
|
|
/// Generate a random network dimension tree.
|
|
///
|
|
/// The resulting initial `NetworkDimension` will have a number of nodes within
|
|
/// [`min_size`, `max_size`] and a valid number of faulty nodes.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// The minimum `min_size` is 1 and `min_size` must be less than or equal `max_size`.
|
|
pub fn gen<R: Rng>(mut rng: R, min_size: usize, max_size: usize) -> Self {
|
|
// A common mistake, add an extra assert for a more helpful error message.
|
|
assert!(min_size > 0, "minimum network size is 1");
|
|
|
|
let total = rng.gen_range(min_size, max_size + 1);
|
|
let max_faulty = (total - 1) / 3;
|
|
let faulty = rng.gen_range(0, max_faulty + 1);
|
|
|
|
let high = NetworkDimension {
|
|
size: total,
|
|
faulty,
|
|
};
|
|
assert!(high.is_bft());
|
|
|
|
let low = NetworkDimension {
|
|
size: min_size,
|
|
faulty: 0,
|
|
};
|
|
assert!(low.is_bft());
|
|
|
|
NetworkDimensionTree {
|
|
high,
|
|
current: high,
|
|
low,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ValueTree for NetworkDimensionTree {
|
|
type Value = NetworkDimension;
|
|
|
|
fn current(&self) -> Self::Value {
|
|
self.current
|
|
}
|
|
|
|
fn simplify(&mut self) -> bool {
|
|
// Shrinking is simply done through `average_higher`.
|
|
let prev = *self;
|
|
|
|
self.high = prev.current;
|
|
self.current = self.low.average_higher(prev.high);
|
|
|
|
(prev.high != self.high || prev.current != self.current)
|
|
}
|
|
|
|
fn complicate(&mut self) -> bool {
|
|
let prev = *self;
|
|
|
|
// Minimally increase the faulty-node ratio by adjusting the number of faulty nodes and the
|
|
// size slightly less. If we are at the maximum number of faulty nodes, we would end up
|
|
// increasing the network size instead (see branch below though).
|
|
let mut new_low = self.current;
|
|
new_low.faulty += 1;
|
|
new_low.size = (new_low.size + 2).max(new_low.faulty * 3 + 1);
|
|
assert!(new_low.is_bft());
|
|
|
|
// Instead of growing the network, return unchanged if the new network would be larger than
|
|
// the current high.
|
|
if new_low.size > self.high.size {
|
|
return false;
|
|
}
|
|
|
|
self.current = new_low.average_higher(self.high);
|
|
self.low = new_low;
|
|
|
|
(prev.current != self.current || prev.low != self.low)
|
|
}
|
|
}
|
|
|
|
/// Network dimension strategy for proptest.
|
|
#[derive(Debug)]
|
|
pub struct NetworkDimensionStrategy {
|
|
/// Minimum number of nodes for newly generated networks dimensions.
|
|
pub min_size: usize,
|
|
/// Maximum number of nodes for newly generated networks dimensions.
|
|
pub max_size: usize,
|
|
}
|
|
|
|
impl Strategy for NetworkDimensionStrategy {
|
|
type Value = NetworkDimension;
|
|
type Tree = NetworkDimensionTree;
|
|
|
|
fn new_tree(&self, runner: &mut TestRunner) -> Result<Self::Tree, Reason> {
|
|
Ok(NetworkDimensionTree::gen(
|
|
runner.rng(),
|
|
self.min_size,
|
|
self.max_size,
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Adversary configuration.
|
|
///
|
|
/// Describes a generic adversary and can be used to instantiate it. All configurations are ordered
|
|
/// in terms of approximate complexity.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
|
pub enum AdversaryConfiguration {
|
|
/// A `NullAdversary`.
|
|
Null,
|
|
/// A `NodeOrderAdversary`.
|
|
NodeOrder,
|
|
/// A `SilentAdversary`.
|
|
Silent,
|
|
/// A `ReorderingAdversary`.
|
|
///
|
|
/// Includes an opaque complexity value that specifies how active the adversary acts.
|
|
Reordering(u8), // random complexity value
|
|
/// A `RandomAdversary`.
|
|
///
|
|
/// Includes an opaque complexity value that specifies how active the adversary acts.
|
|
Random(u8), // random complexity value, not a seed!
|
|
}
|
|
|
|
impl AdversaryConfiguration {
|
|
pub fn average_higher(&self, high: AdversaryConfiguration) -> Self {
|
|
assert!(*self <= high);
|
|
|
|
let l: u8 = (*self).into();
|
|
let h: u8 = high.into();
|
|
|
|
AdversaryConfiguration::from(l + (h - l) / 2)
|
|
}
|
|
|
|
pub fn create_adversary<D>(&self) -> Box<dyn Adversary<D>>
|
|
where
|
|
D: DistAlgorithm,
|
|
D::Message: Clone,
|
|
D::Output: Clone,
|
|
{
|
|
match self {
|
|
AdversaryConfiguration::Null => Box::new(adversary::NullAdversary::new()),
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<u8> for AdversaryConfiguration {
|
|
fn from(raw: u8) -> AdversaryConfiguration {
|
|
match raw.min(34) {
|
|
0 => AdversaryConfiguration::Null,
|
|
1 => AdversaryConfiguration::NodeOrder,
|
|
2 => AdversaryConfiguration::Silent,
|
|
// `Reordering` and `Random` adversary each know 16 different complexities.
|
|
n if n <= 18 => AdversaryConfiguration::Reordering(n - 2),
|
|
n if n <= 34 => AdversaryConfiguration::Random(n - 18),
|
|
// The `.min` above ensure no values exceeds the tested ones.
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<AdversaryConfiguration> for u8 {
|
|
fn from(at: AdversaryConfiguration) -> u8 {
|
|
match at {
|
|
AdversaryConfiguration::Null => 0,
|
|
AdversaryConfiguration::NodeOrder => 1,
|
|
AdversaryConfiguration::Silent => 2,
|
|
AdversaryConfiguration::Reordering(n) => n + 2,
|
|
AdversaryConfiguration::Random(n) => n + 18,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AdversaryTree<D> {
|
|
high: AdversaryConfiguration,
|
|
current: AdversaryConfiguration,
|
|
low: AdversaryConfiguration,
|
|
current_instance: cell::RefCell<Option<Box<dyn Adversary<D>>>>,
|
|
}
|
|
|
|
impl<D> ValueTree for AdversaryTree<D>
|
|
where
|
|
Adversary<D>: fmt::Debug + Clone,
|
|
D: DistAlgorithm,
|
|
D::Message: Clone,
|
|
D::Output: Clone,
|
|
{
|
|
type Value = Box<Adversary<D> + 'static>;
|
|
|
|
fn current(&self) -> Self::Value {
|
|
// Through `current_instance` we only instantiate the adversary once its requested. This
|
|
// is not done for performance but code structuring purposes (actual gains would likely
|
|
// be very small). If this causes any issues due to the resulting `?Sync`, the cell can be
|
|
// removed and an instance created inside `simplify` and `complicate` each time the state
|
|
// changes.
|
|
self.current_instance
|
|
.borrow_mut()
|
|
.get_or_insert_with(|| self.current.create_adversary())
|
|
.clone()
|
|
}
|
|
|
|
fn simplify(&mut self) -> bool {
|
|
let prev_high = self.high;
|
|
let prev_current = self.current;
|
|
|
|
self.high = self.current;
|
|
self.current = self.low.average_higher(prev_high);
|
|
|
|
(prev_high != self.high || prev_current != self.current)
|
|
}
|
|
|
|
fn complicate(&mut self) -> bool {
|
|
let new_low: AdversaryConfiguration = (u8::from(self.low) + 1).into();
|
|
let prev_low = self.low;
|
|
let prev_current = self.current;
|
|
|
|
if new_low > self.high {
|
|
// We already hit the max.
|
|
return false;
|
|
}
|
|
|
|
self.current = new_low.average_higher(self.high);
|
|
self.low = new_low;
|
|
|
|
(prev_current != self.current || prev_low != self.low)
|
|
}
|
|
}
|
|
|
|
fn boxed_null_adversary<D>() -> Box<dyn Adversary<D>>
|
|
where
|
|
D: DistAlgorithm,
|
|
D::Message: Clone,
|
|
D::Output: Clone,
|
|
{
|
|
adversary::NullAdversary::new().boxed()
|
|
}
|
|
|
|
fn boxed_node_order_adversary<D>() -> Box<dyn Adversary<D>>
|
|
where
|
|
D: DistAlgorithm,
|
|
D::Message: Clone,
|
|
D::Output: Clone,
|
|
{
|
|
adversary::NodeOrderAdversary::new().boxed()
|
|
}
|
|
|
|
fn generic_adversary<D>()
|
|
// -> impl Strategy
|
|
where
|
|
D: DistAlgorithm,
|
|
D::Message: Clone,
|
|
D::Output: Clone,
|
|
{
|
|
// let b1 = || boxed_adversary(adversary::NullAdversary::new);
|
|
// prop_oneof![
|
|
// boxed_null_adversary::<D>,
|
|
// boxed_node_order_adversary::<D>(),
|
|
// // LazyJust::new(|| Box::new(adversary::NodeOrderAdversary::new()))
|
|
// ]
|
|
// unimplemented!()
|
|
}
|