From cda1d26a88cdbacd52d4a989db8ec1828d20bd25 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 24 Oct 2024 20:28:31 +0200 Subject: [PATCH] Docs and tweaks (#40) * Multiple documentation additions and fixes * Better names for `testing` mocks * `Debug` impls for public items --- .gitignore | 3 + CHANGELOG.md | 2 + examples/Cargo.toml | 2 +- examples/README.md | 8 +- examples/src/simple.rs | 9 +- examples/src/simple_malicious.rs | 16 +-- examples/tests/{async.rs => async_runner.rs} | 79 +++++++++----- manul/benches/empty_rounds.rs | 11 +- manul/src/lib.rs | 3 +- manul/src/protocol.rs | 4 +- manul/src/protocol/errors.rs | 1 + manul/src/protocol/object_safe.rs | 14 +-- manul/src/protocol/round.rs | 14 ++- manul/src/session.rs | 5 +- manul/src/session/echo.rs | 6 +- manul/src/session/message.rs | 18 +++- manul/src/session/session.rs | 104 +++++++++++-------- manul/src/session/transcript.rs | 2 + manul/src/testing.rs | 11 +- manul/src/testing/identity.rs | 56 +++++----- manul/src/testing/macros.rs | 11 +- manul/src/testing/run_sync.rs | 6 +- 22 files changed, 244 insertions(+), 141 deletions(-) rename examples/tests/{async.rs => async_runner.rs} (68%) diff --git a/.gitignore b/.gitignore index ea8c4bf..6f7d5ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +Cargo.lock +*.sublime-project +*.sublime-workspace diff --git a/CHANGELOG.md b/CHANGELOG.md index b0f0161..ebeb76f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Session` is now generic over `SessionParameters` instead of a bunch of separate types. ([#36]) - `MessageBundle` is not generic anymore. ([#36]) - `ProcessedArtifact` is now also generic on `SessionParameters`. ([#37]) +- Added a `Test` prefix to `testing::Signer`/`Verifier`/`Signature`/`Hasher` and renamed `TestingSessionParams` to `TestSessionParams`. ([#40]) ### Added @@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#32]: https://github.com/entropyxyz/manul/pull/32 [#36]: https://github.com/entropyxyz/manul/pull/36 [#37]: https://github.com/entropyxyz/manul/pull/37 +[#40]: https://github.com/entropyxyz/manul/pull/40 ## [0.0.1] - 2024-10-12 diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 61bb1ad..dace9db 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" edition = "2021" authors = ['Entropy Cryptography '] license = "MIT" -description = "Examples of usage for `manul` crate" +description = "Usage examples for the `manul` crate" repository = "https://github.com/entropyxyz/manul/examples" readme = "README.md" diff --git a/examples/README.md b/examples/README.md index 253d83f..4113c6f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,9 @@ +# Manul examples + This crate illustrates the usage of `manul` for implementing distributed protocols. -The library itself is the perspective of the protocol implementor, where they create a set of `Round` impls and write unit-tests for them. +The library itself takes the perspective of the protocol implementor, as they create a set of `Round` impls and write unit-tests for them. + +The integration tests are written from the perspective of the protocol user, emulating an asynchronous execution of the protocol on multiple nodes. -The integration tests are the perspective of the protocol user, emulating an asynchronous execution of the protocol on multiple nodes. +To run the example, execute: `RUST_LOG=debug cargo t -p manul-example --test async_runner` diff --git a/examples/src/simple.rs b/examples/src/simple.rs index 51f1cfc..9bb9db1 100644 --- a/examples/src/simple.rs +++ b/examples/src/simple.rs @@ -99,12 +99,14 @@ pub struct Inputs { pub all_ids: BTreeSet, } +#[derive(Debug)] pub(crate) struct Context { pub(crate) id: Id, pub(crate) other_ids: BTreeSet, pub(crate) ids_to_positions: BTreeMap, } +#[derive(Debug)] pub struct Round1 { pub(crate) context: Context, } @@ -246,6 +248,7 @@ impl Round for Round1 { } } +#[derive(Debug)] pub(crate) struct Round2 { round1_sum: u8, pub(crate) context: Context, @@ -356,7 +359,7 @@ mod tests { use manul::{ session::{signature::Keypair, SessionOutcome}, - testing::{run_sync, Signer, TestingSessionParams, Verifier}, + testing::{run_sync, TestSessionParams, TestSigner, TestVerifier}, }; use rand_core::OsRng; use tracing_subscriber::EnvFilter; @@ -365,7 +368,7 @@ mod tests { #[test] fn round() { - let signers = (0..3).map(Signer::new).collect::>(); + let signers = (0..3).map(TestSigner::new).collect::>(); let all_ids = signers .iter() .map(|signer| signer.verifying_key()) @@ -386,7 +389,7 @@ mod tests { .with_env_filter(EnvFilter::from_default_env()) .finish(); let reports = tracing::subscriber::with_default(my_subscriber, || { - run_sync::, TestingSessionParams>(&mut OsRng, inputs).unwrap() + run_sync::, TestSessionParams>(&mut OsRng, inputs).unwrap() }); for (_id, report) in reports { diff --git a/examples/src/simple_malicious.rs b/examples/src/simple_malicious.rs index e7e73da..b7ea83f 100644 --- a/examples/src/simple_malicious.rs +++ b/examples/src/simple_malicious.rs @@ -6,7 +6,7 @@ use manul::{ Artifact, DirectMessage, FinalizeError, FinalizeOutcome, FirstRound, LocalError, Payload, Round, SessionId, }, session::signature::Keypair, - testing::{round_override, run_sync, RoundOverride, RoundWrapper, Signer, TestingSessionParams, Verifier}, + testing::{round_override, run_sync, RoundOverride, RoundWrapper, TestSessionParams, TestSigner, TestVerifier}, }; use rand_core::{CryptoRngCore, OsRng}; use tracing_subscriber::EnvFilter; @@ -26,6 +26,7 @@ struct MaliciousInputs { behavior: Behavior, } +#[derive(Debug)] struct MaliciousRound1 { round: Round1, behavior: Behavior, @@ -105,6 +106,7 @@ impl RoundOverride for Mali round_override!(MaliciousRound1); +#[derive(Debug)] struct MaliciousRound2 { round: Round2, behavior: Behavior, @@ -143,7 +145,7 @@ round_override!(MaliciousRound2); #[test] fn serialized_garbage() { - let signers = (0..3).map(Signer::new).collect::>(); + let signers = (0..3).map(TestSigner::new).collect::>(); let all_ids = signers .iter() .map(|signer| signer.verifying_key()) @@ -172,7 +174,7 @@ fn serialized_garbage() { .with_env_filter(EnvFilter::from_default_env()) .finish(); let mut reports = tracing::subscriber::with_default(my_subscriber, || { - run_sync::, TestingSessionParams>(&mut OsRng, run_inputs).unwrap() + run_sync::, TestSessionParams>(&mut OsRng, run_inputs).unwrap() }); let v0 = signers[0].verifying_key(); @@ -189,7 +191,7 @@ fn serialized_garbage() { #[test] fn attributable_failure() { - let signers = (0..3).map(Signer::new).collect::>(); + let signers = (0..3).map(TestSigner::new).collect::>(); let all_ids = signers .iter() .map(|signer| signer.verifying_key()) @@ -218,7 +220,7 @@ fn attributable_failure() { .with_env_filter(EnvFilter::from_default_env()) .finish(); let mut reports = tracing::subscriber::with_default(my_subscriber, || { - run_sync::, TestingSessionParams>(&mut OsRng, run_inputs).unwrap() + run_sync::, TestSessionParams>(&mut OsRng, run_inputs).unwrap() }); let v0 = signers[0].verifying_key(); @@ -235,7 +237,7 @@ fn attributable_failure() { #[test] fn attributable_failure_round2() { - let signers = (0..3).map(Signer::new).collect::>(); + let signers = (0..3).map(TestSigner::new).collect::>(); let all_ids = signers .iter() .map(|signer| signer.verifying_key()) @@ -264,7 +266,7 @@ fn attributable_failure_round2() { .with_env_filter(EnvFilter::from_default_env()) .finish(); let mut reports = tracing::subscriber::with_default(my_subscriber, || { - run_sync::, TestingSessionParams>(&mut OsRng, run_inputs).unwrap() + run_sync::, TestSessionParams>(&mut OsRng, run_inputs).unwrap() }); let v0 = signers[0].verifying_key(); diff --git a/examples/tests/async.rs b/examples/tests/async_runner.rs similarity index 68% rename from examples/tests/async.rs rename to examples/tests/async_runner.rs index 9a95552..6ec71f7 100644 --- a/examples/tests/async.rs +++ b/examples/tests/async_runner.rs @@ -1,5 +1,7 @@ extern crate alloc; +use std::fmt::Debug; + use alloc::collections::{BTreeMap, BTreeSet}; use manul::{ @@ -8,16 +10,16 @@ use manul::{ signature::Keypair, CanFinalize, LocalError, MessageBundle, RoundOutcome, Session, SessionId, SessionParameters, SessionReport, }, - testing::{Signer, TestingSessionParams, Verifier}, + testing::{TestSessionParams, TestSigner}, }; -use manul_example::simple::{Inputs, Round1}; +use manul_example::simple::{Inputs, Round1, SimpleProtocol}; use rand::Rng; use rand_core::OsRng; use tokio::{ sync::mpsc, time::{sleep, Duration}, }; -use tracing::debug; +use tracing::{debug, trace}; use tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; struct MessageOut { @@ -31,6 +33,7 @@ struct MessageIn { message: MessageBundle, } +/// Runs a session. Simulates what each participating party would run as the protocol progresses. async fn run_session( tx: mpsc::Sender>, rx: mpsc::Receiver>, @@ -38,17 +41,30 @@ async fn run_session( ) -> Result, LocalError> where P: 'static + Protocol, - SP: 'static + SessionParameters, + SP: 'static + SessionParameters + Debug, { let rng = &mut OsRng; let mut rx = rx; let mut session = session; + // Some rounds can finalize early and put off sending messages to the next round. Such messages + // will be stored here and applied after the messages for this round are sent. let mut cached_messages = Vec::new(); let key = session.verifier(); + // Each iteration of the loop progresses the session as follows: + // - Send out messages as dictated by the session "destinations". + // - Apply any cached messages. + // - Enter a nested loop: + // - Try to finalize the session; if we're done, exit the inner loop. + // - Wait until we get an incoming message. + // - Process the message we received and continue the loop. + // - When all messages have been sent and received as specified by the protocol, finalize the + // round. + // - If the protocol outcome is a new round, go to the top of the loop and start over with a + // new session. loop { debug!("{key:?}: *** starting round {:?} ***", session.round_id()); @@ -67,7 +83,7 @@ where // and the artifact will be sent back to the host task // to be added to the accumulator. let (message, artifact) = session.make_message(rng, destination)?; - debug!("{key:?}: sending a message to {destination:?}",); + debug!("{key:?}: Sending a message to {destination:?}",); tx.send(MessageOut { from: key.clone(), to: destination.clone(), @@ -76,16 +92,16 @@ where .await .unwrap(); - // This will happen in a host task + // This would happen in a host task session.add_artifact(&mut accum, artifact)?; } for preprocessed in cached_messages { - // In production usage, this will happen in a spawned task. - debug!("{key:?}: applying a cached message"); + // In production usage, this would happen in a spawned task and relayed back to the main task. + debug!("{key:?}: Applying a cached message"); let processed = session.process_message(rng, preprocessed); - // This will happen in a host task. + // This would happen in a host task. session.add_processed_message(&mut accum, processed)?; } @@ -96,26 +112,31 @@ where // Due to already registered invalid messages from nodes, // even if the remaining nodes send correct messages, it won't be enough. // Terminating. - CanFinalize::Never => return session.terminate(accum), + CanFinalize::Never => { + tracing::warn!("{key:?}: This session cannot ever be finalized. Terminating."); + return session.terminate(accum); + } } - debug!("{key:?}: waiting for a message"); + debug!("{key:?}: Waiting for a message"); let incoming = rx.recv().await.unwrap(); // Perform quick checks before proceeding with the verification. - let preprocessed = session.preprocess_message(&mut accum, &incoming.from, incoming.message)?; - - if let Some(preprocessed) = preprocessed { - // In production usage, this will happen in a spawned task. - debug!("{key:?}: applying a message from {:?}", incoming.from); - let processed = session.process_message(rng, preprocessed); - - // This will happen in a host task. - session.add_processed_message(&mut accum, processed)?; + match session.preprocess_message(&mut accum, &incoming.from, incoming.message)? { + Some(preprocessed) => { + // In production usage, this would happen in a separate task. + debug!("{key:?}: Applying a message from {:?}", incoming.from); + let processed = session.process_message(rng, preprocessed); + // In production usage, this would be a host task. + session.add_processed_message(&mut accum, processed)?; + } + None => { + trace!("{key:?} Pre-processing complete. Current state: {accum:?}") + } } } - debug!("{key:?}: finalizing the round"); + debug!("{key:?}: Finalizing the round"); match session.finalize_round(rng, accum)? { RoundOutcome::Finished(report) => break Ok(report), @@ -176,7 +197,7 @@ async fn message_dispatcher( async fn run_nodes(sessions: Vec>) -> Vec> where P: 'static + Protocol + Send, - SP: 'static + SessionParameters, + SP: 'static + SessionParameters + Debug, P::Result: Send, SP::Signer: Send, { @@ -204,7 +225,7 @@ where }) .collect::>(); - // Drop the last copy of the dispatcher's incoming channel so that it could finish. + // Drop the last copy of the dispatcher's incoming channel so that it can finish. drop(dispatcher_tx); let mut results = Vec::with_capacity(num_parties); @@ -219,20 +240,25 @@ where #[tokio::test] async fn async_run() { - let signers = (0..3).map(Signer::new).collect::>(); + // The kind of Session we need to run the `SimpleProtocol`. + type SimpleSession = Session; + + // Create 4 parties + let signers = (0..3).map(TestSigner::new).collect::>(); let all_ids = signers .iter() .map(|signer| signer.verifying_key()) .collect::>(); let session_id = SessionId::random(&mut OsRng); + + // Create 4 `Session`s let sessions = signers .into_iter() .map(|signer| { let inputs = Inputs { all_ids: all_ids.clone(), }; - Session::<_, TestingSessionParams>::new::>(&mut OsRng, session_id.clone(), signer, inputs) - .unwrap() + SimpleSession::new::>(&mut OsRng, session_id.clone(), signer, inputs).unwrap() }) .collect::>(); @@ -242,5 +268,6 @@ async fn async_run() { .try_init() .unwrap(); + // Run the protocol run_nodes(sessions).await; } diff --git a/manul/benches/empty_rounds.rs b/manul/benches/empty_rounds.rs index f0612b2..2019380 100644 --- a/manul/benches/empty_rounds.rs +++ b/manul/benches/empty_rounds.rs @@ -10,7 +10,7 @@ use manul::{ LocalError, Payload, Protocol, ProtocolError, ProtocolValidationError, ReceiveError, Round, RoundId, }, session::{signature::Keypair, SessionId, SessionOutcome}, - testing::{run_sync, Signer, TestingSessionParams, Verifier}, + testing::{run_sync, TestSessionParams, TestSigner, TestVerifier}, }; use rand_core::{CryptoRngCore, OsRng}; use serde::{Deserialize, Serialize}; @@ -51,12 +51,13 @@ impl Protocol for EmptyProtocol { } } +#[derive(Debug)] struct EmptyRound { round_counter: u8, inputs: Inputs, } -#[derive(Clone)] +#[derive(Debug, Clone)] struct Inputs { rounds_num: u8, echo: bool, @@ -178,7 +179,7 @@ fn bench_empty_rounds(c: &mut Criterion) { let nodes = 25; let rounds_num = 5; - let signers = (0..nodes).map(Signer::new).collect::>(); + let signers = (0..nodes).map(TestSigner::new).collect::>(); let all_ids = signers .iter() .map(|signer| signer.verifying_key()) @@ -204,7 +205,7 @@ fn bench_empty_rounds(c: &mut Criterion) { group.bench_function("25 nodes, 5 rounds, no echo", |b| { b.iter(|| { assert!( - run_sync::, TestingSessionParams>(&mut OsRng, inputs_no_echo.clone()) + run_sync::, TestSessionParams>(&mut OsRng, inputs_no_echo.clone()) .unwrap() .values() .all(|report| matches!(report.outcome, SessionOutcome::Result(_))) @@ -234,7 +235,7 @@ fn bench_empty_rounds(c: &mut Criterion) { group.bench_function("25 nodes, 5 rounds, echo each round", |b| { b.iter(|| { assert!( - run_sync::, TestingSessionParams>(&mut OsRng, inputs_echo.clone()) + run_sync::, TestSessionParams>(&mut OsRng, inputs_echo.clone()) .unwrap() .values() .all(|report| matches!(report.outcome, SessionOutcome::Result(_))) diff --git a/manul/src/lib.rs b/manul/src/lib.rs index 6291440..8d3323f 100644 --- a/manul/src/lib.rs +++ b/manul/src/lib.rs @@ -10,7 +10,8 @@ rust_2018_idioms, trivial_casts, trivial_numeric_casts, - unused_qualifications + unused_qualifications, + missing_debug_implementations )] extern crate alloc; diff --git a/manul/src/protocol.rs b/manul/src/protocol.rs index ab12a48..9133cc7 100644 --- a/manul/src/protocol.rs +++ b/manul/src/protocol.rs @@ -4,8 +4,8 @@ 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 +The starting point is a type that implements [`FirstRound`]. +All the rounds must 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. diff --git a/manul/src/protocol/errors.rs b/manul/src/protocol/errors.rs index 045c062..27ef651 100644 --- a/manul/src/protocol/errors.rs +++ b/manul/src/protocol/errors.rs @@ -114,6 +114,7 @@ where } /// An error that can occur during [`Round::finalize`](`super::Round::finalize`). +#[derive(Debug)] pub enum FinalizeError { /// A local error, usually indicating a bug in the implementation. Local(LocalError), diff --git a/manul/src/protocol/object_safe.rs b/manul/src/protocol/object_safe.rs index b5c8f48..5d42262 100644 --- a/manul/src/protocol/object_safe.rs +++ b/manul/src/protocol/object_safe.rs @@ -3,7 +3,7 @@ use alloc::{ collections::{BTreeMap, BTreeSet}, format, }; -use core::marker::PhantomData; +use core::{fmt::Debug, marker::PhantomData}; use rand_core::{CryptoRng, CryptoRngCore, RngCore}; @@ -17,9 +17,9 @@ use super::{ /// to be passed to statically typed round methods. struct BoxedRng<'a>(&'a mut dyn CryptoRngCore); -impl<'a> CryptoRng for BoxedRng<'a> {} +impl CryptoRng for BoxedRng<'_> {} -impl<'a> RngCore for BoxedRng<'a> { +impl RngCore for BoxedRng<'_> { fn next_u32(&mut self) -> u32 { self.0.next_u32() } @@ -37,7 +37,7 @@ impl<'a> RngCore for BoxedRng<'a> { // Since we want `Round` methods to take `&mut impl CryptoRngCore` arguments // (which is what all cryptographic libraries generally take), it cannot be object-safe. // Thus we have to add this crate-private object-safe layer on top of `Round`. -pub(crate) trait ObjectSafeRound: 'static + Send + Sync { +pub(crate) trait ObjectSafeRound: 'static + Send + Sync + Debug { type Protocol: Protocol; fn id(&self) -> RoundId; @@ -75,7 +75,9 @@ pub(crate) trait ObjectSafeRound: 'static + Send + Sync { fn get_type_id(&self) -> core::any::TypeId; } -// The `fn(Id) -> Id` bit is so that `ObjectSafeRoundWrapper` didn't require a bound on `Id` to be `Send + Sync`. +// The `fn(Id) -> Id` bit is so that `ObjectSafeRoundWrapper` didn't require a bound on `Id` to be +// `Send + Sync`. +#[derive(Debug)] pub(crate) struct ObjectSafeRoundWrapper { round: R, phantom: PhantomData Id>, @@ -92,7 +94,7 @@ impl> ObjectSafeRoundWrapper { impl ObjectSafeRound for ObjectSafeRoundWrapper where - Id: 'static, + Id: 'static + Debug, R: Round, { type Protocol = >::Protocol; diff --git a/manul/src/protocol/round.rs b/manul/src/protocol/round.rs index 37b752b..71e4724 100644 --- a/manul/src/protocol/round.rs +++ b/manul/src/protocol/round.rs @@ -20,6 +20,7 @@ use super::{ use crate::session::SessionId; /// Possible successful outcomes of [`Round::finalize`]. +#[derive(Debug)] pub enum FinalizeOutcome { /// Transition to a new round. AnotherRound(AnotherRound), @@ -29,7 +30,7 @@ pub enum FinalizeOutcome { impl FinalizeOutcome where - Id: 'static, + Id: 'static + Debug, P: 'static + Protocol, { /// A helper method to create an [`AnotherRound`](`Self::AnotherRound`) variant. @@ -40,11 +41,12 @@ where // 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`]. +#[derive(Debug)] pub struct AnotherRound(Box>); impl AnotherRound where - Id: 'static, + Id: 'static + Debug, P: 'static + Protocol, { /// Wraps an object implementing [`Round`]. @@ -119,7 +121,7 @@ impl RoundId { /// A distributed protocol. pub trait Protocol: Debug + Sized { /// The successful result of an execution of this protocol. - type Result; + type Result: Debug; /// 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>; @@ -127,7 +129,7 @@ pub trait Protocol: Debug + Sized { /// 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>; + type CorrectnessProof: Send + Serialize + for<'de> Deserialize<'de> + Debug; /// Serializes the given object into a bytestring. fn serialize(value: T) -> Result, LocalError>; @@ -272,6 +274,7 @@ impl EchoBroadcast { } /// Message payload created in [`Round::receive_message`]. +#[derive(Debug)] pub struct Payload(pub Box); impl Payload { @@ -301,6 +304,7 @@ impl Payload { } /// Associated data created alongside a message in [`Round::make_direct_message`]. +#[derive(Debug)] pub struct Artifact(pub Box); impl Artifact { @@ -358,7 +362,7 @@ The way a round will be used by an external caller: - 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 { +pub trait Round: 'static + Send + Sync + Debug { /// The protocol this round is a part of. type Protocol: Protocol; diff --git a/manul/src/session.rs b/manul/src/session.rs index 44f4cf8..db7c68f 100644 --- a/manul/src/session.rs +++ b/manul/src/session.rs @@ -1,7 +1,10 @@ /*! API for protocol users. -This is some description. +The round-based protocols `manul` is designed to build use a [`Session`] object to drive the protocol forward. +Each participant constructs a [`Session`], defining the actions needed for each round (who to send messages +to, what kind of message and what to do next etc). The rest of the API from this module provide auxilliary +types: setup and parametrization, errors and outcomes. */ mod echo; diff --git a/manul/src/session/echo.rs b/manul/src/session/echo.rs index 649abf2..7b6d916 100644 --- a/manul/src/session/echo.rs +++ b/manul/src/session/echo.rs @@ -34,6 +34,10 @@ pub struct EchoRoundMessage { pub(crate) echo_messages: SerializableMap>, } +/// Each protocol round can contain one `EchoRound` with "echo messages" that are sent to all +/// participants. The execution layer of the protocol guarantees that all participants have received +/// the messages. +#[derive(Debug)] pub struct EchoRound { verifier: SP::Verifier, echo_messages: BTreeMap>, @@ -82,7 +86,7 @@ where impl Round for EchoRound where P: 'static + Protocol, - SP: 'static + SessionParameters, + SP: 'static + SessionParameters + Debug, { type Protocol = P; diff --git a/manul/src/session/message.rs b/manul/src/session/message.rs index 2358748..484cd63 100644 --- a/manul/src/session/message.rs +++ b/manul/src/session/message.rs @@ -158,7 +158,9 @@ impl VerifiedMessage { } } -/// A message bundle to be sent to another node. +/// A message bundle destined for another node. +/// +/// During message pre-processing, a `MessageBundle` transitions to a `CheckedMessageBundle`. /// /// Note that this is already signed. #[derive(Clone, Debug)] @@ -188,7 +190,6 @@ impl MessageBundle { } pub(crate) fn unify_metadata(self) -> Option { - let metadata = self.direct_message.message_with_metadata.metadata.clone(); if !self .echo_broadcast .as_ref() @@ -198,6 +199,7 @@ impl MessageBundle { return None; } + let metadata = self.direct_message.message_with_metadata.metadata.clone(); Some(CheckedMessageBundle { metadata, direct_message: self.direct_message, @@ -206,6 +208,10 @@ impl MessageBundle { } } +/// A `CheckedMessageBundle` is like a [`MessageBundle`] but where we have checked that the metadata +/// (i.e. SessionId and RoundId) from the Echo message (if any) matches with that of the +/// [`DirectMessage`]. +/// `CheckedMessageBundle`s can transition to [`VerifiedMessageBundle`]. #[derive(Clone, Debug)] pub(crate) struct CheckedMessageBundle { metadata: MessageMetadata, @@ -237,6 +243,9 @@ impl CheckedMessageBundle { } } +/// A `VerifiedMessageBundle` is the final evolution of a [`MessageBundle`]. At this point in the +/// process, the [`DirectMessage`] and an eventual [`EchoBroadcast`] have been fully checked and the +/// signature on the [`SignedMessage`] from the original [`MessageBundle`] successfully verified. #[derive(Clone, Debug)] pub struct VerifiedMessageBundle { from: SP::Verifier, @@ -261,7 +270,10 @@ where self.direct_message.payload() } - pub(crate) fn into_unverified(self) -> (Option>, SignedMessage) { + /// 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) diff --git a/manul/src/session/session.rs b/manul/src/session/session.rs index b954f44..395aa65 100644 --- a/manul/src/session/session.rs +++ b/manul/src/session/session.rs @@ -11,7 +11,7 @@ use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; use serde_encoded_bytes::{Base64, SliceLike}; use signature::{DigestVerifier, Keypair, RandomizedDigestSigner}; -use tracing::debug; +use tracing::{debug, trace}; use super::{ echo::EchoRound, @@ -31,7 +31,7 @@ use crate::protocol::{ /// is used in the network in which they are running the protocol. pub trait SessionParameters { /// The signer type. - type Signer: RandomizedDigestSigner + Keypair; + type Signer: Debug + RandomizedDigestSigner + Keypair; /// The hash type that will be used to pre-hash message payloads before signing. type Digest: Digest; @@ -81,6 +81,7 @@ 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. +#[derive(Debug)] pub struct Session { session_id: SessionId, signer: SP::Signer, @@ -93,6 +94,7 @@ pub struct Session { } /// Possible non-erroneous results of finalizing a round. +#[derive(Debug)] pub enum RoundOutcome { /// The execution is finished. Finished(SessionReport), @@ -108,7 +110,7 @@ pub enum RoundOutcome { impl Session where P: 'static + Protocol, - SP: 'static + SessionParameters, + SP: 'static + SessionParameters + Debug, { /// Initializes a new session. pub fn new( @@ -221,6 +223,18 @@ where } /// Performs some preliminary checks on the message to verify its integrity. + /// + /// On the happy path, the return values are as follows: + /// - `Ok(Some(…))` if the message passes all checks. + /// - `Ok(None)` if the message passed all checks, but is not for this round. In this case + /// the preprocessed message is buffered in the "cached" message and applied in the next round. + /// + /// On the unhappy path, the return values are: + /// - `Ok(None)` when something goes wrong, either because there's a problem at the + /// sending side or the message was received in the wrong context (e.g. wrong session, wrong + /// round etc). In most cases a `RemoteError` is added to the `RoundAccumulator`. + /// - `Err` means an error while processing the data locally (likely bugs or + /// deserialization issues). pub fn preprocess_message( &self, accum: &mut RoundAccumulator, @@ -228,49 +242,48 @@ where message: MessageBundle, ) -> Result>, LocalError> { // Quick preliminary checks, before we proceed with more expensive verification - + let key = self.verifier(); if self.transcript.is_banned(from) || accum.is_banned(from) { + trace!("{key:?} Banned."); return Ok(None); } let checked_message = match message.unify_metadata() { Some(checked_message) => checked_message, None => { - accum.register_unprovable_error(from, RemoteError::new("Mismatched metadata in bundled messages"))?; + let err = "Mismatched metadata in bundled messages."; + accum.register_unprovable_error(from, RemoteError::new(err))?; + trace!("{key:?} {err}"); return Ok(None); } }; let message_round_id = checked_message.metadata().round_id(); if checked_message.metadata().session_id() != &self.session_id { - accum.register_unprovable_error( - from, - RemoteError::new("The received message has an incorrect session ID"), - )?; + let err = "The received message has an incorrect session ID"; + accum.register_unprovable_error(from, RemoteError::new(err))?; + trace!("{key:?} {err}"); return Ok(None); } if message_round_id == self.round_id() { if accum.message_is_being_processed(from) { - accum.register_unprovable_error( - from, - RemoteError::new("Message from this party is already being processed"), - )?; + let err = "Message from this party is already being processed"; + accum.register_unprovable_error(from, RemoteError::new(err))?; + trace!("{key:?} {err}"); return Ok(None); } } else if self.possible_next_rounds.contains(&message_round_id) { if accum.message_is_cached(from, message_round_id) { - accum.register_unprovable_error( - from, - RemoteError::new(format!("Message for {:?} is already cached", message_round_id)), - )?; + let err = format!("Message for {:?} is already cached", message_round_id); + accum.register_unprovable_error(from, RemoteError::new(&err))?; + trace!("{key:?} {err}"); return Ok(None); } } else { - accum.register_unprovable_error( - from, - RemoteError::new(format!("Unexpected message round ID: {:?}", message_round_id)), - )?; + let err = format!("Unexpected message round ID: {:?}", message_round_id); + accum.register_unprovable_error(from, RemoteError::new(&err))?; + trace!("{key:?} {err}"); return Ok(None); } @@ -279,18 +292,21 @@ where let verified_message = match checked_message.verify::(from) { Ok(verified_message) => verified_message, Err(MessageVerificationError::InvalidSignature) => { - accum.register_unprovable_error(from, RemoteError::new("The signature could not be deserialized"))?; + let err = "The signature could not be deserialized."; + accum.register_unprovable_error(from, RemoteError::new(err))?; + trace!("{key:?} {err}"); return Ok(None); } Err(MessageVerificationError::SignatureMismatch) => { - accum.register_unprovable_error(from, RemoteError::new("Message verification failed"))?; + let err = "Message verification failed."; + accum.register_unprovable_error(from, RemoteError::new(err))?; + trace!("{key:?} {err}"); return Ok(None); } Err(MessageVerificationError::Local(error)) => return Err(error), }; debug!( - "{:?}: received {:?} message from {:?}", - self.verifier(), + "{key:?}: Received {:?} message from {:?}", verified_message.metadata().round_id(), from ); @@ -300,12 +316,13 @@ where Ok(Some(verified_message)) } else if self.possible_next_rounds.contains(&message_round_id) { debug!( - "{:?}: caching message from {:?} for {:?}", - self.verifier(), + "{key:?}: Caching message from {:?} for {:?}", verified_message.from(), verified_message.metadata().round_id() ); accum.cache_message(verified_message)?; + // TODO(dp): this is a bit awkward. It means "all good, but nothing to do here right + // now". Ok(None) } else { unreachable!() @@ -452,6 +469,7 @@ pub enum CanFinalize { } /// A mutable accumulator for collecting the results and errors from processing messages for a single round. +#[derive(Debug)] pub struct RoundAccumulator { still_have_not_sent_messages: BTreeSet, expecting_messages_from: BTreeSet, @@ -585,7 +603,7 @@ where let error = match processed.processed { Ok(payload) => { - let (echo_broadcast, direct_message) = processed.message.into_unverified(); + let (echo_broadcast, direct_message) = processed.message.into_parts(); if let Some(echo) = echo_broadcast { self.echo_broadcasts.insert(from.clone(), echo); } @@ -598,19 +616,19 @@ where match error.0 { ReceiveErrorType::InvalidDirectMessage(error) => { - let (_echo_broadcast, direct_message) = processed.message.into_unverified(); + let (_echo_broadcast, direct_message) = processed.message.into_parts(); let evidence = Evidence::new_invalid_direct_message(&from, direct_message, error); self.register_provable_error(&from, evidence) } ReceiveErrorType::InvalidEchoBroadcast(error) => { - let (echo_broadcast, _direct_message) = processed.message.into_unverified(); + 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) } ReceiveErrorType::Protocol(error) => { - let (echo_broadcast, direct_message) = processed.message.into_unverified(); + let (echo_broadcast, direct_message) = processed.message.into_parts(); let evidence = Evidence::new_protocol_error(&from, echo_broadcast, direct_message, error, transcript)?; self.register_provable_error(&from, evidence) } @@ -619,7 +637,7 @@ where Ok(()) } ReceiveErrorType::Echo(error) => { - let (_echo_broadcast, direct_message) = processed.message.into_unverified(); + let (_echo_broadcast, direct_message) = processed.message.into_parts(); let evidence = Evidence::new_echo_round_error(&from, direct_message, error, transcript)?; self.register_provable_error(&from, evidence) } @@ -641,11 +659,13 @@ where } } +#[derive(Debug)] pub struct ProcessedArtifact { destination: SP::Verifier, artifact: Artifact, } +#[derive(Debug)] pub struct ProcessedMessage { message: VerifiedMessageBundle, processed: Result>, @@ -674,18 +694,18 @@ mod tests { DeserializationError, DirectMessage, EchoBroadcast, LocalError, Protocol, ProtocolError, ProtocolValidationError, RoundId, }, - testing::TestingSessionParams, + testing::TestSessionParams, }; #[test] fn test_concurrency_bounds() { // In order to support parallel message creation and processing we need that - // certain generic types could be Send and/or Sync. + // certain generic types be Send and/or Sync. // // Since they are generic, this depends on the exact type parameters supplied by the user, - // so if the user does not want parallelism, they may not use Send/Sync generic parameters. - // But we want to make sure that if the generic parameters are Send/Sync, - // our types are too. + // so if the user does not want parallelism, they can use generic parameters that are not + // Send/Sync. But we want to make sure that if the generic parameters are + // Send/Sync, our types are too. #[derive(Debug)] struct DummyProtocol; @@ -726,15 +746,15 @@ mod tests { // We need `Session` to be `Send` so that we send a `Session` object to a task // to run the loop there. - assert!(impls!(Session: Send)); + assert!(impls!(Session: Send)); // This is needed so that message processing offloaded to a task could use `&Session`. - assert!(impls!(Session: Sync)); + assert!(impls!(Session: Sync)); // These objects are sent to/from message processing tasks assert!(impls!(MessageBundle: Send)); - assert!(impls!(ProcessedArtifact: Send)); - assert!(impls!(VerifiedMessageBundle: Send)); - assert!(impls!(ProcessedMessage: Send)); + assert!(impls!(ProcessedArtifact: Send)); + assert!(impls!(VerifiedMessageBundle: Send)); + assert!(impls!(ProcessedMessage: Send)); } } diff --git a/manul/src/session/transcript.rs b/manul/src/session/transcript.rs index a5b7d4f..3803d2e 100644 --- a/manul/src/session/transcript.rs +++ b/manul/src/session/transcript.rs @@ -7,6 +7,7 @@ use core::fmt::Debug; use super::{evidence::Evidence, message::SignedMessage, session::SessionParameters, LocalError, RemoteError}; use crate::protocol::{DirectMessage, EchoBroadcast, Protocol, RoundId}; +#[derive(Debug)] pub(crate) struct Transcript { echo_broadcasts: BTreeMap>>, direct_messages: BTreeMap>>, @@ -152,6 +153,7 @@ pub enum SessionOutcome { } /// The report of a session execution. +#[derive(Debug)] pub struct SessionReport { /// The session outcome. pub outcome: SessionOutcome

, diff --git a/manul/src/testing.rs b/manul/src/testing.rs index 820bb10..f5c767d 100644 --- a/manul/src/testing.rs +++ b/manul/src/testing.rs @@ -1,11 +1,20 @@ /*! Utilities for testing protocols. + +When testing round based protocols it can be complicated to "inject" the proper faults into the +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, +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. */ mod identity; mod macros; mod run_sync; -pub use identity::{Hasher, Signature, Signer, TestingSessionParams, Verifier}; +pub use identity::{TestHasher, TestSessionParams, TestSignature, TestSigner, TestVerifier}; pub use macros::{round_override, RoundOverride, RoundWrapper}; pub use run_sync::run_sync; diff --git a/manul/src/testing/identity.rs b/manul/src/testing/identity.rs index 4220ca2..db3b1a1 100644 --- a/manul/src/testing/identity.rs +++ b/manul/src/testing/identity.rs @@ -6,49 +6,49 @@ use crate::session::SessionParameters; /// A simple signer for testing purposes. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Signer(u8); +pub struct TestSigner(u8); -/// A verifier corresponding to [`Signer`]. +/// A verifier corresponding to [`TestSigner`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Verifier(u8); +pub struct TestVerifier(u8); -/// A signature produced by [`Signer`]. +/// A signature produced by [`TestSigner`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Signature { +pub struct TestSignature { signed_by: u8, randomness: u64, } -impl Signer { - /// Creates a new signer. +impl TestSigner { + /// Creates a new signer for testing purposes. pub fn new(id: u8) -> Self { Self(id) } } -impl signature::RandomizedDigestSigner for Signer { +impl signature::RandomizedDigestSigner for TestSigner { fn try_sign_digest_with_rng( &self, rng: &mut impl CryptoRngCore, _digest: D, - ) -> Result { - Ok(Signature { + ) -> Result { + Ok(TestSignature { signed_by: self.0, randomness: rng.next_u64(), }) } } -impl signature::Keypair for Signer { - type VerifyingKey = Verifier; +impl signature::Keypair for TestSigner { + type VerifyingKey = TestVerifier; fn verifying_key(&self) -> Self::VerifyingKey { - Verifier(self.0) + TestVerifier(self.0) } } -impl signature::DigestVerifier for Verifier { - fn verify_digest(&self, _digest: D, signature: &Signature) -> Result<(), signature::Error> { +impl signature::DigestVerifier for TestVerifier { + fn verify_digest(&self, _digest: D, signature: &TestSignature) -> Result<(), signature::Error> { if self.0 == signature.signed_by { Ok(()) } else { @@ -60,14 +60,14 @@ 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 { +pub struct TestHasher { cursor: usize, buffer: [u8; 32], } -impl digest::HashMarker for Hasher {} +impl digest::HashMarker for TestHasher {} -impl digest::Update for Hasher { +impl digest::Update for TestHasher { fn update(&mut self, data: &[u8]) { // A very simple algorithm for testing, just xor the data in buffer-sized chunks. for byte in data { @@ -77,35 +77,35 @@ impl digest::Update for Hasher { } } -impl digest::FixedOutput for Hasher { +impl digest::FixedOutput for TestHasher { fn finalize_into(self, out: &mut digest::Output) { AsMut::<[u8]>::as_mut(out).copy_from_slice(&self.buffer) } } -impl digest::OutputSizeUser for Hasher { +impl digest::OutputSizeUser for TestHasher { type OutputSize = typenum::U8; } /// An implementation of [`SessionParameters`] using the testing signer/verifier types. #[derive(Debug, Clone, Copy)] -pub struct TestingSessionParams; +pub struct TestSessionParams; -impl SessionParameters for TestingSessionParams { - type Signer = Signer; - type Verifier = Verifier; - type Signature = Signature; - type Digest = Hasher; +impl SessionParameters for TestSessionParams { + type Signer = TestSigner; + type Verifier = TestVerifier; + type Signature = TestSignature; + type Digest = TestHasher; } #[cfg(test)] mod tests { use impls::impls; - use super::Hasher; + use super::TestHasher; #[test] fn test_hasher_bounds() { - assert!(impls!(Hasher: digest::Digest)); + assert!(impls!(TestHasher: digest::Digest)); } } diff --git a/manul/src/testing/macros.rs b/manul/src/testing/macros.rs index 9ce75c2..4ca9593 100644 --- a/manul/src/testing/macros.rs +++ b/manul/src/testing/macros.rs @@ -18,11 +18,11 @@ pub trait RoundWrapper: 'static + Sized + Send + Sync { fn inner_round(self) -> Self::InnerRound; } -/// This trait defines overrides of some methods [`RoundWrapper::InnerRound`]. +/// This trait defines overrides of some methods of [`RoundWrapper::InnerRound`]. /// -/// Intended to be used with [`round_override`] to generate the [`Round`] implementation. +/// Intended to be used with the [`round_override`] macro to generate the [`Round`] implementation. /// -/// The blanket implementations default to the methods of the wrapped round. +/// 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( @@ -56,14 +56,15 @@ pub trait RoundOverride: RoundWrapper { /// 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 +/// with one of its fields implementing [`Round`]. +/// Then, the macro will implement the [`Round`] trait for `$round` by delegating non-overridden methods to /// the internal [`RoundWrapper::InnerRound`]. #[macro_export] macro_rules! round_override { ($round: ident) => { impl Round for $round where + Id: Debug, $round: RoundOverride, { type Protocol = diff --git a/manul/src/testing/run_sync.rs b/manul/src/testing/run_sync.rs index 7d3a6cb..19d6670 100644 --- a/manul/src/testing/run_sync.rs +++ b/manul/src/testing/run_sync.rs @@ -1,3 +1,5 @@ +use core::fmt::Debug; + use alloc::{collections::BTreeMap, vec::Vec}; use rand::Rng; @@ -35,7 +37,7 @@ fn propagate( ) -> Result<(State, Vec>), LocalError> where P: 'static + Protocol, - SP: 'static + SessionParameters, + SP: 'static + SessionParameters + Debug, { let mut messages = Vec::new(); @@ -95,7 +97,7 @@ pub fn run_sync( ) -> Result>, LocalError> where R: 'static + FirstRound, - SP: 'static + SessionParameters, + SP: 'static + SessionParameters + Debug, { let session_id = SessionId::random(rng);