From d2627272fe4b9dea1c81a90f34725f43eeccaa68 Mon Sep 17 00:00:00 2001 From: Marc Brinkmann Date: Tue, 2 Oct 2018 16:24:51 +0200 Subject: [PATCH] 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. --- Cargo.toml | 4 +- examples/simulation.rs | 4 +- src/binary_agreement/mod.rs | 8 +-- src/binary_agreement/sbv_broadcast.rs | 4 +- src/broadcast/mod.rs | 2 +- src/dynamic_honey_badger/builder.rs | 31 ++++++++-- .../dynamic_honey_badger.rs | 44 +++++++++++-- src/dynamic_honey_badger/votes.rs | 6 +- src/honey_badger/builder.rs | 15 ++++- src/honey_badger/honey_badger.rs | 32 +++++++++- src/lib.rs | 1 + src/messaging.rs | 13 ++-- src/sync_key_gen.rs | 23 ++++--- src/util.rs | 24 ++++++++ tests/README.md | 26 ++++++++ tests/broadcast.rs | 2 +- tests/net/mod.rs | 61 +++++++++++++++++-- tests/net/proptest.rs | 22 +++++++ tests/net_dynamic_hb.rs | 14 +++-- tests/network/mod.rs | 4 +- tests/sync_key_gen.rs | 9 ++- 21 files changed, 290 insertions(+), 59 deletions(-) create mode 100644 src/util.rs diff --git a/Cargo.toml b/Cargo.toml index 2af1dfd..db6fa47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/simulation.rs b/examples/simulation.rs index 66b62d8..a05ba9e 100644 --- a/examples/simulation.rs +++ b/examples/simulation.rs @@ -267,8 +267,8 @@ where F: Fn(NetworkInfo) -> (D, Step), { 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)) }; diff --git a/src/binary_agreement/mod.rs b/src/binary_agreement/mod.rs index fdbc62c..9f79625 100644 --- a/src/binary_agreement/mod.rs +++ b/src/binary_agreement/mod.rs @@ -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!(), } } diff --git a/src/binary_agreement/sbv_broadcast.rs b/src/binary_agreement/sbv_broadcast.rs index 39e1b7a..f37caee 100644 --- a/src/binary_agreement/sbv_broadcast.rs +++ b/src/binary_agreement/sbv_broadcast.rs @@ -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!(), } } diff --git a/src/broadcast/mod.rs b/src/broadcast/mod.rs index 315fad2..2cbe33a 100644 --- a/src/broadcast/mod.rs +++ b/src/broadcast/mod.rs @@ -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. diff --git a/src/dynamic_honey_badger/builder.rs b/src/dynamic_honey_badger/builder.rs index 5e70f43..7dc681f 100644 --- a/src/dynamic_honey_badger/builder.rs +++ b/src/dynamic_honey_badger/builder.rs @@ -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 { /// 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, _phantom: PhantomData<(C, N)>, } @@ -25,6 +29,7 @@ impl Default for DynamicHoneyBadgerBuilder { // 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(&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) -> Result<(DynamicHoneyBadger, Step)> { + pub fn build( + &mut self, + netinfo: NetworkInfo, + ) -> Result<(DynamicHoneyBadger, Step)> { 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, Step)> { - 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, Step)> { + 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, @@ -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 { diff --git a/src/dynamic_honey_badger/dynamic_honey_badger.rs b/src/dynamic_honey_badger/dynamic_honey_badger.rs index 2cc0b82..23f990f 100644 --- a/src/dynamic_honey_badger/dynamic_honey_badger.rs +++ b/src/dynamic_honey_badger/dynamic_honey_badger.rs @@ -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 { /// Shared network data. pub(super) netinfo: NetworkInfo, @@ -36,6 +37,29 @@ pub struct DynamicHoneyBadger { pub(super) key_gen_state: Option>, /// A queue for messages from future epochs that cannot be handled yet. pub(super) incoming_queue: Vec<(N, Message)>, + /// A random number generator used for secret key generation. + // Boxed to avoid overloading the algorithm's type with more generics. + pub(super) rng: Box, +} + +impl fmt::Debug for DynamicHoneyBadger +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", &"") + .finish() + } } impl DistAlgorithm for DynamicHoneyBadger @@ -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> { - let handle = |kgs: &mut KeyGenState| 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| 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()), diff --git a/src/dynamic_honey_badger/votes.rs b/src/dynamic_honey_badger/votes.rs index 537ce57..c70fa3f 100644 --- a/src/dynamic_honey_badger/votes.rs +++ b/src/dynamic_honey_badger/votes.rs @@ -188,6 +188,8 @@ impl SignedVote { 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>, Vec>>) { // 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 = diff --git a/src/honey_badger/builder.rs b/src/honey_badger/builder.rs index cc20964..3518959 100644 --- a/src/honey_badger/builder.rs +++ b/src/honey_badger/builder.rs @@ -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 @@ -18,6 +19,8 @@ where netinfo: Arc>, /// 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, _phantom: PhantomData, } @@ -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(&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, Step) { + pub fn build(&mut self) -> (HoneyBadger, Step) { 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. diff --git a/src/honey_badger/honey_badger.rs b/src/honey_badger/honey_badger.rs index 6204cc6..a2462dd 100644 --- a/src/honey_badger/honey_badger.rs +++ b/src/honey_badger/honey_badger.rs @@ -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 { /// Shared network data. pub(super) netinfo: Arc>, @@ -28,6 +28,28 @@ pub struct HoneyBadger { pub(super) incoming_queue: BTreeMap)>>, /// Known current epochs of remote nodes. pub(super) remote_epochs: BTreeMap, + /// A random number generator used for secret key generation. + // Boxed to avoid overloading the algorithm's type with more generics. + pub(super) rng: Box, +} + +impl fmt::Debug for HoneyBadger +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", &"") + .finish() + } } pub type Step = messaging::Step>; @@ -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()?); diff --git a/src/lib.rs b/src/lib.rs index b3dc9fe..c6c76d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { diff --git a/src/messaging.rs b/src/messaging.rs index 1bd5ddf..c2af99a 100644 --- a/src/messaging.rs +++ b/src/messaging.rs @@ -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 NetworkInfo { } /// Generates a map of matching `NetworkInfo`s for testing. - pub fn generate_map(ids: I) -> Result>, crypto::error::Error> + pub fn generate_map( + ids: I, + rng: &mut R, + ) -> Result>, crypto::error::Error> where I: IntoIterator, + R: rand::Rng, { - use rand::{self, Rng}; - use crypto::SecretKeySet; - let mut rng = rand::thread_rng(); - let all_ids: BTreeSet = 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. diff --git a/src/sync_key_gen.rs b/src/sync_key_gen.rs index 043c2fb..4c8ed40 100644 --- a/src/sync_key_gen.rs +++ b/src/sync_key_gen.rs @@ -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 SyncKeyGen { /// /// 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( + rng: &mut R, our_id: N, sec_key: SecretKey, pub_keys: BTreeMap, @@ -308,13 +312,13 @@ impl SyncKeyGen { 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 SyncKeyGen { /// /// 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( &mut self, + rng: &mut R, sender_id: &N, Part(commit, rows): Part, ) -> Option> { @@ -369,7 +374,7 @@ impl SyncKeyGen { 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))) diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6dee245 --- /dev/null +++ b/src/util.rs @@ -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; +} + +impl SubRng for R +where + R: rand::Rng, +{ + fn sub_rng(&mut self) -> Box { + // 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::(); + Box::new(rng) + } +} diff --git a/tests/README.md b/tests/README.md index f12a230..bf8b639 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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. diff --git a/tests/broadcast.rs b/tests/broadcast.rs index 31c1ecf..4a0ea66 100644 --- a/tests/broadcast.rs +++ b/tests/broadcast.rs @@ -79,7 +79,7 @@ impl Adversary> 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(), diff --git a/tests/net/mod.rs b/tests/net/mod.rs index 325cba1..bb70ba8 100644 --- a/tests/net/mod.rs +++ b/tests/net/mod.rs @@ -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 where D: DistAlgorithm, @@ -245,6 +245,28 @@ where pub netinfo: NetworkInfo, /// 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, +} + +impl fmt::Debug for NewNodeInfo +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", &"") + .finish() + } } /// Virtual network builder. @@ -274,6 +296,8 @@ where message_limit: Option, /// Optional time limit. time_limit: Option, + /// Random number generator used to generate keys. + rng: Option>, } impl fmt::Debug for NetBuilder @@ -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", &"") .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(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, crypto::error::Error> { + let rng: Box = 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(node_ids: I, faulty: usize, cons: F) -> Result + fn new( + node_ids: I, + faulty: usize, + mut rng: R, + cons: F, + ) -> Result where F: Fn(NewNodeInfo) -> (D, Step), I: IntoIterator, + 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)) diff --git a/tests/net/proptest.rs b/tests/net/proptest.rs index 463ae98..16d1a0f 100644 --- a/tests/net/proptest.rs +++ b/tests/net/proptest.rs @@ -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 { + 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 { + any::().no_shrink() +} /// Node network dimension. /// diff --git a/tests/net_dynamic_hb.rs b/tests/net_dynamic_hb.rs index de55420..653132b 100644 --- a/tests/net_dynamic_hb.rs +++ b/tests/net_dynamic_hb.rs @@ -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::()) // 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() diff --git a/tests/network/mod.rs b/tests/network/mod.rs index d02e97a..a49f5c4 100644 --- a/tests/network/mod.rs +++ b/tests/network/mod.rs @@ -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( diff --git a/tests/sync_key_gen.rs b/tests/sync_key_gen.rs index cb9b501..ec43dbb 100644 --- a/tests/sync_key_gen.rs +++ b/tests/sync_key_gen.rs @@ -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"), };