-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The ACS and BA algorithms #11
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
d3b974f
Binary Agreement implementation and its wiring into Common Subset
vkomenda 5215156
defined the output from the Common Subset algorithm
vkomenda 2205f90
added a TODO file and changed indentation in the .proto file
vkomenda 394462c
changed code according to review comments
vkomenda 3e35cc6
added element_proposer_id to the Agreement input to the Common Subset…
vkomenda e0005a6
removed the separate field in Agreement and corrected computation of…
vkomenda b6a6bb3
clear the received AUX messages on every epoch update
vkomenda 259d536
corrected the count of AUX messages
vkomenda 51ef11b
fixed the count of matching AUX messages
vkomenda cf33bac
added the proposer ID to common_subset::handle_broadcast and mae it a…
vkomenda fb50e38
replaced the map of estimated values with only one optional value for…
vkomenda 49c33d2
added a dev dependency on rand for CI
vkomenda 68e6a7a
added the missing agreement broadcast message on epoch change
vkomenda 57ff64c
correction: Agreement outputs a value only once
vkomenda File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
TODO | ||
==== | ||
|
||
* Fix the inappropriate use of Common Coin in the Byzantine Agreement protocol | ||
|
||
This bug is explained in https://github.com/amiller/HoneyBadgerBFT/issues/59 | ||
where a solution is suggested introducing an additional type of message, | ||
CONF. | ||
|
||
There may be alternative solutions, such as using a different Byzantine | ||
Agreement protocol altogether, for example, | ||
https://people.csail.mit.edu/silvio/Selected%20Scientific%20Papers/Distributed%20Computation/BYZANTYNE%20AGREEMENT%20MADE%20TRIVIAL.pdf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,295 @@ | ||
//! Binary Byzantine agreement protocol from a common coin protocol. | ||
|
||
use proto::AgreementMessage; | ||
use std::collections::{BTreeSet, VecDeque}; | ||
use itertools::Itertools; | ||
use std::collections::{BTreeSet, HashMap, VecDeque}; | ||
use std::hash::Hash; | ||
|
||
#[derive(Default)] | ||
pub struct Agreement { | ||
input: Option<bool>, | ||
_bin_values: BTreeSet<bool>, | ||
use proto::message; | ||
|
||
/// Type of output from the Agreement message handler. The first component is | ||
/// the value on which the Agreement has decided, also called "output" in the | ||
/// HoneyadgerBFT paper. The second component is a queue of messages to be sent | ||
/// to remote nodes as a result of handling the incomming message. | ||
type AgreementOutput = (Option<bool>, VecDeque<AgreementMessage>); | ||
|
||
/// Messages sent during the binary Byzantine agreement stage. | ||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] | ||
pub enum AgreementMessage { | ||
/// BVAL message with an epoch. | ||
BVal((u32, bool)), | ||
/// AUX message with an epoch. | ||
Aux((u32, bool)), | ||
} | ||
|
||
impl AgreementMessage { | ||
pub fn into_proto(self) -> message::AgreementProto { | ||
let mut p = message::AgreementProto::new(); | ||
match self { | ||
AgreementMessage::BVal((e, b)) => { | ||
p.set_epoch(e); | ||
p.set_bval(b); | ||
} | ||
AgreementMessage::Aux((e, b)) => { | ||
p.set_epoch(e); | ||
p.set_aux(b); | ||
} | ||
} | ||
p | ||
} | ||
|
||
// TODO: Re-enable lint once implemented. | ||
#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] | ||
pub fn from_proto(mp: message::AgreementProto) -> Option<Self> { | ||
let epoch = mp.get_epoch(); | ||
if mp.has_bval() { | ||
Some(AgreementMessage::BVal((epoch, mp.get_bval()))) | ||
} else if mp.has_aux() { | ||
Some(AgreementMessage::Aux((epoch, mp.get_aux()))) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
/// Binary Agreement instance. | ||
pub struct Agreement<NodeUid> { | ||
/// The UID of the corresponding proposer node. | ||
uid: NodeUid, | ||
num_nodes: usize, | ||
num_faulty_nodes: usize, | ||
epoch: u32, | ||
/// Bin values. Reset on every epoch update. | ||
bin_values: BTreeSet<bool>, | ||
/// Values received in BVAL messages. Reset on every epoch update. | ||
received_bval: HashMap<NodeUid, BTreeSet<bool>>, | ||
/// Sent BVAL values. Reset on every epoch update. | ||
sent_bval: BTreeSet<bool>, | ||
/// Values received in AUX messages. Reset on every epoch update. | ||
received_aux: HashMap<NodeUid, bool>, | ||
/// The estimate of the decision value in the current epoch. | ||
estimated: Option<bool>, | ||
/// The value output by the agreement instance. It is set once to `Some(b)` | ||
/// and then never changed. That is, no instance of Binary Agreement can | ||
/// decide on two different values of output. | ||
output: Option<bool>, | ||
/// Termination flag. The Agreement instance doesn't terminate immediately | ||
/// upon deciding on the agreed value. This is done in order to help other | ||
/// nodes decide despite asynchrony of communication. Once the instance | ||
/// determines that all the remote nodes have reached agreement, it sets the | ||
/// `terminated` flag and accepts no more incoming messages. | ||
terminated: bool, | ||
} | ||
|
||
impl Agreement { | ||
pub fn new() -> Self { | ||
impl<NodeUid: Clone + Eq + Hash> Agreement<NodeUid> { | ||
pub fn new(uid: NodeUid, num_nodes: usize) -> Self { | ||
let num_faulty_nodes = (num_nodes - 1) / 3; | ||
|
||
Agreement { | ||
input: None, | ||
_bin_values: BTreeSet::new(), | ||
uid, | ||
num_nodes, | ||
num_faulty_nodes, | ||
epoch: 0, | ||
bin_values: BTreeSet::new(), | ||
received_bval: HashMap::new(), | ||
sent_bval: BTreeSet::new(), | ||
received_aux: HashMap::new(), | ||
estimated: None, | ||
output: None, | ||
terminated: false, | ||
} | ||
} | ||
|
||
pub fn set_input(&mut self, input: bool) -> AgreementMessage { | ||
self.input = Some(input); | ||
/// Algorithm has terminated. | ||
pub fn terminated(&self) -> bool { | ||
self.terminated | ||
} | ||
|
||
/// Sets the input value for agreement. | ||
pub fn set_input(&mut self, input: bool) -> Result<AgreementMessage, Error> { | ||
if self.epoch != 0 { | ||
return Err(Error::InputNotAccepted); | ||
} | ||
|
||
// Set the initial estimated value to the input value. | ||
self.estimated = Some(input); | ||
// Receive the BVAL message locally. | ||
self.received_bval | ||
.entry(self.uid.clone()) | ||
.or_insert_with(BTreeSet::new) | ||
.insert(input); | ||
// Multicast BVAL | ||
AgreementMessage::BVal(input) | ||
Ok(AgreementMessage::BVal((self.epoch, input))) | ||
} | ||
|
||
pub fn has_input(&self) -> bool { | ||
self.input.is_some() | ||
/// Acceptance check to be performed before setting the input value. | ||
pub fn accepts_input(&self) -> bool { | ||
self.epoch == 0 && self.estimated.is_none() | ||
} | ||
|
||
/// Receive input from a remote node. | ||
pub fn on_input( | ||
&self, | ||
_message: &AgreementMessage, | ||
) -> Result<VecDeque<AgreementMessage>, Error> { | ||
Err(Error::NotImplemented) | ||
/// | ||
/// Outputs an optional agreement result and a queue of agreement messages | ||
/// to remote nodes. There can be up to 2 messages. | ||
pub fn handle_agreement_message( | ||
&mut self, | ||
sender_id: &NodeUid, | ||
message: &AgreementMessage, | ||
) -> Result<AgreementOutput, Error> { | ||
match *message { | ||
// The algorithm instance has already terminated. | ||
_ if self.terminated => Err(Error::Terminated), | ||
|
||
AgreementMessage::BVal((epoch, b)) if epoch == self.epoch => { | ||
self.handle_bval(sender_id, b) | ||
} | ||
|
||
AgreementMessage::Aux((epoch, b)) if epoch == self.epoch => { | ||
self.handle_aux(sender_id, b) | ||
} | ||
|
||
// Epoch does not match. Ignore the message. | ||
_ => Ok((None, VecDeque::new())), | ||
} | ||
} | ||
|
||
fn handle_bval(&mut self, sender_id: &NodeUid, b: bool) -> Result<AgreementOutput, Error> { | ||
let mut outgoing = VecDeque::new(); | ||
|
||
self.received_bval | ||
.entry(sender_id.clone()) | ||
.or_insert_with(BTreeSet::new) | ||
.insert(b); | ||
let count_bval = self.received_bval | ||
.values() | ||
.filter(|values| values.contains(&b)) | ||
.count(); | ||
|
||
// upon receiving BVAL_r(b) messages from 2f + 1 nodes, | ||
// bin_values_r := bin_values_r ∪ {b} | ||
if count_bval == 2 * self.num_faulty_nodes + 1 { | ||
self.bin_values.insert(b); | ||
|
||
// wait until bin_values_r != 0, then multicast AUX_r(w) | ||
// where w ∈ bin_values_r | ||
if self.bin_values.len() == 1 { | ||
// Send an AUX message at most once per epoch. | ||
outgoing.push_back(AgreementMessage::Aux((self.epoch, b))); | ||
// Receive the AUX message locally. | ||
self.received_aux.insert(self.uid.clone(), b); | ||
} | ||
|
||
let (decision, maybe_message) = self.try_coin(); | ||
outgoing.extend(maybe_message); | ||
Ok((decision, outgoing)) | ||
} | ||
// upon receiving BVAL_r(b) messages from f + 1 nodes, if | ||
// BVAL_r(b) has not been sent, multicast BVAL_r(b) | ||
else if count_bval == self.num_faulty_nodes + 1 && !self.sent_bval.contains(&b) { | ||
outgoing.push_back(AgreementMessage::BVal((self.epoch, b))); | ||
// Receive the BVAL message locally. | ||
self.received_bval | ||
.entry(self.uid.clone()) | ||
.or_insert_with(BTreeSet::new) | ||
.insert(b); | ||
Ok((None, outgoing)) | ||
} else { | ||
Ok((None, outgoing)) | ||
} | ||
} | ||
|
||
fn handle_aux(&mut self, sender_id: &NodeUid, b: bool) -> Result<AgreementOutput, Error> { | ||
self.received_aux.insert(sender_id.clone(), b); | ||
let mut outgoing = VecDeque::new(); | ||
if !self.bin_values.is_empty() { | ||
let (decision, maybe_message) = self.try_coin(); | ||
outgoing.extend(maybe_message); | ||
Ok((decision, outgoing)) | ||
} else { | ||
Ok((None, outgoing)) | ||
} | ||
} | ||
|
||
/// AUX_r messages such that the set of values carried by those messages is | ||
/// a subset of bin_values_r. Outputs this subset. | ||
/// | ||
/// FIXME: Clarify whether the values of AUX messages should be the same or | ||
/// not. It is assumed in `count_aux` that they can differ. | ||
This comment was marked as resolved.
Sorry, something went wrong. |
||
/// | ||
/// In general, we can't expect every good node to send the same AUX value, | ||
/// so waiting for N - f agreeing messages would not always terminate. We | ||
/// can, however, expect every good node to send an AUX value that will | ||
/// eventually end up in our bin_values. | ||
fn count_aux(&self) -> (usize, BTreeSet<bool>) { | ||
let (vals_cnt, vals) = self.received_aux | ||
.values() | ||
.filter(|b| self.bin_values.contains(b)) | ||
.tee(); | ||
|
||
(vals_cnt.count(), vals.cloned().collect()) | ||
} | ||
|
||
/// Waits until at least (N − f) AUX_r messages have been received, such that | ||
/// the set of values carried by these messages, vals, are a subset of | ||
/// bin_values_r (note that bin_values_r may continue to change as BVAL_r | ||
/// messages are received, thus this condition may be triggered upon arrival | ||
/// of either an AUX_r or a BVAL_r message). | ||
/// | ||
/// Once the (N - f) messages are received, gets a common coin and uses it | ||
/// to compute the next decision estimate and outputs the optional decision | ||
/// value. The function may start the next epoch. In that case, it also | ||
/// returns a message for broadcast. | ||
fn try_coin(&mut self) -> (Option<bool>, VecDeque<AgreementMessage>) { | ||
let (count_aux, vals) = self.count_aux(); | ||
if count_aux < self.num_nodes - self.num_faulty_nodes { | ||
// Continue waiting for the (N - f) AUX messages. | ||
return (None, VecDeque::new()); | ||
} | ||
|
||
// FIXME: Implement the Common Coin algorithm. At the moment the | ||
// coin value is common across different nodes but not random. | ||
let coin = (self.epoch % 2) == 0; | ||
|
||
// Check the termination condition: "continue looping until both a | ||
// value b is output in some round r, and the value Coin_r' = b for | ||
// some round r' > r." | ||
self.terminated = self.terminated || self.output == Some(coin); | ||
|
||
// Start the next epoch. | ||
self.bin_values.clear(); | ||
self.received_aux.clear(); | ||
self.epoch += 1; | ||
|
||
let decision = if vals.len() != 1 { | ||
self.estimated = Some(coin); | ||
None | ||
} else { | ||
// NOTE: `vals` has exactly one element due to `vals.len() == 1` | ||
let v: Vec<bool> = vals.into_iter().collect(); | ||
let b = v[0]; | ||
self.estimated = Some(b); | ||
// Outputting a value is allowed only once. | ||
if self.output.is_none() && b == coin { | ||
// Output the agreement value. | ||
self.output = Some(b); | ||
self.output | ||
} else { | ||
None | ||
} | ||
}; | ||
|
||
( | ||
decision, | ||
vec![AgreementMessage::BVal(( | ||
self.epoch, | ||
self.estimated.unwrap(), | ||
))].into_iter() | ||
.collect(), | ||
) | ||
} | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
pub enum Error { | ||
NotImplemented, | ||
Terminated, | ||
InputNotAccepted, | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rustfmt doesn't work with anything but Rust sources:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry! You're right, of course, and indentation 2 is fine in a proto file!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed indentation to 4 spaces and committed.