mirror of https://github.com/poanetwork/hbbft.git
246 lines
7.2 KiB
Rust
246 lines
7.2 KiB
Rust
//! Proptest helpers and strategies.
|
|
//!
|
|
//! This module houses strategies to generate (and reduce/expand) various `hbbft` and `net` related
|
|
//! structures.
|
|
|
|
use integer_sqrt::IntegerSquareRoot;
|
|
use proptest::arbitrary::any;
|
|
use proptest::prelude::Rng;
|
|
use proptest::strategy::{Strategy, ValueTree};
|
|
use proptest::test_runner::{Reason, TestRunner};
|
|
use rand::{self, SeedableRng};
|
|
|
|
/// Random number generator type used in testing.
|
|
pub type TestRng = rand_xorshift::XorShiftRng;
|
|
|
|
/// Seed type of the random number generator used in testing.
|
|
pub type TestRngSeed = [u8; 16];
|
|
|
|
/// Generates a random instance of a random number generator.
|
|
pub fn gen_rng() -> impl Strategy<Value = TestRng> {
|
|
gen_seed().prop_map(TestRng::from_seed)
|
|
}
|
|
|
|
/// Generates a random seed to instantiate a `TestRng`.
|
|
///
|
|
/// The random seed is non-shrinkable, to avoid meaningless shrinking in case of failed tests.
|
|
pub fn gen_seed() -> impl Strategy<Value = TestRngSeed> {
|
|
any::<TestRngSeed>().no_shrink()
|
|
}
|
|
|
|
/// 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, Ord, PartialOrd)]
|
|
pub struct NetworkDimension {
|
|
/// Total number of nodes in network.
|
|
size: u16,
|
|
/// Number of faulty nodes in a network.
|
|
faulty: u16,
|
|
}
|
|
|
|
impl NetworkDimension {
|
|
/// Creates a new `NetworkDimension` with the supplied parameters.
|
|
///
|
|
/// # Panics
|
|
///
|
|
|
|
#[inline]
|
|
pub fn new(size: u16, faulty: u16) -> Self {
|
|
let dim = NetworkDimension { size, faulty };
|
|
assert!(
|
|
dim.is_bft(),
|
|
"Tried to create network dimension that violates BFT-property."
|
|
);
|
|
dim
|
|
}
|
|
|
|
#[inline]
|
|
pub fn faulty(self) -> usize {
|
|
self.faulty.into()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn size(self) -> usize {
|
|
self.size.into()
|
|
}
|
|
|
|
/// Checks whether the network dimension satisfies the `3 * faulty + 1 <= size` condition.
|
|
#[inline]
|
|
fn is_bft(self) -> bool {
|
|
self.faulty * 3 < self.size
|
|
}
|
|
|
|
/// Creates a proptest strategy to create network dimensions within a certain range.
|
|
#[inline]
|
|
pub fn range(min_size: u16, max_size: u16) -> NetworkDimensionStrategy {
|
|
NetworkDimensionStrategy { min_size, max_size }
|
|
}
|
|
|
|
/// Returns next-larger network dimension.
|
|
///
|
|
/// The order on `NetworkDimension` is canonically defined by `(size, faulty)`. The `succ`
|
|
/// function returns the next-higher valid instance by first trying to increase `faulty`, then
|
|
/// `size`.
|
|
#[inline]
|
|
pub fn succ(self) -> NetworkDimension {
|
|
let mut n = self.size;
|
|
let mut f = self.faulty + 1;
|
|
|
|
if 3 * f >= n {
|
|
f = 0;
|
|
n += 1;
|
|
}
|
|
|
|
NetworkDimension::new(n, f)
|
|
}
|
|
}
|
|
|
|
/// 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: u32,
|
|
/// The currently generated network dimension.
|
|
current: u32,
|
|
/// The lower bound for any generated dimension value (changes during generation or shrinking).
|
|
low: u32,
|
|
}
|
|
|
|
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: u16, max_size: u16) -> 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::new(total, faulty);
|
|
|
|
NetworkDimensionTree {
|
|
high: high.into(),
|
|
current: high.into(),
|
|
low: u32::from(min_size),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ValueTree for NetworkDimensionTree {
|
|
type Value = NetworkDimension;
|
|
|
|
fn current(&self) -> Self::Value {
|
|
self.current.into()
|
|
}
|
|
|
|
fn simplify(&mut self) -> bool {
|
|
let prev = *self;
|
|
|
|
self.high = self.current;
|
|
self.current = (self.low + self.high) / 2;
|
|
|
|
(prev.high != self.high || prev.current != self.current)
|
|
}
|
|
|
|
fn complicate(&mut self) -> bool {
|
|
let prev = *self;
|
|
|
|
if self.high == self.current {
|
|
return false;
|
|
}
|
|
|
|
self.low = self.current + 1;
|
|
self.current = (self.low + self.high) / 2;
|
|
|
|
(prev.current != self.current || prev.low != self.low)
|
|
}
|
|
}
|
|
|
|
impl From<NetworkDimension> for u32 {
|
|
fn from(dim: NetworkDimension) -> u32 {
|
|
// `b` is the "Block index" here. Counting through `NetworkDimensions` a pattern shows:
|
|
//
|
|
// n f
|
|
// 1 0 \
|
|
// 2 0 |- Block 0
|
|
// 3 0 /
|
|
// 4 0 \
|
|
// 4 1 \
|
|
// 5 0 > Block 1
|
|
// 5 1 |
|
|
// 6 0 /
|
|
// 6 1 /
|
|
// 7 0 ...
|
|
//
|
|
// We observe that each block starts at index `3 * (b(b+1)/2)`. Along with the offset,
|
|
// we can calculate a mapping onto the natural numbers using this:
|
|
|
|
let b = (u32::from(dim.size) - 1) / 3;
|
|
let start = 3 * b * (b + 1) / 2;
|
|
let offset = (u32::from(dim.size) - 3 * b - 1) * (b + 1) + u32::from(dim.faulty);
|
|
|
|
start + offset
|
|
}
|
|
}
|
|
|
|
impl From<u32> for NetworkDimension {
|
|
fn from(n: u32) -> NetworkDimension {
|
|
// Inverse of `u32 as From<NetworkDimension>`:
|
|
|
|
// Find the block number first:
|
|
let b = max_sum(n / 3);
|
|
|
|
// Calculate the block start and the resulting offset of `n`:
|
|
let start = 3 * b * (b + 1) / 2;
|
|
let offset = n - start;
|
|
|
|
let faulty = offset % (b + 1);
|
|
let size = 3 * b + 1 + offset / (b + 1);
|
|
|
|
NetworkDimension::new(size as u16, faulty as u16)
|
|
}
|
|
}
|
|
|
|
/// Finds the largest consecutive summand less or equal than `n`.
|
|
///
|
|
/// The return value `k` will satisfy `SUM 1..k <= n`.
|
|
pub fn max_sum(n: u32) -> u32 {
|
|
// Derived by quadratically solving `n(n+1)/2`; we only want the "positive" result.
|
|
// `integer_sqrt` functions as a `floor` function here.
|
|
((1 + 8 * n).integer_sqrt() - 1) / 2
|
|
}
|
|
|
|
/// Network dimension strategy for proptest.
|
|
#[derive(Debug)]
|
|
pub struct NetworkDimensionStrategy {
|
|
/// Minimum number of nodes for newly generated networks dimensions.
|
|
pub min_size: u16,
|
|
/// Maximum number of nodes for newly generated networks dimensions.
|
|
pub max_size: u16,
|
|
}
|
|
|
|
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,
|
|
))
|
|
}
|
|
}
|