diff --git a/src/binary_agreement/binary_agreement.rs b/src/binary_agreement/binary_agreement.rs index 6d00cdbb..61ac1752 100644 --- a/src/binary_agreement/binary_agreement.rs +++ b/src/binary_agreement/binary_agreement.rs @@ -11,7 +11,7 @@ use {DistAlgorithm, NetworkInfo, NodeIdT, Target}; /// The state of the current epoch's coin. In some epochs this is fixed, in others it starts /// with in `InProgress`. #[derive(Debug)] -enum CoinState { +pub enum CoinState { /// The value was fixed in the current epoch, or the coin has already terminated. Decided(bool), /// The coin value is not known yet. @@ -20,7 +20,7 @@ enum CoinState { impl CoinState { /// Returns the value, if this coin has already decided. - fn value(&self) -> Option { + pub fn value(&self) -> Option { match self { CoinState::Decided(value) => Some(*value), CoinState::InProgress(_) => None, diff --git a/src/binary_agreement/mod.rs b/src/binary_agreement/mod.rs index 403552b2..cdd01e8b 100644 --- a/src/binary_agreement/mod.rs +++ b/src/binary_agreement/mod.rs @@ -73,7 +73,8 @@ use rand; use self::bool_set::BoolSet; use threshold_sign; -pub use self::binary_agreement::BinaryAgreement; +pub use self::binary_agreement::{CoinState, BinaryAgreement}; +pub use self::sbv_broadcast::Message as SbvMessage; /// An Binary Agreement error. #[derive(Clone, Eq, PartialEq, Debug, Fail)] @@ -146,7 +147,7 @@ impl rand::Rand for MessageContent { } #[derive(Clone, Debug)] -struct Nonce(Vec); +pub struct Nonce(Vec); impl Nonce { pub fn new( diff --git a/tests/binary_agreement_mitm.rs b/tests/binary_agreement_mitm.rs new file mode 100644 index 00000000..58096d72 --- /dev/null +++ b/tests/binary_agreement_mitm.rs @@ -0,0 +1,439 @@ +#![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, Mutex}; + +use hbbft::binary_agreement::{BinaryAgreement, CoinState, MessageContent, Nonce, SbvMessage}; +use hbbft::threshold_sign::ThresholdSign; +use hbbft::{DistAlgorithm, NetworkInfo, Step}; + +use net::adversary::{NetMutHandle, QueuePosition}; +use net::err::CrankError; +use net::{Adversary, NetBuilder, NetMessage}; + +type NodeId = usize; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MessageType { + BVal, + Aux, + Coin, +} + +fn message_type_and_content(msg: &MessageContent) -> Option<(MessageType, Option)> { + match msg { + MessageContent::SbvBroadcast(sbv_msg) => match sbv_msg { + SbvMessage::BVal(v) => Some((MessageType::BVal, Some(*v))), + SbvMessage::Aux(v) => Some((MessageType::Aux, Some(*v))), + }, + MessageContent::Coin(_) => Some((MessageType::Coin, None)), + _ => None, + } +} + +/// A boolean XOR a value from the state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum BoolFromState { + AEstimated(bool), + CoinValue(bool), +} + +struct Stage { + source_groups: &'static [usize], + dest_groups: &'static [usize], + msg_type: MessageType, + msg_contents: Option, + msg_count: usize, +} + +const STAGES: &[Stage] = &[ + // x sends BVAL(\neg v) to the nodes in A0 + Stage { + source_groups: &[3], + dest_groups: &[0], + msg_type: MessageType::BVal, + msg_contents: Some(BoolFromState::AEstimated(true)), + msg_count: 1 * NODES_PER_GROUP, + }, + // and BVAL(v) to the nodes in A1. + Stage { + source_groups: &[3], + dest_groups: &[1], + msg_type: MessageType::BVal, + msg_contents: Some(BoolFromState::AEstimated(false)), + msg_count: 1 * NODES_PER_GROUP, + }, + // Also, all votes from nodes in B are delivered to all nodes in A. + Stage { + source_groups: &[2], + dest_groups: &[0, 1], + msg_type: MessageType::BVal, + msg_contents: None, + msg_count: NODES_PER_GROUP * (NODES_PER_GROUP * 2), + }, + // Messages within A0 are delivered. + // Thus nodes in A0 see |B|+|F|=f+1 votes for \neg v; so all nodes in A0 broadcast BVAL(\neg v) and all nodes in A0 see |A0|+|B|+|F|=2f+1 votes for \neg v; so all nodes in A0 broadcast AUX(\neg v). + Stage { + source_groups: &[0], + dest_groups: &[0], + msg_type: MessageType::BVal, + msg_contents: None, + msg_count: NODES_PER_GROUP * (NODES_PER_GROUP - 1), + }, + // Then all messages within A1 are delivered, + Stage { + source_groups: &[1], + dest_groups: &[1], + msg_type: MessageType::BVal, + msg_contents: None, + msg_count: NODES_PER_GROUP * (NODES_PER_GROUP - 1), + }, + // as well as the BVAL(v) messages from A0 to A1. + // Thus the nodes in A1 see |A0|+|A1|+|F|=2f+1 votes for v and broadcast AUX(v). + Stage { + source_groups: &[0], + dest_groups: &[1], + msg_type: MessageType::BVal, + msg_contents: Some(BoolFromState::AEstimated(false)), + msg_count: NODES_PER_GROUP * NODES_PER_GROUP, + }, + // After this all messages within A are delivered + Stage { + source_groups: &[0, 1], + dest_groups: &[0, 1], + msg_type: MessageType::BVal, + msg_contents: None, + msg_count: (NODES_PER_GROUP * 2) * (NODES_PER_GROUP * 2 - 1), + }, + Stage { + source_groups: &[0, 1], + dest_groups: &[0, 1], + msg_type: MessageType::Aux, + msg_contents: None, + msg_count: (NODES_PER_GROUP * 2) * (NODES_PER_GROUP * 2 - 1), + }, + // and x sends both BVAL(0) and BVAL(1) to every node in A. + // Thus every node in A broadcasts both BVAL(0) and BVAL(1) and sets bin_values=\{0,1\}. + Stage { + source_groups: &[3], + dest_groups: &[0, 1], + msg_type: MessageType::BVal, + msg_contents: Some(BoolFromState::AEstimated(false)), + msg_count: 1 * (NODES_PER_GROUP * 2), + }, + Stage { + source_groups: &[3], + dest_groups: &[0, 1], + msg_type: MessageType::BVal, + msg_contents: Some(BoolFromState::AEstimated(true)), + msg_count: 1 * (NODES_PER_GROUP * 2), + }, + // !! Not mentioned in the GitHub issue, but seems necessary. + // F sends Aux(_) to A, because nodes in A need 2f+1 Aux messages before they broadcasts their coins. + Stage { + source_groups: &[3], + dest_groups: &[0, 1], + msg_type: MessageType::Aux, + msg_contents: Some(BoolFromState::AEstimated(false)), + msg_count: 1 * (NODES_PER_GROUP * 2), + }, + // Now all nodes in A broadcast their threshold shares over the coin, so since |A|+|F|=2f+1, the adversary can construct the random coin value s. + Stage { + source_groups: &[0, 1], + dest_groups: &[3], + msg_type: MessageType::Coin, + msg_contents: None, + msg_count: (NODES_PER_GROUP * 2), + }, + // The nodes in F send BVAL(\neg s) to all the nodes in B, and all the BVAL(\neg s) messages from nodes in A are delivered to all nodes in B. + // Thus all the nodes in B broadcast AUX(\neg s). + Stage { + source_groups: &[0, 1, 3], + dest_groups: &[2], + msg_type: MessageType::BVal, + msg_contents: Some(BoolFromState::CoinValue(true)), + msg_count: (NODES_PER_GROUP * 2 + 1) * NODES_PER_GROUP, + }, + // Deliver all AUX(\neg s) messages; there are 2f+1 of them, since either every node in A0 broadcast AUX(\neg s) or every node in A1 broadcast AUX(\neg s). + // Thus all nodes in B see 2f+1 AUX(\neg s) messages and get to the end of the round with bin_values=\neg s. + // Thus the nodes in B continue to the next round voting \neg s while the nodes in A continue to the next round voting s. + Stage { + source_groups: &[0, 1, 2, 3], + dest_groups: &[2], + msg_type: MessageType::Aux, + msg_contents: Some(BoolFromState::CoinValue(true)), + msg_count: (NODES_PER_GROUP + 1) * (NODES_PER_GROUP) + + (NODES_PER_GROUP * (NODES_PER_GROUP - 1)), + }, + // At this point all messages from the round are delivered, and the process repeats. +]; + +/// An adversary for the reordering attack. +/// Described here: https://github.com/amiller/HoneyBadgerBFT/issues/59#issue-310368284 +/// Excluding the first node, which is F, +/// A0 is the first third of nodes, A1 is the second third, and the rest are B. +struct ReorderingAdversary { + stage: usize, + stage_progress: usize, + sent_stage_messages: bool, + epoch: u32, + coin_state: CoinState, + /// The estimated value for nodes in A. + a_estimated: bool, + // TODO this is really hacky but there's no better way to get this value + netinfo_mutex: Arc>>>>, +} + +const NODES_PER_GROUP: usize = 2; +const NUM_NODES: usize = (NODES_PER_GROUP * 3 + 1); + +impl ReorderingAdversary { + fn new(netinfo_mutex: Arc>>>>) -> Self { + Self::new_with_epoch(netinfo_mutex, 0, false) + } + + fn new_with_epoch( + netinfo_mutex: Arc>>>>, + epoch: u32, + a_estimated: bool, + ) -> Self { + ReorderingAdversary { + stage: 0, + stage_progress: 0, + sent_stage_messages: false, + epoch: epoch, + coin_state: match epoch % 3 { + 0 => CoinState::Decided(true), + 1 => CoinState::Decided(false), + 2 => { + let netinfo = netinfo_mutex + .lock() + .unwrap() + .as_ref() + .cloned() + .expect("Adversary netinfo mutex not populated"); + let nonce = Nonce::new( + netinfo.invocation_id().as_ref(), + 0, // session_id + 0, // proposer_id + epoch, + ); + let mut coin = ThresholdSign::new(netinfo, nonce); + let _ = coin + .handle_input(()) + .expect("Calling handle_input on Coin failed"); + CoinState::InProgress(Box::new(coin)) + } + _ => unreachable!(), + }, + netinfo_mutex, + a_estimated, + } + } + + fn eval_state_bool(&self, state_bool: BoolFromState) -> bool { + match state_bool { + BoolFromState::AEstimated(v) => self.a_estimated ^ v, + BoolFromState::CoinValue(v) => { + self.coin_state + .value() + .expect("State relied upon coin value before it was known") + ^ v + } + } + } + + fn inject_stage_messages(&mut self, net: &mut NetMutHandle>) { + if self.sent_stage_messages { + return; + } + self.sent_stage_messages = true; + if let Some(stage) = STAGES.get(self.stage) { + if stage.source_groups.iter().any(|&x| x == 3) { + let contents = self.eval_state_bool( + stage + .msg_contents + .expect("Stage has adversary as source but no contents"), + ); + let message_content = match stage.msg_type { + MessageType::BVal => MessageContent::SbvBroadcast(SbvMessage::BVal(contents)), + MessageType::Aux => MessageContent::SbvBroadcast(SbvMessage::Aux(contents)), + MessageType::Coin => { + panic!("Stage expected adversary node to send Coin message"); + } + }; + let message = message_content.with_epoch(self.epoch); + for &dst_grp in stage.dest_groups { + if dst_grp == 3 { + continue; + } + for i in 0..NODES_PER_GROUP { + let dst = 1 + NODES_PER_GROUP * dst_grp + i; + net.inject_message( + QueuePosition::Front, + NetMessage::>::new(0, message.clone(), dst), + ) + } + } + } + } + } + + /// Should be called whenever stage_progress is changed. + fn on_stage_progress_update(&mut self) { + let stage_finished = STAGES + .get(self.stage) + .map(|x| { + (x.msg_type == MessageType::Coin && self.coin_state.value().is_some()) + || self.stage_progress >= x.msg_count + }).unwrap_or(false); + if stage_finished { + self.stage += 1; + self.stage_progress = 0; + self.sent_stage_messages = false; + self.on_stage_progress_update(); + } + } + + fn stage_matches_msg(&self, message: &NetMessage>) -> bool { + if let Some(stage) = STAGES.get(self.stage) { + let from = *message.from(); + let src_group = if from == 0 { + 3 + } else { + (from - 1) / NODES_PER_GROUP + }; + let to = *message.to(); + let dst_group = if to == 0 { + 3 + } else { + (to - 1) / NODES_PER_GROUP + }; + if let Some((ty, content)) = message_type_and_content(&message.payload().content) { + let content_matches = match (stage.msg_contents, content) { + (Some(x), Some(y)) => self.eval_state_bool(x) == y, + _ => true, + }; + return stage.source_groups.iter().any(|&x| x == src_group) + && stage.dest_groups.iter().any(|&x| x == dst_group) + && stage.msg_type == ty + && content_matches; + } + } + false + } +} + +impl Adversary> for ReorderingAdversary { + fn pre_crank(&mut self, mut net: NetMutHandle>) { + self.inject_stage_messages(&mut net); + net.sort_messages_by(|a, b| { + a.payload() + .epoch + .cmp(&b.payload().epoch) + .then_with(|| self.stage_matches_msg(b).cmp(&self.stage_matches_msg(a))) + }); + let mut redo_crank = false; + if let Some(msg) = net.get_messages().front() { + if msg.payload().epoch == self.epoch && self.stage_matches_msg(&msg) { + self.stage_progress += 1; + self.on_stage_progress_update(); + } + if msg.payload().epoch > self.epoch { + // This assert should fail if the attack is prevented: + // assert_eq!(self.stage, STAGES.len()); + let netinfo = self.netinfo_mutex.clone(); + *self = Self::new_with_epoch( + netinfo, + msg.payload().epoch, + self.coin_state + .value() + .expect("Coin value not known at end of epoch"), + ); + redo_crank = true; + } + } + if redo_crank { + self.pre_crank(net); + } + } + + fn tamper( + &mut self, + _: NetMutHandle>, + msg: NetMessage>, + ) -> Result>, CrankError>> { + if let MessageContent::Coin(ref coin_msg) = msg.payload().content { + let mut new_coin_state = None; + if let CoinState::InProgress(ref mut coin) = self.coin_state { + let res = coin.handle_message(msg.from(), *coin_msg.clone()); + if let Ok(step) = res { + if let Some(coin) = step.output.into_iter().next() { + new_coin_state = Some(coin.parity().into()); + } + } + } + if let Some(new_coin_state) = new_coin_state { + self.coin_state = new_coin_state; + } + } + Ok(Step::default()) + } +} + +#[test] +fn reordering_attack() { + let _ = env_logger::try_init(); + let ids: Vec = (0..NUM_NODES).collect(); + let adversary_netinfo: Arc>>>> = Default::default(); + let mut net = NetBuilder::new(ids.iter().cloned()) + .adversary(ReorderingAdversary::new(adversary_netinfo.clone())) + .crank_limit(10000) + .using(move |info| { + let netinfo = Arc::new(info.netinfo); + if info.id == 0 { + *adversary_netinfo.lock().unwrap() = Some(netinfo.clone()); + } + BinaryAgreement::new(netinfo, 0, 0).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 estimated = None; + for node in net.nodes().skip(1) { + if let Some(b) = estimated { + assert!(iter::once(&b).eq(node.outputs())); + } else { + assert_eq!(1, node.outputs().len()); + estimated = 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 3ada3909..e7b9ee06 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.