Better proptest persistence through deterministic randomness. (#248)

* Add support for RNG instantiation in proptests.

* Use `proptest` module strategy to create the rng for `net_dynamic_honey_badger`.

* Use seed generation instead of RNG instantiation in tests.

* Remove fixed RNG in `generate_map`.

* `VirtualNet` now supports setting the random generator through the builder.

* Add missing `time_limit` field to `::std::fmt::Debug` trait implementation on `NetBuilder`.

* Pass an instantiated random number generator through `NewNodeInfo` as a convenience.

* Make the random number generator of `DynamicHoneyBadgerBuilder` configurable, at the cost of now requiring mutability to call `build_first_node()`.

* Ensure RNGs are derive from passed in seed in `net_dynamic_hb` tests.

* Correct inappropriate use of `random::Random` instead of `Rng::gen` to generate dependent values in `binary_agreement`.

The original implementation used `rand::random()`, which will always use the `thread_rng`, ignoring the fact that an RNG has actually been passed in.

* Do not use `OsRng` but passed in RNG instead.

* Use reference/non-reference passing of rngs more in line with the `rand` crates conventions.

* Document `rng` field on `DynamicHoneyBadger`.

* Make `SyncKeyGen` work with the extend (`encrypt_with_rng`) API of `threshold_crypto`.

* Use passed-in random number generator in `HoneyBadger`.

* Create `SubRng` crate in new `util` module to replace `create_rng()`.

* Use an RNG seeded from the configure RNG when reinitializing `DynamicHoneyBadger`.

* Use the correct branch of `threshold_crypto` with support for passing RNGs.
This commit is contained in:
Marc Brinkmann 2018-10-02 16:24:51 +02:00 committed by GitHub
parent 1d05ad60fb
commit d2627272fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 290 additions and 59 deletions

View File

@ -30,7 +30,7 @@ rand_derive = "0.3.1"
reed-solomon-erasure = "3.1.0"
serde = "1.0.55"
serde_derive = "1.0.55"
threshold_crypto = { git = "https://github.com/poanetwork/threshold_crypto", tag = "0.1.0" }
threshold_crypto = { git = "https://github.com/poanetwork/threshold_crypto", tag = "0.1.0-rng-fix" }
tiny-keccak = "1.4"
[dev-dependencies]
@ -41,7 +41,7 @@ docopt = "1.0"
itertools = "0.7"
serde_derive = "1.0.55"
signifix = "0.9"
proptest = "0.8.6"
proptest = "0.8.7"
[[example]]
name = "consensus-node"

View File

@ -267,8 +267,8 @@ where
F: Fn(NetworkInfo<NodeId>) -> (D, Step<D>),
{
let node_ids = (0..(good_num + adv_num)).map(NodeId);
let netinfos =
NetworkInfo::generate_map(node_ids).expect("Failed to create `NetworkInfo` map");
let netinfos = NetworkInfo::generate_map(node_ids, &mut rand::thread_rng())
.expect("Failed to create `NetworkInfo` map");
let new_node = |(id, netinfo): (NodeId, NetworkInfo<_>)| {
(id, TestNode::new(new_algo(netinfo), hw_quality))
};

View File

@ -139,10 +139,10 @@ impl rand::Rand for MessageContent {
let message_type = *rng.choose(&["sbvb", "conf", "term", "coin"]).unwrap();
match message_type {
"sbvb" => MessageContent::SbvBroadcast(rand::random()),
"conf" => MessageContent::Conf(rand::random()),
"term" => MessageContent::Term(rand::random()),
"coin" => MessageContent::Coin(Box::new(rand::random())),
"sbvb" => MessageContent::SbvBroadcast(rng.gen()),
"conf" => MessageContent::Conf(rng.gen()),
"term" => MessageContent::Term(rng.gen()),
"coin" => MessageContent::Coin(Box::new(rng.gen())),
_ => unreachable!(),
}
}

View File

@ -35,8 +35,8 @@ impl rand::Rand for Message {
let message_type = *rng.choose(&["bval", "aux"]).unwrap();
match message_type {
"bval" => Message::BVal(rand::random()),
"aux" => Message::Aux(rand::random()),
"bval" => Message::BVal(rng.gen()),
"aux" => Message::Aux(rng.gen()),
_ => unreachable!(),
}
}

View File

@ -70,7 +70,7 @@
//! let mut rng = thread_rng();
//!
//! // Create a random set of keys for testing.
//! let netinfos = NetworkInfo::generate_map(0..NUM_NODES)
//! let netinfos = NetworkInfo::generate_map(0..NUM_NODES, &mut rng)
//! .expect("Failed to create `NetworkInfo` map");
//!
//! // Create initial nodes by instantiating a `Broadcast` for each.

View File

@ -11,12 +11,16 @@ use super::{ChangeState, DynamicHoneyBadger, JoinPlan, Result, Step, VoteCounter
use honey_badger::HoneyBadger;
use messaging::NetworkInfo;
use traits::{Contribution, NodeIdT};
use util::SubRng;
/// A Dynamic Honey Badger builder, to configure the parameters and create new instances of
/// `DynamicHoneyBadger`.
pub struct DynamicHoneyBadgerBuilder<C, N> {
/// The maximum number of future epochs for which we handle messages simultaneously.
max_future_epochs: usize,
/// Random number generator passed on to algorithm instance for key generation. Also used to
/// instantiate `HoneyBadger`.
rng: Box<dyn rand::Rng>,
_phantom: PhantomData<(C, N)>,
}
@ -25,6 +29,7 @@ impl<C, N> Default for DynamicHoneyBadgerBuilder<C, N> {
// TODO: Use the defaults from `HoneyBadgerBuilder`.
DynamicHoneyBadgerBuilder {
max_future_epochs: 3,
rng: Box::new(rand::thread_rng()),
_phantom: PhantomData,
}
}
@ -47,11 +52,21 @@ where
self
}
/// Sets the random number generator to be used to instantiate cryptographic structures.
pub fn rng<R: rand::Rng + 'static>(&mut self, rng: R) -> &mut Self {
self.rng = Box::new(rng);
self
}
/// Creates a new Dynamic Honey Badger instance with an empty buffer.
pub fn build(&self, netinfo: NetworkInfo<N>) -> Result<(DynamicHoneyBadger<C, N>, Step<C, N>)> {
pub fn build(
&mut self,
netinfo: NetworkInfo<N>,
) -> Result<(DynamicHoneyBadger<C, N>, Step<C, N>)> {
let arc_netinfo = Arc::new(netinfo.clone());
let (honey_badger, hb_step) = HoneyBadger::builder(arc_netinfo.clone())
.max_future_epochs(self.max_future_epochs)
.rng(self.rng.sub_rng())
.build();
let mut dhb = DynamicHoneyBadger {
netinfo,
@ -62,18 +77,21 @@ where
honey_badger,
key_gen_state: None,
incoming_queue: Vec::new(),
rng: Box::new(self.rng.sub_rng()),
};
let step = dhb.process_output(hb_step)?;
Ok((dhb, step))
}
/// Creates a new `DynamicHoneyBadger` configured to start a new network as a single validator.
pub fn build_first_node(&self, our_id: N) -> Result<(DynamicHoneyBadger<C, N>, Step<C, N>)> {
let mut rng = rand::thread_rng();
let sk_set = SecretKeySet::random(0, &mut rng)?;
pub fn build_first_node(
&mut self,
our_id: N,
) -> Result<(DynamicHoneyBadger<C, N>, Step<C, N>)> {
let sk_set = SecretKeySet::random(0, &mut self.rng)?;
let pk_set = sk_set.public_keys();
let sks = sk_set.secret_key_share(0)?;
let sk: SecretKey = rng.gen();
let sk: SecretKey = self.rng.gen();
let pub_keys = once((our_id.clone(), sk.public_key())).collect();
let netinfo = NetworkInfo::new(our_id, sks, pk_set, sk, pub_keys);
self.build(netinfo)
@ -82,7 +100,7 @@ where
/// Creates a new `DynamicHoneyBadger` configured to join the network at the epoch specified in
/// the `JoinPlan`.
pub fn build_joining(
&self,
&mut self,
our_id: N,
secret_key: SecretKey,
join_plan: JoinPlan<N>,
@ -108,6 +126,7 @@ where
honey_badger,
key_gen_state: None,
incoming_queue: Vec::new(),
rng: Box::new(self.rng.sub_rng()),
};
let mut step = dhb.process_output(hb_step)?;
match join_plan.change {

View File

@ -1,9 +1,10 @@
use rand::Rand;
use std::mem;
use std::sync::Arc;
use std::{fmt, mem};
use bincode;
use crypto::Signature;
use rand;
use serde::{Deserialize, Serialize};
use super::votes::{SignedVote, VoteCounter};
@ -16,9 +17,9 @@ use honey_badger::{self, HoneyBadger, Message as HbMessage};
use messaging::{DistAlgorithm, NetworkInfo, Target};
use sync_key_gen::{Ack, Part, PartOutcome, SyncKeyGen};
use traits::{Contribution, NodeIdT};
use util::SubRng;
/// A Honey Badger instance that can handle adding and removing nodes.
#[derive(Debug)]
pub struct DynamicHoneyBadger<C, N: Rand> {
/// Shared network data.
pub(super) netinfo: NetworkInfo<N>,
@ -36,6 +37,29 @@ pub struct DynamicHoneyBadger<C, N: Rand> {
pub(super) key_gen_state: Option<KeyGenState<N>>,
/// A queue for messages from future epochs that cannot be handled yet.
pub(super) incoming_queue: Vec<(N, Message<N>)>,
/// A random number generator used for secret key generation.
// Boxed to avoid overloading the algorithm's type with more generics.
pub(super) rng: Box<dyn rand::Rng>,
}
impl<C, N> fmt::Debug for DynamicHoneyBadger<C, N>
where
C: fmt::Debug,
N: Rand + fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("DynamicHoneyBadger")
.field("netinfo", &self.netinfo)
.field("max_future_epochs", &self.max_future_epochs)
.field("start_epoch", &self.start_epoch)
.field("vote_counter", &self.vote_counter)
.field("key_gen_msg_buffer", &self.key_gen_msg_buffer)
.field("honey_badger", &self.honey_badger)
.field("key_gen_state", &self.key_gen_state)
.field("incoming_queue", &self.incoming_queue)
.field("rng", &"<RNG>")
.finish()
}
}
impl<C, N> DistAlgorithm for DynamicHoneyBadger<C, N>
@ -312,7 +336,7 @@ where
let threshold = (pub_keys.len() - 1) / 3;
let sk = self.netinfo.secret_key().clone();
let our_id = self.our_id().clone();
let (key_gen, part) = SyncKeyGen::new(our_id, sk, pub_keys, threshold)?;
let (key_gen, part) = SyncKeyGen::new(&mut self.rng, our_id, sk, pub_keys, threshold)?;
self.key_gen_state = Some(KeyGenState::new(key_gen, change.clone()));
if let Some(part) = part {
let step_on_send = self.send_transaction(KeyGenMessage::Part(part))?;
@ -330,6 +354,7 @@ where
mem::replace(&mut self.vote_counter, counter);
let (hb, hb_step) = HoneyBadger::builder(netinfo)
.max_future_epochs(self.max_future_epochs)
.rng(self.rng.sub_rng())
.build();
self.honey_badger = hb;
self.process_output(hb_step)
@ -337,8 +362,17 @@ where
/// Handles a `Part` message that was output by Honey Badger.
fn handle_part(&mut self, sender_id: &N, part: Part) -> Result<Step<C, N>> {
let handle = |kgs: &mut KeyGenState<N>| kgs.key_gen.handle_part(&sender_id, part);
match self.key_gen_state.as_mut().and_then(handle) {
// Awkward construction due to borrowck shortcomings; we need to borrow two struct fields
// mutably and have the borrow end early enough for us to call `send_transaction`.
// FIXME: This part should be cleaned up and restructured.
let res = {
let kgs = self.key_gen_state.as_mut();
let rng = &mut self.rng;
let handle = |kgs: &mut KeyGenState<N>| kgs.key_gen.handle_part(rng, &sender_id, part);
kgs.and_then(handle)
};
match res {
Some(PartOutcome::Valid(ack)) => self.send_transaction(KeyGenMessage::Ack(ack)),
Some(PartOutcome::Invalid(fault_log)) => Ok(fault_log.into()),
None => Ok(Step::default()),

View File

@ -188,6 +188,8 @@ impl<N> SignedVote<N> {
mod tests {
use std::sync::Arc;
use rand;
use super::{Change, SignedVote, VoteCounter};
use fault_log::{FaultKind, FaultLog};
use messaging::NetworkInfo;
@ -199,8 +201,8 @@ mod tests {
/// order.
fn setup(node_num: usize, era: u64) -> (Vec<VoteCounter<usize>>, Vec<Vec<SignedVote<usize>>>) {
// Create keys for threshold cryptography.
let netinfos =
NetworkInfo::generate_map(0..node_num).expect("Failed to generate `NetworkInfo` map");
let netinfos = NetworkInfo::generate_map(0..node_num, &mut rand::thread_rng())
.expect("Failed to generate `NetworkInfo` map");
// Create a `VoteCounter` instance for each node.
let create_counter =

View File

@ -2,12 +2,13 @@ use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::sync::Arc;
use rand::Rand;
use rand::{self, Rand, Rng};
use serde::{Deserialize, Serialize};
use super::{HoneyBadger, Message, Step};
use messaging::{NetworkInfo, Target};
use traits::{Contribution, NodeIdT};
use util::SubRng;
/// A Honey Badger builder, to configure the parameters and create new instances of `HoneyBadger`.
pub struct HoneyBadgerBuilder<C, N>
@ -18,6 +19,8 @@ where
netinfo: Arc<NetworkInfo<N>>,
/// The maximum number of future epochs for which we handle messages simultaneously.
max_future_epochs: usize,
/// Random number generator passed on to algorithm instance for signing and encrypting.
rng: Box<dyn Rng>,
_phantom: PhantomData<C>,
}
@ -32,10 +35,17 @@ where
HoneyBadgerBuilder {
netinfo,
max_future_epochs: 3,
rng: Box::new(rand::thread_rng()),
_phantom: PhantomData,
}
}
/// Sets the random number generator for the public key cryptography.
pub fn rng<R: Rng + 'static>(&mut self, rng: R) -> &mut Self {
self.rng = Box::new(rng);
self
}
/// Sets the maximum number of future epochs for which we handle messages simultaneously.
pub fn max_future_epochs(&mut self, max_future_epochs: usize) -> &mut Self {
self.max_future_epochs = max_future_epochs;
@ -44,7 +54,7 @@ where
/// Creates a new Honey Badger instance in epoch 0 and makes the initial `Step` on that
/// instance.
pub fn build(&self) -> (HoneyBadger<C, N>, Step<C, N>) {
pub fn build(&mut self) -> (HoneyBadger<C, N>, Step<C, N>) {
let hb = HoneyBadger {
netinfo: self.netinfo.clone(),
epoch: 0,
@ -53,6 +63,7 @@ where
max_future_epochs: self.max_future_epochs as u64,
incoming_queue: BTreeMap::new(),
remote_epochs: BTreeMap::new(),
rng: Box::new(self.rng.sub_rng()),
};
let step = if self.netinfo.is_validator() {
// The first message in an epoch announces the epoch transition.

View File

@ -1,9 +1,10 @@
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::fmt;
use std::sync::Arc;
use bincode;
use rand::Rand;
use rand::{Rand, Rng};
use serde::{Deserialize, Serialize};
use super::epoch_state::EpochState;
@ -12,7 +13,6 @@ use messaging::{self, DistAlgorithm, NetworkInfo, Target};
use traits::{Contribution, NodeIdT};
/// An instance of the Honey Badger Byzantine fault tolerant consensus algorithm.
#[derive(Debug)]
pub struct HoneyBadger<C, N: Rand> {
/// Shared network data.
pub(super) netinfo: Arc<NetworkInfo<N>>,
@ -28,6 +28,28 @@ pub struct HoneyBadger<C, N: Rand> {
pub(super) incoming_queue: BTreeMap<u64, Vec<(N, MessageContent<N>)>>,
/// Known current epochs of remote nodes.
pub(super) remote_epochs: BTreeMap<N, u64>,
/// A random number generator used for secret key generation.
// Boxed to avoid overloading the algorithm's type with more generics.
pub(super) rng: Box<dyn Rng>,
}
impl<C, N> fmt::Debug for HoneyBadger<C, N>
where
N: Rand + fmt::Debug,
C: fmt::Debug,
{
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("HoneyBadger")
.field("netinfo", &self.netinfo)
.field("epoch", &self.epoch)
.field("has_input", &self.has_input)
.field("epochs", &self.epochs)
.field("max_future_epochs", &self.max_future_epochs)
.field("incoming_queue", &self.incoming_queue)
.field("remote_epochs", &self.remote_epochs)
.field("rng", &"<RNG>")
.finish()
}
}
pub type Step<C, N> = messaging::Step<HoneyBadger<C, N>>;
@ -79,7 +101,11 @@ where
self.has_input = true;
let ser_prop =
bincode::serialize(&proposal).map_err(|err| ErrorKind::ProposeBincode(*err))?;
let ciphertext = self.netinfo.public_key_set().public_key().encrypt(ser_prop);
let ciphertext = self
.netinfo
.public_key_set()
.public_key()
.encrypt_with_rng(&mut self.rng, ser_prop);
let epoch = self.epoch;
let mut step = self.epoch_state_mut(epoch)?.propose(&ciphertext)?;
step.extend(self.try_output_batches()?);

View File

@ -149,6 +149,7 @@ pub mod subset;
pub mod sync_key_gen;
pub mod threshold_decryption;
pub mod transaction_queue;
pub mod util;
/// Common supertraits.
pub mod traits {

View File

@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::iter::once;
use failure::Fail;
use rand;
use crypto::{self, PublicKey, PublicKeySet, PublicKeyShare, SecretKey, SecretKeyShare};
use fault_log::{Fault, FaultLog};
@ -357,21 +358,21 @@ impl<N: NodeIdT> NetworkInfo<N> {
}
/// Generates a map of matching `NetworkInfo`s for testing.
pub fn generate_map<I>(ids: I) -> Result<BTreeMap<N, NetworkInfo<N>>, crypto::error::Error>
pub fn generate_map<I, R>(
ids: I,
rng: &mut R,
) -> Result<BTreeMap<N, NetworkInfo<N>>, crypto::error::Error>
where
I: IntoIterator<Item = N>,
R: rand::Rng,
{
use rand::{self, Rng};
use crypto::SecretKeySet;
let mut rng = rand::thread_rng();
let all_ids: BTreeSet<N> = ids.into_iter().collect();
let num_faulty = (all_ids.len() - 1) / 3;
// Generate the keys for threshold cryptography.
let sk_set = SecretKeySet::random(num_faulty, &mut rng)?;
let sk_set = SecretKeySet::random(num_faulty, rng)?;
let pk_set = sk_set.public_keys();
// Generate keys for individually signing and encrypting messages.

View File

@ -59,6 +59,9 @@
//! use threshold_crypto::{PublicKey, SecretKey, SignatureShare};
//! use hbbft::sync_key_gen::{PartOutcome, SyncKeyGen};
//!
//! // Use a default random number generator for any randomness:
//! let mut rng = rand::thread_rng();
//!
//! // Two out of four shares will suffice to sign or encrypt something.
//! let (threshold, node_num) = (1, 4);
//!
@ -75,7 +78,7 @@
//! let mut nodes = BTreeMap::new();
//! let mut parts = Vec::new();
//! for (id, sk) in sec_keys.into_iter().enumerate() {
//! let (sync_key_gen, opt_part) = SyncKeyGen::new(id, sk, pub_keys.clone(), threshold)
//! let (sync_key_gen, opt_part) = SyncKeyGen::new(&mut rng, id, sk, pub_keys.clone(), threshold)
//! .unwrap_or_else(|_| panic!("Failed to create `SyncKeyGen` instance for node #{}", id));
//! nodes.insert(id, sync_key_gen);
//! parts.push((id, opt_part.unwrap())); // Would be `None` for observer nodes.
@ -85,7 +88,7 @@
//! let mut acks = Vec::new();
//! for (sender_id, part) in parts {
//! for (&id, node) in &mut nodes {
//! match node.handle_part(&sender_id, part.clone()) {
//! match node.handle_part(&mut rng, &sender_id, part.clone()) {
//! Some(PartOutcome::Valid(ack)) => acks.push((id, ack)),
//! Some(PartOutcome::Invalid(faults)) => panic!("Invalid part: {:?}", faults),
//! None => panic!("We are not an observer, so we should send Ack."),
@ -169,7 +172,7 @@ use crypto::{
};
use pairing::bls12_381::{Fr, G1Affine};
use pairing::{CurveAffine, Field};
use rand::OsRng;
use rand;
use fault_log::{AckMessageFault as Fault, FaultKind, FaultLog};
use messaging::NetworkInfo;
@ -287,7 +290,8 @@ impl<N: NodeIdT> SyncKeyGen<N> {
///
/// If we are not a validator but only an observer, no `Part` message is produced and no
/// messages need to be sent.
pub fn new(
pub fn new<R: rand::Rng>(
rng: &mut R,
our_id: N,
sec_key: SecretKey,
pub_keys: BTreeMap<N, PublicKey>,
@ -308,13 +312,13 @@ impl<N: NodeIdT> SyncKeyGen<N> {
if our_idx.is_none() {
return Ok((key_gen, None)); // No part: we are an observer.
}
let mut rng = OsRng::new().expect("OS random number generator");
let our_part = BivarPoly::random(threshold, &mut rng).map_err(Error::Creation)?;
let our_part = BivarPoly::random(threshold, rng).map_err(Error::Creation)?;
let commit = our_part.commitment();
let encrypt = |(i, pk): (usize, &PublicKey)| {
let row = our_part.row(i + 1).map_err(Error::Creation)?;
let bytes = bincode::serialize(&row).expect("failed to serialize row");
Ok(pk.encrypt(&bytes))
Ok(pk.encrypt_with_rng(rng, &bytes))
};
let rows = key_gen
.pub_keys
@ -331,8 +335,9 @@ impl<N: NodeIdT> SyncKeyGen<N> {
///
/// All participating nodes must handle the exact same sequence of messages.
/// Note that `handle_part` also needs to explicitly be called with this instance's own `Part`.
pub fn handle_part(
pub fn handle_part<R: rand::Rng>(
&mut self,
rng: &mut R,
sender_id: &N,
Part(commit, rows): Part,
) -> Option<PartOutcome<N>> {
@ -369,7 +374,7 @@ impl<N: NodeIdT> SyncKeyGen<N> {
let wrap = FieldWrap::new(val);
// TODO: Handle errors.
let ser_val = bincode::serialize(&wrap).expect("failed to serialize value");
pk.encrypt(ser_val)
pk.encrypt_with_rng(rng, ser_val)
};
let values = self.pub_keys.values().enumerate().map(encrypt).collect();
Some(PartOutcome::Valid(Ack(sender_idx, values)))

24
src/util.rs Normal file
View File

@ -0,0 +1,24 @@
//! Utility functions
//!
//! Functions not large enough to warrant their own crate or module, but flexible enough to be used
//! in multiple disjunct places in the library. May also contain backports, workarounds.
use rand;
/// Workaround trait for creating new random number generators
pub trait SubRng {
fn sub_rng(&mut self) -> Box<dyn rand::Rng>;
}
impl<R> SubRng for R
where
R: rand::Rng,
{
fn sub_rng(&mut self) -> Box<dyn rand::Rng> {
// Currently hard-coded to be an `Isaac64Rng`, until better options emerge. This is either
// dependant on `rand` 0.5 support or an API re-design of parts of `threshold_crypto` and
// `hbbft`.
let rng = self.gen::<rand::isaac::Isaac64Rng>();
Box::new(rng)
}
}

View File

@ -140,6 +140,32 @@ If the time limit has been reached, `crank` will return a `TimeLimitHit` error.
It's also possible to run tests without a time-limit on a per-run basis by setting the `HBBFT_NO_TIME_LIMIT` environment variable to "true".
### Randomness
The test framework supports deterministic randomness by allowing all random values to be dervied from a single random generator. By seeding this generator with a fixed value, tests can be reproduced precisely:
```rust
#[test]
fn do_example_test(seed: TestRngSeed, dimension: NetworkDimension) {
// Instantiates a new random number generator for the test.
let mut rng: TestRng = TestRng::from_seed(cfg.seed);
let mut net = NetBuilder::new(0..dimension.size)
.num_faulty(cfg.dimension.faulty)
// Setting `rng` ensures that randomness will only be retrieved by
// `VirtualNet` from the `TestRng` instantiated above.
.rng(rng)
.using_step(move |node: NewNodeInfo<_>| {
DynamicHoneyBadger::builder()
// `DynamicHoneyBadger` can also be configured with a random
// number generator. `NewNodeInfo` includes a convenience
// field that contains a ready-to-use random number generator:
.rng(node.rng)
// ...
})
// ...
}
### Property based testing
Many higher-level tests allow for a variety of different input parameters like the number of nodes in a network or the amount of faulty ones among them. Other possible parameters include transaction, batch or contribution sizes. To test a variety of randomized combinations of these, the [proptest](https://docs.rs/proptest) crate should be used.

View File

@ -79,7 +79,7 @@ impl Adversary<Broadcast<NodeId>> for ProposeAdversary {
// FIXME: Take the correct, known keys from the network.
let netinfo = Arc::new(
NetworkInfo::generate_map(node_ids)
NetworkInfo::generate_map(node_ids, &mut rand::thread_rng())
.expect("Failed to create `NetworkInfo` map")
.remove(&id)
.unwrap(),

View File

@ -22,10 +22,11 @@ use std::io::Write;
use std::{cmp, collections, env, fmt, fs, io, ops, process, time};
use rand;
use rand::Rand;
use rand::{Rand, Rng};
use threshold_crypto as crypto;
use hbbft::messaging::{self, DistAlgorithm, NetworkInfo, Step};
use hbbft::util::SubRng;
pub use self::adversary::Adversary;
pub use self::err::CrankError;
@ -234,7 +235,6 @@ where
/// New network node construction information.
///
/// Helper structure passed to node constructors when building virtual networks.
#[derive(Debug)]
pub struct NewNodeInfo<D>
where
D: DistAlgorithm,
@ -245,6 +245,28 @@ where
pub netinfo: NetworkInfo<D::NodeId>,
/// Whether or not the node is marked faulty.
pub faulty: bool,
/// An initialized random number generated for exclusive use by the node.
///
/// Can be ignored, but usually comes in handy with algorithms that require additional
/// randomness for instantiation or operation.
///
/// Note that the random number generator type may differ from the one set for generation on
/// the `VirtualNet`, due to limitations of the `rand` crates API.
pub rng: Box<dyn rand::Rng>,
}
impl<D> fmt::Debug for NewNodeInfo<D>
where
D: DistAlgorithm,
{
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("NewNodeInfo")
.field("id", &self.id)
.field("netinfo", &self.netinfo)
.field("faulty", &self.faulty)
.field("rng", &"<RNG>")
.finish()
}
}
/// Virtual network builder.
@ -274,6 +296,8 @@ where
message_limit: Option<usize>,
/// Optional time limit.
time_limit: Option<time::Duration>,
/// Random number generator used to generate keys.
rng: Option<Box<dyn Rng>>,
}
impl<D, I> fmt::Debug for NetBuilder<D, I>
@ -289,6 +313,8 @@ where
.field("trace", &self.trace)
.field("crank_limit", &self.crank_limit)
.field("message_limit", &self.message_limit)
.field("time_limit", &self.time_limit)
.field("rng", &"<RNG>")
.finish()
}
}
@ -319,6 +345,7 @@ where
crank_limit: None,
message_limit: None,
time_limit: DEFAULT_TIME_LIMIT,
rng: None,
}
}
@ -374,6 +401,20 @@ where
self
}
/// Random number generator.
///
/// Overrides the random number generator used. If not specified, a `thread_rng` will be
/// used on construction.
///
/// The passed in generator is used for key generation.
pub fn rng<R>(mut self, rng: R) -> Self
where
R: Rng + 'static,
{
self.rng = Some(Box::new(rng));
self
}
/// Time limit.
///
/// Sets the time limit; `crank` will fail if called after this much time as elapsed since
@ -427,6 +468,8 @@ where
/// If the total number of nodes is not `> 3 * num_faulty`, construction will panic.
#[inline]
pub fn build(self) -> Result<VirtualNet<D>, crypto::error::Error> {
let rng: Box<dyn Rng> = self.rng.unwrap_or_else(|| Box::new(rand::thread_rng()));
// The time limit can be overriden through environment variables:
let override_time_limit = env::var("HBBFT_NO_TIME_LIMIT")
// We fail early, to avoid tricking the user into thinking that they have set the time
@ -448,7 +491,7 @@ where
// Note: Closure is not redundant, won't compile without it.
#[cfg_attr(feature = "cargo-clippy", allow(redundant_closure))]
let mut net = VirtualNet::new(self.node_ids, self.num_faulty, move |node| cons(node))?;
let mut net = VirtualNet::new(self.node_ids, self.num_faulty, rng, move |node| cons(node))?;
if self.adversary.is_some() {
net.adversary = self.adversary;
@ -647,13 +690,19 @@ where
///
/// The total number of nodes, that is `node_ids.count()` must be `> 3 * faulty`, otherwise
/// the construction function will panic.
fn new<F, I>(node_ids: I, faulty: usize, cons: F) -> Result<Self, crypto::error::Error>
fn new<F, I, R>(
node_ids: I,
faulty: usize,
mut rng: R,
cons: F,
) -> Result<Self, crypto::error::Error>
where
F: Fn(NewNodeInfo<D>) -> (D, Step<D>),
I: IntoIterator<Item = D::NodeId>,
R: rand::Rng,
{
// Generate a new set of cryptographic keys for threshold cryptography.
let net_infos = messaging::NetworkInfo::generate_map(node_ids)?;
let net_infos = messaging::NetworkInfo::generate_map(node_ids, &mut rng)?;
assert!(
faulty * 3 < net_infos.len(),
@ -668,10 +717,12 @@ where
.enumerate()
.map(|(idx, (id, netinfo))| {
let is_faulty = idx < faulty;
let (algorithm, step) = cons(NewNodeInfo {
id: id.clone(),
netinfo,
faulty: is_faulty,
rng: rng.sub_rng(),
});
steps.insert(id.clone(), step);
(id, Node::new(algorithm, is_faulty))

View File

@ -3,9 +3,31 @@
//! This module houses strategies to generate (and reduce/expand) various `hbbft` and `net` related
//! structures.
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::XorShiftRng;
/// Seed type of the random number generator used in testing.
// Note: In `rand` 0.5, this is an associated type of the `SeedableRng` trait, but for 0.4 and below
// we still need to alias this type.
pub type TestRngSeed = [u32; 4];
/// 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.
///

View File

@ -11,9 +11,10 @@ use std::collections;
use hbbft::dynamic_honey_badger::{Change, ChangeState, DynamicHoneyBadger, Input};
use hbbft::messaging::DistAlgorithm;
use net::proptest::NetworkDimension;
use net::proptest::{gen_seed, NetworkDimension, TestRng, TestRngSeed};
use net::NetBuilder;
use proptest::prelude::ProptestConfig;
use rand::{Rng, SeedableRng};
/// Choose a node's contribution for an epoch.
///
@ -53,6 +54,8 @@ struct TestConfig {
batch_size: usize,
/// Individual nodes contribution size.
contribution_size: usize,
/// Random number generator to be passed to subsystems.
seed: TestRngSeed,
}
prop_compose! {
@ -61,10 +64,11 @@ prop_compose! {
(dimension in NetworkDimension::range(3, 15),
total_txs in 20..60usize,
batch_size in 10..20usize,
contribution_size in 1..10usize)
contribution_size in 1..10usize,
seed in gen_seed())
-> TestConfig {
TestConfig{
dimension, total_txs, batch_size, contribution_size,
dimension, total_txs, batch_size, contribution_size, seed
}
}
}
@ -85,15 +89,17 @@ proptest!{
/// running a regular honey badger network.
#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))]
fn do_drop_and_readd(cfg: TestConfig) {
let mut rng = rand::thread_rng();
let mut rng: TestRng = TestRng::from_seed(cfg.seed);
// First, we create a new test network with Honey Badger instances.
let mut net = NetBuilder::new(0..cfg.dimension.size)
.num_faulty(cfg.dimension.faulty)
.message_limit(200_000) // Limited to 200k messages for now.
.rng(rng.gen::<TestRng>()) // Ensure runs are reproducible.
.using_step(move |node| {
println!("Constructing new dynamic honey badger node #{}", node.id);
DynamicHoneyBadger::builder()
.rng(node.rng)
.build(node.netinfo)
.expect("cannot build instance")
}).build()

View File

@ -399,8 +399,8 @@ where
{
let mut rng = rand::thread_rng();
let node_ids = (0..(good_num + adv_num)).map(NodeId);
let mut netinfos =
NetworkInfo::generate_map(node_ids).expect("Failed to generate `NetworkInfo` map");
let mut netinfos = NetworkInfo::generate_map(node_ids, &mut rng)
.expect("Failed to generate `NetworkInfo` map");
let obs_netinfo = {
let node_ni = netinfos.values().next().unwrap();
NetworkInfo::new(

View File

@ -28,8 +28,11 @@ fn test_sync_key_gen_with(threshold: usize, node_num: usize) {
.into_iter()
.enumerate()
.map(|(id, sk)| {
let (sync_key_gen, proposal) = SyncKeyGen::new(id, sk, pub_keys.clone(), threshold)
.unwrap_or_else(|_err| panic!("Failed to create `SyncKeyGen` instance #{}", id));
let (sync_key_gen, proposal) =
SyncKeyGen::new(&mut rand::thread_rng(), id, sk, pub_keys.clone(), threshold)
.unwrap_or_else(|_err| {
panic!("Failed to create `SyncKeyGen` instance #{}", id)
});
nodes.push(sync_key_gen);
proposal
}).collect();
@ -39,7 +42,7 @@ fn test_sync_key_gen_with(threshold: usize, node_num: usize) {
for (sender_id, proposal) in proposals[..=threshold].iter().enumerate() {
for (node_id, node) in nodes.iter_mut().enumerate() {
let proposal = proposal.clone().expect("proposal");
let ack = match node.handle_part(&sender_id, proposal) {
let ack = match node.handle_part(&mut rand::thread_rng(), &sender_id, proposal) {
Some(PartOutcome::Valid(ack)) => ack,
_ => panic!("invalid proposal"),
};