2018-10-25 08:21:24 -07:00
|
|
|
use std::collections::BTreeMap;
|
2018-05-29 05:17:30 -07:00
|
|
|
use std::iter::once;
|
2019-03-14 06:41:23 -07:00
|
|
|
use std::sync::{Arc, Mutex};
|
2018-05-02 06:34:30 -07:00
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
use hbbft::{broadcast::Broadcast, util, ConsensusProtocol, CpStep, NetworkInfo};
|
2019-04-26 07:54:12 -07:00
|
|
|
use hbbft_testing::adversary::{
|
2019-03-14 06:41:23 -07:00
|
|
|
sort_ascending, swap_random, Adversary, NetMutHandle, NodeOrderAdversary, RandomAdversary,
|
|
|
|
ReorderingAdversary,
|
2018-07-05 09:20:53 -07:00
|
|
|
};
|
2019-04-26 07:54:12 -07:00
|
|
|
use hbbft_testing::proptest::{gen_seed, TestRng, TestRngSeed};
|
|
|
|
use hbbft_testing::{CrankError, NetBuilder, NetMessage, NewNodeInfo, VirtualNet};
|
|
|
|
use log::info;
|
|
|
|
use proptest::{prelude::ProptestConfig, proptest};
|
|
|
|
use rand::{Rng, SeedableRng};
|
2018-05-02 06:34:30 -07:00
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
type NodeId = u16;
|
|
|
|
type NetworkInfoMap = BTreeMap<NodeId, Arc<NetworkInfo<NodeId>>>;
|
|
|
|
|
|
|
|
/// A strategy for picking the next node to handle a message.
|
|
|
|
/// The sorting algorithm used is stable - preserves message
|
|
|
|
/// order relative to the node id.
|
|
|
|
pub enum MessageSorting {
|
|
|
|
/// Picks a random node and swaps its messages to the front of the queue
|
|
|
|
RandomPick,
|
|
|
|
/// Sorts the message queue by receiving node id
|
|
|
|
SortAscending,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// For each adversarial node does the following, but only once:
|
|
|
|
///
|
|
|
|
/// * Creates a *new* instance of the Broadcast ConsensusProtocol,
|
|
|
|
/// with the adversarial node ID as proposer
|
|
|
|
/// * Lets it handle a "Fake News" input
|
|
|
|
/// * Records the returned step's messages
|
|
|
|
/// * Injects the messages to the queue
|
|
|
|
pub struct ProposeAdversary {
|
|
|
|
message_strategy: MessageSorting,
|
2018-05-03 01:07:37 -07:00
|
|
|
has_sent: bool,
|
2019-03-14 06:41:23 -07:00
|
|
|
drop_messages: bool,
|
|
|
|
// TODO this is really hacky but there's no better way to get this value
|
|
|
|
// Solution taken from binary_agreement_mitm test - ideally the new network simulator
|
|
|
|
// should be altered to store the netinfo structure alongside nodes similar to
|
|
|
|
// the way the old network simulator did it.
|
|
|
|
netinfo_mutex: Arc<Mutex<NetworkInfoMap>>,
|
2018-05-03 01:07:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ProposeAdversary {
|
2019-03-14 06:41:23 -07:00
|
|
|
/// Creates a new `ProposeAdversary`.
|
|
|
|
#[inline]
|
|
|
|
pub fn new(
|
|
|
|
message_strategy: MessageSorting,
|
|
|
|
netinfo_mutex: Arc<Mutex<NetworkInfoMap>>,
|
|
|
|
drop_messages: bool,
|
|
|
|
) -> Self {
|
2018-05-03 01:07:37 -07:00
|
|
|
ProposeAdversary {
|
2019-03-14 06:41:23 -07:00
|
|
|
message_strategy,
|
2018-05-03 01:07:37 -07:00
|
|
|
has_sent: false,
|
2019-03-14 06:41:23 -07:00
|
|
|
drop_messages,
|
|
|
|
netinfo_mutex,
|
2018-05-03 01:07:37 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-29 09:08:35 -07:00
|
|
|
impl Adversary<Broadcast<NodeId>> for ProposeAdversary {
|
2019-03-14 06:41:23 -07:00
|
|
|
#[inline]
|
|
|
|
fn pre_crank<R: Rng>(
|
|
|
|
&mut self,
|
|
|
|
mut net: NetMutHandle<'_, Broadcast<NodeId>, Self>,
|
|
|
|
rng: &mut R,
|
|
|
|
) {
|
|
|
|
match self.message_strategy {
|
|
|
|
MessageSorting::RandomPick => swap_random(&mut net, rng),
|
|
|
|
MessageSorting::SortAscending => sort_ascending(&mut net),
|
|
|
|
}
|
2018-05-03 01:07:37 -07:00
|
|
|
}
|
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
#[inline]
|
|
|
|
fn tamper<R: Rng>(
|
|
|
|
&mut self,
|
|
|
|
mut net: NetMutHandle<'_, Broadcast<NodeId>, Self>,
|
|
|
|
msg: NetMessage<Broadcast<NodeId>>,
|
|
|
|
mut rng: &mut R,
|
|
|
|
) -> Result<CpStep<Broadcast<NodeId>>, CrankError<Broadcast<NodeId>>> {
|
|
|
|
let mut step = net.dispatch_message(msg, rng)?;
|
2018-05-03 01:07:37 -07:00
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
// optionally drop all messages other than the fake broadcasts
|
|
|
|
if self.drop_messages {
|
|
|
|
step.messages.clear();
|
2018-05-03 01:07:37 -07:00
|
|
|
}
|
2019-03-14 06:41:23 -07:00
|
|
|
|
|
|
|
if !self.has_sent {
|
|
|
|
self.has_sent = true;
|
|
|
|
|
|
|
|
// Get adversarial nodes
|
|
|
|
let faulty_nodes = net.faulty_nodes_mut();
|
|
|
|
|
|
|
|
// Instantiate a temporary broadcast consensus protocol for each faulty node
|
|
|
|
// and add the generated messages to the current step.
|
|
|
|
for faulty_node in faulty_nodes {
|
|
|
|
let netinfo = self
|
|
|
|
.netinfo_mutex
|
|
|
|
.lock()
|
|
|
|
.unwrap()
|
|
|
|
.get(faulty_node.id())
|
|
|
|
.cloned()
|
|
|
|
.expect("Adversary netinfo mutex not populated");
|
|
|
|
|
|
|
|
let fake_step = Broadcast::new(netinfo, *faulty_node.id())
|
2018-10-25 08:21:24 -07:00
|
|
|
.expect("broadcast instance")
|
OsRng / external RNG Refactoring (#357)
* Use `OsRng` in place of `thread_rng`.
This changes the defaults of any builder by instantiating an `OsRng` instead of
a `thread_rng`, the former being much more secure than the latter.
Additionally, all the unit tests that still instantiate RNGs manually used `OsRng`s
as well; while there is no actual need for this level of security in tests, the performance overhead is very small and random number generation complexity has such a small impact on these tests that the convenience of being able to ban `thread_rng` from the codebase altogether, setting a good example and avoid issues when refactoring later greatly outweigh the negatives.
* Instead of storing random number generators in the various consensus algorithm instances, pass them in from the outside whenever they are needed.
This changes a large amount of interfaces (and in this commit is only partially done, since `DistAlgorithm` needs to be fundamentally altered as well.
It also obsoletes parts of the `util` module.
* Added an `R: Rng` type parameter to both methods of `DistAlgorithm`, forcing callers to pass in their own Rngs.
* Fixed documentation grammar and spelling in some of the altered interfaces due to RNG refactoring.
* Move `rng` argument to the end of the argument for most functions.
Also includes a reformatting due to Rust 1.30.
* Updated tests, accomodate `rng`-API changes.
* Fixed remaining compilation issues with new RNG code.
* Fix illegal `self` import outside curly braces.
* Cleaned up comments and fixed broken definition of `broadcast_input`.
* Updated existing test cases to properly work with static dispatch randomness.
* Do not use boxed `Rng`s for key generation in test networks.
* Use the passed-in `Rng` in `ReorderingAdversary`, instead of storing a boxed one.
* Fixed clippy lints after refactoring.
* Removed some no-longer necessary manual `fmt::Debug` implementations in test framework.
* Use `OsRng` even in tests in `binary_agreement_mitm`.
* Use a proper deterministic RNG in tests `binary_agreement_mitm`.
* Refactor `examples/simulation.rs` by not using `ThreadRng`, passing generic `Rng` parameters throughout and using a type alias instead of a newtype as the `Transaction`.
* Remove `thread_rng` use from `examples/node.rs`.
* Explicitly construct `InternalContrib` in `DynamicHoneyBadger::propose`.
* Fixed typo in description of `DistAlgorithm` trait.
2018-12-14 04:51:09 -08:00
|
|
|
.handle_input(b"Fake news".to_vec(), &mut rng)
|
2019-03-14 06:41:23 -07:00
|
|
|
.expect("propose");
|
|
|
|
|
|
|
|
step.messages.extend(fake_step.messages);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(step)
|
2018-05-03 01:07:37 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-02 06:34:30 -07:00
|
|
|
/// Broadcasts a value from node 0 and expects all good nodes to receive it.
|
2018-08-29 09:08:35 -07:00
|
|
|
fn test_broadcast<A: Adversary<Broadcast<NodeId>>>(
|
2019-03-14 06:41:23 -07:00
|
|
|
mut net: VirtualNet<Broadcast<NodeId>, A>,
|
2018-05-14 05:35:06 -07:00
|
|
|
proposed_value: &[u8],
|
2019-03-14 06:41:23 -07:00
|
|
|
rng: &mut TestRng,
|
|
|
|
proposer_id: NodeId,
|
2018-05-14 05:35:06 -07:00
|
|
|
) {
|
2018-05-08 07:20:32 -07:00
|
|
|
// This returns an error in all but the first test.
|
|
|
|
let _ = env_logger::try_init();
|
2018-05-01 09:32:01 -07:00
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
let proposer_is_faulty = net.get(proposer_id).unwrap().is_faulty();
|
|
|
|
|
2018-05-04 02:14:19 -07:00
|
|
|
// Make node 0 propose the value.
|
2019-03-14 06:41:23 -07:00
|
|
|
let _step = net
|
|
|
|
.send_input(proposer_id, proposed_value.to_vec(), rng)
|
|
|
|
.expect("Setting input failed");
|
2018-05-01 09:32:01 -07:00
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
// Handle messages until all good nodes have terminated.
|
|
|
|
// If the proposer is faulty it is legal for the queue to starve
|
|
|
|
while !net.nodes().all(|node| node.algorithm().terminated()) {
|
|
|
|
if proposer_is_faulty && net.messages_len() == 0 {
|
|
|
|
info!("Expected starvation of messages with a faulty proposer");
|
|
|
|
// The output of all correct nodes needs to be empty in this case.
|
|
|
|
// We check for the output of the first node to be empty and
|
|
|
|
// rely on the identity checks at the end of this function to
|
|
|
|
// verify that all other correct nodes have empty output as well.
|
|
|
|
let first = net
|
|
|
|
.correct_nodes()
|
|
|
|
.nth(0)
|
|
|
|
.expect("At least one correct node needs to exist");
|
|
|
|
assert!(first.outputs().is_empty());
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
let _ = net.crank_expect(rng);
|
2018-05-01 09:32:01 -07:00
|
|
|
}
|
2018-05-02 06:34:30 -07:00
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
if proposer_is_faulty {
|
|
|
|
// If the proposer was faulty it is sufficient for all correct nodes having the same value.
|
|
|
|
let first = net.correct_nodes().nth(0).unwrap().outputs();
|
|
|
|
assert!(net.nodes().all(|node| node.outputs() == first));
|
|
|
|
} else {
|
|
|
|
// In the case where the proposer was valid it must be the value it proposed.
|
|
|
|
assert!(net
|
|
|
|
.nodes()
|
|
|
|
.all(|node| once(&proposed_value.to_vec()).eq(node.outputs())));
|
|
|
|
}
|
2018-05-16 05:23:57 -07:00
|
|
|
}
|
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
fn test_broadcast_different_sizes<A, F>(
|
|
|
|
new_adversary: F,
|
|
|
|
proposed_value: &[u8],
|
|
|
|
seed: TestRngSeed,
|
|
|
|
adversary_netinfo: &Arc<Mutex<NetworkInfoMap>>,
|
|
|
|
) where
|
2018-08-29 09:08:35 -07:00
|
|
|
A: Adversary<Broadcast<NodeId>>,
|
2019-03-14 06:41:23 -07:00
|
|
|
F: Fn() -> A,
|
2018-05-14 07:16:57 -07:00
|
|
|
{
|
2019-03-14 06:41:23 -07:00
|
|
|
let mut rng: TestRng = TestRng::from_seed(seed);
|
2018-05-14 07:16:57 -07:00
|
|
|
let sizes = (1..6)
|
2018-05-29 05:17:30 -07:00
|
|
|
.chain(once(rng.gen_range(6, 20)))
|
|
|
|
.chain(once(rng.gen_range(30, 50)));
|
2018-05-14 07:16:57 -07:00
|
|
|
for size in sizes {
|
2019-03-14 06:41:23 -07:00
|
|
|
// cloning since it gets moved into a closure
|
|
|
|
let cloned_netinfo_map = adversary_netinfo.clone();
|
2018-11-22 04:00:02 -08:00
|
|
|
let num_faulty_nodes = util::max_faulty(size);
|
2018-05-17 08:38:45 -07:00
|
|
|
info!(
|
2018-05-14 07:16:57 -07:00
|
|
|
"Network size: {} good nodes, {} faulty nodes",
|
2019-03-14 06:41:23 -07:00
|
|
|
size - num_faulty_nodes,
|
|
|
|
num_faulty_nodes
|
2018-05-14 07:16:57 -07:00
|
|
|
);
|
2019-03-14 06:41:23 -07:00
|
|
|
|
|
|
|
let proposer_id = rng.gen_range(0, size) as NodeId;
|
|
|
|
|
|
|
|
let (net, _) = NetBuilder::new(0..size as u16)
|
|
|
|
.num_faulty(num_faulty_nodes as usize)
|
|
|
|
.message_limit(10_000 * size as usize)
|
|
|
|
.no_time_limit()
|
|
|
|
.adversary(new_adversary())
|
|
|
|
.using(move |info| {
|
|
|
|
let netinfo = Arc::new(info.netinfo);
|
|
|
|
cloned_netinfo_map
|
|
|
|
.lock()
|
|
|
|
.unwrap()
|
|
|
|
.insert(info.id, netinfo.clone());
|
|
|
|
Broadcast::new(netinfo, proposer_id)
|
|
|
|
.expect("Failed to create a Broadcast instance.")
|
|
|
|
})
|
|
|
|
.build(&mut rng)
|
|
|
|
.expect("Could not construct test network.");
|
|
|
|
|
|
|
|
test_broadcast(net, proposed_value, &mut rng, proposer_id);
|
2018-05-14 07:16:57 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
proptest! {
|
|
|
|
#![proptest_config(ProptestConfig {
|
|
|
|
cases: 1, .. ProptestConfig::default()
|
|
|
|
})]
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[allow(clippy::unnecessary_operation)]
|
|
|
|
fn test_8_broadcast_equal_leaves_silent(seed in gen_seed()) {
|
|
|
|
do_test_8_broadcast_equal_leaves_silent(seed)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[allow(clippy::unnecessary_operation)]
|
|
|
|
fn test_broadcast_random_delivery_silent(seed in gen_seed()) {
|
|
|
|
do_test_broadcast_random_delivery_silent(seed)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[allow(clippy::unnecessary_operation)]
|
|
|
|
fn test_broadcast_first_delivery_silent(seed in gen_seed()) {
|
|
|
|
do_test_broadcast_first_delivery_silent(seed)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[allow(clippy::unnecessary_operation)]
|
|
|
|
fn test_broadcast_first_delivery_adv_propose(seed in gen_seed()) {
|
|
|
|
do_test_broadcast_first_delivery_adv_propose(seed)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[allow(clippy::unnecessary_operation)]
|
|
|
|
fn test_broadcast_random_delivery_adv_propose(seed in gen_seed()) {
|
|
|
|
do_test_broadcast_random_delivery_adv_propose(seed)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[allow(clippy::unnecessary_operation)]
|
|
|
|
fn test_broadcast_random_delivery_adv_propose_and_drop(seed in gen_seed()) {
|
|
|
|
do_test_broadcast_random_delivery_adv_propose_and_drop(seed)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[allow(clippy::unnecessary_operation)]
|
|
|
|
fn test_broadcast_random_adversary(seed in gen_seed()) {
|
|
|
|
do_test_broadcast_random_adversary(seed)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn do_test_8_broadcast_equal_leaves_silent(seed: TestRngSeed) {
|
|
|
|
let mut rng: TestRng = TestRng::from_seed(seed);
|
|
|
|
let size = 8;
|
|
|
|
|
|
|
|
let num_faulty = 0;
|
|
|
|
let proposer_id = rng.gen_range(0, size);
|
|
|
|
let (net, _) = NetBuilder::new(0..size as u16)
|
|
|
|
.num_faulty(num_faulty as usize)
|
|
|
|
.message_limit(10_000 * size as usize)
|
|
|
|
.no_time_limit()
|
|
|
|
.adversary(ReorderingAdversary::new())
|
|
|
|
.using(move |node_info: NewNodeInfo<_>| {
|
|
|
|
Broadcast::new(Arc::new(node_info.netinfo), proposer_id)
|
|
|
|
.expect("Failed to create a Broadcast instance.")
|
|
|
|
})
|
|
|
|
.build(&mut rng)
|
|
|
|
.expect("Could not construct test network.");
|
|
|
|
|
2018-05-04 02:14:19 -07:00
|
|
|
// Space is ASCII character 32. So 32 spaces will create shards that are all equal, even if the
|
|
|
|
// length of the value is inserted.
|
2019-03-14 06:41:23 -07:00
|
|
|
test_broadcast(net, &[b' '; 32], &mut rng, proposer_id);
|
2018-05-04 02:14:19 -07:00
|
|
|
}
|
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
fn do_test_broadcast_random_delivery_silent(seed: TestRngSeed) {
|
|
|
|
test_broadcast_different_sizes(ReorderingAdversary::new, b"Foo", seed, &Default::default());
|
2018-05-04 02:14:19 -07:00
|
|
|
}
|
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
fn do_test_broadcast_first_delivery_silent(seed: TestRngSeed) {
|
|
|
|
test_broadcast_different_sizes(NodeOrderAdversary::new, b"Foo", seed, &Default::default());
|
2018-05-08 07:20:32 -07:00
|
|
|
}
|
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
fn do_test_broadcast_first_delivery_adv_propose(seed: TestRngSeed) {
|
|
|
|
let adversary_netinfo: Arc<Mutex<NetworkInfoMap>> = Default::default();
|
|
|
|
let new_adversary = || {
|
|
|
|
ProposeAdversary::new(
|
|
|
|
MessageSorting::SortAscending,
|
|
|
|
adversary_netinfo.clone(),
|
|
|
|
false,
|
|
|
|
)
|
|
|
|
};
|
|
|
|
test_broadcast_different_sizes(new_adversary, b"Foo", seed, &adversary_netinfo);
|
2018-05-03 01:07:37 -07:00
|
|
|
}
|
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
fn do_test_broadcast_random_delivery_adv_propose(seed: TestRngSeed) {
|
|
|
|
let adversary_netinfo: Arc<Mutex<NetworkInfoMap>> = Default::default();
|
|
|
|
let new_adversary =
|
|
|
|
|| ProposeAdversary::new(MessageSorting::RandomPick, adversary_netinfo.clone(), false);
|
|
|
|
test_broadcast_different_sizes(new_adversary, b"Foo", seed, &adversary_netinfo);
|
2018-05-03 01:07:37 -07:00
|
|
|
}
|
2018-07-05 09:20:53 -07:00
|
|
|
|
2019-03-14 06:41:23 -07:00
|
|
|
fn do_test_broadcast_random_delivery_adv_propose_and_drop(seed: TestRngSeed) {
|
|
|
|
let adversary_netinfo: Arc<Mutex<NetworkInfoMap>> = Default::default();
|
|
|
|
let new_adversary =
|
|
|
|
|| ProposeAdversary::new(MessageSorting::RandomPick, adversary_netinfo.clone(), true);
|
|
|
|
test_broadcast_different_sizes(new_adversary, b"Foo", seed, &adversary_netinfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn do_test_broadcast_random_adversary(seed: TestRngSeed) {
|
|
|
|
let new_adversary = || RandomAdversary::new(0.2, 0.2);
|
|
|
|
test_broadcast_different_sizes(new_adversary, b"RandomFoo", seed, &Default::default());
|
2018-07-05 09:20:53 -07:00
|
|
|
}
|