From a255c3739365ac4e839a935a03efe49f0edf6f7e Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 13 Oct 2024 14:15:55 -0700 Subject: [PATCH] Add docs --- example/src/simple.rs | 8 +- example/src/simple_malicious.rs | 4 +- example/tests/async.rs | 4 +- manul/src/lib.rs | 4 +- manul/src/protocol.rs | 15 ++- manul/src/protocol/error.rs | 2 + manul/src/protocol/object_safe.rs | 4 +- manul/src/protocol/round.rs | 212 ++++++++++++++++++++++++++++-- manul/src/session.rs | 8 +- manul/src/session/echo.rs | 2 +- manul/src/session/evidence.rs | 4 +- manul/src/session/message.rs | 3 + manul/src/session/session.rs | 39 +++++- manul/src/session/transcript.rs | 23 ++-- manul/src/testing.rs | 4 + manul/src/testing/identity.rs | 10 +- manul/src/testing/macros.rs | 26 +++- manul/src/testing/run_sync.rs | 2 + 18 files changed, 324 insertions(+), 50 deletions(-) diff --git a/example/src/simple.rs b/example/src/simple.rs index f407258..4cbaf80 100644 --- a/example/src/simple.rs +++ b/example/src/simple.rs @@ -93,7 +93,7 @@ impl Protocol for SimpleProtocol { if round_id == RoundId::new(1) { return message.verify_is_invalid::(); } - Err(MessageValidationError::Other("Invalid round number".into()))? + Err(MessageValidationError::InvalidEvidence("Invalid round number".into()))? } } @@ -223,7 +223,7 @@ impl Round for Round1 { _rng: &mut impl CryptoRngCore, payloads: BTreeMap, _artifacts: BTreeMap, - ) -> Result, FinalizeError> { + ) -> Result, FinalizeError> { debug!( "{:?}: finalizing with messages from {:?}", self.context.id, @@ -327,7 +327,7 @@ impl Round for Round2 { _rng: &mut impl CryptoRngCore, payloads: BTreeMap, _artifacts: BTreeMap, - ) -> Result, FinalizeError> { + ) -> Result, FinalizeError> { debug!( "{:?}: finalizing with messages from {:?}", self.context.id, @@ -359,7 +359,7 @@ mod tests { use alloc::collections::BTreeSet; use manul::{ - session::{Keypair, SessionOutcome}, + session::{signature::Keypair, SessionOutcome}, testing::{run_sync, Signature, Signer, Verifier}, }; use rand_core::OsRng; diff --git a/example/src/simple_malicious.rs b/example/src/simple_malicious.rs index 8656f8d..46d38b5 100644 --- a/example/src/simple_malicious.rs +++ b/example/src/simple_malicious.rs @@ -5,7 +5,7 @@ use manul::{ protocol::{ Artifact, DirectMessage, FinalizeError, FinalizeOutcome, FirstRound, LocalError, Payload, Round, SessionId, }, - session::Keypair, + session::signature::Keypair, testing::{round_override, run_sync, RoundOverride, RoundWrapper, Signature, Signer, Verifier}, }; use rand_core::{CryptoRngCore, OsRng}; @@ -85,7 +85,7 @@ impl RoundOverride for Mali artifacts: BTreeMap, ) -> Result< FinalizeOutcome>::InnerRound as Round>::Protocol>, - FinalizeError>::InnerRound as Round>::Protocol>, + FinalizeError<<>::InnerRound as Round>::Protocol>, > { let behavior = self.behavior; let outcome = self.inner_round().finalize(rng, payloads, artifacts)?; diff --git a/example/tests/async.rs b/example/tests/async.rs index d178acf..fecbd6b 100644 --- a/example/tests/async.rs +++ b/example/tests/async.rs @@ -4,7 +4,9 @@ use alloc::collections::{BTreeMap, BTreeSet}; use manul::{ protocol::{Protocol, Round}, - session::{CanFinalize, Keypair, LocalError, MessageBundle, RoundOutcome, Session, SessionId, SessionReport}, + session::{ + signature::Keypair, CanFinalize, LocalError, MessageBundle, RoundOutcome, Session, SessionId, SessionReport, + }, testing::{Signature, Signer, Verifier}, }; use manul_example::simple::{Inputs, Round1}; diff --git a/manul/src/lib.rs b/manul/src/lib.rs index 5e43312..8f5c507 100644 --- a/manul/src/lib.rs +++ b/manul/src/lib.rs @@ -1,9 +1,11 @@ #![no_std] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![doc = include_str!("../../README.md")] #![warn( clippy::mod_module_files, clippy::unwrap_used, clippy::indexing_slicing, - //missing_docs, + missing_docs, missing_copy_implementations, rust_2018_idioms, trivial_casts, diff --git a/manul/src/protocol.rs b/manul/src/protocol.rs index 13352e6..d98c256 100644 --- a/manul/src/protocol.rs +++ b/manul/src/protocol.rs @@ -1,3 +1,16 @@ +/*! +API for protocol implementors. + +A protocol is a directed acyclic graph with the nodes being objects of types implementing [`Round`] +(to be specific, "acyclic" means that the values returned by [`Round::id`] +should not repeat during the protocol execution; the types might). +The starting point is a type also implementing [`FirstRound`]. +All the rounds should have their associated type [`Round::Protocol`] set to the same [`Protocol`] instance +to be executed by a [`Session`](`crate::session::Session`). + +For more details, see the documentation of the mentioned traits. +*/ + mod error; mod object_safe; mod round; @@ -13,4 +26,4 @@ pub use round::{ pub(crate) use object_safe::{ObjectSafeRound, ObjectSafeRoundWrapper}; pub(crate) use round::ReceiveErrorType; -pub use digest::Digest; +pub use digest; diff --git a/manul/src/protocol/error.rs b/manul/src/protocol/error.rs index eaa6f21..69fa537 100644 --- a/manul/src/protocol/error.rs +++ b/manul/src/protocol/error.rs @@ -7,6 +7,7 @@ use core::fmt::Debug; pub struct LocalError(String); impl LocalError { + /// Creates a new error from anything castable to string. pub fn new(message: impl Into) -> Self { Self(message.into()) } @@ -18,6 +19,7 @@ impl LocalError { pub struct RemoteError(String); impl RemoteError { + /// Creates a new error from anything castable to string. pub fn new(message: impl Into) -> Self { Self(message.into()) } diff --git a/manul/src/protocol/object_safe.rs b/manul/src/protocol/object_safe.rs index 439518a..fc898a4 100644 --- a/manul/src/protocol/object_safe.rs +++ b/manul/src/protocol/object_safe.rs @@ -70,7 +70,7 @@ pub(crate) trait ObjectSafeRound: 'static + Send + Sync { rng: &mut dyn CryptoRngCore, payloads: BTreeMap, artifacts: BTreeMap, - ) -> Result, FinalizeError>; + ) -> Result, FinalizeError>; fn expecting_messages_from(&self) -> &BTreeSet; @@ -145,7 +145,7 @@ where rng: &mut dyn CryptoRngCore, payloads: BTreeMap, artifacts: BTreeMap, - ) -> Result, FinalizeError> { + ) -> Result, FinalizeError> { let mut boxed_rng = BoxedRng(rng); self.round.finalize(&mut boxed_rng, payloads, artifacts) } diff --git a/manul/src/protocol/round.rs b/manul/src/protocol/round.rs index c0599b7..04d1153 100644 --- a/manul/src/protocol/round.rs +++ b/manul/src/protocol/round.rs @@ -20,31 +20,41 @@ use crate::{ session::{EchoRoundError, SessionId}, }; +/// An error that can be returned from [`Round::receive_message`]. #[derive(Debug)] pub struct ReceiveError(pub(crate) ReceiveErrorType); #[derive(Debug)] pub(crate) enum ReceiveErrorType { + /// A local error, indicating an implemenation bug or a misuse by the upper layer. Local(LocalError), + /// The given direct message cannot be deserialized. InvalidDirectMessage(DirectMessageError), + /// The given echo broadcast cannot be deserialized. InvalidEchoBroadcast(EchoBroadcastError), + /// A provable error occurred. Protocol(P::ProtocolError), + /// An unprovable error occurred. Unprovable(RemoteError), // Note that this variant should not be instantiated by the user (a protocol author), // so this whole enum is crate-private and the variants are created // via constructors and From impls. + /// An echo round error occurred. Echo(EchoRoundError), } impl ReceiveError { + /// A local error, indicating an implemenation bug or a misuse by the upper layer. pub fn local(message: impl Into) -> Self { Self(ReceiveErrorType::Local(LocalError::new(message.into()))) } + /// An unprovable error occurred. pub fn unprovable(message: impl Into) -> Self { Self(ReceiveErrorType::Unprovable(RemoteError::new(message.into()))) } + /// A provable error occurred. pub fn protocol(error: P::ProtocolError) -> Self { Self(ReceiveErrorType::Protocol(error)) } @@ -95,8 +105,11 @@ where } } +/// Possible successful outcomes of [`Round::finalize`]. pub enum FinalizeOutcome { + /// Transition to a new round. AnotherRound(AnotherRound), + /// The protocol reached a result. Result(P::Result), } @@ -105,12 +118,14 @@ where Id: 'static, P: 'static + Protocol, { + /// A helper method to create an [`AnotherRound`](`Self::AnotherRound`) variant. pub fn another_round(round: impl Round) -> Self { Self::AnotherRound(AnotherRound::new(round)) } } // We do not want to expose `ObjectSafeRound` to the user, so it is hidden in a struct. +/// A wrapped new round that may be returned by [`Round::finalize`]. pub struct AnotherRound(Box>); impl AnotherRound @@ -118,29 +133,37 @@ where Id: 'static, P: 'static + Protocol, { + /// Wraps an object implementing [`Round`]. pub fn new(round: impl Round) -> Self { Self(Box::new(ObjectSafeRoundWrapper::new(round))) } + /// Returns the inner boxed type. + /// This is an internal method to be used in `Session`. pub(crate) fn into_boxed(self) -> Box> { self.0 } + /// Attempts to extract an object of a concrete type. pub fn downcast>(self) -> Result { self.0.downcast::() } + /// Attempts to extract an object of a concrete type, preserving the original on failure. pub fn try_downcast>(self) -> Result { self.0.try_downcast::().map_err(Self) } } -pub enum FinalizeError { +/// An error that can occur during [`Round::finalize`]. +pub enum FinalizeError { + /// A local error, usually indicating a bug in the implementation. Local(LocalError), + /// An unattributable error, with an attached proof that this node performed its duties correctly. Unattributable(P::CorrectnessProof), - Unprovable { party: Id, error: RemoteError }, } +/// A round identifier. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct RoundId { round_num: u8, @@ -148,6 +171,7 @@ pub struct RoundId { } impl RoundId { + /// Creates a new round identifier. pub fn new(round_num: u8) -> Self { Self { round_num, @@ -155,6 +179,9 @@ impl RoundId { } } + /// Returns the identifier of the echo round corresponding to the given non-echo round. + /// + /// Panics if `self` is already an echo round identifier. pub(crate) fn echo(&self) -> Self { // If this panic happens, there is something wrong with the internal logic // of managing echo-broadcast rounds. @@ -167,6 +194,9 @@ impl RoundId { } } + /// Returns the identifier of the non-echo round corresponding to the given echo round. + /// + /// Panics if `self` is already a non-echo round identifier. pub(crate) fn non_echo(&self) -> Self { // If this panic happens, there is something wrong with the internal logic // of managing echo-broadcast rounds. @@ -180,10 +210,14 @@ impl RoundId { } } +/// An error that can occur during the validation of an evidence of an invalid message. #[derive(Debug, Clone)] pub enum MessageValidationError { + /// Indicates a local problem, usually a bug in the library code. Local(LocalError), - Other(String), + /// Indicates a problem with the evidence, for example the given round not sending such messages, + /// or the message actually deserializing successfully. + InvalidEvidence(String), } /// An error that can be returned during deserialization error. @@ -192,8 +226,9 @@ pub enum MessageValidationError { pub struct DeserializationError(String); impl DeserializationError { - pub fn new(message: String) -> Self { - Self(message) + /// Creates a new deserialization error. + pub fn new(message: impl Into) -> Self { + Self(message.into()) } } @@ -203,38 +238,65 @@ impl From for MessageValidationError { } } +/// A distributed protocol. pub trait Protocol: Debug + Sized { + /// The successful result of an execution of this protocol. type Result; + + /// An object of this type will be returned when a provable error happens during [`Round::receive_message`]. type ProtocolError: ProtocolError + Serialize + for<'de> Deserialize<'de>; + + /// An object of this type will be returned when an unattributable error happens during [`Round::finalize`]. + /// + /// It proves that the node did its job correctly, to be adjudicated by a third party. type CorrectnessProof: Send + Serialize + for<'de> Deserialize<'de>; + + /// The hasher used by this protocol. + /// + /// This will be used to generate message signatures. type Digest: Digest; + /// Serializes the given object into a bytestring. fn serialize(value: T) -> Result, LocalError>; + + /// Tries to deserialize the given bytestring as an object of type `T`. fn deserialize<'de, T: Deserialize<'de>>(bytes: &'de [u8]) -> Result; + /// 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. fn verify_direct_message_is_invalid( round_id: RoundId, #[allow(unused_variables)] message: &DirectMessage, ) -> Result<(), MessageValidationError> { - Err(MessageValidationError::Other(format!( + Err(MessageValidationError::InvalidEvidence(format!( "There are no direct messages in {round_id:?}" ))) } + /// 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. fn verify_echo_broadcast_is_invalid( round_id: RoundId, #[allow(unused_variables)] message: &EchoBroadcast, ) -> Result<(), MessageValidationError> { - Err(MessageValidationError::Other(format!( + Err(MessageValidationError::InvalidEvidence(format!( "There are no echo broadcasts in {round_id:?}" ))) } } +/// An error that can occur during the validation of an evidence of a protocol error. #[derive(Debug, Clone)] pub enum ProtocolValidationError { + /// Indicates a local problem, usually a bug in the library code. Local(LocalError), - Other(String), + /// Indicates a problem with the evidence, for example missing messages, + /// or messages that cannot be deserialized. + InvalidEvidence(String), } // If fail to deserialize a message when validating the evidence @@ -242,13 +304,13 @@ pub enum ProtocolValidationError { // processed separately, generating its own evidence. impl From for ProtocolValidationError { fn from(error: DirectMessageError) -> Self { - Self::Other(format!("Failed to deserialize direct message: {error:?}")) + Self::InvalidEvidence(format!("Failed to deserialize direct message: {error:?}")) } } impl From for ProtocolValidationError { fn from(error: EchoBroadcastError) -> Self { - Self::Other(format!("Failed to deserialize echo broadcast: {error:?}")) + Self::InvalidEvidence(format!("Failed to deserialize echo broadcast: {error:?}")) } } @@ -258,16 +320,47 @@ impl From for ProtocolValidationError { } } +/// Describes provable errors originating during protocol execution. +/// +/// Provable here means that we can create an evidence object entirely of messages signed by some party, +/// which, in combination, prove the party's malicious actions. pub trait ProtocolError: Debug + Clone + Send { + /// The rounds direct messages from which are required to prove malicious behavior for this error. + /// + /// **Note:** Should not include the round where the error happened. fn required_direct_messages(&self) -> BTreeSet { BTreeSet::new() } + + /// The rounds echo broadcasts from which are required to prove malicious behavior for this error. + /// + /// **Note:** Should not include the round where the error happened. fn required_echo_broadcasts(&self) -> BTreeSet { BTreeSet::new() } + + /// The rounds combined echos from which are required to prove malicious behavior for this error. + /// + /// **Note:** Should not include the round where the error happened. + /// + /// The combined echos are echo broadcasts sent by a party during the echo round, + /// where it bundles all the received broadcasts and sends them back to everyone. fn required_combined_echos(&self) -> BTreeSet { BTreeSet::new() } + + /// Returns `Ok(())` if the attached messages indeed prove that a malicious action happened. + /// + /// The signatures and metadata of the messages will be checked by the calling code, + /// the responsibility of this method is just to check the message contents. + /// + /// `echo_broadcast` and `direct_message` are the messages that triggered the error + /// during [`Round::receive_message`]. + /// `echo_broadcasts` and `direct_messages` are messages from the previous rounds, as requested by + /// [`required_direct_messages`](`Self::required_direct_messages`) and + /// [`required_echo_broadcasts`](`Self::required_echo_broadcasts`). + /// `combined_echos` are bundled echos from other parties from the previous rounds, + /// as requested by [`required_combined_echos`](`Self::required_combined_echos`). fn verify_messages_constitute_error( &self, echo_broadcast: &Option, @@ -288,63 +381,85 @@ pub struct DirectMessageError(DeserializationError); #[displaydoc("Echo broadcast error: {0}")] pub struct EchoBroadcastError(DeserializationError); +/// A serialized direct message. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DirectMessage(#[serde(with = "serde_bytes")] 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::Other( + 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) } } +/// A serialized echo broadcast. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EchoBroadcast(#[serde(with = "serde_bytes")] 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::Other( + 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) } } +/// Message payload created in [`Round::receive_message`]. pub struct Payload(pub Box); impl Payload { + /// Creates a new payload. + /// + /// Would be normally called in [`Round::receive_message`]. pub fn new(payload: T) -> Self { Self(Box::new(payload)) } + /// Creates an empty payload. + /// + /// Use it in [`Round::receive_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`]. pub fn try_to_typed(self) -> Result { Ok(*(self .0 @@ -353,17 +468,27 @@ impl Payload { } } +/// Associated data created alongside a message in [`Round::make_direct_message`]. pub struct Artifact(pub Box); impl Artifact { + /// Creates a new artifact. + /// + /// Would be normally called in [`Round::make_direct_message`]. pub fn new(artifact: T) -> Self { 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`]. pub fn try_to_typed(self) -> Result { Ok(*(self .0 @@ -372,8 +497,18 @@ impl Artifact { } } +/// A round that initiates a protocol. +/// +/// This is a round that can be created directly; +/// all the others are only reachable throud [`Round::finalize`] by the execution layer. pub trait FirstRound: Round + Sized { + /// Additional inputs for the protocol (besides the mandatory ones in [`new`](`Self::new`)). type Inputs; + + /// Creates the round. + /// + /// `session_id` can be assumed to be the same for each node participating in a session. + /// `id` is the ID of this node. fn new( rng: &mut impl CryptoRngCore, session_id: &SessionId, @@ -382,21 +517,56 @@ pub trait FirstRound: Round + Sized { ) -> Result; } +/** +A type representing a single round of a protocol. + +The way a round will be used by an external caller: +- create messages to send out (by calling [`make_direct_message`](`Self::make_direct_message`) + and [`make_echo_broadcast`](`Self::make_echo_broadcast`)); +- process received messages from other nodes (by calling [`receive_message`](`Self::receive_message`)); +- attempt to finalize (by calling [`finalize`](`Self::finalize`)) to produce the next round, or return a result. +*/ pub trait Round: 'static + Send + Sync { + /// The protocol this round is a part of. type Protocol: Protocol; + /// The round ID. + /// + /// **Note:** these should not repeat during execution. fn id(&self) -> RoundId; + /// The round IDs of the rounds this round can finalize into. + /// + /// Returns an empty set if this round only finalizes into a result. fn possible_next_rounds(&self) -> BTreeSet; + /// The destinations of the messages to be sent out by this round. + /// + /// The way it is interpreted by the execution layer is + /// - An echo broadcast (if any) is sent to all of these destinations; + /// - A direct message is sent to each of these destinations, + /// which means [`make_direct_message`](`Self::make_direct_message`) may be called + /// for each element of the returned set. fn message_destinations(&self) -> &BTreeSet; + /// Returns the direct message to the given destination and an accompanying artifact. + /// + /// 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( &self, rng: &mut impl CryptoRngCore, destination: &Id, ) -> Result<(DirectMessage, Artifact), LocalError>; + /// Returns the echo broadcast for this round, or `None` if the round does not require echo-broadcasting. + /// + /// Returns `None` if not implemented. + /// + /// The execution layer will guarantee that all the destinations are sure they all received the same broadcast. + /// This also means that a message with the broadcasts from all nodes signed by each node is available + /// if an evidence of malicious behavior has to be constructed. fn make_echo_broadcast( &self, #[allow(unused_variables)] rng: &mut impl CryptoRngCore, @@ -404,6 +574,10 @@ pub trait Round: 'static + Send + Sync { None } + /// Processes the received message and generates the payload that will be used in [`finalize`](`Self::finalize`). + /// + /// Note that there is no need to authenticate the message at this point; + /// it has already been done by the execution layer. fn receive_message( &self, rng: &mut impl CryptoRngCore, @@ -412,19 +586,31 @@ pub trait Round: 'static + Send + Sync { 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`). fn finalize( self, rng: &mut impl CryptoRngCore, payloads: BTreeMap, artifacts: BTreeMap, - ) -> Result, FinalizeError>; + ) -> Result, FinalizeError>; + /// Returns the set of node IDs from which this round expects messages. + /// + /// The execution layer will not call [`finalize`](`Self::finalize`) until all these nodes have responded + /// (and the corresponding [`receive_message`](`Self::receive_message`) finished successfully). fn expecting_messages_from(&self) -> &BTreeSet; + /// A convenience method to create an [`EchoBroadcast`] object + /// to return in [`make_echo_broadcast`](`Self::make_echo_broadcast`). fn serialize_echo_broadcast(message: impl Serialize) -> Result { EchoBroadcast::new::(message) } + /// A convenience method to create a [`DirectMessage`] object + /// to return in [`make_direct_message`](`Self::make_direct_message`). fn serialize_direct_message(message: impl Serialize) -> Result { DirectMessage::new::(message) } diff --git a/manul/src/session.rs b/manul/src/session.rs index 49c5f51..d03cb5d 100644 --- a/manul/src/session.rs +++ b/manul/src/session.rs @@ -1,3 +1,9 @@ +/*! +API for protocol users. + +This is some description. +*/ + mod echo; mod evidence; mod message; @@ -12,4 +18,4 @@ pub use transcript::{SessionOutcome, SessionReport}; pub(crate) use echo::EchoRoundError; -pub use signature::{DigestVerifier, Keypair, RandomizedDigestSigner}; +pub use signature; diff --git a/manul/src/session/echo.rs b/manul/src/session/echo.rs index 809a174..e1d9959 100644 --- a/manul/src/session/echo.rs +++ b/manul/src/session/echo.rs @@ -217,7 +217,7 @@ where rng: &mut impl CryptoRngCore, _payloads: BTreeMap, _artifacts: BTreeMap, - ) -> Result, FinalizeError> { + ) -> Result, FinalizeError> { self.main_round.finalize(rng, self.payloads, self.artifacts) } } diff --git a/manul/src/session/evidence.rs b/manul/src/session/evidence.rs index 6e16490..ad3bca6 100644 --- a/manul/src/session/evidence.rs +++ b/manul/src/session/evidence.rs @@ -40,7 +40,7 @@ impl From for EvidenceError { fn from(error: MessageValidationError) -> Self { match error { MessageValidationError::Local(error) => Self::Local(error), - MessageValidationError::Other(error) => Self::InvalidEvidence(error), + MessageValidationError::InvalidEvidence(error) => Self::InvalidEvidence(error), } } } @@ -49,7 +49,7 @@ impl From for EvidenceError { fn from(error: ProtocolValidationError) -> Self { match error { ProtocolValidationError::Local(error) => Self::Local(error), - ProtocolValidationError::Other(error) => Self::InvalidEvidence(error), + ProtocolValidationError::InvalidEvidence(error) => Self::InvalidEvidence(error), } } } diff --git a/manul/src/session/message.rs b/manul/src/session/message.rs index 987a4ad..2703fe3 100644 --- a/manul/src/session/message.rs +++ b/manul/src/session/message.rs @@ -127,6 +127,9 @@ impl VerifiedMessage { } } +/// A message bundle to be sent to another node. +/// +/// Note that this is already signed. #[derive(Clone, Debug)] pub struct MessageBundle { direct_message: SignedMessage, diff --git a/manul/src/session/session.rs b/manul/src/session/session.rs index 6af7a87..adc8d5c 100644 --- a/manul/src/session/session.rs +++ b/manul/src/session/session.rs @@ -30,13 +30,20 @@ use crate::{ #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct SessionId(#[serde(with = "serde_bytes")] Box<[u8]>); +/// A session ID. +/// +/// This must be the same for all nodes executing a session. +/// +/// Must be created uniquely for each session execution, otherwise there is a danger of replay attacks. impl SessionId { + /// Creates a random session identifier. pub fn random(rng: &mut impl CryptoRngCore) -> Self { let mut buffer = [0u8; 256]; rng.fill_bytes(&mut buffer); Self(buffer.into()) } + /// Creates a session identifier from the given bytestring. pub fn new(bytes: &[u8]) -> Self { Self(bytes.into()) } @@ -48,6 +55,8 @@ impl AsRef<[u8]> for SessionId { } } +/// An object encapsulating the currently active round, transport protocol, +/// and the database of messages and errors from the previous rounds. pub struct Session { session_id: SessionId, signer: Signer, @@ -59,10 +68,15 @@ pub struct Session { transcript: Transcript, } +/// Possible non-erroneous results of finalizing a round. pub enum RoundOutcome { + /// The execution is finished. Finished(SessionReport), + /// Transitioned to another round. AnotherRound { + /// The session object for the new round. session: Session, + /// The messages intended for the new round cached during the previous round. cached_messages: Vec>, }, } @@ -83,6 +97,7 @@ where + Sync, S: Debug + Clone + Eq + 'static + Serialize + for<'de> Deserialize<'de> + Send + Sync, { + /// Initializes a new session. pub fn new( rng: &mut impl CryptoRngCore, session_id: SessionId, @@ -135,18 +150,24 @@ where }) } + /// Returns the verifier corresponding to the session's signer. pub fn verifier(&self) -> Verifier { self.verifier.clone() } + /// Returns the session ID. pub fn session_id(&self) -> &SessionId { &self.session_id } + /// Returns the set of message destinations for the current round. pub fn message_destinations(&self) -> &BTreeSet { &self.message_destinations } + /// Creates the message to be sent to the given destination. + /// + /// The destination must be one of those returned by [`message_destinations`](`Self::message_destinations`). pub fn make_message( &self, rng: &mut impl CryptoRngCore, @@ -172,6 +193,7 @@ where )) } + /// Adds the artifact from [`make_message`](`Self::make_message`) to the accumulator. pub fn add_artifact( &self, accum: &mut RoundAccumulator, @@ -180,10 +202,12 @@ where accum.add_artifact(processed) } + /// Returns the ID of the current round. pub fn round_id(&self) -> RoundId { self.round.id() } + /// Performs some preliminary checks on the message to verify its integrity. pub fn preprocess_message( &self, accum: &mut RoundAccumulator, @@ -271,6 +295,9 @@ where } } + /// Processes a verified message. + /// + /// This can be called in a spawned task if it is known to take a long time. pub fn process_message( &self, rng: &mut impl CryptoRngCore, @@ -287,6 +314,7 @@ where ProcessedMessage { message, processed } } + /// Adds a result of [`process_message`](`Self::process_message`) to the accumulator. pub fn add_processed_message( &self, accum: &mut RoundAccumulator, @@ -295,10 +323,12 @@ where accum.add_processed_message(&self.transcript, processed) } + /// Makes an accumulator for a new round. pub fn make_accumulator(&self) -> RoundAccumulator { RoundAccumulator::new(self.round.expecting_messages_from()) } + /// Terminates the session. pub fn terminate( self, accum: RoundAccumulator, @@ -315,6 +345,7 @@ where Ok(SessionReport::new(SessionOutcome::NotEnoughMessages, transcript)) } + /// Attempts to finalize the current round. pub fn finalize_round( self, rng: &mut impl CryptoRngCore, @@ -384,20 +415,17 @@ where SessionOutcome::StalledWithProof(correctness_proof), transcript, )), - FinalizeError::Unprovable { party, error } => { - let mut transcript = transcript; - transcript.register_unprovable_error(&party, error)?; - RoundOutcome::Finished(SessionReport::new(SessionOutcome::UnprovableError, transcript)) - } }), } } + /// Checks if the round can be finalized. pub fn can_finalize(&self, accum: &RoundAccumulator) -> CanFinalize { accum.can_finalize() } } +/// Possible answers to whether the round can be finalized. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CanFinalize { /// There are enough messages successfully processed to finalize the round. @@ -409,6 +437,7 @@ pub enum CanFinalize { Never, } +/// A mutable accumulator for collecting the results and errors from processing messages for a single round. pub struct RoundAccumulator { still_have_not_sent_messages: BTreeSet, expecting_messages_from: BTreeSet, diff --git a/manul/src/session/transcript.rs b/manul/src/session/transcript.rs index baf541a..c5df56c 100644 --- a/manul/src/session/transcript.rs +++ b/manul/src/session/transcript.rs @@ -136,30 +136,31 @@ where .cloned() .ok_or_else(|| LocalError::new(format!("Echo-broadcasts for {round_id:?} are not in the transcript"))) } - - pub fn register_unprovable_error(&mut self, from: &Verifier, error: RemoteError) -> Result<(), LocalError> { - if self.unprovable_errors.insert(from.clone(), error).is_some() { - return Err(LocalError::new(format!( - "An unprovable errors entry for {from:?} already exists" - ))); - } - Ok(()) - } } +/// Possible outcomes of running a session. #[derive(Debug)] pub enum SessionOutcome { + /// The protocol successfully produced a result. Result(P::Result), + /// The execution stalled because of an unattributable error, + /// but the protocol created a proof that this node performed its duties correctly. + /// + /// This protocol is supposed to be passed to a third party for adjudication. StalledWithProof(P::CorrectnessProof), + /// The execution stalled because not enough messages were received to finalize the round. NotEnoughMessages, - ProvableError, - UnprovableError, } +/// The report of a session execution. pub struct SessionReport { + /// The session outcome. pub outcome: SessionOutcome

, + /// The provable errors collected during the execution, as the evidences that can be published to prove them. pub provable_errors: BTreeMap>, + /// The unprovable errors collected during the execution. pub unprovable_errors: BTreeMap, + /// The nodes that did not send their messages in time for the corresponding round. pub missing_messages: BTreeMap>, } diff --git a/manul/src/testing.rs b/manul/src/testing.rs index 7c39355..f958aca 100644 --- a/manul/src/testing.rs +++ b/manul/src/testing.rs @@ -1,3 +1,7 @@ +/*! +Utilities for testing protocols. +*/ + mod identity; mod macros; mod run_sync; diff --git a/manul/src/testing/identity.rs b/manul/src/testing/identity.rs index fa5388d..49b7eb1 100644 --- a/manul/src/testing/identity.rs +++ b/manul/src/testing/identity.rs @@ -2,12 +2,15 @@ use digest::generic_array::typenum; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; +/// A simple signer for testing purposes. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Signer(u8); +/// A verifier corresponding to [`Signer`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Verifier(u8); +/// A signature produced by [`Signer`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Signature { signed_by: u8, @@ -15,13 +18,10 @@ pub struct Signature { } impl Signer { + /// Creates a new signer. pub fn new(id: u8) -> Self { Self(id) } - - pub fn as_integer(&self) -> u8 { - self.0 - } } impl signature::RandomizedDigestSigner for Signer { @@ -55,6 +55,8 @@ impl signature::DigestVerifier for Verifier { } } +/// A very simple hasher for testing purposes. +/// Not in any way secure. #[derive(Debug, Clone, Copy, Default)] pub struct Hasher { cursor: usize, diff --git a/manul/src/testing/macros.rs b/manul/src/testing/macros.rs index c8be5ef..9ce75c2 100644 --- a/manul/src/testing/macros.rs +++ b/manul/src/testing/macros.rs @@ -6,13 +6,25 @@ use crate::protocol::{ Artifact, DirectMessage, EchoBroadcast, FinalizeError, FinalizeOutcome, LocalError, Payload, Round, }; +/// A trait defining a wrapper around an existing type implementing [`Round`]. pub trait RoundWrapper: 'static + Sized + Send + Sync { + /// The inner round type. type InnerRound: Round; + + /// Returns a reference to the inner round. fn inner_round_ref(&self) -> &Self::InnerRound; + + /// Returns the inner round by value. fn inner_round(self) -> Self::InnerRound; } +/// This trait defines overrides of some methods [`RoundWrapper::InnerRound`]. +/// +/// Intended to be used with [`round_override`] to generate the [`Round`] implementation. +/// +/// The blanket implementations default to the methods of the wrapped round. pub trait RoundOverride: RoundWrapper { + /// An override for [`Round::make_direct_message`]. fn make_direct_message( &self, rng: &mut impl CryptoRngCore, @@ -20,9 +32,13 @@ pub trait RoundOverride: RoundWrapper { ) -> Result<(DirectMessage, Artifact), LocalError> { 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> { self.inner_round_ref().make_echo_broadcast(rng) } + + /// An override for [`Round::finalize`]. #[allow(clippy::type_complexity)] fn finalize( self, @@ -31,12 +47,18 @@ pub trait RoundOverride: RoundWrapper { artifacts: BTreeMap, ) -> Result< FinalizeOutcome>::InnerRound as Round>::Protocol>, - FinalizeError>::InnerRound as Round>::Protocol>, + FinalizeError<<>::InnerRound as Round>::Protocol>, > { self.inner_round().finalize(rng, payloads, artifacts) } } +/// A macro for "inheriting" from a [`Round`]-implementing type, and overriding some of its behavior. +/// +/// The given `$round` must implement [`RoundOverride`], and is generally some type +/// with one of its fiels implementing [`Round`]. +/// Then, using the macro will implement [`Round`] for `$round` by delegating non-overridden methods to +/// the internal [`RoundWrapper::InnerRound`]. #[macro_export] macro_rules! round_override { ($round: ident) => { @@ -92,7 +114,7 @@ macro_rules! round_override { artifacts: ::alloc::collections::BTreeMap, ) -> Result< $crate::protocol::FinalizeOutcome, - $crate::protocol::FinalizeError + $crate::protocol::FinalizeError > { >::finalize(self, rng, payloads, artifacts) } diff --git a/manul/src/testing/run_sync.rs b/manul/src/testing/run_sync.rs index 98dbac5..63d623b 100644 --- a/manul/src/testing/run_sync.rs +++ b/manul/src/testing/run_sync.rs @@ -98,6 +98,8 @@ where Ok((state, messages)) } +/// Execute sessions for multiple nodes concurrently, given the the inputs +/// for the first round `R` and the signer for each node. #[allow(clippy::type_complexity)] pub fn run_sync( rng: &mut impl CryptoRngCore,