diff --git a/src/binary_agreement/mod.rs b/src/binary_agreement/mod.rs index 10c084e7..c67dbfcc 100644 --- a/src/binary_agreement/mod.rs +++ b/src/binary_agreement/mod.rs @@ -74,6 +74,7 @@ use self::bool_set::BoolSet; use coin::{self, CoinMessage}; pub use self::binary_agreement::BinaryAgreement; +pub use self::sbv_broadcast::Message as SbvMessage; /// An Binary Agreement error. #[derive(Clone, Eq, PartialEq, Debug, Fail)] diff --git a/tests/binary_agreement_mitm.rs b/tests/binary_agreement_mitm.rs new file mode 100644 index 00000000..936b6c5f --- /dev/null +++ b/tests/binary_agreement_mitm.rs @@ -0,0 +1,209 @@ +#![deny(unused_must_use)] +//! Tests the BinaryAgreement protocol with a MTIM adversary. + +extern crate env_logger; +extern crate failure; +extern crate hbbft; +extern crate integer_sqrt; +extern crate proptest; +extern crate rand; +extern crate threshold_crypto; + +pub mod net; + +use std::iter; +use std::sync::Arc; + +use hbbft::binary_agreement::{BinaryAgreement, Message, MessageContent, SbvMessage}; +use hbbft::{DistAlgorithm, Step}; + +use net::adversary::{NetMutHandle, QueuePosition}; +use net::err::CrankError; +use net::{Adversary, NetBuilder, NetMessage}; + +type NodeId = usize; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum ReorderingStage { + DeliverBBVal, + DeliverA0BVal, + DeliverA0Aux, + Finished, +} + +impl Default for ReorderingStage { + fn default() -> Self { + ReorderingStage::DeliverBBVal + } +} + +/// An adversary for the reordering attack. +/// Described here: https://github.com/amiller/HoneyBadgerBFT/issues/59#issue-310368284 +/// A0 is the first third of nodes, A1 is the second third, and the rest are B. +#[derive(Default)] +struct ReorderingAdversary { + stage: ReorderingStage, + stage_progress: usize, + sent_initial_messages: bool, + sent_final_messages: bool, +} + +const NODES_PER_GROUP: usize = 10; +const NUM_NODES: usize = (NODES_PER_GROUP * 3 + 1); + +impl ReorderingAdversary { + fn message_priority(&self, message: &NetMessage>) -> i16 { + let src = *message.from(); + let src_group = if src == 0 { + // This is the faulty node. + // We always prioritize our own messages. + return 10; + } else { + (src - 1) / NODES_PER_GROUP + }; + let dst = *message.to(); + let dst_group = if dst == 0 { + 3 + } else { + (src - 1) / NODES_PER_GROUP + }; + match message.payload().content { + MessageContent::SbvBroadcast(ref message) => match message { + SbvMessage::BVal(v) => match self.stage { + ReorderingStage::DeliverBBVal if src_group == 2 => 1, + ReorderingStage::DeliverA0BVal if src_group == 0 => { + let wanted_dst_group = if *v { 0 } else { 1 }; + if wanted_dst_group == dst_group { + 1 + } else { + -1 + } + } + _ => 0, + }, + SbvMessage::Aux(_) => match self.stage { + ReorderingStage::DeliverA0Aux if src_group == 0 => 1, + _ => 0, + }, + }, + _ => 0, + } + } +} + +impl Adversary> for ReorderingAdversary { + fn pre_crank(&mut self, mut net: NetMutHandle>) { + if self.stage != ReorderingStage::Finished { + net.sort_messages_by(|a, b| self.message_priority(b).cmp(&self.message_priority(a))); + if let Some(msg) = net.get_messages().front() { + if self.message_priority(msg) == 1 { + self.stage_progress += 1; + // Each message should be sent by each member of the group to every other node. + let stage_messages = match self.stage { + ReorderingStage::DeliverBBVal => NODES_PER_GROUP * (NUM_NODES - 1), + ReorderingStage::DeliverA0BVal => NODES_PER_GROUP * NODES_PER_GROUP, + ReorderingStage::DeliverA0Aux => NODES_PER_GROUP * (NUM_NODES - 1), + ReorderingStage::Finished => unreachable!(), + }; + if self.stage_progress >= stage_messages { + self.stage = match self.stage { + ReorderingStage::DeliverBBVal => ReorderingStage::DeliverA0BVal, + ReorderingStage::DeliverA0BVal => ReorderingStage::DeliverA0Aux, + ReorderingStage::DeliverA0Aux => ReorderingStage::Finished, + ReorderingStage::Finished => unreachable!(), + }; + self.stage_progress = 0; + } + } + } + } + } + + fn tamper( + &mut self, + mut net: NetMutHandle>, + _: NetMessage>, + ) -> Result>, CrankError>> { + const BVAL_FALSE_MSG: Message = Message { + epoch: 0, + content: MessageContent::SbvBroadcast(SbvMessage::BVal(false)), + }; + const BVAL_TRUE_MSG: Message = Message { + epoch: 0, + content: MessageContent::SbvBroadcast(SbvMessage::BVal(true)), + }; + if !self.sent_initial_messages { + self.sent_initial_messages = true; + for target in 1..(1 + NODES_PER_GROUP) { + // Disagree with the nodes in A0. + net.inject_message( + QueuePosition::Front, + NetMessage::>::new(0, BVAL_TRUE_MSG.clone(), target), + ); + } + for target in (1 + NODES_PER_GROUP)..(1 + NODES_PER_GROUP * 2) { + // Agree with the nodes in A1. + net.inject_message( + QueuePosition::Front, + NetMessage::>::new(0, BVAL_FALSE_MSG.clone(), target), + ); + } + } else if self.stage == ReorderingStage::Finished && !self.sent_final_messages { + self.sent_final_messages = true; + for target in 1..(1 + NODES_PER_GROUP * 2) { + // Both agree and disgree with nodes in A. + net.inject_message( + QueuePosition::Front, + NetMessage::>::new(0, BVAL_FALSE_MSG.clone(), target), + ); + net.inject_message( + QueuePosition::Front, + NetMessage::>::new(0, BVAL_TRUE_MSG.clone(), target), + ); + } + } + Ok(Step::default()) + } +} + +#[test] +fn reordering_attack() { + let _ = env_logger::try_init(); + let ids: Vec = (0..NUM_NODES).collect(); + let mut net = NetBuilder::new(ids.iter().cloned()) + .adversary(ReorderingAdversary::default()) + .crank_limit(10000) + .using(|info| { + BinaryAgreement::new(Arc::new(info.netinfo), 0, info.id) + .expect("failed to create BinaryAgreement instance") + }).num_faulty(1) + .build() + .unwrap(); + + for id in ids { + if id == 0 { + // This is the faulty node. + } else if id < (1 + NODES_PER_GROUP * 2) { + // Group A + let _ = net.send_input(id, false).unwrap(); + } else { + // Group B + let _ = net.send_input(id, true).unwrap(); + } + } + + while !net.nodes().skip(1).all(|n| n.algorithm().terminated()) { + net.crank_expect(); + } + + // Verify that all instances output the same value. + let mut expected = None; + for node in net.nodes().skip(1) { + if let Some(b) = expected { + assert!(iter::once(&b).eq(node.outputs())); + } else { + assert_eq!(1, node.outputs().len()); + expected = Some(node.outputs()[0]); + } + } +} diff --git a/tests/net/adversary.rs b/tests/net/adversary.rs index a2a59db3..21f302c9 100644 --- a/tests/net/adversary.rs +++ b/tests/net/adversary.rs @@ -34,6 +34,7 @@ //! `NodeHandle::node()` and `NodeHandle::node_mut()`). use std::cmp; +use std::collections::VecDeque; use hbbft::{DistAlgorithm, Step}; @@ -154,7 +155,7 @@ where /// Panics if `position` is equal to `Before(idx)`, with `idx` being out of bounds. #[inline] pub fn inject_message(&mut self, position: QueuePosition, msg: NetMessage) { - // Ensure the node is not faulty. + // Ensure the source node is faulty. assert!( self.0 .get(msg.from.clone()) @@ -199,6 +200,12 @@ where { self.0.sort_messages_by(f) } + + /// Returns a reference to the queue of messages + #[inline] + pub fn get_messages(&self) -> &VecDeque> { + &self.0.messages + } } // Downgrade-conversion. diff --git a/tests/net/mod.rs b/tests/net/mod.rs index d706ee9c..780ddcd7 100644 --- a/tests/net/mod.rs +++ b/tests/net/mod.rs @@ -149,9 +149,27 @@ pub struct NetworkMessage { impl NetworkMessage { /// Create a new network message. #[inline] - fn new(from: N, payload: M, to: N) -> NetworkMessage { + pub fn new(from: N, payload: M, to: N) -> NetworkMessage { NetworkMessage { from, to, payload } } + + /// Returns the source of the message + #[inline] + pub fn from(&self) -> &N { + &self.from + } + + /// Returns the destination of the message + #[inline] + pub fn to(&self) -> &N { + &self.to + } + + /// Returns the contents of the message + #[inline] + pub fn payload(&self) -> &M { + &self.payload + } } /// Mapping from node IDs to actual node instances.