From 2874b36ff46dbdcdedae89f760bf3f79d86af481 Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Wed, 28 Aug 2024 23:07:24 +0300 Subject: [PATCH 1/9] feat: refactored --- src/error.rs | 257 ++++++++++++++--- src/leader.rs | 109 +++++++ src/lib.rs | 9 +- src/message.rs | 114 +++++--- src/party.rs | 754 ++++++++++++++++++++++++++----------------------- 5 files changed, 809 insertions(+), 434 deletions(-) create mode 100644 src/leader.rs diff --git a/src/error.rs b/src/error.rs index b9d84b8..0cab4e1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,56 +1,239 @@ //! Definition of the BPCon errors. -use std::fmt; +use crate::party::PartyStatus; +use crate::Value; +use std::fmt::{Display, Formatter, Result}; -#[derive(Debug)] -pub enum BallotError { - MessageParsing(String), - ValueParsing(String), - InvalidState(String), - Communication(String), - LeaderElection(String), +pub enum LaunchBallotError { + FailedToSendEvent(String), + EventChannelClosed, + MessageChannelClosed, + FollowEventError(FollowEventError), + LeaderElectionError(String), } -impl fmt::Display for BallotError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - BallotError::MessageParsing(ref err) => write!(f, "Message parsing error: {}", err), - BallotError::ValueParsing(ref err) => write!(f, "Value parsing error: {}", err), - BallotError::InvalidState(ref err) => write!(f, "Invalid state error: {}", err), - BallotError::Communication(ref err) => write!(f, "Communication error: {}", err), - BallotError::LeaderElection(ref err) => write!(f, "Leader election error: {}", err), +pub enum FollowEventError { + PartyStatusMismatch(PartyStatusMismatch), + SerializationError(SerializationError), + FailedToSendMessage(String), +} + +pub enum UpdateStateError { + PartyStatusMismatch(PartyStatusMismatch), + BallotNumberMismatch(BallotNumberMismatch), + LeaderMismatch(LeaderMismatch), + ValueMismatch(ValueMismatch), + ValueVerificationFailed, + DeserializationError(DeserializationError), +} + +pub struct PartyStatusMismatch { + pub party_status: PartyStatus, + pub needed_status: PartyStatus, +} + +pub struct BallotNumberMismatch { + pub party_ballot_number: u64, + pub message_ballot_number: u64, +} + +pub struct LeaderMismatch { + pub party_leader: u64, + pub message_sender: u64, +} + +pub struct ValueMismatch { + pub party_value: V, + pub message_value: V, +} + +pub enum DeserializationError { + Message(String), + Value(String), +} + +pub enum SerializationError { + Message(String), + Value(String), +} + +impl From for LaunchBallotError { + fn from(error: FollowEventError) -> Self { + LaunchBallotError::FollowEventError(error) + } +} + +impl From for FollowEventError { + fn from(error: PartyStatusMismatch) -> Self { + FollowEventError::PartyStatusMismatch(error) + } +} + +impl From for FollowEventError { + fn from(error: SerializationError) -> Self { + FollowEventError::SerializationError(error) + } +} + +impl From for UpdateStateError { + fn from(error: PartyStatusMismatch) -> Self { + UpdateStateError::PartyStatusMismatch(error) + } +} + +impl From for UpdateStateError { + fn from(error: LeaderMismatch) -> Self { + UpdateStateError::LeaderMismatch(error) + } +} + +impl From for UpdateStateError { + fn from(error: BallotNumberMismatch) -> Self { + UpdateStateError::BallotNumberMismatch(error) + } +} + +impl From> for UpdateStateError { + fn from(error: ValueMismatch) -> Self { + UpdateStateError::ValueMismatch(error) + } +} + +impl From for UpdateStateError { + fn from(error: DeserializationError) -> Self { + UpdateStateError::DeserializationError(error) + } +} + +impl Display for PartyStatusMismatch { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "Party status mismatch: party status is {:?} whilst needed status is {:?}.", + self.party_status, self.needed_status + ) + } +} + +impl Display for BallotNumberMismatch { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "Ballot number mismatch: party's ballot number is {} whilst received {} in the message.", + self.party_ballot_number, self.message_ballot_number + ) + } +} + +impl Display for LeaderMismatch { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "Leader mismatch: party's leader is {} whilst the message was sent by {}.", + self.party_leader, self.message_sender + ) + } +} + +impl Display for ValueMismatch { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "Value mismatch: party's value is {} whilst received {} in the message.", + self.party_value, self.message_value + ) + } +} + +impl Display for DeserializationError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + DeserializationError::Message(err) => { + write!(f, "Message deserialization error: {}", err) + } + DeserializationError::Value(err) => { + write!(f, "Value deserialization error: {}", err) + } } } } -impl std::error::Error for BallotError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - // Since these are all simple String errors, there is no underlying source error. - None +impl Display for SerializationError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + SerializationError::Message(err) => { + write!(f, "Message serialization error: {}", err) + } + SerializationError::Value(err) => { + write!(f, "Value serialization error: {}", err) + } + } } } -#[cfg(test)] -mod tests { - use super::*; +impl Display for LaunchBallotError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match *self { + LaunchBallotError::FailedToSendEvent(ref err) => write!( + f, + "Got error while running ballot: failed to send event: {}", + err + ), + LaunchBallotError::EventChannelClosed => { + write!(f, "Got error while running ballot: event channel closed") + } + LaunchBallotError::MessageChannelClosed => { + write!(f, "Got error while running ballot: message channel closed") + } + LaunchBallotError::FollowEventError(ref err) => { + write!(f, "Got error while running ballot: {}", err) + } + LaunchBallotError::LeaderElectionError(ref err) => write!( + f, + "Got error while running ballot: leader election error{}", + err + ), + } + } +} - #[test] - fn test_ballot_error_message_parsing() { - let error = BallotError::MessageParsing("Parsing failed".into()); - if let BallotError::MessageParsing(msg) = error { - assert_eq!(msg, "Parsing failed"); - } else { - panic!("Expected MessageParsing error"); +impl Display for FollowEventError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match *self { + FollowEventError::PartyStatusMismatch(ref err) => { + write!(f, "Unable to follow event: {}", err) + } + FollowEventError::SerializationError(ref err) => { + write!(f, "Unable to follow event: {}", err) + } + FollowEventError::FailedToSendMessage(ref err) => { + write!(f, "Unable to follow event: {}", err) + } } } +} - #[test] - fn test_ballot_error_invalid_state() { - let error = BallotError::InvalidState("Invalid state transition".into()); - if let BallotError::InvalidState(msg) = error { - assert_eq!(msg, "Invalid state transition"); - } else { - panic!("Expected InvalidState error"); +impl Display for UpdateStateError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match *self { + UpdateStateError::PartyStatusMismatch(ref err) => { + write!(f, "Unable to update state: {}", err) + } + UpdateStateError::BallotNumberMismatch(ref err) => { + write!(f, "Unable to update state: {}", err) + } + UpdateStateError::LeaderMismatch(ref err) => { + write!(f, "Unable to update state: {}", err) + } + UpdateStateError::ValueMismatch(ref err) => { + write!(f, "Unable to update state: {}", err) + } + UpdateStateError::ValueVerificationFailed => { + write!(f, "Unable to update state: value verification failed") + } + UpdateStateError::DeserializationError(ref err) => { + write!(f, "Unable to update state: {}", err) + } } } } diff --git a/src/leader.rs b/src/leader.rs new file mode 100644 index 0000000..248dc9f --- /dev/null +++ b/src/leader.rs @@ -0,0 +1,109 @@ +use crate::party::Party; +use crate::{Value, ValueSelector}; +use seeded_random::{Random, Seed}; +use std::cmp::Ordering; +use std::hash::{DefaultHasher, Hash, Hasher}; + +/// Trait incorporating logic for leader election. +pub trait LeaderElector>: Send { + type LeaderElectorError; + + /// Get leader for current ballot. + /// Returns id of the elected party or error. + fn get_leader(&self, party: &Party) -> Result; +} + +#[derive(Clone, Debug)] +pub struct DefaultLeaderElector {} + +impl DefaultLeaderElector { + /// Compute seed for randomized leader election. + fn compute_seed>(party: &Party) -> u64 { + let mut hasher = DefaultHasher::new(); + + // Hash each field that should contribute to the seed + party.cfg.party_weights.hash(&mut hasher); + party.cfg.threshold.hash(&mut hasher); + party.ballot.hash(&mut hasher); + + // You can add more fields as needed + + // Generate the seed from the hash + hasher.finish() + } + + /// Hash the seed to a value within a given range. + fn hash_to_range(seed: u64, range: u64) -> u64 { + // Select the `k` suck that value 2^k >= `range` and 2^k is the smallest. + let mut k = 64; + while 1u64 << (k - 1) >= range { + k -= 1; + } + + // The following algorithm selects a random u64 value using `ChaCha12Rng` + // and reduces the result to the k-bits such that 2^k >= `range` the closes power of to the `range`. + // After we check if the result lies in [0..`range`) or [`range`..2^k). + // In the first case result is an acceptable value generated uniformly. + // In the second case we repeat the process again with the incremented iterations counter. + // Ref: Practical Cryptography 1st Edition by Niels Ferguson, Bruce Schneier, paragraph 10.8 + let rng = Random::from_seed(Seed::unsafe_new(seed)); + loop { + let mut raw_res: u64 = rng.gen(); + raw_res >>= 64 - k; + + if raw_res < range { + return raw_res; + } + // Executing this loop does not require a large number of iterations. + // Check tests for more info + } + } +} + +pub enum DefaultLeaderElectorError { + ZeroWeightSum, +} +// +// impl Display for DefaultLeaderElectorError { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// match *self { +// DefaultLeaderElectorError::ZeroWeightSum => write!(f, "Leader election error: zero weight sum"), +// } +// } +// } + +impl> LeaderElector for DefaultLeaderElector { + type LeaderElectorError = DefaultLeaderElectorError; + + /// Compute leader in a weighed randomized manner. + /// Uses seed from the config, making it deterministic. + fn get_leader(&self, party: &Party) -> Result { + let seed = DefaultLeaderElector::compute_seed(party); + + let total_weight: u64 = party.cfg.party_weights.iter().sum(); + if total_weight == 0 { + return Err(DefaultLeaderElectorError::ZeroWeightSum); + } + + // Generate a random number in the range [0, total_weight) + let random_value = DefaultLeaderElector::hash_to_range(seed, total_weight); + + // Use binary search to find the corresponding participant + let mut cumulative_weights = vec![0; party.cfg.party_weights.len()]; + cumulative_weights[0] = party.cfg.party_weights[0]; + + for i in 1..party.cfg.party_weights.len() { + cumulative_weights[i] = cumulative_weights[i - 1] + party.cfg.party_weights[i]; + } + + match cumulative_weights.binary_search_by(|&weight| { + if random_value < weight { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(index) | Err(index) => Ok(index as u64), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index afa03b0..2a24a53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,21 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt; +use std::fmt::Debug; -mod error; +pub mod error; +pub mod leader; pub mod message; pub mod party; /// General trait for value itself. -pub trait Value: Eq + Serialize + for<'a> Deserialize<'a> + Clone {} +pub trait Value: Eq + Serialize + for<'a> Deserialize<'a> + Clone + Debug + fmt::Display {} /// Trait for value selector and verificator. /// Value selection and verification may depend on different conditions for different values. /// Note that value selection should follow the rules of BPCon: only safe values can be selected. /// Party can not vote for different values, even in different ballots. -pub trait ValueSelector { +pub trait ValueSelector: Clone { /// Verifies if a value is selected correctly. Accepts 2b messages from parties. fn verify(&self, v: &V, m: &HashMap>) -> bool; diff --git a/src/message.rs b/src/message.rs index daa86ab..b2d3551 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,8 +1,11 @@ //! Definition of the BPCon messages. -use rkyv::{AlignedVec, Archive, Deserialize, Serialize}; +use crate::error::{DeserializationError, SerializationError}; +use rkyv::{AlignedVec, Archive, Deserialize, Infallible, Serialize}; +use std::collections::HashSet; /// Message ready for transfer. +#[derive(Debug, Clone)] pub struct MessagePacket { /// Serialized message contents. pub content_bytes: AlignedVec, @@ -11,18 +14,16 @@ pub struct MessagePacket { } /// Full routing information for the message. +#[derive(Debug, Copy, Clone)] pub struct MessageRouting { /// Which participant this message came from. pub sender: u64, - /// Where this message should be delivered. Can be empty if `is_broadcast` is `true` - pub receivers: Vec, - /// Indicates whether this message shall be broadcast to other participants. - pub is_broadcast: bool, /// Stores the BPCon message type. pub msg_type: ProtocolMessage, } /// Representation of message types of the consensus. +#[derive(Debug, Copy, Clone)] pub enum ProtocolMessage { Msg1a, Msg1b, @@ -73,57 +74,76 @@ pub struct Message2bContent { pub ballot: u64, } -impl Message1aContent { - pub fn get_routing(id: u64) -> MessageRouting { - MessageRouting { - sender: id, - receivers: vec![], - is_broadcast: true, - msg_type: ProtocolMessage::Msg1a, +macro_rules! impl_packable { + ($type:ty, $msg_type:expr) => { + impl $type { + pub fn pack(&self, sender: u64) -> Result { + let content_bytes = rkyv::to_bytes::<_, 256>(self) + .map_err(|err| SerializationError::Message(err.to_string()))?; + Ok(MessagePacket { + content_bytes, + routing: Self::route(sender), + }) + } + + pub fn unpack(msg: &MessagePacket) -> Result { + let archived = rkyv::check_archived_root::(msg.content_bytes.as_slice()) + .map_err(|err| DeserializationError::Message(err.to_string()))?; + archived + .deserialize(&mut Infallible) + .map_err(|err| DeserializationError::Message(err.to_string())) + } + + fn route(sender: u64) -> MessageRouting { + MessageRouting { + sender, + msg_type: $msg_type, + } + } } - } + }; } -impl Message1bContent { - pub fn get_routing(id: u64) -> MessageRouting { - MessageRouting { - sender: id, - receivers: vec![], - is_broadcast: true, - msg_type: ProtocolMessage::Msg1b, - } - } +impl_packable!(Message1aContent, ProtocolMessage::Msg1a); +impl_packable!(Message1bContent, ProtocolMessage::Msg1b); +impl_packable!(Message2aContent, ProtocolMessage::Msg2a); +impl_packable!(Message2avContent, ProtocolMessage::Msg2av); +impl_packable!(Message2bContent, ProtocolMessage::Msg2b); + +/// A struct to keep track of senders and the cumulative weight of their messages. +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct MessageRoundState { + senders: HashSet, + weight: u128, } -impl Message2aContent { - pub fn get_routing(id: u64) -> MessageRouting { - MessageRouting { - sender: id, - receivers: vec![], - is_broadcast: true, - msg_type: ProtocolMessage::Msg2a, +impl MessageRoundState { + /// Creates a new instance of `MessageRoundState`. + pub fn new() -> Self { + Self { + senders: HashSet::new(), + weight: 0, } } -} -impl Message2avContent { - pub fn get_routing(id: u64) -> MessageRouting { - MessageRouting { - sender: id, - receivers: vec![], - is_broadcast: true, - msg_type: ProtocolMessage::Msg2av, - } + pub fn get_weight(&self) -> u128 { + self.weight } -} -impl Message2bContent { - pub fn get_routing(id: u64) -> MessageRouting { - MessageRouting { - sender: id, - receivers: vec![], - is_broadcast: true, - msg_type: ProtocolMessage::Msg2b, - } + /// Adds a sender and their corresponding weight. + pub fn add_sender(&mut self, sender: u64, weight: u128) { + self.senders.insert(sender); + self.weight += weight; + } + + /// Checks if the sender has already sent a message. + pub fn contains_sender(&self, sender: &u64) -> bool { + self.senders.contains(sender) + } + + /// Resets the state. + pub fn reset(&mut self) { + self.senders.clear(); + self.weight = 0; } } diff --git a/src/party.rs b/src/party.rs index c32ddd1..60c6fb5 100644 --- a/src/party.rs +++ b/src/party.rs @@ -1,18 +1,24 @@ //! Definition of the BPCon participant structure. -use crate::error::BallotError; +use crate::error::FollowEventError::FailedToSendMessage; +use crate::error::LaunchBallotError::{ + EventChannelClosed, FailedToSendEvent, LeaderElectionError, MessageChannelClosed, +}; +use crate::error::{ + BallotNumberMismatch, DeserializationError, FollowEventError, LaunchBallotError, + LeaderMismatch, PartyStatusMismatch, SerializationError, UpdateStateError, ValueMismatch, +}; +use crate::leader::LeaderElector; use crate::message::{ Message1aContent, Message1bContent, Message2aContent, Message2avContent, Message2bContent, - MessagePacket, MessageRouting, ProtocolMessage, + MessagePacket, MessageRoundState, ProtocolMessage, }; use crate::{Value, ValueSelector}; -use rkyv::{AlignedVec, Deserialize, Infallible}; -use seeded_random::{Random, Seed}; -use std::cmp::{Ordering, PartialEq}; -use std::collections::hash_map::DefaultHasher; +use log::warn; +use std::cmp::PartialEq; use std::collections::hash_map::Entry::Vacant; -use std::collections::{HashMap, HashSet}; -use std::hash::{Hash, Hasher}; +use std::collections::HashMap; +use std::error::Error; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::time::{self, Duration}; @@ -54,7 +60,7 @@ pub struct BPConConfig { /// Party status defines the statuses of the ballot for the particular participant /// depending on local calculations. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] pub(crate) enum PartyStatus { None, Launched, @@ -68,7 +74,7 @@ pub(crate) enum PartyStatus { } /// Party events is used for the ballot flow control. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] pub(crate) enum PartyEvent { Launch1a, Launch1b, @@ -78,126 +84,6 @@ pub(crate) enum PartyEvent { Finalize, } -/// A struct to keep track of senders and the cumulative weight of their messages. -#[derive(PartialEq, Debug)] -struct MessageRoundState { - senders: HashSet, - weight: u128, -} - -impl MessageRoundState { - /// Creates a new instance of `MessageRoundState`. - fn new() -> Self { - Self { - senders: HashSet::new(), - weight: 0, - } - } - - /// Adds a sender and their corresponding weight. - fn add_sender(&mut self, sender: u64, weight: u128) { - self.senders.insert(sender); - self.weight += weight; - } - - /// Checks if the sender has already sent a message. - fn contains_sender(&self, sender: &u64) -> bool { - self.senders.contains(sender) - } - - /// Resets the state. - fn reset(&mut self) { - self.senders.clear(); - self.weight = 0; - } -} - -/// Trait incorporating logic for leader election. -pub trait LeaderElector>: Send { - /// Get leader for current ballot. - /// Returns id of the elected party or error. - fn get_leader(&self, party: &Party) -> Result; -} - -pub struct DefaultLeaderElector {} - -impl DefaultLeaderElector { - /// Compute seed for randomized leader election. - fn compute_seed>(party: &Party) -> u64 { - let mut hasher = DefaultHasher::new(); - - // Hash each field that should contribute to the seed - party.cfg.party_weights.hash(&mut hasher); - party.cfg.threshold.hash(&mut hasher); - party.ballot.hash(&mut hasher); - - // You can add more fields as needed - - // Generate the seed from the hash - hasher.finish() - } - - /// Hash the seed to a value within a given range. - fn hash_to_range(seed: u64, range: u64) -> u64 { - // Select the `k` suck that value 2^k >= `range` and 2^k is the smallest. - let mut k = 64; - while 1u64 << (k - 1) >= range { - k -= 1; - } - - // The following algorithm selects a random u64 value using `ChaCha12Rng` - // and reduces the result to the k-bits such that 2^k >= `range` the closes power of to the `range`. - // After we check if the result lies in [0..`range`) or [`range`..2^k). - // In the first case result is an acceptable value generated uniformly. - // In the second case we repeat the process again with the incremented iterations counter. - // Ref: Practical Cryptography 1st Edition by Niels Ferguson, Bruce Schneier, paragraph 10.8 - let rng = Random::from_seed(Seed::unsafe_new(seed)); - loop { - let mut raw_res: u64 = rng.gen(); - raw_res >>= 64 - k; - - if raw_res < range { - return raw_res; - } - // Executing this loop does not require a large number of iterations. - // Check tests for more info - } - } -} - -impl> LeaderElector for DefaultLeaderElector { - /// Compute leader in a weighed randomized manner. - /// Uses seed from the config, making it deterministic. - fn get_leader(&self, party: &Party) -> Result { - let seed = DefaultLeaderElector::compute_seed(party); - - let total_weight: u64 = party.cfg.party_weights.iter().sum(); - if total_weight == 0 { - return Err(BallotError::LeaderElection("Zero weight sum".into())); - } - - // Generate a random number in the range [0, total_weight) - let random_value = DefaultLeaderElector::hash_to_range(seed, total_weight); - - // Use binary search to find the corresponding participant - let mut cumulative_weights = vec![0; party.cfg.party_weights.len()]; - cumulative_weights[0] = party.cfg.party_weights[0]; - - for i in 1..party.cfg.party_weights.len() { - cumulative_weights[i] = cumulative_weights[i - 1] + party.cfg.party_weights[i]; - } - - match cumulative_weights.binary_search_by(|&weight| { - if random_value < weight { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(index) | Err(index) => Ok(index as u64), - } - } -} /// Party of the BPCon protocol that executes ballot. /// /// The communication between party and external @@ -222,19 +108,19 @@ pub struct Party> { event_sender: UnboundedSender, /// BPCon config (e.g. ballot time bounds, parties weights, etc.). - cfg: BPConConfig, + pub(crate) cfg: BPConConfig, /// Main functional for value selection. value_selector: VS, /// Main functional for leader election. - elector: Box>, + elector: Box>>, /// Status of the ballot execution status: PartyStatus, /// Current ballot number - ballot: u64, + pub(crate) ballot: u64, /// Current ballot leader leader: u64, @@ -270,7 +156,7 @@ impl> Party { id: u64, cfg: BPConConfig, value_selector: VS, - elector: Box>, + elector: Box>>, ) -> ( Self, UnboundedReceiver, @@ -331,11 +217,14 @@ impl> Party { self.value_selector.select(&self.parties_voted_before) } - pub async fn launch_ballot(&mut self) -> Result, BallotError> { - self.prepare_next_ballot()?; - time::sleep(self.cfg.launch_timeout).await; + pub async fn launch_ballot(&mut self) -> Result, LaunchBallotError> { + self.prepare_next_ballot(); + self.leader = self + .elector + .get_leader(self) + .map_err(|err| LeaderElectionError(err.to_string()))?; - self.status = PartyStatus::Launched; + time::sleep(self.cfg.launch_timeout).await; let launch1a_timer = time::sleep(self.cfg.launch1a_timeout); let launch1b_timer = time::sleep(self.cfg.launch1b_timeout); @@ -363,69 +252,69 @@ impl> Party { while self.is_launched() { tokio::select! { _ = &mut launch1a_timer, if !launch1a_fired => { - self.event_sender.send(PartyEvent::Launch1a).map_err(|_| { + self.event_sender.send(PartyEvent::Launch1a).map_err(|err| { self.status = PartyStatus::Failed; - BallotError::Communication("Failed to send Launch1a event".into()) + FailedToSendEvent("Failed to send Launch1a event".into()) })?; launch1a_fired = true; }, _ = &mut launch1b_timer, if !launch1b_fired => { self.event_sender.send(PartyEvent::Launch1b).map_err(|_| { self.status = PartyStatus::Failed; - BallotError::Communication("Failed to send Launch1b event".into()) + FailedToSendEvent("Failed to send Launch1a event".into()) })?; launch1b_fired = true; }, _ = &mut launch2a_timer, if !launch2a_fired => { self.event_sender.send(PartyEvent::Launch2a).map_err(|_| { self.status = PartyStatus::Failed; - BallotError::Communication("Failed to send Launch2a event".into()) + FailedToSendEvent("Failed to send Launch1a event".into()) })?; launch2a_fired = true; }, _ = &mut launch2av_timer, if !launch2av_fired => { self.event_sender.send(PartyEvent::Launch2av).map_err(|_| { self.status = PartyStatus::Failed; - BallotError::Communication("Failed to send Launch2av event".into()) + FailedToSendEvent("Failed to send Launch1a event".into()) })?; launch2av_fired = true; }, _ = &mut launch2b_timer, if !launch2b_fired => { self.event_sender.send(PartyEvent::Launch2b).map_err(|_| { self.status = PartyStatus::Failed; - BallotError::Communication("Failed to send Launch2b event".into()) + FailedToSendEvent("Failed to send Launch1a event".into()) })?; launch2b_fired = true; }, _ = &mut finalize_timer, if !finalize_fired => { self.event_sender.send(PartyEvent::Finalize).map_err(|_| { self.status = PartyStatus::Failed; - BallotError::Communication("Failed to send Finalize event".into()) + FailedToSendEvent("Failed to send Launch1a event".into()) })?; finalize_fired = true; }, - msg_wire = self.msg_in_receiver.recv() => { - tokio::time::sleep(self.cfg.grace_period).await; - if let Some(msg_wire) = msg_wire { - if let Err(err) = self.update_state(msg_wire.content_bytes, msg_wire.routing) { - self.status = PartyStatus::Failed; - return Err(err); + msg = self.msg_in_receiver.recv() => { + time::sleep(self.cfg.grace_period).await; + if let Some(msg) = msg { + if let Err(err) = self.update_state(&msg) { + // shall not fail the party, since invalid message may be sent by anyone + warn!("Unable to update state, got error: {err}") } }else if self.msg_in_receiver.is_closed(){ self.status = PartyStatus::Failed; - return Err(BallotError::Communication("msg-in channel closed".into())); + return Err(MessageChannelClosed.into()) } }, event = self.event_receiver.recv() => { - tokio::time::sleep(self.cfg.grace_period).await; + time::sleep(self.cfg.grace_period).await; if let Some(event) = event { if let Err(err) = self.follow_event(event) { self.status = PartyStatus::Failed; - return Err(err); + return Err(err.into()); } }else if self.event_receiver.is_closed(){ self.status = PartyStatus::Failed; - return Err(BallotError::Communication("event receiver channel closed".into())); + return Err(EventChannelClosed.into()) } }, } @@ -435,10 +324,9 @@ impl> Party { } /// Prepare state before running a ballot. - fn prepare_next_ballot(&mut self) -> Result<(), BallotError> { + fn prepare_next_ballot(&mut self) { self.status = PartyStatus::None; self.ballot += 1; - self.leader = self.elector.get_leader(self)?; // Clean state self.parties_voted_before = HashMap::new(); @@ -452,77 +340,79 @@ impl> Party { while self.msg_in_receiver.try_recv().is_ok() {} self.status = PartyStatus::Launched; - Ok(()) } /// Update party's state based on message type. - fn update_state(&mut self, m: AlignedVec, routing: MessageRouting) -> Result<(), BallotError> { + fn update_state(&mut self, msg: &MessagePacket) -> Result<(), UpdateStateError> { + let routing = msg.routing; + match routing.msg_type { ProtocolMessage::Msg1a => { if self.status != PartyStatus::Launched { - return Err(BallotError::InvalidState( - "Received Msg1a message, while party status is not ".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Launched, + } + .into()); } - let archived = - rkyv::check_archived_root::(&m[..]).map_err(|err| { - BallotError::MessageParsing(format!("Validation error: {:?}", err)) - })?; - let msg: Message1aContent = - archived.deserialize(&mut Infallible).map_err(|err| { - BallotError::MessageParsing(format!("Deserialization error: {:?}", err)) - })?; + let msg = Message1aContent::unpack(&msg)?; if msg.ballot != self.ballot { - return Err(BallotError::InvalidState( - "Ballot number mismatch in Msg1a".into(), - )); + return Err(BallotNumberMismatch { + party_ballot_number: self.ballot, + message_ballot_number: msg.ballot, + } + .into()); } if routing.sender != self.leader { - return Err(BallotError::InvalidState("Invalid leader in Msg1a".into())); + return Err(LeaderMismatch { + party_leader: self.leader, + message_sender: routing.sender, + } + .into()); } self.status = PartyStatus::Passed1a; } ProtocolMessage::Msg1b => { if self.status != PartyStatus::Passed1a { - return Err(BallotError::InvalidState( - "Received Msg1b message, while party status is not ".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed1a, + } + .into()); } - let archived = - rkyv::check_archived_root::(&m[..]).map_err(|err| { - BallotError::MessageParsing(format!("Validation error: {:?}", err)) - })?; - let msg: Message1bContent = - archived.deserialize(&mut Infallible).map_err(|err| { - BallotError::MessageParsing(format!("Deserialization error: {:?}", err)) - })?; + let msg = Message1bContent::unpack(&msg)?; if msg.ballot != self.ballot { - return Err(BallotError::InvalidState( - "Ballot number mismatch in Msg1b".into(), - )); + return Err(BallotNumberMismatch { + party_ballot_number: self.ballot, + message_ballot_number: msg.ballot, + } + .into()); } if let Some(last_ballot_voted) = msg.last_ballot_voted { if last_ballot_voted >= self.ballot { - return Err(BallotError::InvalidState( - "Received outdated 1b message".into(), - )); + return Err(BallotNumberMismatch { + party_ballot_number: self.ballot, + message_ballot_number: msg.ballot, + } + .into()); } } if let Vacant(e) = self.parties_voted_before.entry(routing.sender) { - let value: Option = match msg.last_value_voted { - Some(ref data) => Some(bincode::deserialize(data).map_err(|err| { - BallotError::ValueParsing(format!("Deserialization error: {:?}", err)) - })?), - None => None, - }; + let value: Option = + match msg.last_value_voted { + Some(ref data) => Some(bincode::deserialize(data).map_err(|err| { + DeserializationError::Value(err.to_string()).into() + })?), + None => None, + }; e.insert(value); @@ -536,33 +426,33 @@ impl> Party { } ProtocolMessage::Msg2a => { if self.status != PartyStatus::Passed1b { - return Err(BallotError::InvalidState( - "Received Msg2a message, while party status is not ".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed1b, + } + .into()); } - let archived = - rkyv::check_archived_root::(&m[..]).map_err(|err| { - BallotError::MessageParsing(format!("Validation error: {:?}", err)) - })?; - let msg: Message2aContent = - archived.deserialize(&mut Infallible).map_err(|err| { - BallotError::MessageParsing(format!("Deserialization error: {:?}", err)) - })?; + let msg = Message2aContent::unpack(&msg)?; if msg.ballot != self.ballot { - return Err(BallotError::InvalidState( - "Ballot number mismatch in Msg2a".into(), - )); + return Err(BallotNumberMismatch { + party_ballot_number: self.ballot, + message_ballot_number: msg.ballot, + } + .into()); } if routing.sender != self.leader { - return Err(BallotError::InvalidState("Invalid leader in Msg2a".into())); + return Err(LeaderMismatch { + party_leader: self.leader, + message_sender: routing.sender, + } + .into()); } - let value_received = bincode::deserialize(&msg.value[..]).map_err(|err| { - BallotError::ValueParsing(format!("Failed to parse value in Msg2a: {:?}", err)) - })?; + let value_received = bincode::deserialize(&msg.value[..]) + .map_err(|err| DeserializationError::Value(err.to_string()).into())?; if self .value_selector @@ -571,44 +461,36 @@ impl> Party { self.status = PartyStatus::Passed2a; self.value_2a = Some(value_received); } else { - return Err(BallotError::InvalidState( - "Failed to verify value in Msg2a".into(), - )); + return Err(UpdateStateError::ValueVerificationFailed.into()); } } ProtocolMessage::Msg2av => { if self.status != PartyStatus::Passed2a { - return Err(BallotError::InvalidState( - "Received Msg2av message, while party status is not ".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed2a, + } + .into()); } - let archived = - rkyv::check_archived_root::(&m[..]).map_err(|err| { - BallotError::MessageParsing(format!("Validation error: {:?}", err)) - })?; - let msg: Message2avContent = - archived.deserialize(&mut Infallible).map_err(|err| { - BallotError::MessageParsing(format!("Deserialization error: {:?}", err)) - })?; + let msg = Message2avContent::unpack(&msg)?; if msg.ballot != self.ballot { - return Err(BallotError::InvalidState( - "Ballot number mismatch in Msg2av".into(), - )); + return Err(BallotNumberMismatch { + party_ballot_number: self.ballot, + message_ballot_number: msg.ballot, + } + .into()); } - let value_received: V = - bincode::deserialize(&msg.received_value[..]).map_err(|err| { - BallotError::ValueParsing(format!( - "Failed to parse value in Msg2av: {:?}", - err - )) - })?; + let value_received: V = bincode::deserialize(&msg.received_value[..]) + .map_err(|err| DeserializationError::Value(err.to_string()).into())?; if value_received != self.value_2a.clone().unwrap() { - return Err(BallotError::InvalidState( - "Received different value in Msg2av".into(), - )); + return Err(ValueMismatch { + party_value: self.value_2a.clone().unwrap(), + message_value: value_received.clone(), + } + .into()); } if !self.messages_2av_state.contains_sender(&routing.sender) { @@ -617,31 +499,28 @@ impl> Party { self.cfg.party_weights[routing.sender as usize] as u128, ); - if self.messages_2av_state.weight > self.cfg.threshold { + if self.messages_2av_state.get_weight() > self.cfg.threshold { self.status = PartyStatus::Passed2av; } } } ProtocolMessage::Msg2b => { if self.status != PartyStatus::Passed2av { - return Err(BallotError::InvalidState( - "Received Msg2b message, while party status is not ".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed2av, + } + .into()); } - let archived = - rkyv::check_archived_root::(&m[..]).map_err(|err| { - BallotError::MessageParsing(format!("Validation error: {:?}", err)) - })?; - let msg: Message2bContent = - archived.deserialize(&mut Infallible).map_err(|err| { - BallotError::MessageParsing(format!("Deserialization error: {:?}", err)) - })?; + let msg = Message2bContent::unpack(&msg)?; if msg.ballot != self.ballot { - return Err(BallotError::InvalidState( - "Ballot number mismatch in Msg2b".into(), - )); + return Err(BallotNumberMismatch { + party_ballot_number: self.ballot, + message_ballot_number: msg.ballot, + } + .into()); } if self.messages_2av_state.contains_sender(&routing.sender) @@ -652,7 +531,7 @@ impl> Party { self.cfg.party_weights[routing.sender as usize] as u128, ); - if self.messages_2b_state.weight > self.cfg.threshold { + if self.messages_2b_state.get_weight() > self.cfg.threshold { self.status = PartyStatus::Passed2b; } } @@ -662,126 +541,131 @@ impl> Party { } /// Executes ballot actions according to the received event. - fn follow_event(&mut self, event: PartyEvent) -> Result<(), BallotError> { + fn follow_event(&mut self, event: PartyEvent) -> Result<(), FollowEventError> { match event { PartyEvent::Launch1a => { if self.status != PartyStatus::Launched { - return Err(BallotError::InvalidState( - "Cannot launch 1a, incorrect state".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Launched, + } + .into()); } + + let content = &Message1aContent { + ballot: self.ballot, + }; + let msg = content.pack(self.id)?; + if self.leader == self.id { self.msg_out_sender - .send(MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&Message1aContent { - ballot: self.ballot, - }) - .map_err(|_| { - BallotError::MessageParsing("Failed to serialize Msg1a".into()) - })?, - routing: Message1aContent::get_routing(self.id), - }) - .map_err(|_| BallotError::Communication("Failed to send Msg1a".into()))?; + .send(msg) + .map_err(|err| FailedToSendMessage(err.to_string()).into())?; + self.status = PartyStatus::Passed1a; } } PartyEvent::Launch1b => { if self.status != PartyStatus::Passed1a { - return Err(BallotError::InvalidState( - "Cannot launch 1b, incorrect state".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed1a, + } + .into()); } - self.msg_out_sender - .send(MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&Message1bContent { - ballot: self.ballot, - last_ballot_voted: self.last_ballot_voted, - last_value_voted: self - .last_value_voted - .clone() - .map(|inner_data| { - bincode::serialize(&inner_data).map_err(|_| { - BallotError::ValueParsing( - "Failed to serialize value".into(), - ) - }) - }) - .transpose()?, - }) - .map_err(|_| { - BallotError::MessageParsing("Failed to serialize Msg1b".into()) - })?, - routing: Message1bContent::get_routing(self.id), + + let last_value_voted = self + .last_value_voted + .clone() + .map(|inner_data| { + bincode::serialize(&inner_data) + .map_err(|err| SerializationError::Value(err.to_string()).into()) }) - .map_err(|_| BallotError::Communication("Failed to send Msg1b".into()))?; + .transpose()?; + + let content = &Message1bContent { + ballot: self.ballot, + last_ballot_voted: self.last_ballot_voted, + last_value_voted, + }; + let msg = content.pack(self.id)?; + + self.msg_out_sender + .send(msg) + .map_err(|err| FailedToSendMessage(err.to_string()).into())?; } PartyEvent::Launch2a => { if self.status != PartyStatus::Passed1b { - return Err(BallotError::InvalidState( - "Cannot launch 2a, incorrect state".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed1b, + } + .into()); } + + let value = bincode::serialize(&self.get_value()) + .map_err(|err| SerializationError::Value(err.to_string()).into())?; + + let content = &Message2aContent { + ballot: self.ballot, + value, + }; + let msg = content.pack(self.id)?; + if self.leader == self.id { self.msg_out_sender - .send(MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&Message2aContent { - ballot: self.ballot, - value: bincode::serialize(&self.get_value()).map_err(|_| { - BallotError::ValueParsing("Failed to serialize value".into()) - })?, - }) - .map_err(|_| { - BallotError::MessageParsing("Failed to serialize Msg2a".into()) - })?, - routing: Message2aContent::get_routing(self.id), - }) - .map_err(|_| BallotError::Communication("Failed to send Msg2a".into()))?; + .send(msg) + .map_err(|err| FailedToSendMessage(err.to_string()).into())?; } } PartyEvent::Launch2av => { if self.status != PartyStatus::Passed2a { - return Err(BallotError::InvalidState( - "Cannot launch 2av, incorrect state".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed2a, + } + .into()); } + + let received_value = bincode::serialize(&self.value_2a.clone()) + .map_err(|err| SerializationError::Value(err.to_string()).into())?; + + let content = &Message2avContent { + ballot: self.ballot, + received_value, + }; + let msg = content.pack(self.id)?; + self.msg_out_sender - .send(MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&Message2avContent { - ballot: self.ballot, - received_value: bincode::serialize(&self.value_2a.clone()).map_err( - |_| BallotError::ValueParsing("Failed to serialize value".into()), - )?, - }) - .map_err(|_| { - BallotError::MessageParsing("Failed to serialize Msg2av".into()) - })?, - routing: Message2avContent::get_routing(self.id), - }) - .map_err(|_| BallotError::Communication("Failed to send Msg2av".into()))?; + .send(msg) + .map_err(|err| FailedToSendMessage(err.to_string()).into())?; } PartyEvent::Launch2b => { if self.status != PartyStatus::Passed2av { - return Err(BallotError::InvalidState( - "Cannot launch 2b, incorrect state".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed2av, + } + .into()); } + + let content = &Message2bContent { + ballot: self.ballot, + }; + let msg = content.pack(self.id)?; + self.msg_out_sender - .send(MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&Message2bContent { - ballot: self.ballot, - }) - .map_err(|_| { - BallotError::MessageParsing("Failed to serialize Msg2b".into()) - })?, - routing: Message2bContent::get_routing(self.id), - }) - .map_err(|_| BallotError::Communication("Failed to send Msg2b".into()))?; + .send(msg) + .map_err(|err| FailedToSendMessage(err.to_string()).into())?; } PartyEvent::Finalize => { if self.status != PartyStatus::Passed2b { - return Err(BallotError::InvalidState( - "Cannot finalize, incorrect state".into(), - )); + return Err(PartyStatusMismatch { + party_status: self.status, + needed_status: PartyStatus::Passed2av, + } + .into()); } + self.status = PartyStatus::Finished; } } @@ -796,15 +680,23 @@ mod tests { use rand::Rng; use seeded_random::{Random, Seed}; use std::collections::HashMap; + use std::fmt::{Display, Formatter}; use std::thread; // Mock implementation of Value - #[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Debug)] struct MockValue(u64); // Simple mock type wrapping an integer + impl Display for MockValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "MockValue: {}", self) + } + } + impl Value for MockValue {} // Mock implementation of ValueSelector + #[derive(Clone)] struct MockValueSelector; impl ValueSelector for MockValueSelector { @@ -1163,7 +1055,9 @@ mod tests { .expect("Failed to follow Launch1a event"); // If the party is the leader and in the Launched state, the event should trigger a message. - assert_eq!(party.status, PartyStatus::Launched); // Status remains Launched, as no state change expected here + // And it's status shall update to Passed1a after sending 1a message, + // contrary to other participants, whose `Passed1a` updates only after receiving 1a message. + assert_eq!(party.status, PartyStatus::Passed1a); } #[test] @@ -1343,4 +1237,170 @@ mod tests { println!("{}", rng1.gen::()); println!("{}", rng2.gen::()); } + + #[tokio::test] + async fn test_end_to_end_ballot() { + // Configuration for the parties + let cfg = BPConConfig { + party_weights: vec![1, 2, 3, 4], // Total weight is 10 + threshold: 7, // 2/3 of total weight is ~6.67, so we set 7 as threshold + launch_timeout: Duration::from_secs(1), + launch1a_timeout: Duration::from_secs(5), + launch1b_timeout: Duration::from_secs(5), + launch2a_timeout: Duration::from_secs(5), + launch2av_timeout: Duration::from_secs(5), + launch2b_timeout: Duration::from_secs(5), + finalize_timeout: Duration::from_secs(5), + grace_period: Duration::from_secs(2), + }; + + // ValueSelector and LeaderElector instances + let value_selector = MockValueSelector; + let leader_elector = Box::new(DefaultLeaderElector {}); + + // Create 4 parties + let (mut party0, msg_out_receiver0, msg_in_sender0) = + Party::::new( + 0, + cfg.clone(), + value_selector.clone(), + leader_elector.clone(), + ); + let (mut party1, msg_out_receiver1, msg_in_sender1) = + Party::::new( + 1, + cfg.clone(), + value_selector.clone(), + leader_elector.clone(), + ); + let (mut party2, msg_out_receiver2, msg_in_sender2) = + Party::::new( + 2, + cfg.clone(), + value_selector.clone(), + leader_elector.clone(), + ); + let (mut party3, msg_out_receiver3, msg_in_sender3) = + Party::::new( + 3, + cfg.clone(), + value_selector.clone(), + leader_elector.clone(), + ); + + // Channels for receiving the selected values + let (value_sender0, value_receiver0) = tokio::sync::oneshot::channel(); + let (value_sender1, value_receiver1) = tokio::sync::oneshot::channel(); + let (value_sender2, value_receiver2) = tokio::sync::oneshot::channel(); + let (value_sender3, value_receiver3) = tokio::sync::oneshot::channel(); + + let leader = party0.elector.get_leader(&party0).unwrap(); + println!("Leader: {leader}"); + + // Launch ballot tasks for each party + let ballot_task0 = tokio::spawn(async move { + match party0.launch_ballot().await { + Ok(Some(value)) => { + let _ = value_sender0.send(value); + } + Ok(None) => { + eprintln!("Party 0: No value was selected"); + } + Err(err) => { + eprintln!("Party 0 encountered an error: {:?}", err); + } + } + }); + + let ballot_task1 = tokio::spawn(async move { + match party1.launch_ballot().await { + Ok(Some(value)) => { + let _ = value_sender1.send(value); + } + Ok(None) => { + eprintln!("Party 1: No value was selected"); + } + Err(err) => { + eprintln!("Party 1 encountered an error: {:?}", err); + } + } + }); + + let ballot_task2 = tokio::spawn(async move { + match party2.launch_ballot().await { + Ok(Some(value)) => { + let _ = value_sender2.send(value); + } + Ok(None) => { + eprintln!("Party 2: No value was selected"); + } + Err(err) => { + eprintln!("Party 2 encountered an error: {:?}", err); + } + } + }); + + let ballot_task3 = tokio::spawn(async move { + match party3.launch_ballot().await { + Ok(Some(value)) => { + let _ = value_sender3.send(value); + } + Ok(None) => { + eprintln!("Party 3: No value was selected"); + } + Err(err) => { + eprintln!("Party 3 encountered an error: {:?}", err); + } + } + }); + + // Simulate message passing between the parties + tokio::spawn(async move { + let mut receivers = vec![ + msg_out_receiver0, + msg_out_receiver1, + msg_out_receiver2, + msg_out_receiver3, + ]; + let senders = vec![ + msg_in_sender0, + msg_in_sender1, + msg_in_sender2, + msg_in_sender3, + ]; + + loop { + for i in 0..receivers.len() { + if let Ok(msg) = receivers[i].try_recv() { + // Broadcast the message to all other parties + for j in 0..senders.len() { + if i != j { + let _ = senders[j].send(msg.clone()); + } + } + } + } + + // Delay to simulate network latency + tokio::time::sleep(Duration::from_millis(100)).await; + } + }); + + // Await the completion of ballot tasks + ballot_task0.await.unwrap(); + ballot_task1.await.unwrap(); + ballot_task2.await.unwrap(); + ballot_task3.await.unwrap(); + + // Await results from each party + let value0 = value_receiver0.await.unwrap(); + let value1 = value_receiver1.await.unwrap(); + let value2 = value_receiver2.await.unwrap(); + let value3 = value_receiver3.await.unwrap(); + + // Check that all parties reached the same consensus value + assert_eq!(value0, value1, "Party 0 and 1 agreed on the same value"); + assert_eq!(value1, value2, "Party 1 and 2 agreed on the same value"); + assert_eq!(value2, value3, "Party 2 and 3 agreed on the same value"); + } } From 747cf1ef47d9bdd54c9588144ae619f3bc8940b0 Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Thu, 29 Aug 2024 04:17:07 +0300 Subject: [PATCH 2/9] feat: refactored errors --- src/config.rs | 37 ++++++++++ src/error.rs | 73 +++++++++----------- src/lib.rs | 3 +- src/message.rs | 17 +++-- src/party.rs | 179 ++++++++++++++++++++++--------------------------- 5 files changed, 161 insertions(+), 148 deletions(-) create mode 100644 src/config.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d4bac9d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,37 @@ +use std::time::Duration; + +/// BPCon configuration. Includes ballot time bounds and other stuff. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct BPConConfig { + /// Parties weights: `party_weights[i]` corresponds to the i-th party weight + pub party_weights: Vec, + + /// Threshold weight to define BFT quorum: should be > 2/3 of total weight + pub threshold: u128, + + /// Timeout before ballot is launched. + /// Differs from `launch1a_timeout` having another status and not listening + /// to external events and messages. + pub launch_timeout: Duration, + + /// Timeout before 1a stage is launched. + pub launch1a_timeout: Duration, + + /// Timeout before 1b stage is launched. + pub launch1b_timeout: Duration, + + /// Timeout before 2a stage is launched. + pub launch2a_timeout: Duration, + + /// Timeout before 2av stage is launched. + pub launch2av_timeout: Duration, + + /// Timeout before 2b stage is launched. + pub launch2b_timeout: Duration, + + /// Timeout before finalization stage is launched. + pub finalize_timeout: Duration, + + /// Timeout for a graceful period to help parties with latency. + pub grace_period: Duration, +} diff --git a/src/error.rs b/src/error.rs index 0cab4e1..b66f8b2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,14 @@ //! Definition of the BPCon errors. -use crate::party::PartyStatus; +use crate::party::{PartyEvent, PartyStatus}; use crate::Value; use std::fmt::{Display, Formatter, Result}; pub enum LaunchBallotError { - FailedToSendEvent(String), + FailedToSendEvent((PartyEvent, String)), EventChannelClosed, MessageChannelClosed, - FollowEventError(FollowEventError), + FollowEventError((PartyEvent, FollowEventError)), LeaderElectionError(String), } @@ -57,12 +57,6 @@ pub enum SerializationError { Value(String), } -impl From for LaunchBallotError { - fn from(error: FollowEventError) -> Self { - LaunchBallotError::FollowEventError(error) - } -} - impl From for FollowEventError { fn from(error: PartyStatusMismatch) -> Self { FollowEventError::PartyStatusMismatch(error) @@ -109,7 +103,7 @@ impl Display for PartyStatusMismatch { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( f, - "Party status mismatch: party status is {:?} whilst needed status is {:?}.", + "party status mismatch: party status is {} whilst needed status is {}", self.party_status, self.needed_status ) } @@ -119,7 +113,7 @@ impl Display for BallotNumberMismatch { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( f, - "Ballot number mismatch: party's ballot number is {} whilst received {} in the message.", + "ballot number mismatch: party's ballot number is {} whilst received {} in the message", self.party_ballot_number, self.message_ballot_number ) } @@ -129,7 +123,7 @@ impl Display for LeaderMismatch { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( f, - "Leader mismatch: party's leader is {} whilst the message was sent by {}.", + "leader mismatch: party's leader is {} whilst the message was sent by {}", self.party_leader, self.message_sender ) } @@ -139,7 +133,7 @@ impl Display for ValueMismatch { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!( f, - "Value mismatch: party's value is {} whilst received {} in the message.", + "value mismatch: party's value is {} whilst received {} in the message", self.party_value, self.message_value ) } @@ -149,10 +143,10 @@ impl Display for DeserializationError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { match self { DeserializationError::Message(err) => { - write!(f, "Message deserialization error: {}", err) + write!(f, "message deserialization error: {err}") } DeserializationError::Value(err) => { - write!(f, "Value deserialization error: {}", err) + write!(f, "value deserialization error: {err}") } } } @@ -162,10 +156,10 @@ impl Display for SerializationError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { match self { SerializationError::Message(err) => { - write!(f, "Message serialization error: {}", err) + write!(f, "message serialization error: {err}") } SerializationError::Value(err) => { - write!(f, "Value serialization error: {}", err) + write!(f, "value serialization error: {err}") } } } @@ -173,41 +167,39 @@ impl Display for SerializationError { impl Display for LaunchBallotError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "launch ballot error: ")?; match *self { - LaunchBallotError::FailedToSendEvent(ref err) => write!( - f, - "Got error while running ballot: failed to send event: {}", - err - ), + LaunchBallotError::FailedToSendEvent((ref event, ref err)) => { + write!(f, "failed to send event {event}: {err}",) + } LaunchBallotError::EventChannelClosed => { - write!(f, "Got error while running ballot: event channel closed") + write!(f, "event channel closed") } LaunchBallotError::MessageChannelClosed => { - write!(f, "Got error while running ballot: message channel closed") + write!(f, "message channel closed") + } + LaunchBallotError::FollowEventError((ref event, ref err)) => { + write!(f, "failed to follow event {event}: {err}",) } - LaunchBallotError::FollowEventError(ref err) => { - write!(f, "Got error while running ballot: {}", err) + LaunchBallotError::LeaderElectionError(ref err) => { + write!(f, "leader election error: {err}",) } - LaunchBallotError::LeaderElectionError(ref err) => write!( - f, - "Got error while running ballot: leader election error{}", - err - ), } } } impl Display for FollowEventError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "follow event error: ")?; match *self { FollowEventError::PartyStatusMismatch(ref err) => { - write!(f, "Unable to follow event: {}", err) + write!(f, "{err}") } FollowEventError::SerializationError(ref err) => { - write!(f, "Unable to follow event: {}", err) + write!(f, "{err}") } FollowEventError::FailedToSendMessage(ref err) => { - write!(f, "Unable to follow event: {}", err) + write!(f, "{err}") } } } @@ -215,24 +207,25 @@ impl Display for FollowEventError { impl Display for UpdateStateError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "update state error: ")?; match *self { UpdateStateError::PartyStatusMismatch(ref err) => { - write!(f, "Unable to update state: {}", err) + write!(f, "{err}") } UpdateStateError::BallotNumberMismatch(ref err) => { - write!(f, "Unable to update state: {}", err) + write!(f, "{err}") } UpdateStateError::LeaderMismatch(ref err) => { - write!(f, "Unable to update state: {}", err) + write!(f, "{err}") } UpdateStateError::ValueMismatch(ref err) => { - write!(f, "Unable to update state: {}", err) + write!(f, "{err}") } UpdateStateError::ValueVerificationFailed => { - write!(f, "Unable to update state: value verification failed") + write!(f, "value verification failed") } UpdateStateError::DeserializationError(ref err) => { - write!(f, "Unable to update state: {}", err) + write!(f, "{err}") } } } diff --git a/src/lib.rs b/src/lib.rs index 2a24a53..e9cf60c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::fmt; use std::fmt::Debug; +pub mod config; pub mod error; pub mod leader; pub mod message; @@ -21,6 +22,4 @@ pub trait ValueSelector: Clone { /// Select value depending on inner conditions. Accepts 2b messages from parties. fn select(&self, m: &HashMap>) -> V; - - // TODO: add other fields to update selector state. } diff --git a/src/message.rs b/src/message.rs index b2d3551..56839a2 100644 --- a/src/message.rs +++ b/src/message.rs @@ -14,7 +14,7 @@ pub struct MessagePacket { } /// Full routing information for the message. -#[derive(Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct MessageRouting { /// Which participant this message came from. pub sender: u64, @@ -23,7 +23,7 @@ pub struct MessageRouting { } /// Representation of message types of the consensus. -#[derive(Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum ProtocolMessage { Msg1a, Msg1b, @@ -32,6 +32,12 @@ pub enum ProtocolMessage { Msg2b, } +impl std::fmt::Display for ProtocolMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ProtocolMessage: {:?}", self) + } +} + // Value in messages is stored in serialized format, i.e bytes in order to omit // strict restriction for `Value` trait to be [de]serializable only with `rkyv`. @@ -111,7 +117,7 @@ impl_packable!(Message2avContent, ProtocolMessage::Msg2av); impl_packable!(Message2bContent, ProtocolMessage::Msg2b); /// A struct to keep track of senders and the cumulative weight of their messages. -#[derive(PartialEq, Eq, Clone, Debug)] +#[derive(PartialEq, Eq, Clone, Debug, Default)] pub struct MessageRoundState { senders: HashSet, weight: u128, @@ -120,10 +126,7 @@ pub struct MessageRoundState { impl MessageRoundState { /// Creates a new instance of `MessageRoundState`. pub fn new() -> Self { - Self { - senders: HashSet::new(), - weight: 0, - } + Self::default() } pub fn get_weight(&self) -> u128 { diff --git a/src/party.rs b/src/party.rs index 60c6fb5..3b08adb 100644 --- a/src/party.rs +++ b/src/party.rs @@ -1,9 +1,11 @@ //! Definition of the BPCon participant structure. +use crate::config::BPConConfig; use crate::error::FollowEventError::FailedToSendMessage; use crate::error::LaunchBallotError::{ EventChannelClosed, FailedToSendEvent, LeaderElectionError, MessageChannelClosed, }; +use crate::error::UpdateStateError::ValueVerificationFailed; use crate::error::{ BallotNumberMismatch, DeserializationError, FollowEventError, LaunchBallotError, LeaderMismatch, PartyStatusMismatch, SerializationError, UpdateStateError, ValueMismatch, @@ -20,48 +22,12 @@ use std::collections::hash_map::Entry::Vacant; use std::collections::HashMap; use std::error::Error; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use tokio::time::{self, Duration}; - -/// BPCon configuration. Includes ballot time bounds and other stuff. -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct BPConConfig { - /// Parties weights: `party_weights[i]` corresponds to the i-th party weight - pub party_weights: Vec, - - /// Threshold weight to define BFT quorum: should be > 2/3 of total weight - pub threshold: u128, - - /// Timeout before ballot is launched. - /// Differs from `launch1a_timeout` having another status and not listening - /// to external events and messages. - pub launch_timeout: Duration, - - /// Timeout before 1a stage is launched. - pub launch1a_timeout: Duration, - - /// Timeout before 1b stage is launched. - pub launch1b_timeout: Duration, - - /// Timeout before 2a stage is launched. - pub launch2a_timeout: Duration, - - /// Timeout before 2av stage is launched. - pub launch2av_timeout: Duration, - - /// Timeout before 2b stage is launched. - pub launch2b_timeout: Duration, - - /// Timeout before finalization stage is launched. - pub finalize_timeout: Duration, - - /// Timeout for a graceful period to help parties with latency. - pub grace_period: Duration, -} +use tokio::time::sleep; /// Party status defines the statuses of the ballot for the particular participant /// depending on local calculations. -#[derive(PartialEq, Debug, Copy, Clone)] -pub(crate) enum PartyStatus { +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum PartyStatus { None, Launched, Passed1a, @@ -73,9 +39,15 @@ pub(crate) enum PartyStatus { Failed, } +impl std::fmt::Display for PartyStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PartyStatus: {:?}", self) + } +} + /// Party events is used for the ballot flow control. -#[derive(PartialEq, Debug, Copy, Clone)] -pub(crate) enum PartyEvent { +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub enum PartyEvent { Launch1a, Launch1b, Launch2a, @@ -84,6 +56,12 @@ pub(crate) enum PartyEvent { Finalize, } +impl std::fmt::Display for PartyEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PartyEvent: {:?}", self) + } +} + /// Party of the BPCon protocol that executes ballot. /// /// The communication between party and external @@ -218,20 +196,16 @@ impl> Party { } pub async fn launch_ballot(&mut self) -> Result, LaunchBallotError> { - self.prepare_next_ballot(); - self.leader = self - .elector - .get_leader(self) - .map_err(|err| LeaderElectionError(err.to_string()))?; + self.prepare_next_ballot()?; - time::sleep(self.cfg.launch_timeout).await; + sleep(self.cfg.launch_timeout).await; - let launch1a_timer = time::sleep(self.cfg.launch1a_timeout); - let launch1b_timer = time::sleep(self.cfg.launch1b_timeout); - let launch2a_timer = time::sleep(self.cfg.launch2a_timeout); - let launch2av_timer = time::sleep(self.cfg.launch2av_timeout); - let launch2b_timer = time::sleep(self.cfg.launch2b_timeout); - let finalize_timer = time::sleep(self.cfg.finalize_timeout); + let launch1a_timer = sleep(self.cfg.launch1a_timeout); + let launch1b_timer = sleep(self.cfg.launch1b_timeout); + let launch2a_timer = sleep(self.cfg.launch2a_timeout); + let launch2av_timer = sleep(self.cfg.launch2av_timeout); + let launch2b_timer = sleep(self.cfg.launch2b_timeout); + let finalize_timer = sleep(self.cfg.finalize_timeout); tokio::pin!( launch1a_timer, @@ -254,67 +228,68 @@ impl> Party { _ = &mut launch1a_timer, if !launch1a_fired => { self.event_sender.send(PartyEvent::Launch1a).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent("Failed to send Launch1a event".into()) + FailedToSendEvent((PartyEvent::Launch1a, err.to_string())) })?; launch1a_fired = true; }, _ = &mut launch1b_timer, if !launch1b_fired => { - self.event_sender.send(PartyEvent::Launch1b).map_err(|_| { + self.event_sender.send(PartyEvent::Launch1b).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent("Failed to send Launch1a event".into()) + FailedToSendEvent((PartyEvent::Launch1b, err.to_string())) })?; launch1b_fired = true; }, _ = &mut launch2a_timer, if !launch2a_fired => { - self.event_sender.send(PartyEvent::Launch2a).map_err(|_| { + self.event_sender.send(PartyEvent::Launch2a).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent("Failed to send Launch1a event".into()) + FailedToSendEvent((PartyEvent::Launch2a, err.to_string())) })?; launch2a_fired = true; }, _ = &mut launch2av_timer, if !launch2av_fired => { - self.event_sender.send(PartyEvent::Launch2av).map_err(|_| { + self.event_sender.send(PartyEvent::Launch2av).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent("Failed to send Launch1a event".into()) + FailedToSendEvent((PartyEvent::Launch2av, err.to_string())) })?; launch2av_fired = true; }, _ = &mut launch2b_timer, if !launch2b_fired => { - self.event_sender.send(PartyEvent::Launch2b).map_err(|_| { + self.event_sender.send(PartyEvent::Launch2b).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent("Failed to send Launch1a event".into()) + FailedToSendEvent((PartyEvent::Launch2b, err.to_string())) })?; launch2b_fired = true; }, _ = &mut finalize_timer, if !finalize_fired => { - self.event_sender.send(PartyEvent::Finalize).map_err(|_| { + self.event_sender.send(PartyEvent::Finalize).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent("Failed to send Launch1a event".into()) + FailedToSendEvent((PartyEvent::Finalize, err.to_string())) })?; finalize_fired = true; }, msg = self.msg_in_receiver.recv() => { - time::sleep(self.cfg.grace_period).await; + sleep(self.cfg.grace_period).await; if let Some(msg) = msg { if let Err(err) = self.update_state(&msg) { - // shall not fail the party, since invalid message may be sent by anyone - warn!("Unable to update state, got error: {err}") + // Shouldn't fail the party, since invalid message + // may be sent by anyone. + warn!("Unable to update state with {}, got error: {err}", msg.routing.msg_type) } }else if self.msg_in_receiver.is_closed(){ self.status = PartyStatus::Failed; - return Err(MessageChannelClosed.into()) + return Err(MessageChannelClosed) } }, event = self.event_receiver.recv() => { - time::sleep(self.cfg.grace_period).await; + sleep(self.cfg.grace_period).await; if let Some(event) = event { if let Err(err) = self.follow_event(event) { self.status = PartyStatus::Failed; - return Err(err.into()); + return Err(LaunchBallotError::FollowEventError((event, err))); } }else if self.event_receiver.is_closed(){ self.status = PartyStatus::Failed; - return Err(EventChannelClosed.into()) + return Err(EventChannelClosed) } }, } @@ -324,11 +299,19 @@ impl> Party { } /// Prepare state before running a ballot. - fn prepare_next_ballot(&mut self) { - self.status = PartyStatus::None; + fn prepare_next_ballot(&mut self) -> Result<(), LaunchBallotError> { + self.reset_state(); self.ballot += 1; + self.status = PartyStatus::Launched; + self.leader = self + .elector + .get_leader(self) + .map_err(|err| LeaderElectionError(err.to_string()))?; - // Clean state + Ok(()) + } + + fn reset_state(&mut self) { self.parties_voted_before = HashMap::new(); self.messages_1b_weight = 0; self.value_2a = None; @@ -338,8 +321,6 @@ impl> Party { // Cleaning channels while self.event_receiver.try_recv().is_ok() {} while self.msg_in_receiver.try_recv().is_ok() {} - - self.status = PartyStatus::Launched; } /// Update party's state based on message type. @@ -356,7 +337,7 @@ impl> Party { .into()); } - let msg = Message1aContent::unpack(&msg)?; + let msg = Message1aContent::unpack(msg)?; if msg.ballot != self.ballot { return Err(BallotNumberMismatch { @@ -385,7 +366,7 @@ impl> Party { .into()); } - let msg = Message1bContent::unpack(&msg)?; + let msg = Message1bContent::unpack(msg)?; if msg.ballot != self.ballot { return Err(BallotNumberMismatch { @@ -406,13 +387,13 @@ impl> Party { } if let Vacant(e) = self.parties_voted_before.entry(routing.sender) { - let value: Option = - match msg.last_value_voted { - Some(ref data) => Some(bincode::deserialize(data).map_err(|err| { - DeserializationError::Value(err.to_string()).into() - })?), - None => None, - }; + let value: Option = match msg.last_value_voted { + Some(ref data) => Some( + bincode::deserialize(data) + .map_err(|err| DeserializationError::Value(err.to_string()))?, + ), + None => None, + }; e.insert(value); @@ -433,7 +414,7 @@ impl> Party { .into()); } - let msg = Message2aContent::unpack(&msg)?; + let msg = Message2aContent::unpack(msg)?; if msg.ballot != self.ballot { return Err(BallotNumberMismatch { @@ -452,7 +433,7 @@ impl> Party { } let value_received = bincode::deserialize(&msg.value[..]) - .map_err(|err| DeserializationError::Value(err.to_string()).into())?; + .map_err(|err| DeserializationError::Value(err.to_string()))?; if self .value_selector @@ -461,7 +442,7 @@ impl> Party { self.status = PartyStatus::Passed2a; self.value_2a = Some(value_received); } else { - return Err(UpdateStateError::ValueVerificationFailed.into()); + return Err(ValueVerificationFailed); } } ProtocolMessage::Msg2av => { @@ -473,7 +454,7 @@ impl> Party { .into()); } - let msg = Message2avContent::unpack(&msg)?; + let msg = Message2avContent::unpack(msg)?; if msg.ballot != self.ballot { return Err(BallotNumberMismatch { @@ -483,7 +464,7 @@ impl> Party { .into()); } let value_received: V = bincode::deserialize(&msg.received_value[..]) - .map_err(|err| DeserializationError::Value(err.to_string()).into())?; + .map_err(|err| DeserializationError::Value(err.to_string()))?; if value_received != self.value_2a.clone().unwrap() { return Err(ValueMismatch { @@ -513,7 +494,7 @@ impl> Party { .into()); } - let msg = Message2bContent::unpack(&msg)?; + let msg = Message2bContent::unpack(msg)?; if msg.ballot != self.ballot { return Err(BallotNumberMismatch { @@ -560,7 +541,7 @@ impl> Party { if self.leader == self.id { self.msg_out_sender .send(msg) - .map_err(|err| FailedToSendMessage(err.to_string()).into())?; + .map_err(|err| FailedToSendMessage(err.to_string()))?; self.status = PartyStatus::Passed1a; } } @@ -578,7 +559,7 @@ impl> Party { .clone() .map(|inner_data| { bincode::serialize(&inner_data) - .map_err(|err| SerializationError::Value(err.to_string()).into()) + .map_err(|err| SerializationError::Value(err.to_string())) }) .transpose()?; @@ -591,7 +572,7 @@ impl> Party { self.msg_out_sender .send(msg) - .map_err(|err| FailedToSendMessage(err.to_string()).into())?; + .map_err(|err| FailedToSendMessage(err.to_string()))?; } PartyEvent::Launch2a => { if self.status != PartyStatus::Passed1b { @@ -603,7 +584,7 @@ impl> Party { } let value = bincode::serialize(&self.get_value()) - .map_err(|err| SerializationError::Value(err.to_string()).into())?; + .map_err(|err| SerializationError::Value(err.to_string()))?; let content = &Message2aContent { ballot: self.ballot, @@ -614,7 +595,7 @@ impl> Party { if self.leader == self.id { self.msg_out_sender .send(msg) - .map_err(|err| FailedToSendMessage(err.to_string()).into())?; + .map_err(|err| FailedToSendMessage(err.to_string()))?; } } PartyEvent::Launch2av => { @@ -627,7 +608,7 @@ impl> Party { } let received_value = bincode::serialize(&self.value_2a.clone()) - .map_err(|err| SerializationError::Value(err.to_string()).into())?; + .map_err(|err| SerializationError::Value(err.to_string()))?; let content = &Message2avContent { ballot: self.ballot, @@ -637,7 +618,7 @@ impl> Party { self.msg_out_sender .send(msg) - .map_err(|err| FailedToSendMessage(err.to_string()).into())?; + .map_err(|err| FailedToSendMessage(err.to_string()))?; } PartyEvent::Launch2b => { if self.status != PartyStatus::Passed2av { @@ -655,7 +636,7 @@ impl> Party { self.msg_out_sender .send(msg) - .map_err(|err| FailedToSendMessage(err.to_string()).into())?; + .map_err(|err| FailedToSendMessage(err.to_string()))?; } PartyEvent::Finalize => { if self.status != PartyStatus::Passed2b { From 37ebd5935878c917615b246a334d67450dabc3a2 Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Thu, 29 Aug 2024 06:42:03 +0300 Subject: [PATCH 3/9] feat: improved errors, fixed tests --- Cargo.toml | 3 +- src/config.rs | 18 ++ src/error.rs | 175 ++++------------ src/leader.rs | 140 +++++++++++-- src/party.rs | 548 +++++++++++--------------------------------------- 5 files changed, 305 insertions(+), 579 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4d16495..4ab68dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ serde = { version = "1.0.207", features = ["derive"] } bincode = "1.3.3" tokio = { version = "1.39.2", features = ["full", "test-util"] } rand = "0.9.0-alpha.2" -seeded-random = "0.6.0" \ No newline at end of file +seeded-random = "0.6.0" +thiserror = "1.0.63" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index d4bac9d..f7bd6e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,3 +35,21 @@ pub struct BPConConfig { /// Timeout for a graceful period to help parties with latency. pub grace_period: Duration, } + +impl BPConConfig { + pub fn with_default_timeouts(party_weights: Vec, threshold: u128) -> Self { + Self { + party_weights, + threshold, + // TODO: deduce actually good defaults + launch_timeout: Duration::from_secs(1), + launch1a_timeout: Duration::from_secs(5), + launch1b_timeout: Duration::from_secs(10), + launch2a_timeout: Duration::from_secs(15), + launch2av_timeout: Duration::from_secs(20), + launch2b_timeout: Duration::from_secs(25), + finalize_timeout: Duration::from_secs(30), + grace_period: Duration::from_secs(2), + } + } +} diff --git a/src/error.rs b/src/error.rs index b66f8b2..c722feb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,59 +1,92 @@ -//! Definition of the BPCon errors. - use crate::party::{PartyEvent, PartyStatus}; use crate::Value; -use std::fmt::{Display, Formatter, Result}; +use thiserror::Error; +#[derive(Error, Debug)] pub enum LaunchBallotError { - FailedToSendEvent((PartyEvent, String)), + #[error("failed to send event {0}: {1}")] + FailedToSendEvent(PartyEvent, String), + #[error("event channel closed")] EventChannelClosed, + #[error("message channel closed")] MessageChannelClosed, - FollowEventError((PartyEvent, FollowEventError)), + #[error("failed to follow event {0}: {1}")] + FollowEventError(PartyEvent, FollowEventError), + #[error("leader election error: {0}")] LeaderElectionError(String), } +#[derive(Error, Debug)] pub enum FollowEventError { + #[error("{0}")] PartyStatusMismatch(PartyStatusMismatch), + #[error("{0}")] SerializationError(SerializationError), + #[error("{0}")] FailedToSendMessage(String), } +#[derive(Error, Debug)] pub enum UpdateStateError { + #[error("{0}")] PartyStatusMismatch(PartyStatusMismatch), + #[error("{0}")] BallotNumberMismatch(BallotNumberMismatch), + #[error("{0}")] LeaderMismatch(LeaderMismatch), + #[error("{0}")] ValueMismatch(ValueMismatch), + #[error("value verification failed")] ValueVerificationFailed, + #[error("{0}")] DeserializationError(DeserializationError), } +#[derive(Error, Debug)] +#[error( + "party status mismatch: party status is {party_status} whilst needed status is {needed_status}" +)] pub struct PartyStatusMismatch { pub party_status: PartyStatus, pub needed_status: PartyStatus, } +#[derive(Error, Debug)] +#[error("ballot number mismatch: party's ballot number is {party_ballot_number} whilst received {message_ballot_number} in the message")] pub struct BallotNumberMismatch { pub party_ballot_number: u64, pub message_ballot_number: u64, } +#[derive(Error, Debug)] +#[error("leader mismatch: party's leader is {party_leader} whilst the message was sent by {message_sender}")] pub struct LeaderMismatch { pub party_leader: u64, pub message_sender: u64, } +#[derive(Error, Debug)] +#[error( + "value mismatch: party's value is {party_value} whilst received {message_value} in the message" +)] pub struct ValueMismatch { pub party_value: V, pub message_value: V, } +#[derive(Error, Debug)] pub enum DeserializationError { + #[error("message deserialization error: {0}")] Message(String), + #[error("value deserialization error: {0}")] Value(String), } +#[derive(Error, Debug)] pub enum SerializationError { + #[error("message serialization error: {0}")] Message(String), + #[error("value serialization error: {0}")] Value(String), } @@ -98,135 +131,3 @@ impl From for UpdateStateError { UpdateStateError::DeserializationError(error) } } - -impl Display for PartyStatusMismatch { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!( - f, - "party status mismatch: party status is {} whilst needed status is {}", - self.party_status, self.needed_status - ) - } -} - -impl Display for BallotNumberMismatch { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!( - f, - "ballot number mismatch: party's ballot number is {} whilst received {} in the message", - self.party_ballot_number, self.message_ballot_number - ) - } -} - -impl Display for LeaderMismatch { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!( - f, - "leader mismatch: party's leader is {} whilst the message was sent by {}", - self.party_leader, self.message_sender - ) - } -} - -impl Display for ValueMismatch { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!( - f, - "value mismatch: party's value is {} whilst received {} in the message", - self.party_value, self.message_value - ) - } -} - -impl Display for DeserializationError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self { - DeserializationError::Message(err) => { - write!(f, "message deserialization error: {err}") - } - DeserializationError::Value(err) => { - write!(f, "value deserialization error: {err}") - } - } - } -} - -impl Display for SerializationError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self { - SerializationError::Message(err) => { - write!(f, "message serialization error: {err}") - } - SerializationError::Value(err) => { - write!(f, "value serialization error: {err}") - } - } - } -} - -impl Display for LaunchBallotError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "launch ballot error: ")?; - match *self { - LaunchBallotError::FailedToSendEvent((ref event, ref err)) => { - write!(f, "failed to send event {event}: {err}",) - } - LaunchBallotError::EventChannelClosed => { - write!(f, "event channel closed") - } - LaunchBallotError::MessageChannelClosed => { - write!(f, "message channel closed") - } - LaunchBallotError::FollowEventError((ref event, ref err)) => { - write!(f, "failed to follow event {event}: {err}",) - } - LaunchBallotError::LeaderElectionError(ref err) => { - write!(f, "leader election error: {err}",) - } - } - } -} - -impl Display for FollowEventError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "follow event error: ")?; - match *self { - FollowEventError::PartyStatusMismatch(ref err) => { - write!(f, "{err}") - } - FollowEventError::SerializationError(ref err) => { - write!(f, "{err}") - } - FollowEventError::FailedToSendMessage(ref err) => { - write!(f, "{err}") - } - } - } -} - -impl Display for UpdateStateError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "update state error: ")?; - match *self { - UpdateStateError::PartyStatusMismatch(ref err) => { - write!(f, "{err}") - } - UpdateStateError::BallotNumberMismatch(ref err) => { - write!(f, "{err}") - } - UpdateStateError::LeaderMismatch(ref err) => { - write!(f, "{err}") - } - UpdateStateError::ValueMismatch(ref err) => { - write!(f, "{err}") - } - UpdateStateError::ValueVerificationFailed => { - write!(f, "value verification failed") - } - UpdateStateError::DeserializationError(ref err) => { - write!(f, "{err}") - } - } - } -} diff --git a/src/leader.rs b/src/leader.rs index 248dc9f..ee101bf 100644 --- a/src/leader.rs +++ b/src/leader.rs @@ -3,20 +3,23 @@ use crate::{Value, ValueSelector}; use seeded_random::{Random, Seed}; use std::cmp::Ordering; use std::hash::{DefaultHasher, Hash, Hasher}; +use thiserror::Error; /// Trait incorporating logic for leader election. pub trait LeaderElector>: Send { - type LeaderElectorError; - /// Get leader for current ballot. /// Returns id of the elected party or error. - fn get_leader(&self, party: &Party) -> Result; + fn elect_leader(&self, party: &Party) -> Result>; } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct DefaultLeaderElector {} impl DefaultLeaderElector { + pub fn new() -> Self { + Self::default() + } + /// Compute seed for randomized leader election. fn compute_seed>(party: &Party) -> u64 { let mut hasher = DefaultHasher::new(); @@ -60,29 +63,21 @@ impl DefaultLeaderElector { } } +#[derive(Error, Debug)] pub enum DefaultLeaderElectorError { + #[error("zero weight sum")] ZeroWeightSum, } -// -// impl Display for DefaultLeaderElectorError { -// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { -// match *self { -// DefaultLeaderElectorError::ZeroWeightSum => write!(f, "Leader election error: zero weight sum"), -// } -// } -// } impl> LeaderElector for DefaultLeaderElector { - type LeaderElectorError = DefaultLeaderElectorError; - /// Compute leader in a weighed randomized manner. /// Uses seed from the config, making it deterministic. - fn get_leader(&self, party: &Party) -> Result { + fn elect_leader(&self, party: &Party) -> Result> { let seed = DefaultLeaderElector::compute_seed(party); let total_weight: u64 = party.cfg.party_weights.iter().sum(); if total_weight == 0 { - return Err(DefaultLeaderElectorError::ZeroWeightSum); + return Err(DefaultLeaderElectorError::ZeroWeightSum.into()); } // Generate a random number in the range [0, total_weight) @@ -107,3 +102,116 @@ impl> LeaderElector for DefaultLeaderElect } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::party::tests::{default_config, default_party}; + use rand::Rng; + use std::thread; + use std::time::Duration; + + #[test] + fn test_default_leader_elector_determinism() { + let party = default_party(); + let elector = DefaultLeaderElector::new(); + + let leader1 = elector.elect_leader(&party).unwrap(); + + // Test multiple iterations to ensure the leader remains the same + for i in 2..=10 { + let leader = elector.elect_leader(&party).unwrap(); + assert_eq!( + leader1, leader, + "Leaders should be consistent on repeated calls (iteration {})", + i + ); + } + } + + #[test] + fn test_default_leader_elector_fail_with_zero_weights() { + let mut party = default_party(); + let mut cfg = default_config(); + cfg.party_weights = vec![0, 0, 0]; + party.cfg = cfg; + + let elector = DefaultLeaderElector::new(); + + match elector.elect_leader(&party) { + Err(_) => {} // this is expected behaviour + _ => panic!("Expected DefaultLeaderElectorError::ZeroWeightSum"), + } + } + + fn debug_hash_to_range_new(seed: u64, range: u64) -> u64 { + assert!(range > 1); + + let mut k = 64; + while 1u64 << (k - 1) >= range { + k -= 1; + } + + let rng = Random::from_seed(Seed::unsafe_new(seed)); + + let mut iteration = 1u64; + loop { + let mut raw_res: u64 = rng.gen(); + raw_res >>= 64 - k; + + if raw_res < range { + return raw_res; + } + + iteration += 1; + assert!(iteration <= 50) + } + } + + #[test] + #[ignore] // Ignoring since it takes a while to run + fn test_hash_range_random() { + // test the uniform distribution + + const N: usize = 37; + const M: i64 = 10000000; + + let mut cnt1: [i64; N] = [0; N]; + + for _ in 0..M { + let mut rng = rand::thread_rng(); + let seed: u64 = rng.random(); + + let res1 = debug_hash_to_range_new(seed, N as u64); + assert!(res1 < N as u64); + + cnt1[res1 as usize] += 1; + } + + println!("1: {:?}", cnt1); + + let mut avg1: i64 = 0; + + for item in cnt1.iter().take(N) { + avg1 += (M / (N as i64) - item).abs(); + } + + avg1 /= N as i64; + + println!("Avg 1: {}", avg1); + } + + #[test] + fn test_rng() { + let rng1 = Random::from_seed(Seed::unsafe_new(123456)); + let rng2 = Random::from_seed(Seed::unsafe_new(123456)); + + println!("{}", rng1.gen::()); + println!("{}", rng2.gen::()); + + thread::sleep(Duration::from_secs(2)); + + println!("{}", rng1.gen::()); + println!("{}", rng2.gen::()); + } +} diff --git a/src/party.rs b/src/party.rs index 3b08adb..67da646 100644 --- a/src/party.rs +++ b/src/party.rs @@ -20,7 +20,6 @@ use log::warn; use std::cmp::PartialEq; use std::collections::hash_map::Entry::Vacant; use std::collections::HashMap; -use std::error::Error; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::time::sleep; @@ -92,7 +91,7 @@ pub struct Party> { value_selector: VS, /// Main functional for leader election. - elector: Box>>, + elector: Box>, /// Status of the ballot execution status: PartyStatus, @@ -134,7 +133,7 @@ impl> Party { id: u64, cfg: BPConConfig, value_selector: VS, - elector: Box>>, + elector: Box>, ) -> ( Self, UnboundedReceiver, @@ -228,42 +227,42 @@ impl> Party { _ = &mut launch1a_timer, if !launch1a_fired => { self.event_sender.send(PartyEvent::Launch1a).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent((PartyEvent::Launch1a, err.to_string())) + FailedToSendEvent(PartyEvent::Launch1a, err.to_string()) })?; launch1a_fired = true; }, _ = &mut launch1b_timer, if !launch1b_fired => { self.event_sender.send(PartyEvent::Launch1b).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent((PartyEvent::Launch1b, err.to_string())) + FailedToSendEvent(PartyEvent::Launch1b, err.to_string()) })?; launch1b_fired = true; }, _ = &mut launch2a_timer, if !launch2a_fired => { self.event_sender.send(PartyEvent::Launch2a).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent((PartyEvent::Launch2a, err.to_string())) + FailedToSendEvent(PartyEvent::Launch2a, err.to_string()) })?; launch2a_fired = true; }, _ = &mut launch2av_timer, if !launch2av_fired => { self.event_sender.send(PartyEvent::Launch2av).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent((PartyEvent::Launch2av, err.to_string())) + FailedToSendEvent(PartyEvent::Launch2av, err.to_string()) })?; launch2av_fired = true; }, _ = &mut launch2b_timer, if !launch2b_fired => { self.event_sender.send(PartyEvent::Launch2b).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent((PartyEvent::Launch2b, err.to_string())) + FailedToSendEvent(PartyEvent::Launch2b, err.to_string()) })?; launch2b_fired = true; }, _ = &mut finalize_timer, if !finalize_fired => { self.event_sender.send(PartyEvent::Finalize).map_err(|err| { self.status = PartyStatus::Failed; - FailedToSendEvent((PartyEvent::Finalize, err.to_string())) + FailedToSendEvent(PartyEvent::Finalize, err.to_string()) })?; finalize_fired = true; }, @@ -273,7 +272,7 @@ impl> Party { if let Err(err) = self.update_state(&msg) { // Shouldn't fail the party, since invalid message // may be sent by anyone. - warn!("Unable to update state with {}, got error: {err}", msg.routing.msg_type) + warn!("Failed to update state with {}, got error: {err}", msg.routing.msg_type) } }else if self.msg_in_receiver.is_closed(){ self.status = PartyStatus::Failed; @@ -285,7 +284,7 @@ impl> Party { if let Some(event) = event { if let Err(err) = self.follow_event(event) { self.status = PartyStatus::Failed; - return Err(LaunchBallotError::FollowEventError((event, err))); + return Err(LaunchBallotError::FollowEventError(event, err)); } }else if self.event_receiver.is_closed(){ self.status = PartyStatus::Failed; @@ -305,7 +304,7 @@ impl> Party { self.status = PartyStatus::Launched; self.leader = self .elector - .get_leader(self) + .elect_leader(self) .map_err(|err| LeaderElectionError(err.to_string()))?; Ok(()) @@ -655,30 +654,29 @@ impl> Party { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; - use rand::Rng; - use seeded_random::{Random, Seed}; + use crate::leader::DefaultLeaderElector; + use crate::party::PartyStatus::{Launched, Passed1a, Passed1b, Passed2a}; use std::collections::HashMap; use std::fmt::{Display, Formatter}; - use std::thread; + use std::time::Duration; + use tokio::time; - // Mock implementation of Value #[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Debug)] - struct MockValue(u64); // Simple mock type wrapping an integer + pub(crate) struct MockValue(u64); impl Display for MockValue { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "MockValue: {}", self) + write!(f, "MockValue: {}", self.0) } } impl Value for MockValue {} - // Mock implementation of ValueSelector #[derive(Clone)] - struct MockValueSelector; + pub(crate) struct MockValueSelector; impl ValueSelector for MockValueSelector { fn verify(&self, _v: &MockValue, _m: &HashMap>) -> bool { @@ -686,331 +684,133 @@ mod tests { } fn select(&self, _m: &HashMap>) -> MockValue { - MockValue(42) // For testing, always return the same value + MockValue(1) // For testing, always return the same value } } - fn default_config() -> BPConConfig { - BPConConfig { - party_weights: vec![1, 2, 3], - threshold: 4, - launch_timeout: Duration::from_secs(0), - launch1a_timeout: Duration::from_secs(0), - launch1b_timeout: Duration::from_secs(10), - launch2a_timeout: Duration::from_secs(20), - launch2av_timeout: Duration::from_secs(30), - launch2b_timeout: Duration::from_secs(40), - finalize_timeout: Duration::from_secs(50), - grace_period: Duration::from_secs(1), - } + pub(crate) fn default_config() -> BPConConfig { + BPConConfig::with_default_timeouts(vec![1, 2, 3], 4) } - #[test] - fn test_compute_leader_determinism() { - let cfg = default_config(); - let party = Party::::new( + pub(crate) fn default_party() -> Party { + Party::::new( 0, - cfg, + default_config(), MockValueSelector, - Box::new(DefaultLeaderElector {}), + Box::new(DefaultLeaderElector::new()), ) - .0; - - // Compute the leader multiple times - let leader1 = party.elector.get_leader(&party).unwrap(); - let leader2 = party.elector.get_leader(&party).unwrap(); - let leader3 = party.elector.get_leader(&party).unwrap(); - - // All leaders should be the same due to deterministic seed - assert_eq!( - leader1, leader2, - "Leaders should be consistent on repeated calls" - ); - assert_eq!( - leader2, leader3, - "Leaders should be consistent on repeated calls" - ); - } - - #[test] - fn test_compute_leader_zero_weights() { - let mut cfg = default_config(); - cfg.party_weights = vec![0, 0, 0]; - - let party = Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ) - .0; - - match party.elector.get_leader(&party) { - Err(BallotError::LeaderElection(_)) => { - // The test passes if the error is of type LeaderElection - } - _ => panic!("Expected BallotError::LeaderElection"), - } + .0 } #[test] fn test_update_state_msg1a() { - let cfg = default_config(); - let mut party = Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ) - .0; - party.status = PartyStatus::Launched; - party.ballot = 1; - - // Must send this message from leader of the ballot. - party.leader = 0; // this party's id - - let msg = Message1aContent { ballot: 1 }; - let routing = MessageRouting { - sender: 0, - receivers: vec![2, 3], - is_broadcast: false, - msg_type: ProtocolMessage::Msg1a, + let mut party = default_party(); + party.status = Launched; + let content = Message1aContent { + ballot: party.ballot, }; + let msg = content.pack(party.leader).unwrap(); - let msg_wire = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg).unwrap(), - routing, - }; + party.update_state(&msg).unwrap(); - party - .update_state(msg_wire.content_bytes, msg_wire.routing) - .unwrap(); - assert_eq!(party.status, PartyStatus::Passed1a); + assert_eq!(party.status, Passed1a); } #[test] fn test_update_state_msg1b() { - let cfg = default_config(); - let mut party = Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ) - .0; - party.status = PartyStatus::Passed1a; - party.ballot = 1; - - // First, send a 1b message from party 1 (weight 2) - let msg1 = Message1bContent { - ballot: 1, - last_ballot_voted: Some(0), - last_value_voted: bincode::serialize(&MockValue(42)).ok(), - }; - let routing1 = MessageRouting { - sender: 1, // Party 1 sends the message - receivers: vec![0], - is_broadcast: false, - msg_type: ProtocolMessage::Msg1b, - }; - - let msg_wire1 = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg1).unwrap(), - routing: routing1, - }; - - party - .update_state(msg_wire1.content_bytes, msg_wire1.routing) - .unwrap(); + let mut party = default_party(); + party.status = Passed1a; - // Now, send a 1b message from party 2 (weight 3) - let msg2 = Message1bContent { - ballot: 1, - last_ballot_voted: Some(0), + let content = Message1bContent { + ballot: party.ballot, + last_ballot_voted: None, last_value_voted: bincode::serialize(&MockValue(42)).ok(), }; - let routing2 = MessageRouting { - sender: 2, // Party 2 sends the message - receivers: vec![0], - is_broadcast: false, - msg_type: ProtocolMessage::Msg1b, - }; - let msg_wire2 = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg2).unwrap(), - routing: routing2, - }; + // First, send a 1b message from party 1 (weight 2) + let msg = content.pack(1).unwrap(); + party.update_state(&msg).unwrap(); - party - .update_state(msg_wire2.content_bytes, msg_wire2.routing) - .unwrap(); + // Then, send a 1b message from party 2 (weight 3) + let msg = content.pack(2).unwrap(); + party.update_state(&msg).unwrap(); // After both messages, the cumulative weight is 2 + 3 = 5, which exceeds the threshold - assert_eq!(party.status, PartyStatus::Passed1b); + assert_eq!(party.status, Passed1b); } #[test] fn test_update_state_msg2a() { - let cfg = default_config(); - let mut party = Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ) - .0; - party.status = PartyStatus::Passed1b; - party.ballot = 1; + let mut party = default_party(); + party.status = Passed1b; + party.leader = 1; - // Must send this message from leader of the ballot. - party.leader = 0; // this party's id - - let msg = Message2aContent { - ballot: 1, + let content = Message2aContent { + ballot: party.ballot, value: bincode::serialize(&MockValue(42)).unwrap(), }; - let routing = MessageRouting { - sender: 0, - receivers: vec![0], - is_broadcast: false, - msg_type: ProtocolMessage::Msg2a, - }; + let msg = content.pack(1).unwrap(); + party.update_state(&msg).unwrap(); - let msg_wire = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg).unwrap(), - routing, - }; - - party - .update_state(msg_wire.content_bytes, msg_wire.routing) - .unwrap(); - - assert_eq!(party.status, PartyStatus::Passed2a); + assert_eq!(party.status, Passed2a); } #[test] fn test_update_state_msg2av() { - let cfg = default_config(); - let mut party = Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ) - .0; - party.status = PartyStatus::Passed2a; - party.ballot = 1; - party.value_2a = Some(MockValue(42)); - - // Send first 2av message from party 2 (weight 3) - let msg1 = Message2avContent { - ballot: 1, - received_value: bincode::serialize(&MockValue(42)).unwrap(), - }; - let routing1 = MessageRouting { - sender: 2, - receivers: vec![0], - is_broadcast: false, - msg_type: ProtocolMessage::Msg2av, - }; + let mut party = default_party(); + party.status = Passed2a; + party.value_2a = Some(MockValue(1)); - let msg_wire1 = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg1).unwrap(), - routing: routing1, + let content = Message2avContent { + ballot: party.ballot, + received_value: bincode::serialize(&MockValue(1)).unwrap(), }; - party - .update_state(msg_wire1.content_bytes, msg_wire1.routing) - .unwrap(); + // Send first 2av message from party 1 (weight 2) + let msg = content.pack(1).unwrap(); + party.update_state(&msg).unwrap(); - // Now send a second 2av message from party 1 (weight 2) - let msg2 = Message2avContent { - ballot: 1, - received_value: bincode::serialize(&MockValue(42)).unwrap(), - }; - let routing2 = MessageRouting { - sender: 1, - receivers: vec![0], - is_broadcast: false, - msg_type: ProtocolMessage::Msg2av, - }; + // Now send a second 2av message from party 2 (weight 3) + let msg = content.pack(2).unwrap(); + party.update_state(&msg).unwrap(); - let msg_wire2 = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg2).unwrap(), - routing: routing2, - }; - - party - .update_state(msg_wire2.content_bytes, msg_wire2.routing) - .unwrap(); - - // The cumulative weight (3 + 2) should exceed the threshold of 4 + // The cumulative weight (2 + 3) should exceed the threshold of 4 assert_eq!(party.status, PartyStatus::Passed2av); } #[test] fn test_update_state_msg2b() { - let cfg = default_config(); - let mut party = Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ) - .0; + let mut party = default_party(); party.status = PartyStatus::Passed2av; - party.ballot = 1; - - // Simulate that both party 2 and party 1 already sent 2av messages - party.messages_2av_state.add_sender(1, 1); - party.messages_2av_state.add_sender(2, 2); - - // Send first 2b message from party 2 (weight 3) - let msg1 = Message2bContent { ballot: 1 }; - let routing1 = MessageRouting { - sender: 2, - receivers: vec![0], - is_broadcast: false, - msg_type: ProtocolMessage::Msg2b, - }; - let msg_wire1 = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg1).unwrap(), - routing: routing1, + // Simulate that both party 1 and party 2 already sent 2av messages + party.messages_2av_state.add_sender(1, 2); + party.messages_2av_state.add_sender(2, 3); + + let content = Message2bContent { + ballot: party.ballot, }; - party - .update_state(msg_wire1.content_bytes, msg_wire1.routing) - .unwrap(); + // Send first 2b message from party 1 (weight 2) + let msg = content.pack(1).unwrap(); + party.update_state(&msg).unwrap(); // Print the current state and weight println!( - "After first Msg2b: Status = {:?}, 2b Weight = {}", - party.status, party.messages_2b_state.weight + "After first Msg2b: Status = {}, 2b Weight = {}", + party.status, + party.messages_2b_state.get_weight() ); - // Now send a second 2b message from party 1 (weight 2) - let msg2 = Message2bContent { ballot: 1 }; - let routing2 = MessageRouting { - sender: 1, - receivers: vec![0], - is_broadcast: false, - msg_type: ProtocolMessage::Msg2b, - }; - - let msg_wire2 = MessagePacket { - content_bytes: rkyv::to_bytes::<_, 256>(&msg2).unwrap(), - routing: routing2, - }; - - party - .update_state(msg_wire2.content_bytes, msg_wire2.routing) - .unwrap(); + // Now send a second 2b message from party 2 (weight 3) + let msg = content.pack(2).unwrap(); + party.update_state(&msg).unwrap(); // Print the current state and weight println!( - "After second Msg2b: Status = {:?}, 2b Weight = {}", - party.status, party.messages_2b_state.weight + "After second Msg2b: Status = {}, 2b Weight = {}", + party.status, + party.messages_2b_state.get_weight() ); // The cumulative weight (3 + 2) should exceed the threshold of 4 @@ -1020,16 +820,17 @@ mod tests { #[test] fn test_follow_event_launch1a() { let cfg = default_config(); - let (mut party, _msg_out_receiver, _msg_in_sender) = - Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ); + // Need to take ownership of msg_out_receiver, so that sender doesn't close, + // since otherwise msg_out_receiver will be dropped. + let (mut party, _msg_out_receiver, _) = Party::::new( + 0, + cfg, + MockValueSelector, + Box::new(DefaultLeaderElector {}), + ); - party.status = PartyStatus::Launched; - party.ballot = 1; + party.status = Launched; + party.leader = party.id; party .follow_event(PartyEvent::Launch1a) @@ -1038,65 +839,28 @@ mod tests { // If the party is the leader and in the Launched state, the event should trigger a message. // And it's status shall update to Passed1a after sending 1a message, // contrary to other participants, whose `Passed1a` updates only after receiving 1a message. - assert_eq!(party.status, PartyStatus::Passed1a); - } - - #[test] - fn test_ballot_reset_after_failure() { - let cfg = default_config(); - let (mut party, _, _) = Party::::new( - 0, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ); - - party.status = PartyStatus::Failed; - party.ballot = 1; - - party.prepare_next_ballot().unwrap(); - - // Check that state has been reset - assert_eq!(party.status, PartyStatus::Launched); - assert_eq!(party.ballot, 2); // Ballot number should have incremented - assert!(party.parties_voted_before.is_empty()); - assert_eq!(party.messages_1b_weight, 0); - assert!(party.messages_2av_state.senders.is_empty()); - assert_eq!(party.messages_2av_state.weight, 0); - assert!(party.messages_2b_state.senders.is_empty()); - assert_eq!(party.messages_2b_state.weight, 0); + assert_eq!(party.status, Passed1a); } #[test] fn test_follow_event_communication_failure() { - let cfg = default_config(); - - // This party id is precomputed for this specific party_weights, threshold and ballot. - // Because we need leader to send 1a. - let party_id = 0; - - let (mut party, msg_out_receiver, _) = Party::::new( - party_id, - cfg, - MockValueSelector, - Box::new(DefaultLeaderElector {}), - ); - - party.status = PartyStatus::Launched; - party.ballot = 1; - - drop(msg_out_receiver); // Drop the receiver to simulate a communication failure + // msg_out_receiver channel, bound to corresponding sender, which will try to use + // follow event, is getting dropped since we don't take ownership of it + // upon creation of the party + let mut party = default_party(); + party.status = Launched; + party.leader = party.id; let result = party.follow_event(PartyEvent::Launch1a); match result { - Err(BallotError::Communication(err_msg)) => { - assert_eq!( - err_msg, "Failed to send Msg1a", - "Expected specific communication error message" - ); + Err(FailedToSendMessage(_)) => { + // this is expected outcome } - _ => panic!("Expected BallotError::Communication, got {:?}", result), + _ => panic!( + "Expected FollowEventError::FailedToSendMessage, got {:?}", + result + ), } } @@ -1107,6 +871,7 @@ mod tests { // Set up the Party with necessary configuration let cfg = default_config(); + let (event_sender, mut event_receiver) = unbounded_channel(); // Need to return all 3 values, so that they don't get dropped @@ -1116,7 +881,7 @@ mod tests { 0, cfg.clone(), MockValueSelector, - Box::new(DefaultLeaderElector {}), + Box::new(DefaultLeaderElector::new()), ); // Same here, we would like to not lose party's event_receiver, so that test doesn't fail. @@ -1128,7 +893,10 @@ mod tests { party.launch_ballot().await.unwrap(); }); + time::advance(cfg.launch_timeout).await; + // Sequential time advance and event check + time::advance(cfg.launch1a_timeout).await; assert_eq!(event_receiver.recv().await.unwrap(), PartyEvent::Launch1a); @@ -1148,78 +916,8 @@ mod tests { assert_eq!(event_receiver.recv().await.unwrap(), PartyEvent::Finalize); } - fn debug_hash_to_range_new(seed: u64, range: u64) -> u64 { - assert!(range > 1); - - let mut k = 64; - while 1u64 << (k - 1) >= range { - k -= 1; - } - - let rng = Random::from_seed(Seed::unsafe_new(seed)); - - let mut iteration = 1u64; - loop { - let mut raw_res: u64 = rng.gen(); - raw_res >>= 64 - k; - - if raw_res < range { - return raw_res; - } - - iteration += 1; - assert!(iteration <= 50) - } - } - - #[test] - #[ignore] // Ignoring since it takes a while to run - fn test_hash_range_random() { - // test the uniform distribution - - const N: usize = 37; - const M: i64 = 10000000; - - let mut cnt1: [i64; N] = [0; N]; - - for _ in 0..M { - let mut rng = rand::thread_rng(); - let seed: u64 = rng.random(); - - let res1 = debug_hash_to_range_new(seed, N as u64); - assert!(res1 < N as u64); - - cnt1[res1 as usize] += 1; - } - - println!("1: {:?}", cnt1); - - let mut avg1: i64 = 0; - - for item in cnt1.iter().take(N) { - avg1 += (M / (N as i64) - item).abs(); - } - - avg1 /= N as i64; - - println!("Avg 1: {}", avg1); - } - - #[test] - fn test_rng() { - let rng1 = Random::from_seed(Seed::unsafe_new(123456)); - let rng2 = Random::from_seed(Seed::unsafe_new(123456)); - - println!("{}", rng1.gen::()); - println!("{}", rng2.gen::()); - - thread::sleep(Duration::from_secs(2)); - - println!("{}", rng1.gen::()); - println!("{}", rng2.gen::()); - } - #[tokio::test] + #[ignore] // this is unfinished test async fn test_end_to_end_ballot() { // Configuration for the parties let cfg = BPConConfig { @@ -1275,7 +973,7 @@ mod tests { let (value_sender2, value_receiver2) = tokio::sync::oneshot::channel(); let (value_sender3, value_receiver3) = tokio::sync::oneshot::channel(); - let leader = party0.elector.get_leader(&party0).unwrap(); + let leader = party0.elector.elect_leader(&party0).unwrap(); println!("Leader: {leader}"); // Launch ballot tasks for each party @@ -1337,13 +1035,13 @@ mod tests { // Simulate message passing between the parties tokio::spawn(async move { - let mut receivers = vec![ + let mut receivers = [ msg_out_receiver0, msg_out_receiver1, msg_out_receiver2, msg_out_receiver3, ]; - let senders = vec![ + let senders = [ msg_in_sender0, msg_in_sender1, msg_in_sender2, @@ -1351,19 +1049,19 @@ mod tests { ]; loop { - for i in 0..receivers.len() { - if let Ok(msg) = receivers[i].try_recv() { + for (i, receiver) in receivers.iter_mut().enumerate() { + if let Ok(msg) = receiver.try_recv() { // Broadcast the message to all other parties - for j in 0..senders.len() { + for (j, sender) in senders.iter().enumerate() { if i != j { - let _ = senders[j].send(msg.clone()); + let _ = sender.send(msg.clone()); } } } } // Delay to simulate network latency - tokio::time::sleep(Duration::from_millis(100)).await; + sleep(Duration::from_millis(100)).await; } }); From b5412fd220299ee3ed1ec20792c6fd03e68b636e Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Thu, 29 Aug 2024 09:16:18 +0300 Subject: [PATCH 4/9] fix: resolved end to end ballot -> working --- src/config.rs | 4 ++-- src/party.rs | 64 +++++++++++++++++++++------------------------------ 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/src/config.rs b/src/config.rs index f7bd6e4..56c5341 100644 --- a/src/config.rs +++ b/src/config.rs @@ -42,14 +42,14 @@ impl BPConConfig { party_weights, threshold, // TODO: deduce actually good defaults - launch_timeout: Duration::from_secs(1), + launch_timeout: Duration::from_secs(0), launch1a_timeout: Duration::from_secs(5), launch1b_timeout: Duration::from_secs(10), launch2a_timeout: Duration::from_secs(15), launch2av_timeout: Duration::from_secs(20), launch2b_timeout: Duration::from_secs(25), finalize_timeout: Duration::from_secs(30), - grace_period: Duration::from_secs(2), + grace_period: Duration::from_secs(1), } } } diff --git a/src/party.rs b/src/party.rs index 67da646..e71cb0f 100644 --- a/src/party.rs +++ b/src/party.rs @@ -16,7 +16,7 @@ use crate::message::{ MessagePacket, MessageRoundState, ProtocolMessage, }; use crate::{Value, ValueSelector}; -use log::warn; +use log::{debug, warn}; use std::cmp::PartialEq; use std::collections::hash_map::Entry::Vacant; use std::collections::HashMap; @@ -269,6 +269,7 @@ impl> Party { msg = self.msg_in_receiver.recv() => { sleep(self.cfg.grace_period).await; if let Some(msg) = msg { + debug!("Party {} received {} from party {}", self.id, msg.routing.msg_type, msg.routing.sender); if let Err(err) = self.update_state(&msg) { // Shouldn't fail the party, since invalid message // may be sent by anyone. @@ -532,12 +533,12 @@ impl> Party { .into()); } - let content = &Message1aContent { - ballot: self.ballot, - }; - let msg = content.pack(self.id)?; - if self.leader == self.id { + let content = &Message1aContent { + ballot: self.ballot, + }; + let msg = content.pack(self.id)?; + self.msg_out_sender .send(msg) .map_err(|err| FailedToSendMessage(err.to_string()))?; @@ -581,20 +582,22 @@ impl> Party { } .into()); } + if self.leader == self.id { + let value = bincode::serialize(&self.get_value()) + .map_err(|err| SerializationError::Value(err.to_string()))?; - let value = bincode::serialize(&self.get_value()) - .map_err(|err| SerializationError::Value(err.to_string()))?; - - let content = &Message2aContent { - ballot: self.ballot, - value, - }; - let msg = content.pack(self.id)?; + let content = &Message2aContent { + ballot: self.ballot, + value, + }; + let msg = content.pack(self.id)?; - if self.leader == self.id { self.msg_out_sender .send(msg) .map_err(|err| FailedToSendMessage(err.to_string()))?; + + self.value_2a = Some(self.get_value()); + self.status = PartyStatus::Passed2a; } } PartyEvent::Launch2av => { @@ -606,7 +609,7 @@ impl> Party { .into()); } - let received_value = bincode::serialize(&self.value_2a.clone()) + let received_value = bincode::serialize(&self.value_2a.clone().unwrap()) .map_err(|err| SerializationError::Value(err.to_string()))?; let content = &Message2avContent { @@ -917,25 +920,13 @@ pub(crate) mod tests { } #[tokio::test] - #[ignore] // this is unfinished test async fn test_end_to_end_ballot() { // Configuration for the parties - let cfg = BPConConfig { - party_weights: vec![1, 2, 3, 4], // Total weight is 10 - threshold: 7, // 2/3 of total weight is ~6.67, so we set 7 as threshold - launch_timeout: Duration::from_secs(1), - launch1a_timeout: Duration::from_secs(5), - launch1b_timeout: Duration::from_secs(5), - launch2a_timeout: Duration::from_secs(5), - launch2av_timeout: Duration::from_secs(5), - launch2b_timeout: Duration::from_secs(5), - finalize_timeout: Duration::from_secs(5), - grace_period: Duration::from_secs(2), - }; + let cfg = BPConConfig::with_default_timeouts(vec![1, 1, 1, 1], 2); // ValueSelector and LeaderElector instances let value_selector = MockValueSelector; - let leader_elector = Box::new(DefaultLeaderElector {}); + let leader_elector = Box::new(DefaultLeaderElector::new()); // Create 4 parties let (mut party0, msg_out_receiver0, msg_in_sender0) = @@ -973,14 +964,11 @@ pub(crate) mod tests { let (value_sender2, value_receiver2) = tokio::sync::oneshot::channel(); let (value_sender3, value_receiver3) = tokio::sync::oneshot::channel(); - let leader = party0.elector.elect_leader(&party0).unwrap(); - println!("Leader: {leader}"); - // Launch ballot tasks for each party let ballot_task0 = tokio::spawn(async move { match party0.launch_ballot().await { Ok(Some(value)) => { - let _ = value_sender0.send(value); + value_sender0.send(value).unwrap(); } Ok(None) => { eprintln!("Party 0: No value was selected"); @@ -994,7 +982,7 @@ pub(crate) mod tests { let ballot_task1 = tokio::spawn(async move { match party1.launch_ballot().await { Ok(Some(value)) => { - let _ = value_sender1.send(value); + value_sender1.send(value).unwrap(); } Ok(None) => { eprintln!("Party 1: No value was selected"); @@ -1008,7 +996,7 @@ pub(crate) mod tests { let ballot_task2 = tokio::spawn(async move { match party2.launch_ballot().await { Ok(Some(value)) => { - let _ = value_sender2.send(value); + value_sender2.send(value).unwrap(); } Ok(None) => { eprintln!("Party 2: No value was selected"); @@ -1022,7 +1010,7 @@ pub(crate) mod tests { let ballot_task3 = tokio::spawn(async move { match party3.launch_ballot().await { Ok(Some(value)) => { - let _ = value_sender3.send(value); + value_sender3.send(value).unwrap(); } Ok(None) => { eprintln!("Party 3: No value was selected"); @@ -1054,7 +1042,7 @@ pub(crate) mod tests { // Broadcast the message to all other parties for (j, sender) in senders.iter().enumerate() { if i != j { - let _ = sender.send(msg.clone()); + sender.send(msg.clone()).unwrap(); } } } From 1f887a0e542861a7653468f94d944102b7b2fc5c Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Thu, 29 Aug 2024 17:14:27 +0300 Subject: [PATCH 5/9] feat: moved value definition into separate file --- src/error.rs | 2 +- src/leader.rs | 2 +- src/lib.rs | 21 +-------------------- src/party.rs | 2 +- src/value.rs | 19 +++++++++++++++++++ 5 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 src/value.rs diff --git a/src/error.rs b/src/error.rs index c722feb..4cabc3b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ use crate::party::{PartyEvent, PartyStatus}; -use crate::Value; +use crate::value::Value; use thiserror::Error; #[derive(Error, Debug)] diff --git a/src/leader.rs b/src/leader.rs index ee101bf..15504a5 100644 --- a/src/leader.rs +++ b/src/leader.rs @@ -1,5 +1,5 @@ use crate::party::Party; -use crate::{Value, ValueSelector}; +use crate::value::{Value, ValueSelector}; use seeded_random::{Random, Seed}; use std::cmp::Ordering; use std::hash::{DefaultHasher, Hash, Hasher}; diff --git a/src/lib.rs b/src/lib.rs index e9cf60c..cdef8ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,25 +1,6 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fmt; -use std::fmt::Debug; - pub mod config; pub mod error; pub mod leader; pub mod message; pub mod party; - -/// General trait for value itself. -pub trait Value: Eq + Serialize + for<'a> Deserialize<'a> + Clone + Debug + fmt::Display {} - -/// Trait for value selector and verificator. -/// Value selection and verification may depend on different conditions for different values. -/// Note that value selection should follow the rules of BPCon: only safe values can be selected. -/// Party can not vote for different values, even in different ballots. -pub trait ValueSelector: Clone { - /// Verifies if a value is selected correctly. Accepts 2b messages from parties. - fn verify(&self, v: &V, m: &HashMap>) -> bool; - - /// Select value depending on inner conditions. Accepts 2b messages from parties. - fn select(&self, m: &HashMap>) -> V; -} +pub mod value; \ No newline at end of file diff --git a/src/party.rs b/src/party.rs index e71cb0f..84b59fe 100644 --- a/src/party.rs +++ b/src/party.rs @@ -15,7 +15,7 @@ use crate::message::{ Message1aContent, Message1bContent, Message2aContent, Message2avContent, Message2bContent, MessagePacket, MessageRoundState, ProtocolMessage, }; -use crate::{Value, ValueSelector}; +use crate::value::{Value, ValueSelector}; use log::{debug, warn}; use std::cmp::PartialEq; use std::collections::hash_map::Entry::Vacant; diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..a00c4b6 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; +use std::fmt; +use std::fmt::Debug; +use serde::{Deserialize, Serialize}; + +/// General trait for value itself. +pub trait Value: Eq + Serialize + for<'a> Deserialize<'a> + Clone + Debug + fmt::Display {} + +/// Trait for value selector and verificator. +/// Value selection and verification may depend on different conditions for different values. +/// Note that value selection should follow the rules of BPCon: only safe values can be selected. +/// Party can not vote for different values, even in different ballots. +pub trait ValueSelector: Clone { + /// Verifies if a value is selected correctly. Accepts 2b messages from parties. + fn verify(&self, v: &V, m: &HashMap>) -> bool; + + /// Select value depending on inner conditions. Accepts 2b messages from parties. + fn select(&self, m: &HashMap>) -> V; +} From 0c3d0ca93578281ef62d10b431a80094f4117752 Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Thu, 29 Aug 2024 18:40:05 +0300 Subject: [PATCH 6/9] chore: enhanced code-centric documentation --- src/config.rs | 82 ++++++++++++++++++++----- src/error.rs | 121 +++++++++++++++++++++++++++++++++++++ src/leader.rs | 91 +++++++++++++++++++++------- src/lib.rs | 2 +- src/message.rs | 120 +++++++++++++++++++++++++++++++++---- src/party.rs | 158 +++++++++++++++++++++++++++++++++++++------------ src/value.rs | 63 ++++++++++++++++---- 7 files changed, 540 insertions(+), 97 deletions(-) diff --git a/src/config.rs b/src/config.rs index 56c5341..5bf9bef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,47 +1,101 @@ +//! Definitions central to BPCon configuration. + use std::time::Duration; -/// BPCon configuration. Includes ballot time bounds and other stuff. +/// Configuration structure for BPCon. +/// +/// `BPConConfig` holds the various timeouts and weight configurations necessary +/// for the BPCon consensus protocol. This configuration controls the timing +/// of different stages in the protocol and the distribution of party weights. #[derive(PartialEq, Eq, Debug, Clone)] pub struct BPConConfig { - /// Parties weights: `party_weights[i]` corresponds to the i-th party weight + /// Parties weights: `party_weights[i]` corresponds to the i-th party's weight. + /// + /// These weights determine the influence each party has in the consensus process. + /// The sum of these weights is used to calculate the `threshold` for reaching a + /// Byzantine Fault Tolerant (BFT) quorum. pub party_weights: Vec, - /// Threshold weight to define BFT quorum: should be > 2/3 of total weight + /// Threshold weight to define BFT quorum. + /// + /// This value must be greater than 2/3 of the total weight of all parties combined. + /// The quorum is the minimum weight required to make decisions in the BPCon protocol. pub threshold: u128, - /// Timeout before ballot is launched. - /// Differs from `launch1a_timeout` having another status and not listening - /// to external events and messages. + /// Timeout before the ballot is launched. + /// + /// This timeout differs from `launch1a_timeout` as it applies to a distinct status + /// and does not involve listening to external events and messages. pub launch_timeout: Duration, - /// Timeout before 1a stage is launched. + /// Timeout before the 1a stage is launched. + /// + /// The 1a stage is the first phase of the BPCon consensus process, where leader + /// is informing other participants about the start of the ballot. + /// This timeout controls the delay before starting this stage. pub launch1a_timeout: Duration, - /// Timeout before 1b stage is launched. + /// Timeout before the 1b stage is launched. + /// + /// In the 1b stage, participants exchange their last voted ballot number and elected value. + /// This timeout controls the delay before starting this stage. pub launch1b_timeout: Duration, - /// Timeout before 2a stage is launched. + /// Timeout before the 2a stage is launched. + /// + /// The 2a stage involves leader proposing a selected value and + /// other participants verifying it. + /// This timeout controls the delay before starting this stage. pub launch2a_timeout: Duration, - /// Timeout before 2av stage is launched. + /// Timeout before the 2av stage is launched. + /// + /// The 2av stage is where 2a obtained value shall gather needed weight to pass. + /// This timeout controls the delay before starting this stage. pub launch2av_timeout: Duration, - /// Timeout before 2b stage is launched. + /// Timeout before the 2b stage is launched. + /// + /// The 2b stage is where the final value is chosen and broadcasted. + /// This timeout controls the delay before starting this stage. pub launch2b_timeout: Duration, - /// Timeout before finalization stage is launched. + /// Timeout before the finalization stage is launched. + /// + /// The finalization stage is not a part of protocol, but rather internal-centric mechanics + /// to conclude ballot. + /// This timeout controls the delay before starting this stage. pub finalize_timeout: Duration, - /// Timeout for a graceful period to help parties with latency. + /// Timeout for a graceful period to accommodate parties with latency. + /// + /// This grace period allows parties with slower communication or processing times + /// to catch up, helping to ensure that all parties can participate fully in the + /// consensus process. pub grace_period: Duration, } impl BPConConfig { + /// Create a new `BPConConfig` with default timeouts. + /// + /// This method initializes a `BPConConfig` instance using default timeout values for each + /// stage of the BPCon consensus protocol. These defaults are placeholders and should be + /// tuned according to the specific needs and characteristics of the network. + /// + /// # Parameters + /// + /// - `party_weights`: A vector of weights corresponding to each party involved in the consensus. + /// - `threshold`: The weight threshold required to achieve a BFT quorum. + /// + /// # Returns + /// + /// A new `BPConConfig` instance with the provided `party_weights` and `threshold`, + /// and default timeouts for all stages. pub fn with_default_timeouts(party_weights: Vec, threshold: u128) -> Self { Self { party_weights, threshold, - // TODO: deduce actually good defaults + // TODO: deduce actually good defaults. launch_timeout: Duration::from_secs(0), launch1a_timeout: Duration::from_secs(5), launch1b_timeout: Duration::from_secs(10), diff --git a/src/error.rs b/src/error.rs index 4cabc3b..3999324 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,131 +1,252 @@ +//! Definitions central to BPCon errors. + use crate::party::{PartyEvent, PartyStatus}; use crate::value::Value; use thiserror::Error; +/// Represents the various errors that can occur during the ballot launch process. #[derive(Error, Debug)] pub enum LaunchBallotError { + /// Occurs when an attempt to send a `PartyEvent` fails. + /// + /// - `0`: The `PartyEvent` that failed to send. + /// - `1`: A string detailing the specific failure reason. #[error("failed to send event {0}: {1}")] FailedToSendEvent(PartyEvent, String), + + /// Occurs when the event channel is unexpectedly closed. #[error("event channel closed")] EventChannelClosed, + + /// Occurs when the message channel is unexpectedly closed. #[error("message channel closed")] MessageChannelClosed, + + /// Occurs when following a `PartyEvent` fails. + /// + /// - `0`: The `PartyEvent` that caused the error. + /// - `1`: The specific `FollowEventError` detailing why the follow operation failed. #[error("failed to follow event {0}: {1}")] FollowEventError(PartyEvent, FollowEventError), + + /// Occurs during leader election if an error is encountered. + /// + /// - `0`: A string providing details about the leader election failure. #[error("leader election error: {0}")] LeaderElectionError(String), } +/// Represents the various errors that can occur while following a party event. #[derive(Error, Debug)] pub enum FollowEventError { + /// Occurs when there is a mismatch in the expected party status. + /// + /// - `0`: The specific `PartyStatusMismatch` that caused the error. #[error("{0}")] PartyStatusMismatch(PartyStatusMismatch), + + /// Occurs when there is an error during serialization. + /// + /// - `0`: The specific `SerializationError` encountered. #[error("{0}")] SerializationError(SerializationError), + + /// Occurs when sending a message related to the event fails. + /// + /// - `0`: A string describing the reason for the failure. #[error("{0}")] FailedToSendMessage(String), } +/// Represents the various errors that can occur when updating the state in a consensus protocol. +/// +/// This enum is generic over `V`, which must implement the `Value` trait. #[derive(Error, Debug)] pub enum UpdateStateError { + /// Occurs when there is a mismatch in the expected party status. + /// + /// - `0`: The specific `PartyStatusMismatch` that caused the error. #[error("{0}")] PartyStatusMismatch(PartyStatusMismatch), + + /// Occurs when there is a mismatch in the ballot number. + /// + /// - `0`: The specific `BallotNumberMismatch` that caused the error. #[error("{0}")] BallotNumberMismatch(BallotNumberMismatch), + + /// Occurs when there is a mismatch in the expected leader. + /// + /// - `0`: The specific `LeaderMismatch` that caused the error. #[error("{0}")] LeaderMismatch(LeaderMismatch), + + /// Occurs when there is a mismatch in the expected value. + /// + /// - `0`: The specific `ValueMismatch` that caused the error, where `V` is the type of the value. #[error("{0}")] ValueMismatch(ValueMismatch), + + /// Occurs when the value verification fails during the state update process. #[error("value verification failed")] ValueVerificationFailed, + + /// Occurs when there is an error during deserialization. + /// + /// - `0`: The specific `DeserializationError` encountered. #[error("{0}")] DeserializationError(DeserializationError), } +/// Represents an error that occurs when there is a mismatch between the current status of the party +/// and the status required for an operation. +/// +/// This error is typically encountered when an operation requires the party to be in a specific +/// state, but the party is in a different state. #[derive(Error, Debug)] #[error( "party status mismatch: party status is {party_status} whilst needed status is {needed_status}" )] pub struct PartyStatusMismatch { + /// The current status of the party. pub party_status: PartyStatus, + /// The status required for the operation to proceed. pub needed_status: PartyStatus, } +/// Represents an error that occurs when there is a mismatch between the ballot number of the party +/// and the ballot number received in a message. #[derive(Error, Debug)] #[error("ballot number mismatch: party's ballot number is {party_ballot_number} whilst received {message_ballot_number} in the message")] pub struct BallotNumberMismatch { + /// The ballot number held by the party. pub party_ballot_number: u64, + /// The ballot number received in the message. pub message_ballot_number: u64, } +/// Represents an error that occurs when there is a mismatch between the expected leader +/// of the party and the sender of the message. #[derive(Error, Debug)] #[error("leader mismatch: party's leader is {party_leader} whilst the message was sent by {message_sender}")] pub struct LeaderMismatch { + /// The leader identifier of the party. pub party_leader: u64, + /// The identifier of the message sender. pub message_sender: u64, } +/// Represents an error that occurs when there is a mismatch between the value held by the party +/// and the value received in a message. +/// +/// This struct is generic over `V`, which must implement the `Value` trait. #[derive(Error, Debug)] #[error( "value mismatch: party's value is {party_value} whilst received {message_value} in the message" )] pub struct ValueMismatch { + /// The value held by the party. pub party_value: V, + /// The value received in the message. pub message_value: V, } +/// Represents the various errors that can occur during the deserialization process. #[derive(Error, Debug)] pub enum DeserializationError { + /// Occurs when there is an error deserializing a message. + /// + /// - `0`: A string describing the specific deserialization error for the message. #[error("message deserialization error: {0}")] Message(String), + + /// Occurs when there is an error deserializing a value. + /// + /// - `0`: A string describing the specific deserialization error for the value. #[error("value deserialization error: {0}")] Value(String), } +/// Represents the various errors that can occur during the serialization process. #[derive(Error, Debug)] pub enum SerializationError { + /// Occurs when there is an error serializing a message. + /// + /// - `0`: A string describing the specific serialization error for the message. #[error("message serialization error: {0}")] Message(String), + + /// Occurs when there is an error serializing a value. + /// + /// - `0`: A string describing the specific serialization error for the value. #[error("value serialization error: {0}")] Value(String), } +/// Converts a `PartyStatusMismatch` into a `FollowEventError`. +/// +/// This conversion allows `PartyStatusMismatch` errors to be treated as `FollowEventError`, +/// which may occur when there is a status mismatch while following an event. impl From for FollowEventError { fn from(error: PartyStatusMismatch) -> Self { FollowEventError::PartyStatusMismatch(error) } } +/// Converts a `SerializationError` into a `FollowEventError`. +/// +/// This conversion is used when a serialization error occurs during the process +/// of following an event, allowing it to be handled as a `FollowEventError`. impl From for FollowEventError { fn from(error: SerializationError) -> Self { FollowEventError::SerializationError(error) } } +/// Converts a `PartyStatusMismatch` into an `UpdateStateError`. +/// +/// This conversion is useful when a status mismatch occurs while updating the state, +/// allowing the error to be handled within the context of state updates. impl From for UpdateStateError { fn from(error: PartyStatusMismatch) -> Self { UpdateStateError::PartyStatusMismatch(error) } } +/// Converts a `LeaderMismatch` into an `UpdateStateError`. +/// +/// This conversion is used when there is a leader mismatch during state updates, +/// allowing the error to be propagated and handled as an `UpdateStateError`. impl From for UpdateStateError { fn from(error: LeaderMismatch) -> Self { UpdateStateError::LeaderMismatch(error) } } +/// Converts a `BallotNumberMismatch` into an `UpdateStateError`. +/// +/// This conversion is used when there is a ballot number mismatch during state updates, +/// allowing the error to be propagated and handled as an `UpdateStateError`. impl From for UpdateStateError { fn from(error: BallotNumberMismatch) -> Self { UpdateStateError::BallotNumberMismatch(error) } } +/// Converts a `ValueMismatch` into an `UpdateStateError`. +/// +/// This conversion is used when there is a value mismatch during state updates, +/// allowing the error to be propagated and handled as an `UpdateStateError`. impl From> for UpdateStateError { fn from(error: ValueMismatch) -> Self { UpdateStateError::ValueMismatch(error) } } +/// Converts a `DeserializationError` into an `UpdateStateError`. +/// +/// This conversion is used when a deserialization error occurs during state updates, +/// allowing the error to be handled within the context of state updates. impl From for UpdateStateError { fn from(error: DeserializationError) -> Self { UpdateStateError::DeserializationError(error) diff --git a/src/leader.rs b/src/leader.rs index 15504a5..d565b9a 100644 --- a/src/leader.rs +++ b/src/leader.rs @@ -1,3 +1,8 @@ +//! Definitions central to BPCon leader election logic. +//! +//! This module defines the traits and structures involved in leader election within the BPCon consensus protocol. +//! The leader election process is crucial in BPCon, ensuring that a single leader is chosen to coordinate the consensus rounds. + use crate::party::Party; use crate::value::{Value, ValueSelector}; use seeded_random::{Random, Seed}; @@ -5,22 +10,52 @@ use std::cmp::Ordering; use std::hash::{DefaultHasher, Hash, Hasher}; use thiserror::Error; -/// Trait incorporating logic for leader election. +/// A trait that encapsulates the logic for leader election in the BPCon protocol. +/// +/// Implementors of this trait provide the mechanism to elect a leader from the participating parties +/// for a given ballot round. The elected leader is responsible for driving the consensus process +/// during that round. +/// +/// # Type Parameters +/// - `V`: The type of values being proposed and agreed upon in the consensus process, which must implement the `Value` trait. +/// - `VS`: The type of value selector used to choose values during the consensus process, which must implement the `ValueSelector` trait. pub trait LeaderElector>: Send { - /// Get leader for current ballot. - /// Returns id of the elected party or error. + /// Elects a leader for the current ballot. + /// + /// This method returns the ID of the party elected as the leader for the current ballot round. + /// + /// # Parameters + /// - `party`: A reference to the `Party` instance containing information about the current ballot and participating parties. + /// + /// # Returns + /// - `Ok(u64)`: The ID of the elected party. + /// - `Err(Box)`: An error if leader election fails. fn elect_leader(&self, party: &Party) -> Result>; } +/// A default implementation of the `LeaderElector` trait for BPCon. +/// +/// This struct provides a standard mechanism for leader election based on weighted randomization, +/// ensuring deterministic leader selection across multiple rounds. #[derive(Clone, Debug, Default)] pub struct DefaultLeaderElector {} impl DefaultLeaderElector { + /// Creates a new `DefaultLeaderElector` instance. pub fn new() -> Self { Self::default() } - /// Compute seed for randomized leader election. + /// Computes a seed for randomized leader election. + /// + /// This method generates a seed value based on the party configuration and the current ballot, + /// ensuring that leader election is deterministic but randomized. + /// + /// # Parameters + /// - `party`: A reference to the `Party` instance containing information about the current ballot and participating parties. + /// + /// # Returns + /// A `u64` seed value for use in leader election. fn compute_seed>(party: &Party) -> u64 { let mut hasher = DefaultHasher::new(); @@ -29,26 +64,30 @@ impl DefaultLeaderElector { party.cfg.threshold.hash(&mut hasher); party.ballot.hash(&mut hasher); - // You can add more fields as needed - // Generate the seed from the hash hasher.finish() } - /// Hash the seed to a value within a given range. + /// Hashes the seed to a value within a specified range. + /// + /// This method uses the computed seed to generate a value within the range [0, range). + /// The algorithm ensures uniform distribution of the resulting value, which is crucial + /// for fair leader election. + /// + /// # Parameters + /// - `seed`: The seed value used for randomization. + /// - `range`: The upper limit for the random value generation, typically the sum of party weights. + /// + /// # Returns + /// A `u64` value within the specified range. fn hash_to_range(seed: u64, range: u64) -> u64 { - // Select the `k` suck that value 2^k >= `range` and 2^k is the smallest. + // Determine the number of bits required to represent the range let mut k = 64; while 1u64 << (k - 1) >= range { k -= 1; } - // The following algorithm selects a random u64 value using `ChaCha12Rng` - // and reduces the result to the k-bits such that 2^k >= `range` the closes power of to the `range`. - // After we check if the result lies in [0..`range`) or [`range`..2^k). - // In the first case result is an acceptable value generated uniformly. - // In the second case we repeat the process again with the incremented iterations counter. - // Ref: Practical Cryptography 1st Edition by Niels Ferguson, Bruce Schneier, paragraph 10.8 + // Use a seeded random generator to produce a value within the desired range let rng = Random::from_seed(Seed::unsafe_new(seed)); loop { let mut raw_res: u64 = rng.gen(); @@ -57,21 +96,31 @@ impl DefaultLeaderElector { if raw_res < range { return raw_res; } - // Executing this loop does not require a large number of iterations. - // Check tests for more info + // If the generated value is not within range, repeat the process } } } +/// Errors that can occur during leader election using the `DefaultLeaderElector`. #[derive(Error, Debug)] pub enum DefaultLeaderElectorError { + /// Error indicating that the sum of party weights is zero, making leader election impossible. #[error("zero weight sum")] ZeroWeightSum, } impl> LeaderElector for DefaultLeaderElector { - /// Compute leader in a weighed randomized manner. - /// Uses seed from the config, making it deterministic. + /// Elects a leader in a weighted randomized manner. + /// + /// This method uses the computed seed and party weights to select a leader. The process is deterministic + /// due to the use of a fixed seed but allows for randomized leader selection based on the weight distribution. + /// + /// # Parameters + /// - `party`: A reference to the `Party` instance containing information about the current ballot and participating parties. + /// + /// # Returns + /// - `Ok(u64)`: The ID of the elected party. + /// - `Err(Box)`: An error if the leader election process fails, such as when the total weight is zero. fn elect_leader(&self, party: &Party) -> Result> { let seed = DefaultLeaderElector::compute_seed(party); @@ -83,7 +132,7 @@ impl> LeaderElector for DefaultLeaderElect // Generate a random number in the range [0, total_weight) let random_value = DefaultLeaderElector::hash_to_range(seed, total_weight); - // Use binary search to find the corresponding participant + // Use binary search to find the corresponding participant based on the cumulative weight let mut cumulative_weights = vec![0; party.cfg.party_weights.len()]; cumulative_weights[0] = party.cfg.party_weights[0]; @@ -139,7 +188,7 @@ mod tests { let elector = DefaultLeaderElector::new(); match elector.elect_leader(&party) { - Err(_) => {} // this is expected behaviour + Err(_) => {} // This is the expected behavior _ => panic!("Expected DefaultLeaderElectorError::ZeroWeightSum"), } } @@ -171,7 +220,7 @@ mod tests { #[test] #[ignore] // Ignoring since it takes a while to run fn test_hash_range_random() { - // test the uniform distribution + // Test the uniform distribution const N: usize = 37; const M: i64 = 10000000; diff --git a/src/lib.rs b/src/lib.rs index cdef8ff..db83c12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,4 +3,4 @@ pub mod error; pub mod leader; pub mod message; pub mod party; -pub mod value; \ No newline at end of file +pub mod value; diff --git a/src/message.rs b/src/message.rs index 56839a2..fdb6302 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,28 +1,35 @@ -//! Definition of the BPCon messages. +//! Definitions central to BPCon messages. +//! +//! This module defines the structures and functionality for messages used in the BPCon consensus protocol. +//! It includes message contents, routing information, and utilities for packing and unpacking messages. use crate::error::{DeserializationError, SerializationError}; use rkyv::{AlignedVec, Archive, Deserialize, Infallible, Serialize}; use std::collections::HashSet; -/// Message ready for transfer. +/// A message ready for transfer within the BPCon protocol. #[derive(Debug, Clone)] pub struct MessagePacket { /// Serialized message contents. pub content_bytes: AlignedVec, - /// Routing information. + /// Routing information for the message. pub routing: MessageRouting, } -/// Full routing information for the message. +/// Full routing information for a BPCon message. +/// This struct is used to manage and route messages through the BPCon protocol. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct MessageRouting { - /// Which participant this message came from. + /// The ID of the participant who sent the message. pub sender: u64, - /// Stores the BPCon message type. + /// The type of BPCon protocol message. pub msg_type: ProtocolMessage, } -/// Representation of message types of the consensus. +/// Enumeration of the different types of protocol messages used in BPCon. +/// +/// These message types represent the various stages of the BPCon consensus protocol, +/// each corresponding to a specific phase in the process. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum ProtocolMessage { Msg1a, @@ -38,51 +45,82 @@ impl std::fmt::Display for ProtocolMessage { } } -// Value in messages is stored in serialized format, i.e bytes in order to omit -// strict restriction for `Value` trait to be [de]serializable only with `rkyv`. +// The following structures represent the contents of different BPCon protocol messages. +// Each message type is serialized before being sent, and deserialized upon receipt. +// Value in messages is stored in serialized format, i.e., bytes, to avoid strict restrictions +// on the `Value` trait being [de]serializable only with `rkyv`. + +/// Contents of a BPCon message of type 1a. #[derive(Archive, Deserialize, Serialize, Debug, Clone)] #[archive(compare(PartialEq), check_bytes)] #[archive_attr(derive(Debug))] pub struct Message1aContent { + /// The ballot number associated with this message. pub ballot: u64, } +/// Contents of a BPCon message of type 1b. #[derive(Archive, Deserialize, Serialize, Debug, Clone)] #[archive(compare(PartialEq), check_bytes)] #[archive_attr(derive(Debug))] pub struct Message1bContent { + /// The ballot number associated with this message. pub ballot: u64, + /// The last ballot number that was voted on, if any. pub last_ballot_voted: Option, + /// The last value that was voted on, serialized as a vector of bytes, if any. pub last_value_voted: Option>, } +/// Contents of a BPCon message of type 2a. #[derive(Archive, Deserialize, Serialize, Debug, Clone)] #[archive(compare(PartialEq), check_bytes)] #[archive_attr(derive(Debug))] pub struct Message2aContent { + /// The ballot number associated with this message. pub ballot: u64, + /// The proposed value, serialized as a vector of bytes. pub value: Vec, } +/// Contents of a BPCon message of type 2av. #[derive(Archive, Deserialize, Serialize, Debug, Clone)] #[archive(compare(PartialEq), check_bytes)] #[archive_attr(derive(Debug))] pub struct Message2avContent { + /// The ballot number associated with this message. pub ballot: u64, + /// The received value, serialized as a vector of bytes. pub received_value: Vec, } +/// Contents of a BPCon message of type 2b. #[derive(Archive, Deserialize, Serialize, Debug, Clone)] #[archive(compare(PartialEq), check_bytes)] #[archive_attr(derive(Debug))] pub struct Message2bContent { + /// The ballot number associated with this message. pub ballot: u64, } +// Macro to implement packing and unpacking logic for each message content type. +// This logic handles serialization to bytes and deserialization from bytes, as well as setting up routing information. + macro_rules! impl_packable { ($type:ty, $msg_type:expr) => { impl $type { + /// Packs the message content into a `MessagePacket` for transfer. + /// + /// This method serializes the message content and combines it with routing information. + /// + /// # Parameters + /// + /// - `sender`: The ID of the participant sending the message. + /// + /// # Returns + /// + /// A `MessagePacket` containing the serialized message and routing information, or a `SerializationError` if packing fails. pub fn pack(&self, sender: u64) -> Result { let content_bytes = rkyv::to_bytes::<_, 256>(self) .map_err(|err| SerializationError::Message(err.to_string()))?; @@ -92,6 +130,17 @@ macro_rules! impl_packable { }) } + /// Unpacks a `MessagePacket` to extract the original message content. + /// + /// This method deserializes the message content from the byte representation stored in the packet. + /// + /// # Parameters + /// + /// - `msg`: A reference to the `MessagePacket` to be unpacked. + /// + /// # Returns + /// + /// The deserialized message content, or a `DeserializationError` if unpacking fails. pub fn unpack(msg: &MessagePacket) -> Result { let archived = rkyv::check_archived_root::(msg.content_bytes.as_slice()) .map_err(|err| DeserializationError::Message(err.to_string()))?; @@ -100,6 +149,17 @@ macro_rules! impl_packable { .map_err(|err| DeserializationError::Message(err.to_string())) } + /// Creates routing information for the message. + /// + /// This method sets up the routing details, including the sender and the message type. + /// + /// # Parameters + /// + /// - `sender`: The ID of the participant sending the message. + /// + /// # Returns + /// + /// A `MessageRouting` instance containing the routing information. fn route(sender: u64) -> MessageRouting { MessageRouting { sender, @@ -110,6 +170,7 @@ macro_rules! impl_packable { }; } +// Implement the packing and unpacking functionality for each message content type. impl_packable!(Message1aContent, ProtocolMessage::Msg1a); impl_packable!(Message1bContent, ProtocolMessage::Msg1b); impl_packable!(Message2aContent, ProtocolMessage::Msg2a); @@ -117,6 +178,9 @@ impl_packable!(Message2avContent, ProtocolMessage::Msg2av); impl_packable!(Message2bContent, ProtocolMessage::Msg2b); /// A struct to keep track of senders and the cumulative weight of their messages. +/// +/// `MessageRoundState` is used to monitor the participants who have sent messages and to accumulate +/// the total weight of these messages. This helps in determining when a quorum has been reached. #[derive(PartialEq, Eq, Clone, Debug, Default)] pub struct MessageRoundState { senders: HashSet, @@ -124,27 +188,57 @@ pub struct MessageRoundState { } impl MessageRoundState { - /// Creates a new instance of `MessageRoundState`. + /// Creates a new, empty `MessageRoundState`. + /// + /// This method initializes an empty state with no senders and zero weight. + /// + /// # Returns + /// + /// A new `MessageRoundState` instance. pub fn new() -> Self { Self::default() } + /// Returns the cumulative weight of all messages received in this round. + /// + /// # Returns + /// + /// The total weight of all messages that have been added to this state. pub fn get_weight(&self) -> u128 { self.weight } - /// Adds a sender and their corresponding weight. + /// Adds a sender and their corresponding weight to the state. + /// + /// This method updates the state to include the specified sender and increments the cumulative + /// weight by the specified amount. + /// + /// # Parameters + /// + /// - `sender`: The ID of the participant sending the message. + /// - `weight`: The weight associated with this sender's message. pub fn add_sender(&mut self, sender: u64, weight: u128) { self.senders.insert(sender); self.weight += weight; } - /// Checks if the sender has already sent a message. + /// Checks if a sender has already sent a message in this round. + /// + /// # Parameters + /// + /// - `sender`: A reference to the ID of the participant. + /// + /// # Returns + /// + /// `true` if the sender has already sent a message, `false` otherwise. pub fn contains_sender(&self, sender: &u64) -> bool { self.senders.contains(sender) } - /// Resets the state. + /// Resets the state for a new round. + /// + /// This method clears all recorded senders and resets the cumulative weight to zero, + /// preparing the state for a new round of message collection. pub fn reset(&mut self) { self.senders.clear(); self.weight = 0; diff --git a/src/party.rs b/src/party.rs index 84b59fe..8282dc6 100644 --- a/src/party.rs +++ b/src/party.rs @@ -1,4 +1,8 @@ -//! Definition of the BPCon participant structure. +//! Definitions central to BPCon participant. +//! +//! This module contains the implementation of a `Party` in the BPCon consensus protocol. +//! The `Party` manages the execution of the ballot, handles incoming messages, and coordinates with other participants +//! to reach a consensus. It uses various components such as leader election and value selection to perform its duties. use crate::config::BPConConfig; use crate::error::FollowEventError::FailedToSendMessage; @@ -23,8 +27,10 @@ use std::collections::HashMap; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::time::sleep; -/// Party status defines the statuses of the ballot for the particular participant -/// depending on local calculations. +/// Represents the status of a `Party` in the BPCon consensus protocol. +/// +/// The status indicates the current phase or outcome of the ballot execution for this party. +/// It transitions through various states as the party progresses through the protocol. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum PartyStatus { None, @@ -44,7 +50,9 @@ impl std::fmt::Display for PartyStatus { } } -/// Party events is used for the ballot flow control. +/// Represents the events that control the flow of the ballot process in a `Party`. +/// +/// These events trigger transitions between different phases of the protocol. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum PartyEvent { Launch1a, @@ -61,74 +69,90 @@ impl std::fmt::Display for PartyEvent { } } -/// Party of the BPCon protocol that executes ballot. +/// A participant in the BPCon protocol responsible for executing ballots. /// -/// The communication between party and external -/// system is done via `in_receiver` and `out_sender` channels. External system should take -/// care about authentication while receiving incoming messages and then push them to the -/// corresponding `Sender` to the `in_receiver`. Same, it should tke care about listening of new -/// messages in the corresponding `Receiver` to the `out_sender` and submitting them to the -/// corresponding party based on information in `MessageRouting`. +/// A `Party` manages the execution of ballots by communicating with other parties, processing incoming messages, +/// and following the protocol's steps. It uses an internal state machine to track its progress through the ballot process. /// -/// After finishing of the ballot protocol, party will place the selected value to the -/// `value_sender` or `BallotError` if ballot failed. +/// # Communication +/// - `msg_in_receiver`: Receives incoming messages from other parties. +/// - `msg_out_sender`: Sends outgoing messages to other parties. +/// - `event_receiver`: Receives events that drive the ballot process. +/// - `event_sender`: Sends events to trigger actions in the ballot process. +/// +/// The `Party` operates within a BPCon configuration and relies on a `ValueSelector` to choose values during the consensus process. +/// A `LeaderElector` is used to determine the leader for each ballot. pub struct Party> { - /// This party's identifier. + /// The identifier of this party. pub id: u64, - /// Communication queues. + /// Queue for receiving incoming messages. msg_in_receiver: UnboundedReceiver, + /// Queue for sending outgoing messages. msg_out_sender: UnboundedSender, - /// Query to receive and send events that run ballot protocol + /// Queue for receiving events that control the ballot process. event_receiver: UnboundedReceiver, + /// Queue for sending events that control the ballot process. event_sender: UnboundedSender, - /// BPCon config (e.g. ballot time bounds, parties weights, etc.). + /// BPCon configuration settings, including timeouts and party weights. pub(crate) cfg: BPConConfig, - /// Main functional for value selection. + /// Component responsible for selecting values during the consensus process. value_selector: VS, - /// Main functional for leader election. + /// Component responsible for electing a leader for each ballot. elector: Box>, - /// Status of the ballot execution + /// The current status of the ballot execution for this party. status: PartyStatus, - /// Current ballot number + /// The current ballot number. pub(crate) ballot: u64, - /// Current ballot leader + /// The leader for the current ballot. leader: u64, - /// Last ballot where party submitted 2b message + /// The last ballot number where this party submitted a 2b message. last_ballot_voted: Option, - /// Last value for which party submitted 2b message + /// The last value for which this party submitted a 2b message. last_value_voted: Option, - /// Local round fields - - /// 1b round state - /// - parties_voted_before: HashMap>, // id <-> value + // Local round fields + /// The state of 1b round, tracking which parties have voted and their corresponding values. + parties_voted_before: HashMap>, + /// The cumulative weight of 1b messages received. messages_1b_weight: u128, - /// 2a round state - /// + /// The state of 2a round, storing the value proposed by this party. value_2a: Option, - /// 2av round state - /// + /// The state of 2av round, tracking which parties have confirmed the 2a value. messages_2av_state: MessageRoundState, - /// 2b round state - /// + /// The state of 2b round, tracking which parties have sent 2b messages. messages_2b_state: MessageRoundState, } impl> Party { + /// Creates a new `Party` instance. + /// + /// This constructor sets up the party with the given ID, BPCon configuration, value selector, and leader elector. + /// It also initializes communication channels for receiving and sending messages and events. + /// + /// # Parameters + /// - `id`: The unique identifier for this party. + /// - `cfg`: The BPCon configuration settings. + /// - `value_selector`: The component responsible for selecting values during the consensus process. + /// - `elector`: The component responsible for electing the leader for each ballot. + /// + /// # Returns + /// - A tuple containing: + /// - The new `Party` instance. + /// - The `UnboundedReceiver` for outgoing messages. + /// - The `UnboundedSender` for incoming messages. pub fn new( id: u64, cfg: BPConConfig, @@ -169,18 +193,32 @@ impl> Party { ) } + /// Returns the current ballot number. pub fn ballot(&self) -> u64 { self.ballot } + /// Checks if the ballot process has been launched. + /// + /// # Returns + /// - `true` if the ballot is currently active; `false` otherwise. pub fn is_launched(&self) -> bool { !self.is_stopped() } + /// Checks if the ballot process has been stopped. + /// + /// # Returns + /// - `true` if the ballot has finished or failed; `false` otherwise. pub fn is_stopped(&self) -> bool { self.status == PartyStatus::Finished || self.status == PartyStatus::Failed } + /// Retrieves the selected value if the ballot process has finished successfully. + /// + /// # Returns + /// - `Some(V)` if the ballot reached a consensus and the value was selected. + /// - `None` if the ballot did not reach consensus or is still ongoing. pub fn get_value_selected(&self) -> Option { // Only `Finished` status means reached BFT agreement if self.status == PartyStatus::Finished { @@ -190,10 +228,27 @@ impl> Party { None } + /// Selects a value based on the current state of the party. + /// + /// This method delegates to the `ValueSelector` to determine the value that should be selected + /// based on the votes received in the 1b round. + /// + /// # Returns + /// - The value selected by the `ValueSelector`. fn get_value(&self) -> V { self.value_selector.select(&self.parties_voted_before) } + /// Launches the ballot process. + /// + /// This method initiates the ballot process, advancing through the different phases of the protocol + /// by sending and receiving events and messages. It handles timeouts for each phase and processes + /// incoming messages to update the party's state. + /// + /// # Returns + /// - `Ok(Some(V))`: The selected value if the ballot reaches consensus. + /// - `Ok(None)`: If the ballot process is terminated without reaching consensus. + /// - `Err(LaunchBallotError)`: If an error occurs during the ballot process. pub async fn launch_ballot(&mut self) -> Result, LaunchBallotError> { self.prepare_next_ballot()?; @@ -298,7 +353,13 @@ impl> Party { Ok(self.get_value_selected()) } - /// Prepare state before running a ballot. + /// Prepares the party's state for the next ballot. + /// + /// This method resets the party's state, increments the ballot number, and elects a new leader. + /// + /// # Returns + /// - `Ok(())`: If the preparation is successful. + /// - `Err(LaunchBallotError)`: If an error occurs during leader election. fn prepare_next_ballot(&mut self) -> Result<(), LaunchBallotError> { self.reset_state(); self.ballot += 1; @@ -311,6 +372,9 @@ impl> Party { Ok(()) } + /// Resets the party's state for a new round of ballot execution. + /// + /// This method clears the state associated with previous rounds and prepares the party for the next ballot. fn reset_state(&mut self) { self.parties_voted_before = HashMap::new(); self.messages_1b_weight = 0; @@ -323,7 +387,17 @@ impl> Party { while self.msg_in_receiver.try_recv().is_ok() {} } - /// Update party's state based on message type. + /// Updates the party's state based on an incoming message. + /// + /// This method processes a message according to its type and updates the party's internal state accordingly. + /// It performs validation checks to ensure that the message is consistent with the current ballot and protocol rules. + /// + /// # Parameters + /// - `msg`: The incoming `MessagePacket` to be processed. + /// + /// # Returns + /// - `Ok(())`: If the state is successfully updated. + /// - `Err(UpdateStateError)`: If an error occurs during the update, such as a mismatch in the ballot number or leader. fn update_state(&mut self, msg: &MessagePacket) -> Result<(), UpdateStateError> { let routing = msg.routing; @@ -522,6 +596,16 @@ impl> Party { } /// Executes ballot actions according to the received event. + /// + /// This method processes an event and triggers the corresponding action in the ballot process, + /// such as launching a new phase or finalizing the ballot. + /// + /// # Parameters + /// - `event`: The `PartyEvent` to process. + /// + /// # Returns + /// - `Ok(())`: If the event is successfully processed. + /// - `Err(FollowEventError)`: If an error occurs while processing the event. fn follow_event(&mut self, event: PartyEvent) -> Result<(), FollowEventError> { match event { PartyEvent::Launch1a => { diff --git a/src/value.rs b/src/value.rs index a00c4b6..dadd753 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,19 +1,60 @@ -use std::collections::HashMap; -use std::fmt; -use std::fmt::Debug; +//! Definitions central to BPCon values. +//! +//! This module defines traits and structures related to the values used within the BPCon consensus protocol. +//! It includes a general `Value` trait for handling values in the protocol, as well as a `ValueSelector` trait +//! for implementing value selection and verification logic. + use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; -/// General trait for value itself. -pub trait Value: Eq + Serialize + for<'a> Deserialize<'a> + Clone + Debug + fmt::Display {} +/// A general trait representing a value in the BPCon consensus protocol. +/// +/// Implementing this trait ensures that values can be safely transmitted and logged during the +/// consensus process. +pub trait Value: Eq + Serialize + for<'a> Deserialize<'a> + Clone + Debug + Display {} -/// Trait for value selector and verificator. -/// Value selection and verification may depend on different conditions for different values. -/// Note that value selection should follow the rules of BPCon: only safe values can be selected. -/// Party can not vote for different values, even in different ballots. +/// A trait for selecting and verifying values in the BPCon consensus protocol. +/// +/// The `ValueSelector` trait provides the functionality for verifying that a value has been +/// selected according to the rules of the protocol and for selecting a value based on +/// specific conditions. +/// +/// ## Important Rules: +/// - **Safety**: Value selection must adhere to the BPCon protocol rules, meaning only "safe" values +/// can be selected. This implies that a party should not vote for different values, even across +/// different ballots. +/// - **Consensus Compliance**: The selection process should consider the state of messages (typically +/// from 2b messages) sent by other parties, ensuring the selected value is compliant with the +/// collective state of the consensus. +/// +/// # Type Parameters +/// - `V`: The type of value being selected and verified. This must implement the `Value` trait. pub trait ValueSelector: Clone { - /// Verifies if a value is selected correctly. Accepts 2b messages from parties. + /// Verifies if a value has been correctly selected. + /// + /// This method checks if the provided value `v` is selected according to the protocol's rules. + /// The verification process typically involves examining 1b messages from other parties, contained + /// within the `HashMap`. + /// + /// # Parameters + /// - `v`: The value to verify. + /// - `m`: A `HashMap` mapping party IDs (`u64`) to their corresponding optional values (`Option`). + /// + /// # Returns + /// `true` if the value is correctly selected; otherwise, `false`. fn verify(&self, v: &V, m: &HashMap>) -> bool; - /// Select value depending on inner conditions. Accepts 2b messages from parties. + /// Selects a value based on internal conditions and messages from other parties. + /// + /// This method determines the value to be selected for the consensus process, based on the state + /// of 1b messages received from other parties. The selection must comply with the BPCon protocol's + /// safety and consistency requirements. + /// + /// # Parameters + /// - `m`: A `HashMap` mapping party IDs (`u64`) to their corresponding optional values (`Option`). + /// + /// # Returns + /// The selected value of type `V`. fn select(&self, m: &HashMap>) -> V; } From 853ac62065bc29522f6299b4189970e1bb22bc0f Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Thu, 29 Aug 2024 19:01:04 +0300 Subject: [PATCH 7/9] chore: extended Cargo.toml --- Cargo.toml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ab68dd..e1f65ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,15 +2,29 @@ name = "bpcon" version = "0.1.0" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +description = "BPCon: A Byzantine Fault-Tolerant Consensus Protocol Implementation in Rust." +license = "MIT" +repository = "https://github.com/distributed-lab/bpcon" +homepage = "https://github.com/distributed-lab/bpcon" +documentation = "https://distributed-lab.github.io/bpcon/" +keywords = ["consensus", "byzantine", "protocol", "distributed-systems", "blockchain"] +categories = ["algorithms"] +authors = ["Distributed Lab"] +readme = "README.md" [dependencies] -log = "0.4.22" -rkyv = { version = "0.7.44", features = ["validation"]} -serde = { version = "1.0.207", features = ["derive"] } -bincode = "1.3.3" -tokio = { version = "1.39.2", features = ["full", "test-util"] } -rand = "0.9.0-alpha.2" -seeded-random = "0.6.0" -thiserror = "1.0.63" \ No newline at end of file +log = "^0.4.22" +serde = { version = "^1.0.207", features = ["derive"] } +bincode = "^1.3.3" +rkyv = { version = "^0.7.44", features = ["validation"] } +tokio = { version = "^1.39.2", features = ["full"] } +rand = "^0.9.0-alpha.2" +seeded-random = "^0.6.0" +thiserror = "^1.0.63" + +[features] +default = ["full"] +full = ["tokio/full", "rkyv/validation"] + +[dev-dependencies] +tokio = { version = "^1.39.2", features = ["test-util"] } From 9c39f718d764c2e0503482ccfd1ea74c3fcd63a3 Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Thu, 29 Aug 2024 20:33:29 +0300 Subject: [PATCH 8/9] chore: updated README --- README.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6d8f44a..2f1388f 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,115 @@ -# BPCon Rust Library +# Weighted BPCon Rust Library [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![Rust CI🌌](https://github.com/distributed-lab/bpcon/actions/workflows/rust.yml/badge.svg)](https://github.com/distributed-lab/bpcon/actions/workflows/rust.yml) +[![Docs 🌌](https://github.com/distributed-lab/bpcon/actions/workflows/docs.yml/badge.svg)](https://github.com/distributed-lab/bpcon/actions/workflows/docs.yml) -This is a generic rust implementation of the `BPCon` consensus mechanism. +> This is a rust library implementing weighted BPCon consensus. -## Library Structure +## Documentation -### src/party.rs +Library is documented with `rustdoc`. +Compiled documentation for `main` branch is available at [GitBook](https://distributed-lab.github.io/bpcon). -Main entity in this implementation is `Party` - it represents member of the consensus. +## Usage -External system shall create desired amount of parties. +### Add dependency in your `Cargo.toml` -We have 2 communication channels - one for sending `MessageWire` - encoded in bytes message and routing information, -and the other for pitching consensus events - this allows for external system to impose custom limitations and rules -regarding runway. +```toml +[dependencies] +bpcon = {version = "0.1.0", git = "https://github.com/distributed-lab/bpcon"} +``` -### src/message.rs +### Implement `Value` trait for consensus subject -Definitions of the general message struct, routing information and type-specific contents. +```rust +... +use bpcon::value::Value; -### src/lib.rs +#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Debug, Hash)] +pub(crate) struct MyValue(u64); -Here we present a trait for the value on which consensus is being conducted. Additionally, there is a trait for -defining custom value selection rules, called `ValueSelector`. +impl Value for MyValue {} +``` +### Implement `ValueSelector` trait + +```rust +... +use bpcon::value::ValueSelector; + +#[derive(Clone)] +pub struct MyValueSelector; + +impl ValueSelector for MyValueSelector { + /// Verifies if the given value `v` has been correctly selected according to the protocol rules. + /// + /// In this basic implementation, we'll consider a value as "correctly selected" if it matches + /// the majority of the values in the provided `HashMap`. + fn verify(&self, v: &MyValue, m: &HashMap>) -> bool { + // Count how many times the value `v` appears in the `HashMap` + let count = m.values().filter(|&val| val.as_ref() == Some(v)).count(); + + // For simplicity, consider the value verified if it appears in more than half of the entries + count > m.len() / 2 + } + + /// Selects a value based on the provided `HashMap` of party votes. + /// + /// This implementation selects the value that appears most frequently in the `HashMap`. + /// If there is a tie, it selects the smallest value (as per the natural ordering of `u64`). + fn select(&self, m: &HashMap>) -> MyValue { + let mut frequency_map = HashMap::new(); + + // Count the frequency of each value in the `HashMap` + for value in m.values().flatten() { + *frequency_map.entry(value).or_insert(0) += 1; + } + + // Find the value with the highest frequency. In case of a tie, select the smallest value. + frequency_map + .into_iter() + .max_by_key(|(value, count)| (*count, value.0)) + .map(|(value, _)| value.clone()) + .expect("No valid value found") + } +} +``` + +### Decide on a `LeaderElector` + +We provide functionality to define your own rules for leader election using corresponding trait. +Note, that there is already default `LeaderElector` available. + +### Configure your ballot + +As a next step, you need to decide on parameters for your ballot: + +1. Amount of parties and their weight. +2. Threshold weight. +3. Time bounds. + +Example: + +```rust +use bpcon::config::BPConConfig; + +let cfg = BPConConfig::with_default_timeouts(vec![1, 1, 1, 1, 1, 1], 4); +``` + +Feel free to explore `config.rs` for more information. + +### Create parties + +Having `BPConConfig`, `ValueSelector` and `LeaderElector` defined, instantiate your parties. +Check out `new` method on a `Party` struct. + +### Launch ballot on parties and handle messages + +Each party interfaces communication with external system via channels. +In a way, you shall propagate outgoing messages to other parties like: + +1. Listen for outgoing message using `msg_out_receiver`. +2. Forward it to other parties using `msg_in_sender`. + +We welcome you to check `test_end_to_end_ballot` in `party.rs` for example. From 128124023b0296f00155f26be358899ba61f5f2b Mon Sep 17 00:00:00 2001 From: Nikita Masych Date: Mon, 2 Sep 2024 17:23:39 +0300 Subject: [PATCH 9/9] chore: added links to docs in README and fixed incorrect message type in docs for ValueSelector --- README.md | 34 +++++++++++++++++++++++++++------- src/value.rs | 2 +- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2f1388f..b61b98c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,13 @@ Compiled documentation for `main` branch is available at [GitBook](https://distr bpcon = {version = "0.1.0", git = "https://github.com/distributed-lab/bpcon"} ``` -### Implement `Value` trait for consensus subject +### Implement [Value](https://distributed-lab.github.io/bpcon/bpcon/value/trait.Value.html) trait + +This is a core trait, which defines what type are you selecting in your consensus. +It may be the next block in blockchain, or leader for some operation, or anything else you need. + +Below is a simple example, where we will operate on selection for `u64` type. +Using it you may interpret `ID` for leader of distributed operation, for instance. ```rust ... @@ -32,7 +38,12 @@ pub(crate) struct MyValue(u64); impl Value for MyValue {} ``` -### Implement `ValueSelector` trait +### Implement [ValueSelector](https://distributed-lab.github.io/bpcon/bpcon/value/trait.ValueSelector.html) trait + +`BPCon` allows you to define specific conditions how proposer (leader) will select value +and how other members will verify its selection. + +Here is a simple example: ```rust ... @@ -76,10 +87,17 @@ impl ValueSelector for MyValueSelector { } ``` -### Decide on a `LeaderElector` +### Decide on a [LeaderElector](https://distributed-lab.github.io/bpcon/bpcon/leader/trait.LeaderElector.html) + +`LeaderElector` trait allows you to define specific conditions, how to select leader for consensus. + +__NOTE: it is important to provide deterministic mechanism, +because each participant will compute leader for itself +and in case it is not deterministic, state divergence occurs.__ -We provide functionality to define your own rules for leader election using corresponding trait. -Note, that there is already default `LeaderElector` available. +We also provide ready-to-use +[DefaultLeaderElector](https://distributed-lab.github.io/bpcon/bpcon/leader/struct.DefaultLeaderElector.html) +which is using weighted randomization. ### Configure your ballot @@ -97,12 +115,14 @@ use bpcon::config::BPConConfig; let cfg = BPConConfig::with_default_timeouts(vec![1, 1, 1, 1, 1, 1], 4); ``` -Feel free to explore `config.rs` for more information. +Feel free to explore [config.rs](https://distributed-lab.github.io/bpcon/bpcon/config/struct.BPConConfig.html) +for more information. ### Create parties Having `BPConConfig`, `ValueSelector` and `LeaderElector` defined, instantiate your parties. -Check out `new` method on a `Party` struct. +Check out [new](https://distributed-lab.github.io/bpcon/bpcon/party/struct.Party.html#method.new) +method on a `Party` struct. ### Launch ballot on parties and handle messages diff --git a/src/value.rs b/src/value.rs index dadd753..95056c7 100644 --- a/src/value.rs +++ b/src/value.rs @@ -25,7 +25,7 @@ pub trait Value: Eq + Serialize + for<'a> Deserialize<'a> + Clone + Debug + Disp /// can be selected. This implies that a party should not vote for different values, even across /// different ballots. /// - **Consensus Compliance**: The selection process should consider the state of messages (typically -/// from 2b messages) sent by other parties, ensuring the selected value is compliant with the +/// from 1b messages) sent by other parties, ensuring the selected value is compliant with the /// collective state of the consensus. /// /// # Type Parameters