use std::iter::once; use hbbft::{broadcast::Broadcast, util, ConsensusProtocol, CpStep}; use hbbft_testing::adversary::{ sort_ascending, swap_random, Adversary, NetMutHandle, NodeOrderAdversary, RandomAdversary, ReorderingAdversary, }; 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}; type NodeId = u16; /// 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, has_sent: bool, drop_messages: bool, } impl ProposeAdversary { /// Creates a new `ProposeAdversary`. #[inline] pub fn new(message_strategy: MessageSorting, drop_messages: bool) -> Self { ProposeAdversary { message_strategy, has_sent: false, drop_messages, } } } impl Adversary> for ProposeAdversary { #[inline] fn pre_crank( &mut self, mut net: NetMutHandle<'_, Broadcast, Self>, rng: &mut R, ) { match self.message_strategy { MessageSorting::RandomPick => swap_random(&mut net, rng), MessageSorting::SortAscending => sort_ascending(&mut net), } } #[inline] fn tamper( &mut self, mut net: NetMutHandle<'_, Broadcast, Self>, msg: NetMessage>, mut rng: &mut R, ) -> Result>, CrankError>> { let mut step = net.dispatch_message(msg, rng)?; // optionally drop all messages other than the fake broadcasts if self.drop_messages { step.messages.clear(); } 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 validators = faulty_node.algorithm().validator_set().clone(); let fake_step = Broadcast::new(*faulty_node.id(), validators, *faulty_node.id()) .expect("broadcast instance") .handle_input(b"Fake news".to_vec(), &mut rng) .expect("propose"); step.messages.extend(fake_step.messages); } } Ok(step) } } /// Broadcasts a value from node 0 and expects all good nodes to receive it. fn test_broadcast>>( mut net: VirtualNet, A>, proposed_value: &[u8], rng: &mut TestRng, proposer_id: NodeId, ) { // This returns an error in all but the first test. let _ = env_logger::try_init(); let proposer_is_faulty = net.get(proposer_id).unwrap().is_faulty(); // Make node 0 propose the value. let _step = net .send_input(proposer_id, proposed_value.to_vec(), rng) .expect("Setting input failed"); // 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() .next() .expect("At least one correct node needs to exist"); assert!(first.outputs().is_empty()); break; } let _ = net.crank_expect(rng); } 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().next().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()))); } } fn test_broadcast_different_sizes(new_adversary: F, proposed_value: &[u8], seed: TestRngSeed) where A: Adversary>, F: Fn() -> A, { let mut rng: TestRng = TestRng::from_seed(seed); let sizes = (1..6) .chain(once(rng.gen_range(6, 20))) .chain(once(rng.gen_range(30, 50))); for size in sizes { // cloning since it gets moved into a closure let num_faulty_nodes = util::max_faulty(size); info!( "Network size: {} good nodes, {} faulty nodes", size - num_faulty_nodes, num_faulty_nodes ); 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 validators = info.netinfo.validator_set().clone(); let id = *info.netinfo.our_id(); Broadcast::new(id, validators, 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); } } 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<_>| { let id = *node_info.netinfo.our_id(); let validators = node_info.netinfo.validator_set().clone(); Broadcast::new(id, validators, proposer_id) .expect("Failed to create a Broadcast instance.") }) .build(&mut rng) .expect("Could not construct test network."); // Space is ASCII character 32. So 32 spaces will create shards that are all equal, even if the // length of the value is inserted. test_broadcast(net, &[b' '; 32], &mut rng, proposer_id); } fn do_test_broadcast_random_delivery_silent(seed: TestRngSeed) { test_broadcast_different_sizes(ReorderingAdversary::new, b"Foo", seed); } fn do_test_broadcast_first_delivery_silent(seed: TestRngSeed) { test_broadcast_different_sizes(NodeOrderAdversary::new, b"Foo", seed); } fn do_test_broadcast_first_delivery_adv_propose(seed: TestRngSeed) { let new_adversary = || ProposeAdversary::new(MessageSorting::SortAscending, false); test_broadcast_different_sizes(new_adversary, b"Foo", seed); } fn do_test_broadcast_random_delivery_adv_propose(seed: TestRngSeed) { let new_adversary = || ProposeAdversary::new(MessageSorting::RandomPick, false); test_broadcast_different_sizes(new_adversary, b"Foo", seed); } fn do_test_broadcast_random_delivery_adv_propose_and_drop(seed: TestRngSeed) { let new_adversary = || ProposeAdversary::new(MessageSorting::RandomPick, true); test_broadcast_different_sizes(new_adversary, b"Foo", seed); } 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); }