diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be1f1e..95a02ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a `Test` prefix to `testing::Signer`/`Verifier`/`Signature`/`Hasher` and renamed `TestingSessionParams` to `TestSessionParams`. ([#40]) - `SessionId::new()` renamed to `from_seed()`. ([#41]) - `FirstRound::new()` takes a `&[u8]` instead of a `SessionId` object. ([#41]) +- The signatures of `Round::make_echo_broadcast()`, `Round::make_direct_message()`, and `Round::receive_message()`, take messages without `Option`s. ([#46]) +- `Round::make_direct_message_with_artifact()` is the method returning an artifact now; `Round::make_direct_message()` is a shortcut for cases where no artifact is returned. ([#46]) +- `Artifact::empty()` removed, the user should return `None` instead. ([#46]) ### Added - `SerializableMap` wrapper for `BTreeMap` supporting more formats and providing some safety features. (#[32]) +- `DirectMessage::assert_is_none()` and `verify_is_some()`, same for `EchoBroadcast`. Users can now check that a part of the round message (echo or direct) is `None` as expected, and make a verifiable evidence if it is not. ([#46]) [#32]: https://github.com/entropyxyz/manul/pull/32 @@ -25,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#37]: https://github.com/entropyxyz/manul/pull/37 [#40]: https://github.com/entropyxyz/manul/pull/40 [#41]: https://github.com/entropyxyz/manul/pull/41 +[#46]: https://github.com/entropyxyz/manul/pull/46 ## [0.0.1] - 2024-10-12 diff --git a/examples/src/simple.rs b/examples/src/simple.rs index 47a1162..f94ee71 100644 --- a/examples/src/simple.rs +++ b/examples/src/simple.rs @@ -36,7 +36,7 @@ impl ProtocolError for SimpleProtocolError { fn verify_messages_constitute_error( &self, - _echo_broadcast: &Option, + _echo_broadcast: &EchoBroadcast, direct_message: &DirectMessage, _echo_broadcasts: &BTreeMap, _direct_messages: &BTreeMap, @@ -87,10 +87,22 @@ impl Protocol for SimpleProtocol { round_id: RoundId, message: &DirectMessage, ) -> Result<(), MessageValidationError> { - if round_id == RoundId::new(1) { - return message.verify_is_invalid::(); + match round_id { + r if r == RoundId::new(1) => message.verify_is_not::(), + r if r == RoundId::new(2) => message.verify_is_not::(), + _ => Err(MessageValidationError::InvalidEvidence("Invalid round number".into())), + } + } + + fn verify_echo_broadcast_is_invalid( + round_id: RoundId, + message: &EchoBroadcast, + ) -> Result<(), MessageValidationError> { + match round_id { + r if r == RoundId::new(1) => message.verify_is_some(), + r if r == RoundId::new(2) => message.verify_is_not::(), + _ => Err(MessageValidationError::InvalidEvidence("Invalid round number".into())), } - Err(MessageValidationError::InvalidEvidence("Invalid round number".into()))? } } @@ -171,21 +183,21 @@ impl Round for Round1 { &self.context.other_ids } - fn make_echo_broadcast(&self, _rng: &mut impl CryptoRngCore) -> Option> { + fn make_echo_broadcast(&self, _rng: &mut impl CryptoRngCore) -> Result { debug!("{:?}: making echo broadcast", self.context.id); let message = Round1Echo { my_position: self.context.ids_to_positions[&self.context.id], }; - Some(Self::serialize_echo_broadcast(message)) + Self::serialize_echo_broadcast(message) } fn make_direct_message( &self, _rng: &mut impl CryptoRngCore, destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError> { + ) -> Result { debug!("{:?}: making direct message for {:?}", self.context.id, destination); let message = Round1Message { @@ -193,19 +205,19 @@ impl Round for Round1 { your_position: self.context.ids_to_positions[destination], }; let dm = Self::serialize_direct_message(message)?; - let artifact = Artifact::empty(); - Ok((dm, artifact)) + Ok(dm) } fn receive_message( &self, _rng: &mut impl CryptoRngCore, from: &Id, - _echo_broadcast: Option, + echo_broadcast: EchoBroadcast, direct_message: DirectMessage, ) -> Result> { debug!("{:?}: receiving message from {:?}", self.context.id, from); + let _echo = echo_broadcast.deserialize::()?; let message = direct_message.deserialize::()?; debug!("{:?}: received message: {:?}", self.context.id, message); @@ -275,21 +287,13 @@ impl Round for Round2 { &self.context.other_ids } - fn make_echo_broadcast(&self, _rng: &mut impl CryptoRngCore) -> Option> { - debug!("{:?}: making echo broadcast", self.context.id); - - let message = Round1Echo { - my_position: self.context.ids_to_positions[&self.context.id], - }; - - Some(Self::serialize_echo_broadcast(message)) - } + // Does not send echo broadcasts fn make_direct_message( &self, _rng: &mut impl CryptoRngCore, destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError> { + ) -> Result { debug!("{:?}: making direct message for {:?}", self.context.id, destination); let message = Round1Message { @@ -297,19 +301,20 @@ impl Round for Round2 { your_position: self.context.ids_to_positions[destination], }; let dm = Self::serialize_direct_message(message)?; - let artifact = Artifact::empty(); - Ok((dm, artifact)) + Ok(dm) } fn receive_message( &self, _rng: &mut impl CryptoRngCore, from: &Id, - _echo_broadcast: Option, + echo_broadcast: EchoBroadcast, direct_message: DirectMessage, ) -> Result> { debug!("{:?}: receiving message from {:?}", self.context.id, from); + echo_broadcast.assert_is_none()?; + let message = direct_message.deserialize::()?; debug!("{:?}: received message: {:?}", self.context.id, message); diff --git a/examples/src/simple_malicious.rs b/examples/src/simple_malicious.rs index ab070d3..a5f0835 100644 --- a/examples/src/simple_malicious.rs +++ b/examples/src/simple_malicious.rs @@ -57,21 +57,17 @@ impl FirstRound for Malicio } impl RoundOverride for MaliciousRound1 { - fn make_direct_message( - &self, - rng: &mut impl CryptoRngCore, - destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError> { + fn make_direct_message(&self, rng: &mut impl CryptoRngCore, destination: &Id) -> Result { if matches!(self.behavior, Behavior::SerializedGarbage) { let dm = DirectMessage::new::<>::Protocol, _>(&[99u8]).unwrap(); - Ok((dm, Artifact::empty())) + Ok(dm) } else if matches!(self.behavior, Behavior::AttributableFailure) { let message = Round1Message { my_position: self.round.context.ids_to_positions[&self.round.context.id], your_position: self.round.context.ids_to_positions[&self.round.context.id], }; let dm = DirectMessage::new::<>::Protocol, _>(&message)?; - Ok((dm, Artifact::empty())) + Ok(dm) } else { self.inner_round_ref().make_direct_message(rng, destination) } @@ -121,18 +117,14 @@ impl RoundWrapper for Malic } impl RoundOverride for MaliciousRound2 { - fn make_direct_message( - &self, - rng: &mut impl CryptoRngCore, - destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError> { + fn make_direct_message(&self, rng: &mut impl CryptoRngCore, destination: &Id) -> Result { if matches!(self.behavior, Behavior::AttributableFailureRound2) { let message = Round2Message { my_position: self.round.context.ids_to_positions[&self.round.context.id], your_position: self.round.context.ids_to_positions[&self.round.context.id], }; let dm = DirectMessage::new::<>::Protocol, _>(&message)?; - Ok((dm, Artifact::empty())) + Ok(dm) } else { self.inner_round_ref().make_direct_message(rng, destination) } diff --git a/manul/benches/empty_rounds.rs b/manul/benches/empty_rounds.rs index 2b58cf3..3ab376b 100644 --- a/manul/benches/empty_rounds.rs +++ b/manul/benches/empty_rounds.rs @@ -24,7 +24,7 @@ pub struct EmptyProtocolError; impl ProtocolError for EmptyProtocolError { fn verify_messages_constitute_error( &self, - _echo_broadcast: &Option, + _echo_broadcast: &EchoBroadcast, _direct_message: &DirectMessage, _echo_broadcasts: &BTreeMap, _direct_messages: &BTreeMap, @@ -108,34 +108,36 @@ impl Round for EmptyRound Option> { + fn make_echo_broadcast(&self, _rng: &mut impl CryptoRngCore) -> Result { if self.inputs.echo { - Some(Self::serialize_echo_broadcast(Round1EchoBroadcast)) + Self::serialize_echo_broadcast(Round1EchoBroadcast) } else { - None + Ok(EchoBroadcast::none()) } } - fn make_direct_message( + fn make_direct_message_with_artifact( &self, _rng: &mut impl CryptoRngCore, _destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError> { + ) -> Result<(DirectMessage, Option), LocalError> { let dm = Self::serialize_direct_message(Round1DirectMessage)?; let artifact = Artifact::new(Round1Artifact); - Ok((dm, artifact)) + Ok((dm, Some(artifact))) } fn receive_message( &self, _rng: &mut impl CryptoRngCore, _from: &Id, - echo_broadcast: Option, + echo_broadcast: EchoBroadcast, direct_message: DirectMessage, ) -> Result> { - let _echo_broadcast = echo_broadcast - .map(|echo| echo.deserialize::()) - .transpose()?; + if self.inputs.echo { + let _echo_broadcast = echo_broadcast.deserialize::()?; + } else { + echo_broadcast.assert_is_none()?; + } let _direct_message = direct_message.deserialize::()?; Ok(Payload::new(Round1Payload)) } diff --git a/manul/src/protocol.rs b/manul/src/protocol.rs index 942b4b3..f1ba788 100644 --- a/manul/src/protocol.rs +++ b/manul/src/protocol.rs @@ -12,6 +12,7 @@ For more details, see the documentation of the mentioned traits. */ mod errors; +mod message; mod object_safe; mod round; @@ -19,9 +20,9 @@ pub use errors::{ DeserializationError, DirectMessageError, EchoBroadcastError, FinalizeError, LocalError, MessageValidationError, ProtocolValidationError, ReceiveError, RemoteError, }; +pub use message::{DirectMessage, EchoBroadcast}; pub use round::{ - AnotherRound, Artifact, DirectMessage, EchoBroadcast, FinalizeOutcome, FirstRound, Payload, Protocol, - ProtocolError, Round, RoundId, + AnotherRound, Artifact, FinalizeOutcome, FirstRound, Payload, Protocol, ProtocolError, Round, RoundId, }; pub(crate) use errors::ReceiveErrorType; diff --git a/manul/src/protocol/errors.rs b/manul/src/protocol/errors.rs index 27ef651..b94f3b0 100644 --- a/manul/src/protocol/errors.rs +++ b/manul/src/protocol/errors.rs @@ -193,7 +193,13 @@ impl From for ProtocolValidationError { pub struct DirectMessageError(DeserializationError); impl DirectMessageError { - pub(crate) fn new(error: DeserializationError) -> Self { + pub(crate) fn new(message: impl Into) -> Self { + Self(DeserializationError::new(message)) + } +} + +impl From for DirectMessageError { + fn from(error: DeserializationError) -> Self { Self(error) } } @@ -204,7 +210,13 @@ impl DirectMessageError { pub struct EchoBroadcastError(DeserializationError); impl EchoBroadcastError { - pub(crate) fn new(error: DeserializationError) -> Self { + pub(crate) fn new(message: impl Into) -> Self { + Self(DeserializationError::new(message)) + } +} + +impl From for EchoBroadcastError { + fn from(error: DeserializationError) -> Self { Self(error) } } diff --git a/manul/src/protocol/message.rs b/manul/src/protocol/message.rs new file mode 100644 index 0000000..70e1e2f --- /dev/null +++ b/manul/src/protocol/message.rs @@ -0,0 +1,149 @@ +use alloc::boxed::Box; + +use serde::{Deserialize, Serialize}; +use serde_encoded_bytes::{Base64, SliceLike}; + +use super::{ + errors::{DirectMessageError, EchoBroadcastError, LocalError, MessageValidationError}, + round::Protocol, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct DirectMessagePayload(#[serde(with = "SliceLike::")] Box<[u8]>); + +/// A serialized direct message. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DirectMessage(Option); + +impl DirectMessage { + /// Creates an empty message. + /// + /// Use in case the round does not send a message of this type. + pub fn none() -> Self { + Self(None) + } + + /// Creates a new serialized direct message. + pub fn new(message: T) -> Result { + Ok(Self(Some(DirectMessagePayload(P::serialize(message)?)))) + } + + /// Returns `true` if this is an empty message. + pub(crate) fn is_none(&self) -> bool { + self.0.is_none() + } + + /// Returns `Ok(())` if the message is indeed an empty message. + pub fn assert_is_none(&self) -> Result<(), DirectMessageError> { + if self.is_none() { + Ok(()) + } else { + Err(DirectMessageError::new("The expected direct message is missing")) + } + } + + /// Returns `Ok(())` if the message cannot be deserialized into `T`. + /// + /// This is intended to be used in the implementations of [`Protocol::verify_direct_message_is_invalid`]. + pub fn verify_is_not Deserialize<'de>>(&self) -> Result<(), MessageValidationError> { + if self.deserialize::().is_err() { + Ok(()) + } else { + Err(MessageValidationError::InvalidEvidence( + "Message deserialized successfully".into(), + )) + } + } + + /// Returns `Ok(())` if the message contains a payload. + /// + /// This is intended to be used in the implementations of [`Protocol::verify_direct_message_is_invalid`]. + pub fn verify_is_some(&self) -> Result<(), MessageValidationError> { + if self.0.is_some() { + Ok(()) + } else { + Err(MessageValidationError::InvalidEvidence( + "Message's payload is None".into(), + )) + } + } + + /// Deserializes the direct message. + pub fn deserialize Deserialize<'de>>(&self) -> Result { + let payload = self + .0 + .as_ref() + .ok_or_else(|| DirectMessageError::new("The direct message is missing in the payload"))?; + P::deserialize(&payload.0).map_err(DirectMessageError::from) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct EchoBroadcastPayload(#[serde(with = "SliceLike::")] Box<[u8]>); + +/// A serialized echo broadcast. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EchoBroadcast(Option); + +impl EchoBroadcast { + /// Creates an empty message. + /// + /// Use in case the round does not send a message of this type. + pub fn none() -> Self { + Self(None) + } + + /// Creates a new serialized echo broadcast. + pub fn new(message: T) -> Result { + Ok(Self(Some(EchoBroadcastPayload(P::serialize(message)?)))) + } + + /// Returns `true` if this is an empty message. + pub(crate) fn is_none(&self) -> bool { + self.0.is_none() + } + + /// Returns `Ok(())` if the message is indeed an empty message. + pub fn assert_is_none(&self) -> Result<(), EchoBroadcastError> { + if self.is_none() { + Ok(()) + } else { + Err(EchoBroadcastError::new("The expected echo broadcast is missing")) + } + } + + /// Returns `Ok(())` if the message cannot be deserialized into `T`. + /// + /// This is intended to be used in the implementations of [`Protocol::verify_direct_message_is_invalid`]. + pub fn verify_is_not Deserialize<'de>>(&self) -> Result<(), MessageValidationError> { + if self.deserialize::().is_err() { + Ok(()) + } else { + Err(MessageValidationError::InvalidEvidence( + "Message deserialized successfully".into(), + )) + } + } + + /// Returns `Ok(())` if the message contains a payload. + /// + /// This is intended to be used in the implementations of [`Protocol::verify_direct_message_is_invalid`]. + pub fn verify_is_some(&self) -> Result<(), MessageValidationError> { + if self.0.is_some() { + Ok(()) + } else { + Err(MessageValidationError::InvalidEvidence( + "Message's payload is None".into(), + )) + } + } + + /// Deserializes the echo broadcast. + pub fn deserialize Deserialize<'de>>(&self) -> Result { + let payload = self + .0 + .as_ref() + .ok_or_else(|| EchoBroadcastError::new("The direct message is missing in the payload"))?; + P::deserialize(&payload.0).map_err(EchoBroadcastError::from) + } +} diff --git a/manul/src/protocol/object_safe.rs b/manul/src/protocol/object_safe.rs index 5d42262..bc416c9 100644 --- a/manul/src/protocol/object_safe.rs +++ b/manul/src/protocol/object_safe.rs @@ -9,7 +9,8 @@ use rand_core::{CryptoRng, CryptoRngCore, RngCore}; use super::{ errors::{FinalizeError, LocalError, ReceiveError}, - round::{Artifact, DirectMessage, EchoBroadcast, FinalizeOutcome, Payload, Protocol, Round, RoundId}, + message::{DirectMessage, EchoBroadcast}, + round::{Artifact, FinalizeOutcome, Payload, Protocol, Round, RoundId}, }; /// Since object-safe trait methods cannot take `impl CryptoRngCore` arguments, @@ -46,19 +47,19 @@ pub(crate) trait ObjectSafeRound: 'static + Send + Sync + Debug { fn message_destinations(&self) -> &BTreeSet; - fn make_direct_message( + fn make_direct_message_with_artifact( &self, rng: &mut dyn CryptoRngCore, destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError>; + ) -> Result<(DirectMessage, Option), LocalError>; - fn make_echo_broadcast(&self, rng: &mut dyn CryptoRngCore) -> Option>; + fn make_echo_broadcast(&self, rng: &mut dyn CryptoRngCore) -> Result; fn receive_message( &self, rng: &mut dyn CryptoRngCore, from: &Id, - echo_broadcast: Option, + echo_broadcast: EchoBroadcast, direct_message: DirectMessage, ) -> Result>; @@ -111,16 +112,17 @@ where self.round.message_destinations() } - fn make_direct_message( + fn make_direct_message_with_artifact( &self, rng: &mut dyn CryptoRngCore, destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError> { + ) -> Result<(DirectMessage, Option), LocalError> { let mut boxed_rng = BoxedRng(rng); - self.round.make_direct_message(&mut boxed_rng, destination) + self.round + .make_direct_message_with_artifact(&mut boxed_rng, destination) } - fn make_echo_broadcast(&self, rng: &mut dyn CryptoRngCore) -> Option> { + fn make_echo_broadcast(&self, rng: &mut dyn CryptoRngCore) -> Result { let mut boxed_rng = BoxedRng(rng); self.round.make_echo_broadcast(&mut boxed_rng) } @@ -129,7 +131,7 @@ where &self, rng: &mut dyn CryptoRngCore, from: &Id, - echo_broadcast: Option, + echo_broadcast: EchoBroadcast, direct_message: DirectMessage, ) -> Result> { let mut boxed_rng = BoxedRng(rng); diff --git a/manul/src/protocol/round.rs b/manul/src/protocol/round.rs index b22aa6b..865c841 100644 --- a/manul/src/protocol/round.rs +++ b/manul/src/protocol/round.rs @@ -8,13 +8,12 @@ use core::{any::Any, fmt::Debug}; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; -use serde_encoded_bytes::{Base64, SliceLike}; use super::{ errors::{ - DeserializationError, DirectMessageError, EchoBroadcastError, FinalizeError, LocalError, - MessageValidationError, ProtocolValidationError, ReceiveError, + DeserializationError, FinalizeError, LocalError, MessageValidationError, ProtocolValidationError, ReceiveError, }, + message::{DirectMessage, EchoBroadcast}, object_safe::{ObjectSafeRound, ObjectSafeRoundWrapper}, }; @@ -139,7 +138,7 @@ pub trait Protocol: Debug + Sized { /// Returns `Ok(())` if the given direct message cannot be deserialized /// assuming it is a direct message from the round `round_id`. /// - /// Normally one would use [`DirectMessage::verify_is_invalid`] when implementing this. + /// Normally one would use [`DirectMessage::verify_is_not`] when implementing this. fn verify_direct_message_is_invalid( round_id: RoundId, #[allow(unused_variables)] message: &DirectMessage, @@ -152,7 +151,7 @@ pub trait Protocol: Debug + Sized { /// Returns `Ok(())` if the given echo broadcast cannot be deserialized /// assuming it is an echo broadcast from the round `round_id`. /// - /// Normally one would use [`EchoBroadcast::verify_is_invalid`] when implementing this. + /// Normally one would use [`EchoBroadcast::verify_is_not`] when implementing this. fn verify_echo_broadcast_is_invalid( round_id: RoundId, #[allow(unused_variables)] message: &EchoBroadcast, @@ -206,7 +205,7 @@ pub trait ProtocolError: Debug + Clone + Send { /// as requested by [`required_combined_echos`](`Self::required_combined_echos`). fn verify_messages_constitute_error( &self, - echo_broadcast: &Option, + echo_broadcast: &EchoBroadcast, direct_message: &DirectMessage, echo_broadcasts: &BTreeMap, direct_messages: &BTreeMap, @@ -214,64 +213,6 @@ pub trait ProtocolError: Debug + Clone + Send { ) -> Result<(), ProtocolValidationError>; } -/// A serialized direct message. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DirectMessage(#[serde(with = "SliceLike::")] Box<[u8]>); - -impl DirectMessage { - /// Creates a new serialized direct message. - pub fn new(message: T) -> Result { - P::serialize(message).map(Self) - } - - /// Returns `Ok(())` if the message cannot be deserialized into `T`. - /// - /// This is intended to be used in the implementations of [`Protocol::verify_direct_message_is_invalid`]. - pub fn verify_is_invalid Deserialize<'de>>(&self) -> Result<(), MessageValidationError> { - if self.deserialize::().is_err() { - Ok(()) - } else { - Err(MessageValidationError::InvalidEvidence( - "Message deserialized successfully".into(), - )) - } - } - - /// Deserializes the direct message. - pub fn deserialize Deserialize<'de>>(&self) -> Result { - P::deserialize(&self.0).map_err(DirectMessageError::new) - } -} - -/// A serialized echo broadcast. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct EchoBroadcast(#[serde(with = "SliceLike::")] Box<[u8]>); - -impl EchoBroadcast { - /// Creates a new serialized echo broadcast. - pub fn new(message: T) -> Result { - P::serialize(message).map(Self) - } - - /// Returns `Ok(())` if the message cannot be deserialized into `T`. - /// - /// This is intended to be used in the implementations of [`Protocol::verify_direct_message_is_invalid`]. - pub fn verify_is_invalid Deserialize<'de>>(&self) -> Result<(), MessageValidationError> { - if self.deserialize::().is_err() { - Ok(()) - } else { - Err(MessageValidationError::InvalidEvidence( - "Message deserialized successfully".into(), - )) - } - } - - /// Deserializes the echo broadcast. - pub fn deserialize Deserialize<'de>>(&self) -> Result { - P::deserialize(&self.0).map_err(EchoBroadcastError::new) - } -} - /// Message payload created in [`Round::receive_message`]. #[derive(Debug)] pub struct Payload(pub Box); @@ -314,13 +255,6 @@ impl Artifact { Self(Box::new(artifact)) } - /// Creates an empty artifact. - /// - /// Use it in [`Round::make_direct_message`] if it does not need to create artifacts. - pub fn empty() -> Self { - Self::new(()) - } - /// Attempts to downcast back to the concrete type. /// /// Would be normally called in [`Round::finalize`]. @@ -384,16 +318,37 @@ pub trait Round: 'static + Send + Sync + Debug { /// for each element of the returned set. fn message_destinations(&self) -> &BTreeSet; - /// Returns the direct message to the given destination and an accompanying artifact. + /// Returns the direct message to the given destination and (maybe) an accompanying artifact. + /// + /// Return [`DirectMessage::none`] if this round does not send direct messages. + /// + /// Falls back to [`make_direct_message`](`Self::make_direct_message`) if not implemented. + /// This is the method that will be called by the upper layer when creating direct messages. /// /// In some protocols, when a message to another node is created, there is some associated information /// that needs to be retained for later (randomness, proofs of knowledge, and so on). /// These should be put in an [`Artifact`] and will be available at the time of [`finalize`](`Self::finalize`). - fn make_direct_message( + fn make_direct_message_with_artifact( &self, rng: &mut impl CryptoRngCore, destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError>; + ) -> Result<(DirectMessage, Option), LocalError> { + Ok((self.make_direct_message(rng, destination)?, None)) + } + + /// Returns the direct message to the given destination. + /// + /// This method will not be called by the upper layer directly, + /// only via [`make_direct_message_with_artifact`](`Self::make_direct_message_with_artifact`). + /// + /// Return [`DirectMessage::none`] if this round does not send direct messages. + fn make_direct_message( + &self, + #[allow(unused_variables)] rng: &mut impl CryptoRngCore, + #[allow(unused_variables)] destination: &Id, + ) -> Result { + Ok(DirectMessage::none()) + } /// Returns the echo broadcast for this round, or `None` if the round does not require echo-broadcasting. /// @@ -405,8 +360,8 @@ pub trait Round: 'static + Send + Sync + Debug { fn make_echo_broadcast( &self, #[allow(unused_variables)] rng: &mut impl CryptoRngCore, - ) -> Option> { - None + ) -> Result { + Ok(EchoBroadcast::none()) } /// Processes the received message and generates the payload that will be used in [`finalize`](`Self::finalize`). @@ -417,14 +372,15 @@ pub trait Round: 'static + Send + Sync + Debug { &self, rng: &mut impl CryptoRngCore, from: &Id, - echo_broadcast: Option, + echo_broadcast: EchoBroadcast, direct_message: DirectMessage, ) -> Result>; /// Attempts to finalize the round, producing the next round or the result. /// /// `payloads` here are the ones previously generated by [`receive_message`](`Self::receive_message`), - /// and `artifacts` are the ones previously generated by [`make_direct_message`](`Self::make_direct_message`). + /// and `artifacts` are the ones previously generated by + /// [`make_direct_message_with_artifact`](`Self::make_direct_message_with_artifact`). fn finalize( self, rng: &mut impl CryptoRngCore, diff --git a/manul/src/session/echo.rs b/manul/src/session/echo.rs index b3ac3da..14e9318 100644 --- a/manul/src/session/echo.rs +++ b/manul/src/session/echo.rs @@ -106,7 +106,7 @@ where &self, _rng: &mut impl CryptoRngCore, destination: &SP::Verifier, - ) -> Result<(DirectMessage, Artifact), LocalError> { + ) -> Result { debug!("{:?}: making echo round message for {:?}", self.verifier, destination); // Don't send our own message the second time @@ -122,7 +122,7 @@ where echo_broadcasts: echo_broadcasts.into(), }; let dm = DirectMessage::new::(&message)?; - Ok((dm, Artifact::empty())) + Ok(dm) } fn expecting_messages_from(&self) -> &BTreeSet { @@ -133,11 +133,13 @@ where &self, _rng: &mut impl CryptoRngCore, from: &SP::Verifier, - _echo_broadcast: Option, + echo_broadcast: EchoBroadcast, direct_message: DirectMessage, ) -> Result> { debug!("{:?}: received an echo message from {:?}", self.verifier, from); + echo_broadcast.assert_is_none()?; + let message = direct_message.deserialize::>()?; // Check that the received message contains entries from `destinations` sans `from` diff --git a/manul/src/session/evidence.rs b/manul/src/session/evidence.rs index 2d8fa61..3a53ad3 100644 --- a/manul/src/session/evidence.rs +++ b/manul/src/session/evidence.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use super::{ echo::{EchoRoundError, EchoRoundMessage}, - message::{MessageVerificationError, SignedMessage}, + message::{MessageVerificationError, MissingMessage, SignedMessage}, session::SessionParameters, transcript::Transcript, LocalError, @@ -24,6 +24,15 @@ pub enum EvidenceError { InvalidEvidence(String), } +// Other nodes would send a signed message with the payload being either Some(...) or None. +// We expect the messages in the evidence only be the Some(...) ones, so if it's not the case, it's invalid evidence. +// It's hard to enforce statically since we have to keep the signed messages as they were created by remote nodes. +impl From for EvidenceError { + fn from(_error: MissingMessage) -> Self { + Self::InvalidEvidence("The signed message is missing the expected payload".into()) + } +} + impl From for EvidenceError { fn from(error: MessageVerificationError) -> Self { match error { @@ -74,7 +83,7 @@ where { pub(crate) fn new_protocol_error( verifier: &SP::Verifier, - echo_broadcast: Option>, + echo_broadcast: SignedMessage, direct_message: SignedMessage, error: P::ProtocolError, transcript: &Transcript, @@ -353,7 +362,7 @@ where struct ProtocolEvidence { error: P::ProtocolError, direct_message: SignedMessage, - echo_broadcast: Option>, + echo_broadcast: SignedMessage, direct_messages: SerializableMap>, echo_broadcasts: SerializableMap>, combined_echos: SerializableMap>, @@ -383,17 +392,14 @@ where verified_direct_messages.insert(*round_id, verified_direct_message.payload().clone()); } - let verified_echo_broadcast = if let Some(echo) = self.echo_broadcast.as_ref() { - let metadata = echo.metadata(); - if metadata.session_id() != session_id || metadata.round_id() != self.direct_message.metadata().round_id() { - return Err(EvidenceError::InvalidEvidence( - "Invalid attached message metadata".into(), - )); - } - Some(echo.clone().verify::(verifier)?.payload().clone()) - } else { - None - }; + let verified_echo_broadcast = self.echo_broadcast.clone().verify::(verifier)?.payload().clone(); + if self.echo_broadcast.metadata().session_id() != session_id + || self.echo_broadcast.metadata().round_id() != self.direct_message.metadata().round_id() + { + return Err(EvidenceError::InvalidEvidence( + "Invalid attached message metadata".into(), + )); + } let mut verified_echo_broadcasts = BTreeMap::new(); for (round_id, echo_broadcast) in self.echo_broadcasts.iter() { diff --git a/manul/src/session/message.rs b/manul/src/session/message.rs index 484cd63..2b0d64f 100644 --- a/manul/src/session/message.rs +++ b/manul/src/session/message.rs @@ -135,6 +135,9 @@ where } } +#[derive(Debug, Clone, Copy)] +pub(crate) struct MissingMessage; + #[derive(Debug, Clone)] pub struct VerifiedMessage { signature: SerializedSignature, @@ -166,7 +169,7 @@ impl VerifiedMessage { #[derive(Clone, Debug)] pub struct MessageBundle { direct_message: SignedMessage, - echo_broadcast: Option>, + echo_broadcast: SignedMessage, } impl MessageBundle { @@ -176,7 +179,7 @@ impl MessageBundle { session_id: &SessionId, round_id: RoundId, direct_message: DirectMessage, - echo_broadcast: Option>, + echo_broadcast: SignedMessage, ) -> Result where P: Protocol, @@ -190,12 +193,7 @@ impl MessageBundle { } pub(crate) fn unify_metadata(self) -> Option { - if !self - .echo_broadcast - .as_ref() - .map(|echo| echo.metadata() == self.direct_message.metadata()) - .unwrap_or(true) - { + if self.echo_broadcast.metadata() != self.direct_message.metadata() { return None; } @@ -216,7 +214,7 @@ impl MessageBundle { pub(crate) struct CheckedMessageBundle { metadata: MessageMetadata, direct_message: SignedMessage, - echo_broadcast: Option>, + echo_broadcast: SignedMessage, } impl CheckedMessageBundle { @@ -230,10 +228,7 @@ impl CheckedMessageBundle { SP: SessionParameters, { let direct_message = self.direct_message.verify::(verifier)?; - let echo_broadcast = self - .echo_broadcast - .map(|echo| echo.verify::(verifier)) - .transpose()?; + let echo_broadcast = self.echo_broadcast.verify::(verifier)?; Ok(VerifiedMessageBundle { from: verifier.clone(), metadata: self.metadata, @@ -251,7 +246,7 @@ pub struct VerifiedMessageBundle { from: SP::Verifier, metadata: MessageMetadata, direct_message: VerifiedMessage, - echo_broadcast: Option>, + echo_broadcast: VerifiedMessage, } impl VerifiedMessageBundle @@ -270,16 +265,15 @@ where self.direct_message.payload() } - /// Split the `VerifiedMessageBundle` into its constituent parts: the [`DirectMessage`] and (possibly) - /// the [`EchoBroadcast`] (depending on the protocol). - /// Consumes `self`. - pub(crate) fn into_parts(self) -> (Option>, SignedMessage) { - let direct_message = self.direct_message.into_unverified(); - let echo_broadcast = self.echo_broadcast.map(|echo| echo.into_unverified()); - (echo_broadcast, direct_message) + pub(crate) fn echo_broadcast(&self) -> &EchoBroadcast { + self.echo_broadcast.payload() } - pub(crate) fn echo_broadcast(&self) -> Option<&EchoBroadcast> { - self.echo_broadcast.as_ref().map(|echo| echo.payload()) + /// Split the `VerifiedMessageBundle` into its signed constituent parts: + /// the echo broadcast and the direct message. + pub(crate) fn into_parts(self) -> (SignedMessage, SignedMessage) { + let direct_message = self.direct_message.into_unverified(); + let echo_broadcast = self.echo_broadcast.into_unverified(); + (echo_broadcast, direct_message) } } diff --git a/manul/src/session/session.rs b/manul/src/session/session.rs index bd48c31..2e6ecd0 100644 --- a/manul/src/session/session.rs +++ b/manul/src/session/session.rs @@ -107,7 +107,7 @@ pub struct Session { verifier: SP::Verifier, round: Box>, message_destinations: BTreeSet, - echo_broadcast: Option>, + echo_broadcast: SignedMessage, possible_next_rounds: BTreeSet, transcript: Transcript, } @@ -159,14 +159,11 @@ where transcript: Transcript, ) -> Result { let verifier = signer.verifying_key(); - let echo_broadcast = round - .make_echo_broadcast(rng) - .transpose()? - .map(|echo| SignedMessage::new::(rng, &signer, &session_id, round.id(), echo)) - .transpose()?; + let echo = round.make_echo_broadcast(rng)?; + let echo_broadcast = SignedMessage::new::(rng, &signer, &session_id, round.id(), echo)?; let message_destinations = round.message_destinations().clone(); - let possible_next_rounds = if echo_broadcast.is_none() { + let possible_next_rounds = if echo_broadcast.payload().is_none() { round.possible_next_rounds() } else { BTreeSet::from([round.id().echo()]) @@ -207,7 +204,7 @@ where rng: &mut impl CryptoRngCore, destination: &SP::Verifier, ) -> Result<(MessageBundle, ProcessedArtifact), LocalError> { - let (direct_message, artifact) = self.round.make_direct_message(rng, destination)?; + let (direct_message, artifact) = self.round.make_direct_message_with_artifact(rng, destination)?; let bundle = MessageBundle::new::( rng, @@ -359,7 +356,7 @@ where let processed = self.round.receive_message( rng, message.from(), - message.echo_broadcast().cloned(), + message.echo_broadcast().clone(), message.direct_message().clone(), ); // We could filter out and return a possible `LocalError` at this stage, @@ -413,10 +410,12 @@ where accum.still_have_not_sent_messages, )?; - if let Some(echo_broadcast) = self.echo_broadcast { + let echo_round_needed = !self.echo_broadcast.payload().is_none(); + + if echo_round_needed { let round = Box::new(ObjectSafeRoundWrapper::new(EchoRound::::new( verifier, - echo_broadcast, + self.echo_broadcast, transcript.echo_broadcasts(round_id)?, self.round, accum.payloads, @@ -586,11 +585,12 @@ where } fn add_artifact(&mut self, processed: ProcessedArtifact) -> Result<(), LocalError> { - if self - .artifacts - .insert(processed.destination.clone(), processed.artifact) - .is_some() - { + let artifact = match processed.artifact { + Some(artifact) => artifact, + None => return Ok(()), + }; + + if self.artifacts.insert(processed.destination.clone(), artifact).is_some() { return Err(LocalError::new(format!( "Artifact for destination {:?} has already been recorded", processed.destination @@ -622,11 +622,14 @@ where let error = match processed.processed { Ok(payload) => { + // Note: only inserting the messages if they actually have a payload let (echo_broadcast, direct_message) = processed.message.into_parts(); - if let Some(echo) = echo_broadcast { - self.echo_broadcasts.insert(from.clone(), echo); + if !echo_broadcast.payload().is_none() { + self.echo_broadcasts.insert(from.clone(), echo_broadcast); + } + if !direct_message.payload().is_none() { + self.direct_messages.insert(from.clone(), direct_message); } - self.direct_messages.insert(from.clone(), direct_message); self.payloads.insert(from.clone(), payload); return Ok(()); } @@ -641,8 +644,6 @@ where } ReceiveErrorType::InvalidEchoBroadcast(error) => { let (echo_broadcast, _direct_message) = processed.message.into_parts(); - let echo_broadcast = - echo_broadcast.ok_or_else(|| LocalError::new("Expected a non-None echo broadcast"))?; let evidence = Evidence::new_invalid_echo_broadcast(&from, echo_broadcast, error); self.register_provable_error(&from, evidence) } @@ -681,7 +682,7 @@ where #[derive(Debug)] pub struct ProcessedArtifact { destination: SP::Verifier, - artifact: Artifact, + artifact: Option, } #[derive(Debug)] @@ -735,7 +736,7 @@ mod tests { impl ProtocolError for DummyProtocolError { fn verify_messages_constitute_error( &self, - _echo_broadcast: &Option, + _echo_broadcast: &EchoBroadcast, _direct_message: &DirectMessage, _echo_broadcasts: &BTreeMap, _direct_messages: &BTreeMap, diff --git a/manul/src/testing.rs b/manul/src/testing.rs index f5c767d..3165961 100644 --- a/manul/src/testing.rs +++ b/manul/src/testing.rs @@ -5,10 +5,11 @@ When testing round based protocols it can be complicated to "inject" the proper process, e.g. to emulate a malicious participant. This module provides facilities to make this easier, by providing a [`RoundOverride`] type along with a [`round_override`] macro. -The [`TestSessionParams`] provides an implementation of the [`SessionParameters`](crate::session::SessionParameters) trait, +The [`TestSessionParams`] provides an implementation of the +[`SessionParameters`](crate::session::SessionParameters) trait, which in turn is used to setup [`Session`](crate::session::Session)s to drive the protocol. -The [`run_sync`] method is helpful to execute a protocol synchronously and collect the outcomes. +The [`run_sync()`] method is helpful to execute a protocol synchronously and collect the outcomes. */ mod identity; diff --git a/manul/src/testing/macros.rs b/manul/src/testing/macros.rs index 4ca9593..ce6ddef 100644 --- a/manul/src/testing/macros.rs +++ b/manul/src/testing/macros.rs @@ -24,17 +24,23 @@ pub trait RoundWrapper: 'static + Sized + Send + Sync { /// /// The blanket implementations delegate to the methods of the wrapped round. pub trait RoundOverride: RoundWrapper { - /// An override for [`Round::make_direct_message`]. - fn make_direct_message( + /// An override for [`Round::make_direct_message_with_artifact`]. + fn make_direct_message_with_artifact( &self, rng: &mut impl CryptoRngCore, destination: &Id, - ) -> Result<(DirectMessage, Artifact), LocalError> { + ) -> Result<(DirectMessage, Option), LocalError> { + let dm = self.make_direct_message(rng, destination)?; + Ok((dm, None)) + } + + /// An override for [`Round::make_direct_message`]. + fn make_direct_message(&self, rng: &mut impl CryptoRngCore, destination: &Id) -> Result { self.inner_round_ref().make_direct_message(rng, destination) } /// An override for [`Round::make_echo_broadcast`]. - fn make_echo_broadcast(&self, rng: &mut impl CryptoRngCore) -> Option> { + fn make_echo_broadcast(&self, rng: &mut impl CryptoRngCore) -> Result { self.inner_round_ref().make_echo_broadcast(rng) } @@ -86,14 +92,22 @@ macro_rules! round_override { &self, rng: &mut impl CryptoRngCore, destination: &Id, - ) -> Result<($crate::protocol::DirectMessage, $crate::protocol::Artifact), $crate::protocol::LocalError> { + ) -> Result<$crate::protocol::DirectMessage, $crate::protocol::LocalError> { >::make_direct_message(self, rng, destination) } + fn make_direct_message_with_artifact( + &self, + rng: &mut impl CryptoRngCore, + destination: &Id, + ) -> Result<($crate::protocol::DirectMessage, Option<$crate::protocol::Artifact>), $crate::protocol::LocalError> { + >::make_direct_message_with_artifact(self, rng, destination) + } + fn make_echo_broadcast( &self, rng: &mut impl CryptoRngCore, - ) -> Option> { + ) -> Result<$crate::protocol::EchoBroadcast, $crate::protocol::LocalError> { >::make_echo_broadcast(self, rng) } @@ -101,7 +115,7 @@ macro_rules! round_override { &self, rng: &mut impl CryptoRngCore, from: &Id, - echo_broadcast: Option<$crate::protocol::EchoBroadcast>, + echo_broadcast: $crate::protocol::EchoBroadcast, direct_message: $crate::protocol::DirectMessage, ) -> Result<$crate::protocol::Payload, $crate::protocol::ReceiveError> { self.inner_round_ref()