diff --git a/tests/net/proptest.rs b/tests/net/proptest.rs index 463ae98..c2fc98d 100644 --- a/tests/net/proptest.rs +++ b/tests/net/proptest.rs @@ -3,8 +3,12 @@ //! This module houses strategies to generate (and reduce/expand) various `hbbft` and `net` related //! structures. -use proptest::prelude::Rng; -use proptest::strategy::{Strategy, ValueTree}; +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. @@ -182,3 +186,164 @@ impl Strategy for NetworkDimensionStrategy { )) } } + +/// 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(&self) -> Box> + where + D: DistAlgorithm, + D::Message: Clone, + D::Output: Clone, + { + match self { + AdversaryConfiguration::Null => Box::new(adversary::NullAdversary::new()), + _ => unimplemented!(), + } + } +} + +impl From 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 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 { + high: AdversaryConfiguration, + current: AdversaryConfiguration, + low: AdversaryConfiguration, + current_instance: cell::RefCell>>>, +} + +impl ValueTree for AdversaryTree +where + Adversary: fmt::Debug + Clone, + D: DistAlgorithm, + D::Message: Clone, + D::Output: Clone, +{ + type Value = Box + '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() -> Box> +where + D: DistAlgorithm, + D::Message: Clone, + D::Output: Clone, +{ + adversary::NullAdversary::new().boxed() +} + +fn boxed_node_order_adversary() -> Box> +where + D: DistAlgorithm, + D::Message: Clone, + D::Output: Clone, +{ + adversary::NodeOrderAdversary::new().boxed() +} + +fn generic_adversary() +// -> impl Strategy +where + D: DistAlgorithm, + D::Message: Clone, + D::Output: Clone, +{ + // let b1 = || boxed_adversary(adversary::NullAdversary::new); + // prop_oneof![ + // boxed_null_adversary::, + // boxed_node_order_adversary::(), + // // LazyJust::new(|| Box::new(adversary::NodeOrderAdversary::new())) + // ] + // unimplemented!() +} diff --git a/tests/net_util.rs b/tests/net_util.rs index 5bf717d..64b8514 100644 --- a/tests/net_util.rs +++ b/tests/net_util.rs @@ -7,10 +7,12 @@ extern crate threshold_crypto; pub mod net; +use std::u8; + use proptest::strategy::ValueTree; use proptest::test_runner::TestRunner; -use net::proptest::{NetworkDimension, NetworkDimensionTree}; +use net::proptest::{AdversaryConfiguration, NetworkDimension, NetworkDimensionTree}; /// Checks the `check_sanity` function with various inputs. #[test] @@ -127,3 +129,62 @@ fn network_dimensions_shrink_and_grow() { assert!(tree.current().is_bft()); } } + +/// Checks that the `AdversaryConfiguration` can be roundtripped through `u8`. +#[test] +fn adversary_configuration_round_trip() { + for n in 0..=34 { + assert_eq!(n, u8::from(AdversaryConfiguration::from(n))); + } +} + +/// Verifies that values past the hardcoded limit for the adversary type are set to just the limit. +#[test] +fn adversary_configuration_ceiling() { + let max = 35; + for n in max..u8::MAX { + assert_eq!( + AdversaryConfiguration::from(n), + AdversaryConfiguration::from(max) + ); + } +} + +/// Tests known adversary configuration mid-way points. +#[test] +fn adversary_configuration_average_higher_works() { + assert_eq!( + AdversaryConfiguration::from(15).average_higher(29.into()), + 22.into() + ); + assert_eq!( + AdversaryConfiguration::from(0).average_higher(34.into()), + 17.into() + ); + + assert_eq!( + AdversaryConfiguration::from(0).average_higher(0.into()), + 0.into() + ); + assert_eq!( + AdversaryConfiguration::from(0).average_higher(1.into()), + 0.into() + ); + assert_eq!( + AdversaryConfiguration::from(0).average_higher(2.into()), + 1.into() + ); + + assert_eq!( + AdversaryConfiguration::from(10).average_higher(11.into()), + 10.into() + ); + assert_eq!( + AdversaryConfiguration::from(11).average_higher(11.into()), + 11.into() + ); + assert_eq!( + AdversaryConfiguration::from(11).average_higher(13.into()), + 12.into() + ); +}