#![deny(unused_must_use)] //! Network tests for Honey Badger. use std::collections::BTreeMap; use std::sync::{Arc, Mutex}; use hbbft::honey_badger::{Batch, EncryptionSchedule, HoneyBadger, MessageContent}; use hbbft::sender_queue::{self, SenderQueue, Step}; use hbbft::transaction_queue::TransactionQueue; use hbbft::{threshold_decrypt, util, CpStep, NetworkInfo, Target}; use hbbft_testing::adversary::{ sort_by_random_node, Adversary, NetMutHandle, NodeOrderAdversary, RandomAdversary, ReorderingAdversary, }; use hbbft_testing::proptest::{gen_seed, TestRng, TestRngSeed}; use hbbft_testing::{CrankError, NetBuilder, NetMessage, NewNodeInfo, Node, VirtualNet}; use itertools::Itertools; use log::info; use proptest::{prelude::ProptestConfig, proptest}; use rand::{seq::SliceRandom, Rng, SeedableRng}; type NodeId = u16; type NetworkInfoMap = BTreeMap>>; type UsizeHoneyBadger = SenderQueue, NodeId>>; type HoneyBadgerMessage = NetMessage; /// An adversary whose nodes only send messages with incorrect decryption shares. #[derive(Clone, Debug, Default)] pub struct FaultyShareAdversary { share_triggers: BTreeMap, // 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>, } impl FaultyShareAdversary { /// Creates a new adversary with the necessary network info instances pub fn new(netinfo_mutex: Arc>) -> Self { FaultyShareAdversary { share_triggers: BTreeMap::new(), netinfo_mutex, } } } impl Adversary for FaultyShareAdversary { #[inline] fn pre_crank( &mut self, mut net: NetMutHandle<'_, UsizeHoneyBadger, Self>, rng: &mut R, ) { sort_by_random_node(&mut net, rng); } #[inline] fn tamper( &mut self, mut net: NetMutHandle<'_, UsizeHoneyBadger, Self>, msg: HoneyBadgerMessage, rng: &mut R, ) -> Result, CrankError> { if let sender_queue::Message::Algo(hb_msg) = msg.payload() { let epoch = hb_msg.epoch(); // Set the trigger to simulate decryption share messages // if epoch has not been encountered yet. self.share_triggers.entry(epoch).or_insert(true); } let mut step = net.dispatch_message(msg, rng)?; let fake_proposal = &Vec::from("X marks the spot"); // For each untriggered epoch, send fake shares for (epoch, trigger_set) in &mut self.share_triggers { if *trigger_set { // Unset the trigger. *trigger_set = false; // Get node id vectors up-front to avoid borrow issues let faulty_node_ids: Vec = net.faulty_nodes_mut().map(|node| *node.id()).collect(); let all_node_ids: Vec = net.nodes_mut().map(|node| node.id()).collect(); // Broadcast fake decryption shares from all adversarial nodes. for faulty_node_id in faulty_node_ids { // get the adversarial's net info let netinfo = self .netinfo_mutex .lock() .unwrap() .get(&faulty_node_id) .cloned() .expect("Adversary netinfo mutex not populated"); // encrypt false share let fake_ciphertext = (*netinfo) .public_key_set() .public_key() .encrypt(fake_proposal); let share = netinfo .secret_key_share() .expect("missing adversary key share") .decrypt_share(&fake_ciphertext) .expect("decryption share"); // Send the share to remote nodes. for proposer_id in &all_node_ids { step.messages.push( Target::all().message(sender_queue::Message::Algo( MessageContent::DecryptionShare { proposer_id: *proposer_id, share: threshold_decrypt::Message(share.clone()), } .with_epoch(*epoch), )), ); } } } } Ok(step) } } /// Proposes `num_txs` values and expects nodes to output and order them. fn test_honey_badger( mut net: VirtualNet, num_txs: usize, mut rng: &mut TestRng, ) where A: Adversary, { let mut queues: BTreeMap<_, _> = net .correct_nodes() .map(|node| (*node.id(), (0..num_txs).collect::>())) .collect(); // Returns `true` if the node has not output all transactions yet. // If it has, and has advanced another epoch, it clears all messages for later epochs. let node_busy = |node: &Node| { node.outputs().iter().flat_map(Batch::iter).unique().count() < num_txs }; // Handle messages in random order until all nodes have output all transactions. while net.correct_nodes().any(node_busy) { // If a node is expecting input, take it from the queue. Otherwise handle a message. let input_ids: Vec<_> = net .correct_nodes() .filter(|node| !node.algorithm().algo().has_input()) .map(|node| *node.id()) .collect(); if let Some(id) = input_ids[..].choose(&mut rng) { let queue = queues.get_mut(id).unwrap(); queue.remove_multiple(net.get(*id).unwrap().outputs().iter().flat_map(Batch::iter)); let _ = net.send_input(*id, queue.choose(&mut rng, 3, 10), &mut rng); } else { let _ = net.crank_expect(&mut rng); } } verify_output_sequence(&net); } /// Verifies that all instances output the same sequence of batches. fn verify_output_sequence(network: &VirtualNet) where A: Adversary, { let mut expected: Option> = None; for node in network.correct_nodes() { assert!(!node.outputs().is_empty()); let outputs: BTreeMap>> = node .outputs() .iter() .map(|batch| (batch.epoch, &batch.contributions)) .collect(); if expected.is_none() { expected = Some(outputs); } else if let Some(expected) = &expected { assert_eq!(expected, &outputs); } } } fn new_honey_badger( netinfo: Arc>, ) -> (UsizeHoneyBadger, Step, NodeId>>) { let nc = netinfo.clone(); let peer_ids = nc.other_ids().cloned(); let hb = HoneyBadger::builder(netinfo) .encryption_schedule(EncryptionSchedule::EveryNthEpoch(2)) .build(); let our_id = *nc.our_id(); SenderQueue::builder(hb, peer_ids).build(our_id) } fn test_honey_badger_different_sizes( new_adversary: F, num_txs: usize, seed: TestRngSeed, adversary_netinfo: &Arc>, ) where A: Adversary, F: Fn() -> A, { // This returns an error in all but the first test. let _ = env_logger::try_init(); let mut rng: TestRng = TestRng::from_seed(seed); let sizes = vec![1, 2, 3, 5, rng.gen_range(6, 10)]; for size in sizes { // cloning since it gets moved into a closure let cloned_netinfo_map = adversary_netinfo.clone(); let num_adv_nodes = util::max_faulty(size); let num_good_nodes = size - num_adv_nodes; info!( "Network size: {} good nodes, {} faulty nodes", num_good_nodes, num_adv_nodes ); let (net, _) = NetBuilder::new(0..size as u16) .num_faulty(num_adv_nodes as usize) .message_limit(10_000 * size as usize) .no_time_limit() .adversary(new_adversary()) .using_step(move |info: NewNodeInfo<_>| { let netinfo = Arc::new(info.netinfo); cloned_netinfo_map .lock() .unwrap() .insert(info.id, netinfo.clone()); new_honey_badger(netinfo) }) .build(&mut rng) .expect("Could not construct test network."); test_honey_badger(net, num_txs, &mut rng); } } proptest! { #![proptest_config(ProptestConfig { cases: 1, .. ProptestConfig::default() })] #[test] #[allow(clippy::unnecessary_operation)] fn test_honey_badger_random_delivery_silent(seed in gen_seed()) { do_test_honey_badger_random_delivery_silent(seed) } #[test] #[allow(clippy::unnecessary_operation)] fn test_honey_badger_first_delivery_silent(seed in gen_seed()) { do_test_honey_badger_first_delivery_silent(seed) } #[test] #[allow(clippy::unnecessary_operation)] fn test_honey_badger_faulty_share(seed in gen_seed()) { do_test_honey_badger_faulty_share(seed) } #[test] #[allow(clippy::unnecessary_operation)] fn test_honey_badger_random_adversary(seed in gen_seed()) { do_test_honey_badger_random_adversary(seed) } } fn do_test_honey_badger_random_delivery_silent(seed: TestRngSeed) { test_honey_badger_different_sizes(ReorderingAdversary::new, 30, seed, &Default::default()); } fn do_test_honey_badger_first_delivery_silent(seed: TestRngSeed) { test_honey_badger_different_sizes(NodeOrderAdversary::new, 30, seed, &Default::default()); } fn do_test_honey_badger_faulty_share(seed: TestRngSeed) { let adversary_netinfo: Arc> = Default::default(); let new_adversary = || FaultyShareAdversary::new(adversary_netinfo.clone()); test_honey_badger_different_sizes(new_adversary, 8, seed, &adversary_netinfo); } fn do_test_honey_badger_random_adversary(seed: TestRngSeed) { let new_adversary = || { // A 10% injection chance is roughly ~13k extra messages added. RandomAdversary::new(0.1, 0.1) }; test_honey_badger_different_sizes(new_adversary, 8, seed, &Default::default()); }