Binary Agreement test updated to the proptest framework (#336)

* converted the BA test to net framework

* fixed lints and corrected docs

* seeded the Rng and removed logging

* allowed pass by value of binary_agreement argument

* handling of input via proptest and doc correction
This commit is contained in:
Vladimir Komendantskiy 2018-11-20 15:16:15 +00:00 committed by GitHub
parent 9049dd1793
commit 5735cf23a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 111 additions and 79 deletions

View File

@ -1,108 +1,140 @@
#![deny(unused_must_use)]
//! Tests of the Binary Agreement protocol. Only one proposer instance
//! is tested. Each of the nodes in the simulated network run only one instance
//! of Binary Agreement. This way we only test correctness of the protocol and not
//! message dispatch between multiple proposers.
//! Tests of the Binary Agreement protocol
//!
//! Each of the nodes in the simulated network runs one instance of Binary Agreement. This suffices
//! to test correctness of the protocol.
//!
//! There are three properties that are tested:
//!
//! - Agreement: If any correct node outputs the bit b, then every correct node outputs b.
//! - Agreement: If any correct node outputs the bit `b`, then every correct node outputs `b`.
//!
//! - Termination: If all correct nodes receive input, then every correct node outputs a bit.
//!
//! - Validity: If any correct node outputs b, then at least one correct node received b as input.
//!
//! TODO: Implement adversaries and send BVAL messages at different times.
//! - Validity: If any correct node outputs `b`, then at least one correct node received `b` as
//! input.
extern crate env_logger;
extern crate failure;
extern crate hbbft;
extern crate log;
extern crate integer_sqrt;
extern crate proptest;
extern crate rand;
extern crate rand_derive;
extern crate serde_derive;
extern crate threshold_crypto as crypto;
extern crate threshold_crypto;
mod network;
pub mod net;
use std::iter::once;
use std::sync::Arc;
use std::time;
use log::info;
use rand::Rng;
use proptest::arbitrary::any;
use proptest::{prelude::ProptestConfig, prop_compose, proptest, proptest_helper};
use rand::{Rng, SeedableRng};
use hbbft::binary_agreement::BinaryAgreement;
use hbbft::NetworkInfo;
use hbbft::DistAlgorithm;
use network::{Adversary, MessageScheduler, NodeId, SilentAdversary, TestNetwork, TestNode};
use net::adversary::ReorderingAdversary;
use net::proptest::{gen_seed, NetworkDimension, TestRng, TestRngSeed};
use net::{NetBuilder, NewNodeInfo, VirtualNet};
fn test_binary_agreement<A: Adversary<BinaryAgreement<NodeId, u8>>>(
mut network: TestNetwork<A, BinaryAgreement<NodeId, u8>>,
/// Test configuration for Binary Agreement tests.
#[derive(Debug)]
struct TestConfig {
/// The desired network dimension.
dimension: NetworkDimension,
/// Random number generator to be passed to subsystems.
seed: TestRngSeed,
/// Input to Binary Agreement instances that has the following meaning:
///
/// - `Some(b)`: all instances receive `b` as input.
///
/// - `None`: each instance receives a random `bool` as input.
input: Option<bool>,
) {
let ids: Vec<NodeId> = network.nodes.keys().cloned().collect();
for id in ids {
network.input(id, input.unwrap_or_else(rand::random));
}
}
// Handle messages in random order until all nodes have output the proposed value.
while !network.nodes.values().all(TestNode::terminated) {
network.step();
prop_compose! {
/// Strategy to generate a test configuration.
fn arb_config()
(
dimension in NetworkDimension::range(1, 50),
seed in gen_seed(),
input in any::<Option<bool>>(),
) -> TestConfig
{
TestConfig { dimension, seed, input }
}
// Verify that all instances output the same value.
let mut expected = input;
for node in network.nodes.values() {
if let Some(b) = expected {
assert!(once(&b).eq(node.outputs()));
} else {
assert_eq!(1, node.outputs().len());
expected = Some(node.outputs()[0]);
}
/// Proptest wrapper for `binary_agreement` that runs the test function on generated configurations.
proptest!{
#![proptest_config(ProptestConfig {
cases: 1, .. ProptestConfig::default()
})]
#[test]
#[cfg_attr(feature = "cargo-clippy", allow(unnecessary_operation))]
fn run_binary_agreement(cfg in arb_config()) {
binary_agreement(cfg)
}
}
type NodeId = u16;
type Algo = BinaryAgreement<NodeId, u8>;
impl VirtualNet<Algo> {
fn test_binary_agreement<R>(&mut self, input: Option<bool>, mut rng: R)
where
R: Rng + 'static,
{
let ids: Vec<NodeId> = self.nodes().map(|n| *n.id()).collect();
for id in ids {
let _ = self.send_input(id, input.unwrap_or_else(|| rng.gen::<bool>()));
}
}
assert!(expected.iter().eq(network.observer.outputs()));
}
fn test_binary_agreement_different_sizes<A, F>(new_adversary: F)
where
A: Adversary<BinaryAgreement<NodeId, u8>>,
F: Fn(usize, usize) -> A,
{
// This returns an error in all but the first test.
let _ = env_logger::try_init();
let mut rng = rand::thread_rng();
let sizes = (1..6)
.chain(once(rng.gen_range(6, 20)))
.chain(once(rng.gen_range(30, 50)));
for size in sizes {
let num_faulty_nodes = (size - 1) / 3;
let num_good_nodes = size - num_faulty_nodes;
for &input in &[None, Some(false), Some(true)] {
info!(
"Test start: {} good nodes and {} faulty nodes, input: {:?}",
num_good_nodes, num_faulty_nodes, input
);
let adversary = |_| new_adversary(num_good_nodes, num_faulty_nodes);
let new_ba = |netinfo: Arc<NetworkInfo<NodeId>>| {
BinaryAgreement::new(netinfo, 0).expect("Binary Agreement instance")
};
let network = TestNetwork::new(num_good_nodes, num_faulty_nodes, adversary, new_ba);
test_binary_agreement(network, input);
info!(
"Test success: {} good nodes and {} faulty nodes, input: {:?}",
num_good_nodes, num_faulty_nodes, input
);
// Handle messages in random order until all nodes have output the proposed value.
while !self.nodes().all(|node| node.algorithm().terminated()) {
let _ = self.crank_expect();
}
// Verify that all instances output the same value.
let mut expected = input;
for node in self.nodes() {
if let Some(b) = expected {
assert!(once(&b).eq(node.outputs()));
} else {
assert_eq!(1, node.outputs().len());
expected = Some(node.outputs()[0]);
}
}
// TODO: As soon as observers are added to the test framework, compare the expected output
// against the output of observers.
}
}
#[test]
fn test_binary_agreement_random_silent() {
let new_adversary = |_: usize, _: usize| SilentAdversary::new(MessageScheduler::Random);
test_binary_agreement_different_sizes(new_adversary);
}
#[test]
fn test_binary_agreement_first_silent() {
let new_adversary = |_: usize, _: usize| SilentAdversary::new(MessageScheduler::First);
test_binary_agreement_different_sizes(new_adversary);
/// Tests Binary Agreement on a given configuration.
#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))]
fn binary_agreement(cfg: TestConfig) {
let mut rng: TestRng = TestRng::from_seed(cfg.seed);
let size = cfg.dimension.size();
let num_faulty_nodes = cfg.dimension.faulty();
let num_good_nodes = size - num_faulty_nodes;
println!(
"Test start: {} good nodes and {} faulty nodes, input: {:?}",
num_good_nodes, num_faulty_nodes, cfg.input
);
// Create a network with `size` validators and one observer.
let mut net: VirtualNet<Algo> = NetBuilder::new(0..size as u16)
.num_faulty(num_faulty_nodes as usize)
.message_limit(10_000 * size as usize)
.time_limit(time::Duration::from_secs(30 * size as u64))
.rng(rng.gen::<TestRng>())
.adversary(ReorderingAdversary::new(rng.gen::<TestRng>()))
.using(move |node_info: NewNodeInfo<_>| {
BinaryAgreement::new(Arc::new(node_info.netinfo), 0)
.expect("Failed to create a BinaryAgreement instance.")
}).build()
.expect("Could not construct test network.");
net.test_binary_agreement(cfg.input, rng.gen::<TestRng>());
println!(
"Test success: {} good nodes and {} faulty nodes, input: {:?}",
num_good_nodes, num_faulty_nodes, cfg.input
);
}