Skip to content

Proper threshold support #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/src/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ impl<Id: PartyId> Round<Id> for Round1<Id> {
}

fn communication_info(&self) -> CommunicationInfo<Id> {
CommunicationInfo::regular(&self.context.other_ids)
CommunicationInfo::all_to_all(&self.context.other_ids)
}

fn make_normal_broadcast(
Expand Down
4 changes: 2 additions & 2 deletions manul/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ pub use errors::{
};
pub use message::{DirectMessage, EchoBroadcast, NormalBroadcast, ProtocolMessage, ProtocolMessagePart};
pub use round::{
Artifact, CommunicationInfo, EchoRoundParticipation, EntryPoint, FinalizeOutcome, NoProtocolErrors, PartyId,
Payload, Protocol, ProtocolError, RequiredMessageParts, RequiredMessages, Round,
Artifact, CommunicationInfo, EntryPoint, FinalizeOutcome, IdSet, NoProtocolErrors,
PartyId, Payload, Protocol, ProtocolError, RequiredMessageParts, RequiredMessages, Round, RoundCommunicationInfo,
};
pub use round_id::{RoundId, TransitionInfo};

Expand Down
122 changes: 86 additions & 36 deletions manul/src/protocol/round.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,79 @@ use super::{
round_id::{RoundId, TransitionInfo},
};

/// Describes what other parties this rounds sends messages to, and what other parties it expects messages from.
// TODO: make a trait to implement custom threshold strategies
/// A set of IDs with an associated quorum condition.
#[derive(Debug, Clone)]
pub struct IdSet<Id> {
ids: BTreeSet<Id>,
threshold: usize,
}

impl<Id: Ord> IdSet<Id> {
/// Creates a non-threshold ID set (that is, messages from all `ids` must be present for the quorum).
pub fn new_non_threshold(ids: BTreeSet<Id>) -> Self {
let threshold = ids.len();
Self { ids, threshold }
}

/// Creates an empty ID set.
pub fn empty() -> Self {
Self {
ids: BTreeSet::new(),
threshold: 0,
}
}

pub(crate) fn all(&self) -> &BTreeSet<Id> {
&self.ids
}

pub(crate) fn is_quorum(&self, ids: &BTreeSet<Id>) -> bool {
// TODO: assuming `ids` are a subset of `self.ids`. Can we?
ids.len() >= self.threshold
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth making sure I think: ids.is_subset(&self.ids) && ids.len() >= self.threshold

}

pub(crate) fn is_quorum_possible(&self, banned_ids: &BTreeSet<Id>) -> bool {
let ids = self.ids.intersection(banned_ids).collect::<BTreeSet<_>>();
self.ids.len() - ids.len() >= self.threshold
Comment on lines +55 to +56
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this work here?

        let intersection_count = self.ids.intersection(banned_ids).count();
        self.ids.len() - intersection_count >= self.threshold

}
}

/// Encapsulates the communication info for the main round and the possible echo round.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be better to beef up this doc comment and thin out the lower-level ones a bit, so that the doc reader can learn about the CommunicationInfo struct's purpose and usage in one spot.

#[derive(Debug, Clone)]
pub struct CommunicationInfo<Id> {
/// Communication info for the main part of the round (that is, not considering the echo round).
pub main_round: RoundCommunicationInfo<Id>,

/// The specific way the node participates in the echo round following this round.
///
/// If `None`, and there is an echo round, will be set to `main_round`.
pub echo_round: Option<RoundCommunicationInfo<Id>>,

/// The parties whose echoed messages this node expects to receive in the echo round (if any).
///
/// If `None`, and there is an echo round, will be set to `echo_round.expecting_messages_from`.
pub expected_echos: Option<IdSet<Id>>,
}

impl<Id: PartyId> CommunicationInfo<Id> {
/// A regular round that sends messages to all `other_parties`, and expects messages back from them.
pub fn all_to_all(id_set: &IdSet<Id>) -> Self {
Self {
main_round: RoundCommunicationInfo::all_to_all(id_set),
echo_round: None,
expected_echos: None,
}
}

pub fn set_to_set(senders: &IdSet<Id>, receivers: &IdSet<Id>, my_id: &Id) -> Self {
todo!()
}
}

/// Describes what other parties this rounds sends messages to, and what other parties it expects messages from.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Describes what other parties this rounds sends messages to, and what other parties it expects messages from.
/// Describes which other parties this rounds sends messages to, and which other parties it expects messages from.

#[derive(Debug, Clone)]
pub struct RoundCommunicationInfo<Id> {
/// The destinations of the messages to be sent out by this round.
///
/// The way it is interpreted by the execution layer is
Expand All @@ -31,27 +101,26 @@ pub struct CommunicationInfo<Id> {
/// for each element of the returned set.
pub message_destinations: BTreeSet<Id>,

/// Returns the set of node IDs from which this round expects messages.
/// The set of node IDs from which this round expects messages.
///
/// The execution layer will not call [`finalize`](`Round::finalize`) until all these nodes have responded
/// (and the corresponding [`receive_message`](`Round::receive_message`) finished successfully).
pub expecting_messages_from: BTreeSet<Id>,

/// Returns the specific way the node participates in the echo round following this round.
///
/// Returns [`EchoRoundParticipation::Default`] by default; this works fine when every node
/// sends messages to every other one, or do not send or receive any echo broadcasts.
/// Otherwise, review the options in [`EchoRoundParticipation`] and pick the appropriate one.
pub echo_round_participation: EchoRoundParticipation<Id>,
/// The execution layer will not call [`finalize`](`Round::finalize`) until enough nodes to constitute the quorum
/// have responded (and the corresponding [`receive_message`](`Round::receive_message`) finished successfully).
pub expecting_messages_from: IdSet<Id>,
}

impl<Id: PartyId> CommunicationInfo<Id> {
impl<Id: PartyId> RoundCommunicationInfo<Id> {
/// A regular round that sends messages to all `other_parties`, and expects messages back from them.
pub fn regular(other_parties: &BTreeSet<Id>) -> Self {
pub fn all_to_all(id_set: &IdSet<Id>) -> Self {
Self {
message_destinations: other_parties.clone(),
expecting_messages_from: other_parties.clone(),
echo_round_participation: EchoRoundParticipation::Default,
message_destinations: id_set.all().clone(),
expecting_messages_from: id_set.clone(),
}
}

pub fn none() -> Self {
Self {
message_destinations: BTreeSet::new(),
expecting_messages_from: IdSet::empty(),
}
}
}
Expand Down Expand Up @@ -338,25 +407,6 @@ pub trait PartyId: 'static + Debug + Clone + Ord + Send + Sync + Serialize + for

impl<T> PartyId for T where T: 'static + Debug + Clone + Ord + Send + Sync + Serialize + for<'de> Deserialize<'de> {}

/// The specific way the node participates in the echo round (if any).
#[derive(Debug, Clone)]
pub enum EchoRoundParticipation<Id> {
/// The default behavior: sends broadcasts and receives echoed messages, or does neither.
///
/// That is, this node will be a part of the echo round if [`Round::make_echo_broadcast`] generates a message.
Default,

/// This node sends broadcasts that will be echoed, but does not receive any.
Send,

/// This node receives broadcasts that it needs to echo, but does not send any itself.
Receive {
/// The other participants of the echo round
/// (that is, the nodes to which echoed messages will be sent).
echo_targets: BTreeSet<Id>,
},
}

mod sealed {
/// A dyn safe trait to get the type's ID.
pub trait DynTypeId: 'static {
Expand Down
74 changes: 47 additions & 27 deletions manul/src/session/echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ use tracing::debug;

use super::{
message::{MessageVerificationError, SignedMessageHash, SignedMessagePart},
session::{EchoRoundInfo, SessionParameters},
session::{SessionParameters},
LocalError,
};
use crate::{
protocol::{
Artifact, BoxedFormat, BoxedRound, CommunicationInfo, DirectMessage, EchoBroadcast, EchoRoundParticipation,
protocol::{RoundCommunicationInfo, IdSet,
Artifact, BoxedFormat, BoxedRound, CommunicationInfo, DirectMessage, EchoBroadcast,
FinalizeOutcome, MessageValidationError, NormalBroadcast, Payload, Protocol, ProtocolMessage,
ProtocolMessagePart, ReceiveError, Round, TransitionInfo,
},
Expand All @@ -34,7 +34,12 @@ pub(crate) enum EchoRoundError<Id> {
///
/// The attached identifier points out the sender for whom the echoed message was invalid,
/// to speed up the verification process.
InvalidEcho(Id),
InvalidEcho {
// Even though this will be the same as the message sender, it is convenient to record it here
// because of the way this error will be processed.
guilty_party: Id,
failed_for: Id,
},
/// The originally received message and the one received in the echo pack were both valid,
/// but different.
///
Expand All @@ -49,7 +54,7 @@ pub(crate) enum EchoRoundError<Id> {
impl<Id> EchoRoundError<Id> {
pub(crate) fn description(&self) -> String {
match self {
Self::InvalidEcho(_) => "Invalid message received among the ones echoed".into(),
Self::InvalidEcho { .. } => "Invalid message received among the ones echoed".into(),
Self::MismatchedBroadcasts { .. } => {
"The echoed message is different from the originally received one".into()
}
Expand All @@ -70,8 +75,9 @@ pub(crate) struct EchoRoundMessage<SP: SessionParameters> {
pub struct EchoRound<P: Protocol<SP::Verifier>, SP: SessionParameters> {
verifier: SP::Verifier,
echo_broadcasts: BTreeMap<SP::Verifier, SignedMessagePart<EchoBroadcast>>,
echo_round_info: EchoRoundInfo<SP::Verifier>,
communication_info: CommunicationInfo<SP::Verifier>,
expected_echos: IdSet<SP::Verifier>,
banned_ids: BTreeSet<SP::Verifier>,
main_round: BoxedRound<SP::Verifier, P>,
payloads: BTreeMap<SP::Verifier, Payload>,
artifacts: BTreeMap<SP::Verifier, Artifact>,
Expand All @@ -85,24 +91,25 @@ where
pub fn new(
verifier: SP::Verifier,
echo_broadcasts: BTreeMap<SP::Verifier, SignedMessagePart<EchoBroadcast>>,
echo_round_info: EchoRoundInfo<SP::Verifier>,
communication_info: RoundCommunicationInfo<SP::Verifier>,
expected_echos: IdSet<SP::Verifier>,
banned_ids: BTreeSet<SP::Verifier>,
main_round: BoxedRound<SP::Verifier, P>,
payloads: BTreeMap<SP::Verifier, Payload>,
artifacts: BTreeMap<SP::Verifier, Artifact>,
) -> Self {
debug!("{:?}: initialized echo round with {:?}", verifier, echo_round_info);

debug!("{:?}: initialized echo round with {:?} {:?}", verifier, communication_info, expected_echos);
let communication_info = CommunicationInfo {
message_destinations: echo_round_info.message_destinations.clone(),
expecting_messages_from: echo_round_info.expecting_messages_from.clone(),
echo_round_participation: EchoRoundParticipation::Default,
main_round: communication_info,
echo_round: None,
expected_echos: None,
};

Self {
verifier,
echo_broadcasts,
echo_round_info,
communication_info,
expected_echos,
banned_ids,
main_round,
payloads,
artifacts,
Expand Down Expand Up @@ -192,23 +199,16 @@ where
let message = message.normal_broadcast.deserialize::<EchoRoundMessage<SP>>(format)?;

// Check that the received message contains entries from `expected_echos`.
// It is an unprovable fault.
// Since we cannot guarantee the communication info for the echo round is in the associated data,
// we cannot construct an evidence for this fault.

let mut expected_keys = self.echo_round_info.expected_echos.clone();
let mut expected_keys = self.expected_echos.all().clone();

// We don't expect the node to send its echo the second time.
expected_keys.remove(from);

let message_keys = message.message_hashes.keys().cloned().collect::<BTreeSet<_>>();

let missing_keys = expected_keys.difference(&message_keys).collect::<Vec<_>>();
if !missing_keys.is_empty() {
return Err(ReceiveError::unprovable(format!(
"Missing echoed messages from: {:?}",
missing_keys
)));
}

let extra_keys = message_keys.difference(&expected_keys).collect::<Vec<_>>();
if !extra_keys.is_empty() {
return Err(ReceiveError::unprovable(format!(
Expand All @@ -217,6 +217,14 @@ where
)));
}

// Check that the echos we received, minus the banned IDs, constitute a quorum.
// This is also unprovable since the information about the IDs we banned is not available to third parties.

let expected_keys = message_keys.difference(&self.banned_ids).cloned().collect::<BTreeSet<_>>();
if !self.expected_echos.is_quorum(&expected_keys) {
return Err(ReceiveError::unprovable("Not enough echos to constitute a quorum"));
}

// Check that every entry is equal to what we received previously (in the main round).
// If there's a difference, it's a provable fault,
// since we have both messages signed by `from`.
Expand All @@ -236,17 +244,29 @@ where
// This means `from` sent us an incorrectly signed message.
// Provable fault of `from`.
Err(MessageVerificationError::InvalidSignature) => {
return Err(EchoRoundError::InvalidEcho(sender.clone()).into())
return Err(EchoRoundError::InvalidEcho {
guilty_party: from.clone(),
failed_for: sender.clone(),
}
.into())
}
Err(MessageVerificationError::SignatureMismatch) => {
return Err(EchoRoundError::InvalidEcho(sender.clone()).into())
return Err(EchoRoundError::InvalidEcho {
guilty_party: from.clone(),
failed_for: sender.clone(),
}
.into())
}
};

// `from` sent us a correctly signed message but from another round or another session.
// Provable fault of `from`.
if verified_echo.metadata() != previously_received_echo.metadata() {
return Err(EchoRoundError::InvalidEcho(sender.clone()).into());
return Err(EchoRoundError::InvalidEcho {
guilty_party: from.clone(),
failed_for: sender.clone(),
}
.into());
}

// `sender` sent us and `from` messages with different payloads,
Expand Down
10 changes: 6 additions & 4 deletions manul/src/session/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,18 +164,20 @@ where
}

pub(crate) fn new_echo_round_error(
verifier: &SP::Verifier,
normal_broadcast: SignedMessagePart<NormalBroadcast>,
error: EchoRoundError<SP::Verifier>,
) -> Result<Self, LocalError> {
let description = format!("Echo round error: {}", error.description());
match error {
EchoRoundError::InvalidEcho(from) => Ok(Self {
guilty_party: verifier.clone(),
EchoRoundError::InvalidEcho {
guilty_party,
failed_for,
} => Ok(Self {
guilty_party,
description,
evidence: EvidenceEnum::InvalidEchoPack(InvalidEchoPackEvidence {
normal_broadcast,
invalid_echo_sender: from,
invalid_echo_sender: failed_for,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be renamed to echo_sender you think? To me "failed for" is a bit ambiguous: "failed for who? for this node?".

}),
}),
EchoRoundError::MismatchedBroadcasts {
Expand Down
Loading
Loading