From 3f17681a3609d1eca2ac5b1baa1e830574d2a582 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 26 Nov 2024 13:03:41 +0100 Subject: [PATCH 01/22] Rework simulation Signed-off-by: Denis Varlakov --- Cargo.lock | 4 +- .../random-generation-protocol/Cargo.toml | 1 - .../random-generation-protocol/src/lib.rs | 47 ++---- round-based/Cargo.toml | 6 +- round-based/src/simulation/mod.rs | 158 +++++++++++++++--- round-based/src/simulation/sim_async.rs | 135 ++++++++++++++- round-based/src/simulation/sim_sync.rs | 25 +-- 7 files changed, 302 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9875ff7..70e77d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,7 +455,6 @@ name = "random-generation-protocol" version = "0.1.0" dependencies = [ "displaydoc", - "futures", "generic-array", "hex", "rand", @@ -475,8 +474,11 @@ dependencies = [ "displaydoc", "futures", "futures-util", + "hex", "matches", "phantom-type", + "rand", + "rand_dev", "round-based-derive", "thiserror", "tokio", diff --git a/examples/random-generation-protocol/Cargo.toml b/examples/random-generation-protocol/Cargo.toml index 3ac08ef..90cffda 100644 --- a/examples/random-generation-protocol/Cargo.toml +++ b/examples/random-generation-protocol/Cargo.toml @@ -22,7 +22,6 @@ generic-array = { version = "0.14", features = ["serde"] } [dev-dependencies] round-based = { path = "../../round-based", features = ["derive", "dev", "state-machine"] } tokio = { version = "1.15", features = ["macros", "rt"] } -futures = "0.3" hex = "0.4" rand_dev = "0.1" rand = "0.8" diff --git a/examples/random-generation-protocol/src/lib.rs b/examples/random-generation-protocol/src/lib.rs index e571402..3e6a3b5 100644 --- a/examples/random-generation-protocol/src/lib.rs +++ b/examples/random-generation-protocol/src/lib.rs @@ -173,13 +173,10 @@ pub struct Blame { #[cfg(test)] mod tests { - use alloc::{vec, vec::Vec}; - use rand::Rng; - use round_based::simulation::Simulation; use sha2::{Digest, Sha256}; - use super::{protocol_of_random_generation, Msg}; + use super::protocol_of_random_generation; #[tokio::test] async fn simulation_async() { @@ -187,42 +184,28 @@ mod tests { let n: u16 = 5; - let mut simulation = Simulation::::new(); - let mut party_output = vec![]; - - for i in 0..n { - let party = simulation.add_party(); - let output = protocol_of_random_generation(party, i, n, rng.fork()); - party_output.push(output); - } - - let output = futures::future::try_join_all(party_output).await.unwrap(); - - // Assert that all parties outputed the same randomness - for i in 1..n { - assert_eq!(output[0], output[usize::from(i)]); - } + let randomness = round_based::simulation::run_with_setup( + core::iter::repeat_with(|| rng.fork()).take(n.into()), + |i, party, rng| protocol_of_random_generation(party, i, n, rng), + ) + .await + .expect_success() + .expect_same(); - std::println!("Output randomness: {}", hex::encode(output[0])); + std::println!("Output randomness: {}", hex::encode(randomness)); } #[test] fn simulation_sync() { let mut rng = rand_dev::DevRng::new(); - let simulation = round_based::simulation::SimulationSync::from_async_fn(5, |i, party| { + round_based::simulation::SimulationSync::from_async_fn(5, |i, party| { protocol_of_random_generation(party, i, 5, rng.fork()) - }); - - let outputs = simulation - .run() - .unwrap() - .into_iter() - .collect::, _>>() - .unwrap(); - for output_i in &outputs { - assert_eq!(*output_i, outputs[0]); - } + }) + .run() + .unwrap() + .expect_success() + .expect_same(); } // Emulate the protocol using the state machine interface diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index fc176a7..280aa52 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -32,11 +32,15 @@ trybuild = "1" matches = "0.1" futures = { version = "0.3", default-features = false } tokio = { version = "1", features = ["macros"] } +hex = "0.4" + +rand = "0.8" +rand_dev = "0.1" [features] default = ["std"] state-machine = [] -dev = ["std", "tokio/sync", "tokio-stream"] +dev = ["std", "tokio/sync", "tokio-stream", "futures-util/alloc"] derive = ["round-based-derive"] runtime-tokio = ["tokio"] std = ["thiserror"] diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index 8b3df54..f0bc3e4 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -1,43 +1,54 @@ //! Multiparty protocol simulation //! -//! [`Simulation`] is an essential developer tool for testing the multiparty protocol locally. +//! Simulator is an essential developer tool for testing the multiparty protocol locally. //! It covers most of the boilerplate by mocking networking. //! -//! ## Example +//! The entry point is either [`run`] or [`run_with_setup`] functions. They take a protocol +//! defined as an async function, provide simulated networking, carry out the simulation, +//! and return the result. +//! +//! If you need more control over execution, you can use [`Network`] to simulate the networking +//! and carry out the protocol manually. +//! +//! When `state-machine` feature is enabled, [`SimulationSync`] is available which can carry out +//! protocols defined as a state machine. //! -//! ```rust +//! ## Example +//! ```rust,no_run +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { //! use round_based::{Mpc, PartyIndex}; -//! use round_based::simulation::Simulation; -//! use futures::future::try_join_all; //! //! # type Result = std::result::Result; //! # type Randomness = [u8; 32]; //! # type Msg = (); //! // Any MPC protocol you want to test -//! pub async fn protocol_of_random_generation(party: M, i: PartyIndex, n: u16) -> Result -//! where M: Mpc +//! pub async fn protocol_of_random_generation( +//! party: M, +//! i: PartyIndex, +//! n: u16 +//! ) -> Result +//! where +//! M: Mpc //! { //! // ... //! # todo!() //! } //! -//! async fn test_randomness_generation() { -//! let n = 3; -//! -//! let mut simulation = Simulation::::new(); -//! let mut outputs = vec![]; -//! for i in 0..n { -//! let party = simulation.add_party(); -//! outputs.push(protocol_of_random_generation(party, i, n)); -//! } -//! -//! // Waits each party to complete the protocol -//! let outputs = try_join_all(outputs).await.expect("protocol wasn't completed successfully"); -//! // Asserts that all parties output the same randomness -//! for output in outputs.iter().skip(1) { -//! assert_eq!(&outputs[0], output); -//! } -//! } +//! let n = 3; +//! +//! let output = round_based::simulation::run( +//! n, +//! |i, party| protocol_of_random_generation(party, i, n), +//! ) +//! .await +//! // unwrap `Result`s +//! .expect_success() +//! // check that all parties produced the same response +//! .expect_same(); +//! +//! println!("Output randomness: {}", hex::encode(output)); +//! # } //! ``` mod sim_async; @@ -47,3 +58,102 @@ mod sim_sync; pub use sim_async::*; #[cfg(feature = "state-machine")] pub use sim_sync::*; + +/// Result of the simulation +pub struct SimResult(pub alloc::vec::Vec); + +impl SimResult> +where + E: core::fmt::Debug, +{ + /// Unwraps `Result` produced by each party + /// + /// Panics if at least one of the parties returned `Err(_)`. In this case, + /// a verbose error message will shown specifying which of the parties returned + /// an error. + pub fn expect_success(self) -> SimResult { + let mut oks = alloc::vec::Vec::with_capacity(self.0.len()); + let mut errs = alloc::vec::Vec::with_capacity(self.0.len()); + + for (res, i) in self.0.into_iter().zip(0u16..) { + match res { + Ok(res) => oks.push(res), + Err(res) => errs.push((i, res)), + } + } + + if !errs.is_empty() { + let mut msg = alloc::format!( + "Simulation output didn't match expectations.\n\ + Expected: all parties succeed\n\ + Actual : {success} parties succeeded, {failed} parties returned an error\n\ + Failures:\n", + success = oks.len(), + failed = errs.len(), + ); + + for (i, err) in errs { + msg += &alloc::format!("- Party {i}: {err:?}\n"); + } + + panic!("{msg}"); + } + + SimResult(oks) + } +} + +impl SimResult +where + T: PartialEq + core::fmt::Debug, +{ + /// Checks that outputs of all parties are equally the same + /// + /// Returns the output on success (all the outputs are checked to be the same), otherwise + /// panics with a verbose error message. + /// + /// Panics if simulation contained zero parties. + pub fn expect_same(mut self) -> T { + let Some(first) = self.0.get(0) else { + panic!("simulation contained zero parties"); + }; + + if !self.0[1..].iter().all(|i| i == first) { + let mut msg = alloc::format!( + "Simulation output didn't match expectations.\n\ + Expected: all parties return the same output\n\ + Actual : some of the parties returned a different output\n\ + Outputs :\n" + ); + + for (i, res) in self.0.iter().enumerate() { + msg += &alloc::format!("- Party {i}: {res:?}"); + } + + panic!("{msg}") + } + + self.0 + .pop() + .expect("we checked that the list contains at least one element") + } +} + +impl SimResult { + /// Deconstructs the simulation result returning inner list of results + pub fn into_vec(self) -> alloc::vec::Vec { + self.0 + } +} + +impl From> for SimResult { + fn from(list: alloc::vec::Vec) -> Self { + Self(list) + } +} + +impl From> for alloc::vec::Vec { + fn from(res: SimResult) -> Self { + res.0 + } +} diff --git a/round-based/src/simulation/sim_async.rs b/round-based/src/simulation/sim_async.rs index 165624d..b5ca781 100644 --- a/round-based/src/simulation/sim_async.rs +++ b/round-based/src/simulation/sim_async.rs @@ -1,5 +1,6 @@ use alloc::sync::Arc; use core::{ + future::Future, pin::Pin, sync::atomic::AtomicU64, task::ready, @@ -13,14 +14,16 @@ use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}; use crate::delivery::{Delivery, Incoming, Outgoing}; use crate::{MessageDestination, MessageType, MpcParty, MsgId, PartyIndex}; -/// Multiparty protocol simulator -pub struct Simulation { +use super::SimResult; + +/// Simulated async network +pub struct Network { channel: broadcast::Sender>>, next_party_idx: PartyIndex, next_msg_id: Arc, } -impl Simulation +impl Network where M: Clone + Send + Unpin + 'static, { @@ -71,7 +74,7 @@ where } } -impl Default for Simulation +impl Default for Network where M: Clone + Send + Unpin + 'static, { @@ -176,3 +179,127 @@ impl NextMessageId { self.0.fetch_add(1, core::sync::atomic::Ordering::Relaxed) } } + +/// Simulates execution of the protocol +/// +/// Takes amount of participants, and a function that carries out the protocol for +/// one party. The function takes as input: index of the party, and [`MpcParty`] +/// that can be used to communicate with others. +/// +/// ## Example +/// ```rust,no_run +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// use round_based::{Mpc, PartyIndex}; +/// +/// # type Result = std::result::Result; +/// # type Randomness = [u8; 32]; +/// # type Msg = (); +/// // Any MPC protocol you want to test +/// pub async fn protocol_of_random_generation( +/// party: M, +/// i: PartyIndex, +/// n: u16 +/// ) -> Result +/// where +/// M: Mpc +/// { +/// // ... +/// # todo!() +/// } +/// +/// let n = 3; +/// +/// let output = round_based::simulation::run( +/// n, +/// |i, party| protocol_of_random_generation(party, i, n), +/// ) +/// .await +/// // unwrap `Result`s +/// .expect_success() +/// // check that all parties produced the same response +/// .expect_same(); +/// +/// println!("Output randomness: {}", hex::encode(output)); +/// # } +/// ``` +pub async fn run( + n: u16, + mut party_start: impl FnMut(u16, MpcParty>) -> F, +) -> SimResult +where + M: Clone + Send + Unpin + 'static, + F: Future, +{ + run_with_setup::<(), M, F>(core::iter::repeat(()).take(n.into()), |i, party, ()| { + party_start(i, party) + }) + .await +} + +/// Simulates execution of the protocol +/// +/// Similar to [`run`], but allows some setup to be provided to the protocol execution +/// function. +/// +/// Simulation will have as many parties as `setups` iterator yields +/// +/// ## Example +/// ```rust,no_run +/// # #[tokio::main(flavor = "current_thread")] +/// # async fn main() { +/// use round_based::{Mpc, PartyIndex}; +/// +/// # type Result = std::result::Result; +/// # type Randomness = [u8; 32]; +/// # type Msg = (); +/// // Any MPC protocol you want to test +/// pub async fn protocol_of_random_generation( +/// rng: impl rand::RngCore, +/// party: M, +/// i: PartyIndex, +/// n: u16 +/// ) -> Result +/// where +/// M: Mpc +/// { +/// // ... +/// # todo!() +/// } +/// +/// let mut rng = rand_dev::DevRng::new(); +/// let n = 3; +/// let output = round_based::simulation::run_with_setup( +/// core::iter::repeat_with(|| rng.fork()).take(n.into()), +/// |i, party, rng| protocol_of_random_generation(rng, party, i, n), +/// ) +/// .await +/// // unwrap `Result`s +/// .expect_success() +/// // check that all parties produced the same response +/// .expect_same(); +/// +/// println!("Output randomness: {}", hex::encode(output)); +/// # } +/// ``` +pub async fn run_with_setup( + setups: impl IntoIterator, + mut party_start: impl FnMut(u16, MpcParty>, S) -> F, +) -> SimResult +where + M: Clone + Send + Unpin + 'static, + F: Future, +{ + let mut network = Network::::new(); + + let mut output = alloc::vec![]; + for (setup, i) in setups.into_iter().zip(0u16..) { + output.push({ + let party = network.add_party(); + party_start(i, party, setup) + }); + } + + let result = futures_util::future::join_all(output).await; + SimResult(result) +} diff --git a/round-based/src/simulation/sim_sync.rs b/round-based/src/simulation/sim_sync.rs index d64e8af..d283d23 100644 --- a/round-based/src/simulation/sim_sync.rs +++ b/round-based/src/simulation/sim_sync.rs @@ -2,6 +2,8 @@ use alloc::{boxed::Box, collections::VecDeque, vec, vec::Vec}; use crate::{state_machine::ProceedResult, Incoming, MessageDestination, MessageType, Outgoing}; +use super::SimResult; + /// Simulates MPC protocol with parties defined as [state machines](crate::state_machine) pub struct SimulationSync<'a, O, M> { parties: Vec>, @@ -87,7 +89,7 @@ where } /// Carries out the simulation - pub fn run(mut self) -> Result, SimulationSyncError> { + pub fn run(mut self) -> Result, SimulationSyncError> { let mut messages_queue = MessagesQueue::new(self.parties.len()); let mut parties_left = self.parties.len(); @@ -138,16 +140,17 @@ where } } - Ok(self - .parties - .into_iter() - .map(|party| match party { - Party::Active { .. } => { - unreachable!("there must be no active parties when `parties_left == 0`") - } - Party::Finished(out) => out, - }) - .collect()) + Ok(SimResult( + self.parties + .into_iter() + .map(|party| match party { + Party::Active { .. } => { + unreachable!("there must be no active parties when `parties_left == 0`") + } + Party::Finished(out) => out, + }) + .collect(), + )) } } From 1bd96007d48c0fcd3af24a74a5f0ceef521098a3 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 26 Nov 2024 13:07:06 +0100 Subject: [PATCH 02/22] Bump version & update changelog Signed-off-by: Denis Varlakov --- Cargo.lock | 2 +- round-based/CHANGELOG.md | 5 +++++ round-based/Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70e77d5..06c4627 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -469,7 +469,7 @@ dependencies = [ [[package]] name = "round-based" -version = "0.3.2" +version = "0.4.0" dependencies = [ "displaydoc", "futures", diff --git a/round-based/CHANGELOG.md b/round-based/CHANGELOG.md index 266bcf3..0e0eb38 100644 --- a/round-based/CHANGELOG.md +++ b/round-based/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.4.0 +* Improve ergonomics of protocol simulation, which is used for writing tests [#14] + +[#14]: https://github.com/LFDT-Lockness/round-based/pull/14 + ## v0.3.2 * Update links in crate settings, update readme [#11] diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index 280aa52..ca4a8c3 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "round-based" -version = "0.3.2" +version = "0.4.0" edition = "2021" license = "MIT OR Apache-2.0" description = "Driver for MPC protocols" From 429560dc791795cdafa9388aab42d19cfa3c5ba5 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 26 Nov 2024 13:09:07 +0100 Subject: [PATCH 03/22] clippy fix Signed-off-by: Denis Varlakov --- round-based/src/simulation/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index f0bc3e4..fa41932 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -114,16 +114,16 @@ where /// /// Panics if simulation contained zero parties. pub fn expect_same(mut self) -> T { - let Some(first) = self.0.get(0) else { + let Some(first) = self.0.first() else { panic!("simulation contained zero parties"); }; if !self.0[1..].iter().all(|i| i == first) { - let mut msg = alloc::format!( + let mut msg = alloc::string::String::from( "Simulation output didn't match expectations.\n\ Expected: all parties return the same output\n\ Actual : some of the parties returned a different output\n\ - Outputs :\n" + Outputs :\n", ); for (i, res) in self.0.iter().enumerate() { From 89ae00153416ebbfd86e4f52f4f8d159ef11d184 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 26 Nov 2024 13:12:36 +0100 Subject: [PATCH 04/22] Shorten function names Signed-off-by: Denis Varlakov --- examples/random-generation-protocol/src/lib.rs | 8 ++++---- round-based/src/simulation/mod.rs | 8 ++++---- round-based/src/simulation/sim_async.rs | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/random-generation-protocol/src/lib.rs b/examples/random-generation-protocol/src/lib.rs index 3e6a3b5..694bd2c 100644 --- a/examples/random-generation-protocol/src/lib.rs +++ b/examples/random-generation-protocol/src/lib.rs @@ -189,8 +189,8 @@ mod tests { |i, party, rng| protocol_of_random_generation(party, i, n, rng), ) .await - .expect_success() - .expect_same(); + .expect_ok() + .expect_eq(); std::println!("Output randomness: {}", hex::encode(randomness)); } @@ -204,8 +204,8 @@ mod tests { }) .run() .unwrap() - .expect_success() - .expect_same(); + .expect_ok() + .expect_eq(); } // Emulate the protocol using the state machine interface diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index fa41932..b67841e 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -43,9 +43,9 @@ //! ) //! .await //! // unwrap `Result`s -//! .expect_success() +//! .expect_ok() //! // check that all parties produced the same response -//! .expect_same(); +//! .expect_eq(); //! //! println!("Output randomness: {}", hex::encode(output)); //! # } @@ -71,7 +71,7 @@ where /// Panics if at least one of the parties returned `Err(_)`. In this case, /// a verbose error message will shown specifying which of the parties returned /// an error. - pub fn expect_success(self) -> SimResult { + pub fn expect_ok(self) -> SimResult { let mut oks = alloc::vec::Vec::with_capacity(self.0.len()); let mut errs = alloc::vec::Vec::with_capacity(self.0.len()); @@ -113,7 +113,7 @@ where /// panics with a verbose error message. /// /// Panics if simulation contained zero parties. - pub fn expect_same(mut self) -> T { + pub fn expect_eq(mut self) -> T { let Some(first) = self.0.first() else { panic!("simulation contained zero parties"); }; diff --git a/round-based/src/simulation/sim_async.rs b/round-based/src/simulation/sim_async.rs index b5ca781..bcc6ed6 100644 --- a/round-based/src/simulation/sim_async.rs +++ b/round-based/src/simulation/sim_async.rs @@ -216,9 +216,9 @@ impl NextMessageId { /// ) /// .await /// // unwrap `Result`s -/// .expect_success() +/// .expect_ok() /// // check that all parties produced the same response -/// .expect_same(); +/// .expect_eq(); /// /// println!("Output randomness: {}", hex::encode(output)); /// # } @@ -275,9 +275,9 @@ where /// ) /// .await /// // unwrap `Result`s -/// .expect_success() +/// .expect_ok() /// // check that all parties produced the same response -/// .expect_same(); +/// .expect_eq(); /// /// println!("Output randomness: {}", hex::encode(output)); /// # } From 2ce66fe07ba7e9a77ee2ad25e8aa9d9da10d0535 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 26 Nov 2024 17:24:26 +0100 Subject: [PATCH 05/22] Use state machine based simulation by default Signed-off-by: Denis Varlakov --- .../random-generation-protocol/Cargo.toml | 2 +- .../random-generation-protocol/src/lib.rs | 24 +- round-based/Cargo.toml | 4 +- round-based/src/lib.rs | 6 +- .../simulation/{sim_async.rs => async_env.rs} | 115 +++++- round-based/src/simulation/mod.rs | 363 +++++++++++++++++- round-based/src/simulation/sim_sync.rs | 224 ----------- 7 files changed, 481 insertions(+), 257 deletions(-) rename round-based/src/simulation/{sim_async.rs => async_env.rs} (67%) diff --git a/examples/random-generation-protocol/Cargo.toml b/examples/random-generation-protocol/Cargo.toml index 90cffda..b8e2e3c 100644 --- a/examples/random-generation-protocol/Cargo.toml +++ b/examples/random-generation-protocol/Cargo.toml @@ -20,7 +20,7 @@ thiserror = { version = "1", optional = true } generic-array = { version = "0.14", features = ["serde"] } [dev-dependencies] -round-based = { path = "../../round-based", features = ["derive", "dev", "state-machine"] } +round-based = { path = "../../round-based", features = ["derive", "sim", "state-machine"] } tokio = { version = "1.15", features = ["macros", "rt"] } hex = "0.4" rand_dev = "0.1" diff --git a/examples/random-generation-protocol/src/lib.rs b/examples/random-generation-protocol/src/lib.rs index 694bd2c..7f1b125 100644 --- a/examples/random-generation-protocol/src/lib.rs +++ b/examples/random-generation-protocol/src/lib.rs @@ -178,8 +178,8 @@ mod tests { use super::protocol_of_random_generation; - #[tokio::test] - async fn simulation_async() { + #[test] + fn simulation() { let mut rng = rand_dev::DevRng::new(); let n: u16 = 5; @@ -188,24 +188,28 @@ mod tests { core::iter::repeat_with(|| rng.fork()).take(n.into()), |i, party, rng| protocol_of_random_generation(party, i, n, rng), ) - .await + .unwrap() .expect_ok() .expect_eq(); std::println!("Output randomness: {}", hex::encode(randomness)); } - #[test] - fn simulation_sync() { + #[tokio::test] + async fn simulation_async() { let mut rng = rand_dev::DevRng::new(); - round_based::simulation::SimulationSync::from_async_fn(5, |i, party| { - protocol_of_random_generation(party, i, 5, rng.fork()) - }) - .run() - .unwrap() + let n: u16 = 5; + + let randomness = round_based::simulation::async_env::run_with_setup( + core::iter::repeat_with(|| rng.fork()).take(n.into()), + |i, party, rng| protocol_of_random_generation(party, i, n, rng), + ) + .await .expect_ok() .expect_eq(); + + std::println!("Output randomness: {}", hex::encode(randomness)); } // Emulate the protocol using the state machine interface diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index ca4a8c3..f6589c2 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -40,7 +40,9 @@ rand_dev = "0.1" [features] default = ["std"] state-machine = [] -dev = ["std", "tokio/sync", "tokio-stream", "futures-util/alloc"] +# dev = ["std", "tokio/sync", "tokio-stream", "futures-util/alloc"] +sim = ["std", "state-machine"] +sim-async = ["std", "tokio/sync", "tokio-stream", "futures-util/alloc"] derive = ["round-based-derive"] runtime-tokio = ["tokio"] std = ["thiserror"] diff --git a/round-based/src/lib.rs b/round-based/src/lib.rs index e696550..06c9b34 100644 --- a/round-based/src/lib.rs +++ b/round-based/src/lib.rs @@ -53,11 +53,13 @@ extern crate alloc; #[doc(no_inline)] pub use futures_util::{Sink, SinkExt, Stream, StreamExt}; -/// Fixes false-positive of `unused_crate_dependencies` lint that only occure in the tests +/// Fixes false-positive of `unused_crate_dependencies` lint that only occur in the tests #[cfg(test)] mod false_positives { use futures as _; use trybuild as _; + + use {hex as _, rand as _, rand_dev as _}; } mod delivery; @@ -67,7 +69,7 @@ pub mod runtime; #[cfg(feature = "state-machine")] pub mod state_machine; -#[cfg(feature = "dev")] +#[cfg(feature = "sim")] pub mod simulation; pub use self::delivery::*; diff --git a/round-based/src/simulation/sim_async.rs b/round-based/src/simulation/async_env.rs similarity index 67% rename from round-based/src/simulation/sim_async.rs rename to round-based/src/simulation/async_env.rs index bcc6ed6..5b9336b 100644 --- a/round-based/src/simulation/sim_async.rs +++ b/round-based/src/simulation/async_env.rs @@ -1,3 +1,68 @@ +//! Fully async simulation +//! +//! Simulation provided in a [parent module](super) should be used in most cases. It works +//! by converting all parties (defined as async functions) into [state machines](crate::state_machine), +//! which has certain limitations. In particular, the protocol cannot await on any futures that +//! aren't provided by [`MpcParty`](crate::MpcParty), for instance, awaiting on the timer will +//! cause a simulation error. +//! +//! We suggest to avoid awaiting on the futures that aren't provided by `MpcParty` in the MPC protocol +//! implementation as it likely makes it runtime-dependent. However, if you do ultimately need to +//! do that, then you can't use regular simulation for the tests. +//! +//! This module provides fully async simulation built for tokio runtime, so the protocol can await +//! on any futures supported by the tokio. +//! +//! ## Limitations +//! To implement simulated [network](Network), we used [`tokio::sync::broadcast`] channels, which +//! have internal buffer of stored messages, and once simulated network receives more messages than +//! internal buffer can fit, some of the parties will not receive some of the messages, which will +//! lead to execution error. +//! +//! By default, internal buffer is preallocated to fit 500 messages, which should be more than +//! sufficient for simulating protocols with small amount of parties (say, < 10). +//! +//! If you need to preallocate bigger buffer, use [`Network::with_capacity`]. +//! +//! ## Example +//! Entry point to the simulation are [`run`] and [`run_with_setup`] functions +//! +//! ```rust,no_run +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! use round_based::{Mpc, PartyIndex}; +//! +//! # type Result = std::result::Result; +//! # type Randomness = [u8; 32]; +//! # type Msg = (); +//! // Any MPC protocol you want to test +//! pub async fn protocol_of_random_generation( +//! party: M, +//! i: PartyIndex, +//! n: u16 +//! ) -> Result +//! where +//! M: Mpc +//! { +//! // ... +//! # todo!() +//! } +//! +//! let n = 3; +//! +//! let output = round_based::simulation::async_env::run( +//! n, +//! |i, party| protocol_of_random_generation(party, i, n), +//! ) +//! .await +//! // unwrap `Result`s +//! .expect_ok() +//! // check that all parties produced the same response +//! .expect_eq(); +//! +//! println!("Output randomness: {}", hex::encode(output)); +//! # } +//! ``` use alloc::sync::Arc; use core::{ future::Future, @@ -16,6 +81,8 @@ use crate::{MessageDestination, MessageType, MpcParty, MsgId, PartyIndex}; use super::SimResult; +const DEFAULT_CAPACITY: usize = 500; + /// Simulated async network pub struct Network { channel: broadcast::Sender>>, @@ -210,7 +277,7 @@ impl NextMessageId { /// /// let n = 3; /// -/// let output = round_based::simulation::run( +/// let output = round_based::simulation::async_env::run( /// n, /// |i, party| protocol_of_random_generation(party, i, n), /// ) @@ -224,6 +291,23 @@ impl NextMessageId { /// # } /// ``` pub async fn run( + n: u16, + party_start: impl FnMut(u16, MpcParty>) -> F, +) -> SimResult +where + M: Clone + Send + Unpin + 'static, + F: Future, +{ + run_with_capacity(DEFAULT_CAPACITY, n, party_start).await +} + +/// Simulates execution of the protocol +/// +/// Same as [`run`] but also takes a capacity of internal buffer to be used +/// within simulated network. Size of internal buffer should fit total amount of the +/// messages sent by all participants during the whole protocol execution. +pub async fn run_with_capacity( + capacity: usize, n: u16, mut party_start: impl FnMut(u16, MpcParty>) -> F, ) -> SimResult @@ -231,9 +315,11 @@ where M: Clone + Send + Unpin + 'static, F: Future, { - run_with_setup::<(), M, F>(core::iter::repeat(()).take(n.into()), |i, party, ()| { - party_start(i, party) - }) + run_with_capacity_and_setup( + capacity, + core::iter::repeat(()).take(n.into()), + |i, party, ()| party_start(i, party), + ) .await } @@ -269,7 +355,7 @@ where /// /// let mut rng = rand_dev::DevRng::new(); /// let n = 3; -/// let output = round_based::simulation::run_with_setup( +/// let output = round_based::simulation::async_env::run_with_setup( /// core::iter::repeat_with(|| rng.fork()).take(n.into()), /// |i, party, rng| protocol_of_random_generation(rng, party, i, n), /// ) @@ -283,6 +369,23 @@ where /// # } /// ``` pub async fn run_with_setup( + setups: impl IntoIterator, + party_start: impl FnMut(u16, MpcParty>, S) -> F, +) -> SimResult +where + M: Clone + Send + Unpin + 'static, + F: Future, +{ + run_with_capacity_and_setup::(DEFAULT_CAPACITY, setups, party_start).await +} + +/// Simulates execution of the protocol +/// +/// Same as [`run_with_setup`] but also takes a capacity of internal buffer to be used +/// within simulated network. Size of internal buffer should fit total amount of the +/// messages sent by all participants during the whole protocol execution. +pub async fn run_with_capacity_and_setup( + capacity: usize, setups: impl IntoIterator, mut party_start: impl FnMut(u16, MpcParty>, S) -> F, ) -> SimResult @@ -290,7 +393,7 @@ where M: Clone + Send + Unpin + 'static, F: Future, { - let mut network = Network::::new(); + let mut network = Network::::with_capacity(capacity); let mut output = alloc::vec![]; for (setup, i) in setups.into_iter().zip(0u16..) { diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index b67841e..582ee50 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -51,16 +51,16 @@ //! # } //! ``` -mod sim_async; -#[cfg(feature = "state-machine")] -mod sim_sync; +use alloc::{boxed::Box, collections::VecDeque, vec::Vec}; +use core::future::Future; -pub use sim_async::*; -#[cfg(feature = "state-machine")] -pub use sim_sync::*; +use crate::{state_machine::ProceedResult, Incoming, MessageDestination, MessageType, Outgoing}; + +#[cfg(feature = "sim-async")] +pub mod async_env; /// Result of the simulation -pub struct SimResult(pub alloc::vec::Vec); +pub struct SimResult(pub Vec); impl SimResult> where @@ -72,8 +72,8 @@ where /// a verbose error message will shown specifying which of the parties returned /// an error. pub fn expect_ok(self) -> SimResult { - let mut oks = alloc::vec::Vec::with_capacity(self.0.len()); - let mut errs = alloc::vec::Vec::with_capacity(self.0.len()); + let mut oks = Vec::with_capacity(self.0.len()); + let mut errs = Vec::with_capacity(self.0.len()); for (res, i) in self.0.into_iter().zip(0u16..) { match res { @@ -141,19 +141,356 @@ where impl SimResult { /// Deconstructs the simulation result returning inner list of results - pub fn into_vec(self) -> alloc::vec::Vec { + pub fn into_vec(self) -> Vec { self.0 } } -impl From> for SimResult { - fn from(list: alloc::vec::Vec) -> Self { +impl From> for SimResult { + fn from(list: Vec) -> Self { Self(list) } } -impl From> for alloc::vec::Vec { +impl From> for Vec { fn from(res: SimResult) -> Self { res.0 } } + +/// Simulates MPC protocol with parties defined as [state machines](crate::state_machine) +pub struct Simulation<'a, O, M> { + parties: Vec>, +} + +enum Party<'a, O, M> { + Active { + party: Box + 'a>, + wants_one_more_msg: bool, + }, + Finished(O), +} + +impl<'a, O, M> Simulation<'a, O, M> +where + M: Clone + 'static, +{ + /// Creates empty simulation containing no parties + /// + /// New parties can be added via [`.add_party()`](Self::add_party) + pub fn empty() -> Self { + Self { + parties: Vec::new(), + } + } + + /// Constructs empty simulation containing no parties, with allocated memory that can fit up to `n` parties without re-allocations + pub fn with_capacity(n: u16) -> Self { + Self { + parties: Vec::with_capacity(n.into()), + } + } + + /// Constructs a simulation with `n` parties from async function that defines the protocol + /// + /// Each party has index `0 <= i < n` and instantiated via provided `init` function + pub fn from_async_fn( + n: u16, + mut init: impl FnMut(u16, crate::state_machine::MpcParty) -> F, + ) -> Self + where + F: core::future::Future + 'a, + { + let mut sim = Self::with_capacity(n); + for i in 0..n { + let party = crate::state_machine::wrap_protocol(|party| init(i, party)); + sim.add_party(party) + } + sim + } + + /// Construct a simulation with `n` parties from `init` function that constructs state machine for each party + /// + /// Each party has index `0 <= i < n` and instantiated via provided `init` function + pub fn from_fn(n: u16, mut init: impl FnMut(u16) -> S) -> Self + where + S: crate::state_machine::StateMachine + 'a, + { + let mut sim = Self::with_capacity(n); + for i in 0..n { + sim.add_party(init(i)); + } + sim + } + + /// Adds new party into the protocol + /// + /// New party will be assigned index `i = n - 1` where `n` is amount of parties in the + /// simulation after this party was added. + pub fn add_party( + &mut self, + party: impl crate::state_machine::StateMachine + 'a, + ) { + self.parties.push(Party::Active { + party: Box::new(party), + wants_one_more_msg: false, + }) + } + + /// Returns amount of parties in the simulation + pub fn parties_amount(&self) -> usize { + self.parties.len() + } + + /// Carries out the simulation + pub fn run(mut self) -> Result, SimError> { + let mut messages_queue = MessagesQueue::new(self.parties.len()); + let mut parties_left = self.parties.len(); + + while parties_left > 0 { + 'next_party: for (i, party_state) in (0..).zip(&mut self.parties) { + 'this_party: loop { + let Party::Active { + party, + wants_one_more_msg, + } = party_state + else { + continue 'next_party; + }; + + if *wants_one_more_msg { + if let Some(message) = messages_queue.recv_next_msg(i) { + party + .received_msg(message) + .map_err(|_| Reason::SaveIncomingMsg)?; + *wants_one_more_msg = false; + } else { + continue 'next_party; + } + } + + match party.proceed() { + ProceedResult::SendMsg(msg) => { + messages_queue.send_message(i, msg)?; + continue 'this_party; + } + ProceedResult::NeedsOneMoreMessage => { + *wants_one_more_msg = true; + continue 'this_party; + } + ProceedResult::Output(out) => { + *party_state = Party::Finished(out); + parties_left -= 1; + continue 'next_party; + } + ProceedResult::Yielded => { + continue 'this_party; + } + ProceedResult::Error(err) => { + return Err(Reason::ExecutionError(err).into()); + } + } + } + } + } + + Ok(SimResult( + self.parties + .into_iter() + .map(|party| match party { + Party::Active { .. } => { + unreachable!("there must be no active parties when `parties_left == 0`") + } + Party::Finished(out) => out, + }) + .collect(), + )) + } +} + +/// Error returned by [`SimulationSync::run`] +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct SimError(#[from] Reason); + +#[derive(Debug, thiserror::Error)] +enum Reason { + #[error("save incoming message")] + SaveIncomingMsg, + #[error("execution error")] + ExecutionError(#[source] crate::state_machine::ExecutionError), + #[error("party #{sender} tried to send a message to non existing party #{recipient}")] + UnknownRecipient { sender: u16, recipient: u16 }, +} + +struct MessagesQueue { + queue: Vec>>, + next_id: u64, +} + +impl MessagesQueue { + fn new(n: usize) -> Self { + Self { + queue: alloc::vec![VecDeque::new(); n], + next_id: 0, + } + } + + fn send_message(&mut self, sender: u16, msg: Outgoing) -> Result<(), SimError> { + match msg.recipient { + MessageDestination::AllParties => { + let mut msg_ids = self.next_id..; + for (destination, msg_id) in (0..) + .zip(&mut self.queue) + .filter(|(recipient_index, _)| *recipient_index != sender) + .map(|(_, msg)| msg) + .zip(msg_ids.by_ref()) + { + destination.push_back(Incoming { + id: msg_id, + sender, + msg_type: MessageType::Broadcast, + msg: msg.msg.clone(), + }) + } + self.next_id = msg_ids.next().unwrap(); + } + MessageDestination::OneParty(destination) => { + let next_id = self.next_id; + self.next_id += 1; + + self.queue + .get_mut(usize::from(destination)) + .ok_or(Reason::UnknownRecipient { + sender, + recipient: destination, + })? + .push_back(Incoming { + id: next_id, + sender, + msg_type: MessageType::P2P, + msg: msg.msg, + }) + } + } + + Ok(()) + } + + fn recv_next_msg(&mut self, recipient: u16) -> Option> { + self.queue[usize::from(recipient)].pop_front() + } +} + +/// Simulates execution of the protocol +/// +/// Takes amount of participants, and a function that carries out the protocol for +/// one party. The function takes as input: index of the party, and [`MpcParty`] +/// that can be used to communicate with others. +/// +/// ## Example +/// ```rust,no_run +/// use round_based::{Mpc, PartyIndex}; +/// +/// # type Result = std::result::Result; +/// # type Randomness = [u8; 32]; +/// # type Msg = (); +/// // Any MPC protocol you want to test +/// pub async fn protocol_of_random_generation( +/// party: M, +/// i: PartyIndex, +/// n: u16 +/// ) -> Result +/// where +/// M: Mpc +/// { +/// // ... +/// # todo!() +/// } +/// +/// let n = 3; +/// +/// let output = round_based::simulation::async_env::run( +/// n, +/// |i, party| protocol_of_random_generation(party, i, n), +/// ) +/// .await +/// // unwrap `Result`s +/// .expect_ok() +/// // check that all parties produced the same response +/// .expect_eq(); +/// +/// println!("Output randomness: {}", hex::encode(output)); +/// ``` +pub fn run( + n: u16, + mut party_start: impl FnMut(u16, crate::state_machine::MpcParty) -> F, +) -> Result, SimError> +where + M: Clone + 'static, + F: Future, +{ + run_with_setup(core::iter::repeat(()).take(n.into()), |i, party, ()| { + party_start(i, party) + }) +} + +/// Simulates execution of the protocol +/// +/// Similar to [`run`], but allows some setup to be provided to the protocol execution +/// function. +/// +/// Simulation will have as many parties as `setups` iterator yields +/// +/// ## Example +/// ```rust,no_run +/// use round_based::{Mpc, PartyIndex}; +/// +/// # type Result = std::result::Result; +/// # type Randomness = [u8; 32]; +/// # type Msg = (); +/// // Any MPC protocol you want to test +/// pub async fn protocol_of_random_generation( +/// rng: impl rand::RngCore, +/// party: M, +/// i: PartyIndex, +/// n: u16 +/// ) -> Result +/// where +/// M: Mpc +/// { +/// // ... +/// # todo!() +/// } +/// +/// let mut rng = rand_dev::DevRng::new(); +/// let n = 3; +/// let output = round_based::simulation::run_with_setup( +/// core::iter::repeat_with(|| rng.fork()).take(n.into()), +/// |i, party, rng| protocol_of_random_generation(rng, party, i, n), +/// ) +/// .unwrap() +/// // unwrap `Result`s +/// .expect_ok() +/// // check that all parties produced the same response +/// .expect_eq(); +/// +/// println!("Output randomness: {}", hex::encode(output)); +/// ``` +pub fn run_with_setup( + setups: impl IntoIterator, + mut party_start: impl FnMut(u16, crate::state_machine::MpcParty, S) -> F, +) -> Result, SimError> +where + M: Clone + 'static, + F: Future, +{ + let mut sim = Simulation::empty(); + + for (setup, i) in setups.into_iter().zip(0u16..) { + let party = crate::state_machine::wrap_protocol(|party| party_start(i, party, setup)); + sim.add_party(party); + } + + sim.run() +} diff --git a/round-based/src/simulation/sim_sync.rs b/round-based/src/simulation/sim_sync.rs index d283d23..775fc59 100644 --- a/round-based/src/simulation/sim_sync.rs +++ b/round-based/src/simulation/sim_sync.rs @@ -3,227 +3,3 @@ use alloc::{boxed::Box, collections::VecDeque, vec, vec::Vec}; use crate::{state_machine::ProceedResult, Incoming, MessageDestination, MessageType, Outgoing}; use super::SimResult; - -/// Simulates MPC protocol with parties defined as [state machines](crate::state_machine) -pub struct SimulationSync<'a, O, M> { - parties: Vec>, -} - -enum Party<'a, O, M> { - Active { - party: Box + 'a>, - wants_one_more_msg: bool, - }, - Finished(O), -} - -impl<'a, O, M> SimulationSync<'a, O, M> -where - M: Clone + 'static, -{ - /// Creates empty simulation containing no parties - /// - /// New parties can be added via [`.add_party()`](Self::add_party) - pub fn empty() -> Self { - Self { - parties: Vec::new(), - } - } - - /// Constructs empty simulation containing no parties, with allocated memory that can fit up to `n` parties without re-allocations - pub fn with_capacity(n: u16) -> Self { - Self { - parties: Vec::with_capacity(n.into()), - } - } - - /// Constructs a simulation with `n` parties from async function that defines the protocol - /// - /// Each party has index `0 <= i < n` and instantiated via provided `init` function - pub fn from_async_fn( - n: u16, - mut init: impl FnMut(u16, crate::state_machine::MpcParty) -> F, - ) -> Self - where - F: core::future::Future + 'a, - { - let mut sim = Self::with_capacity(n); - for i in 0..n { - let party = crate::state_machine::wrap_protocol(|party| init(i, party)); - sim.add_party(party) - } - sim - } - - /// Construct a simulation with `n` parties from `init` function that constructs state machine for each party - /// - /// Each party has index `0 <= i < n` and instantiated via provided `init` function - pub fn from_fn(n: u16, mut init: impl FnMut(u16) -> S) -> Self - where - S: crate::state_machine::StateMachine + 'a, - { - let mut sim = Self::with_capacity(n); - for i in 0..n { - sim.add_party(init(i)); - } - sim - } - - /// Adds new party into the protocol - /// - /// New party will be assigned index `i = n - 1` where `n` is amount of parties in the - /// simulation after this party was added. - pub fn add_party( - &mut self, - party: impl crate::state_machine::StateMachine + 'a, - ) { - self.parties.push(Party::Active { - party: Box::new(party), - wants_one_more_msg: false, - }) - } - - /// Returns amount of parties in the simulation - pub fn parties_amount(&self) -> usize { - self.parties.len() - } - - /// Carries out the simulation - pub fn run(mut self) -> Result, SimulationSyncError> { - let mut messages_queue = MessagesQueue::new(self.parties.len()); - let mut parties_left = self.parties.len(); - - while parties_left > 0 { - 'next_party: for (i, party_state) in (0..).zip(&mut self.parties) { - 'this_party: loop { - let Party::Active { - party, - wants_one_more_msg, - } = party_state - else { - continue 'next_party; - }; - - if *wants_one_more_msg { - if let Some(message) = messages_queue.recv_next_msg(i) { - party - .received_msg(message) - .map_err(|_| Reason::SaveIncomingMsg)?; - *wants_one_more_msg = false; - } else { - continue 'next_party; - } - } - - match party.proceed() { - ProceedResult::SendMsg(msg) => { - messages_queue.send_message(i, msg)?; - continue 'this_party; - } - ProceedResult::NeedsOneMoreMessage => { - *wants_one_more_msg = true; - continue 'this_party; - } - ProceedResult::Output(out) => { - *party_state = Party::Finished(out); - parties_left -= 1; - continue 'next_party; - } - ProceedResult::Yielded => { - continue 'this_party; - } - ProceedResult::Error(err) => { - return Err(Reason::ExecutionError(err).into()); - } - } - } - } - } - - Ok(SimResult( - self.parties - .into_iter() - .map(|party| match party { - Party::Active { .. } => { - unreachable!("there must be no active parties when `parties_left == 0`") - } - Party::Finished(out) => out, - }) - .collect(), - )) - } -} - -/// Error returned by [`SimulationSync::run`] -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct SimulationSyncError(#[from] Reason); - -#[derive(Debug, thiserror::Error)] -enum Reason { - #[error("save incoming message")] - SaveIncomingMsg, - #[error("execution error")] - ExecutionError(#[source] crate::state_machine::ExecutionError), - #[error("party #{sender} tried to send a message to non existing party #{recipient}")] - UnknownRecipient { sender: u16, recipient: u16 }, -} - -struct MessagesQueue { - queue: Vec>>, - next_id: u64, -} - -impl MessagesQueue { - fn new(n: usize) -> Self { - Self { - queue: vec![VecDeque::new(); n], - next_id: 0, - } - } - - fn send_message(&mut self, sender: u16, msg: Outgoing) -> Result<(), SimulationSyncError> { - match msg.recipient { - MessageDestination::AllParties => { - let mut msg_ids = self.next_id..; - for (destination, msg_id) in (0..) - .zip(&mut self.queue) - .filter(|(recipient_index, _)| *recipient_index != sender) - .map(|(_, msg)| msg) - .zip(msg_ids.by_ref()) - { - destination.push_back(Incoming { - id: msg_id, - sender, - msg_type: MessageType::Broadcast, - msg: msg.msg.clone(), - }) - } - self.next_id = msg_ids.next().unwrap(); - } - MessageDestination::OneParty(destination) => { - let next_id = self.next_id; - self.next_id += 1; - - self.queue - .get_mut(usize::from(destination)) - .ok_or(Reason::UnknownRecipient { - sender, - recipient: destination, - })? - .push_back(Incoming { - id: next_id, - sender, - msg_type: MessageType::P2P, - msg: msg.msg, - }) - } - } - - Ok(()) - } - - fn recv_next_msg(&mut self, recipient: u16) -> Option> { - self.queue[usize::from(recipient)].pop_front() - } -} From e662e7142eb3c5d96f3202c6cc7f853a2db0486b Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 26 Nov 2024 17:36:29 +0100 Subject: [PATCH 06/22] Elaborate on changes, and add migration guidelines in changelog Signed-off-by: Denis Varlakov --- round-based/CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/round-based/CHANGELOG.md b/round-based/CHANGELOG.md index 0e0eb38..eb88129 100644 --- a/round-based/CHANGELOG.md +++ b/round-based/CHANGELOG.md @@ -1,5 +1,24 @@ ## v0.4.0 * Improve ergonomics of protocol simulation, which is used for writing tests [#14] + * Remove `dev` feature, it's replaced with `sim` and `sim-async` + * `round_based::Simulation` is renamed and moved to `round_based::simulation::async_env::Network` + * Other async simulated network related types are moved to `round_based::simulation::async_env` + * Added convenient `round_based::simulation::{run, run_with_setup}` which make simulation very ergonomic + * Simulation outputs `round_based::simulation::SimResult`, which has convenient most-common methods: + * `.expect_ok()` that unwraps all results, and if any party returned an error, panics with a verbose + error message + * `.expect_eq()` that checks that all outputs are equally the same + * When `sim-async` feature is enabled, you can use `round_based::simulation::async_env::{run, run_with_setup, ...}`, + but typically you don't want to use it + +Migration guidelines: +* Replace `dev` feature with `sim` +* Instead of using `round_based::simulation::Simulation` from previous version, use + `round_based::simulation::{run, run_with_setup}` +* Take advantage of `SimResult::{expect_ok, expect_eq}` to reduce amount of the code + in your tests + +Rather than simulation, there's no breaking changes in this release. [#14]: https://github.com/LFDT-Lockness/round-based/pull/14 From aab8446b59f46014d04e79289eda6ca3dc76e1b7 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 27 Nov 2024 15:48:31 +0100 Subject: [PATCH 07/22] Update docs Signed-off-by: Denis Varlakov --- round-based/src/simulation/async_env.rs | 3 +-- round-based/src/simulation/mod.rs | 30 +++++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/round-based/src/simulation/async_env.rs b/round-based/src/simulation/async_env.rs index 5b9336b..6f28282 100644 --- a/round-based/src/simulation/async_env.rs +++ b/round-based/src/simulation/async_env.rs @@ -3,8 +3,7 @@ //! Simulation provided in a [parent module](super) should be used in most cases. It works //! by converting all parties (defined as async functions) into [state machines](crate::state_machine), //! which has certain limitations. In particular, the protocol cannot await on any futures that -//! aren't provided by [`MpcParty`](crate::MpcParty), for instance, awaiting on the timer will -//! cause a simulation error. +//! aren't provided by [`MpcParty`], for instance, awaiting on the timer will cause a simulation error. //! //! We suggest to avoid awaiting on the futures that aren't provided by `MpcParty` in the MPC protocol //! implementation as it likely makes it runtime-dependent. However, if you do ultimately need to diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index 582ee50..0d44001 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -1,17 +1,33 @@ //! Multiparty protocol simulation //! //! Simulator is an essential developer tool for testing the multiparty protocol locally. -//! It covers most of the boilerplate by mocking networking. +//! It covers most of the boilerplate of emulating MPC protocol execution. //! //! The entry point is either [`run`] or [`run_with_setup`] functions. They take a protocol //! defined as an async function, provide simulated networking, carry out the simulation, //! and return the result. //! -//! If you need more control over execution, you can use [`Network`] to simulate the networking -//! and carry out the protocol manually. +//! If you need more control over execution, you can use [`Simulation`]. For instance, it allows +//! creating a simulation that has parties defined by different functions, which is helpful, for +//! instance, in simulation in presence of an adversary (e.g. one set of parties can be defined +//! with a regular function/protocol implementation, when the other set of parties may be defined +//! by other function which emulates adversary behavior). //! -//! When `state-machine` feature is enabled, [`SimulationSync`] is available which can carry out -//! protocols defined as a state machine. +//! ## Limitations +//! [`Simulation`] works by converting each party (defined as an async function) into the +//! [state machine](crate::state_machine). That should work without problems in most cases, providing +//! better UX, without requiring an async runtime (simulation is entirely sync). +//! +//! However, a protocol wrapped into a state machine cannot poll any futures except provided within +//! [`MpcParty`](crate::MpcParty) (so it can only await on sending/receiving messages and yielding). +//! For instance, if the protocol implementation makes use of tokio timers, it will result into an +//! execution error. +//! +//! In general, we do not recommend awaiting on the futures that aren't provided by `MpcParty` in +//! the MPC protocol implementation, to keep the protocol implementation runtime-agnostic. +//! +//! If you do really need to make use of unsupported futures, you can use [`async_env`] instead, +//! which provides a simulation on tokio runtime, but has its own limitations. //! //! ## Example //! ```rust,no_run @@ -308,7 +324,7 @@ where } } -/// Error returned by [`SimulationSync::run`] +/// Error indicating that simulation failed #[derive(Debug, thiserror::Error)] #[error(transparent)] pub struct SimError(#[from] Reason); @@ -385,7 +401,7 @@ impl MessagesQueue { /// Simulates execution of the protocol /// /// Takes amount of participants, and a function that carries out the protocol for -/// one party. The function takes as input: index of the party, and [`MpcParty`] +/// one party. The function takes as input: index of the party, and [`MpcParty`](crate::MpcParty) /// that can be used to communicate with others. /// /// ## Example From e564787a4481f61dfec21f7ac9ddb72eb93d5e6b Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 27 Nov 2024 15:51:20 +0100 Subject: [PATCH 08/22] Update changelog Signed-off-by: Denis Varlakov --- round-based/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/round-based/CHANGELOG.md b/round-based/CHANGELOG.md index eb88129..c7c69fc 100644 --- a/round-based/CHANGELOG.md +++ b/round-based/CHANGELOG.md @@ -10,6 +10,7 @@ * `.expect_eq()` that checks that all outputs are equally the same * When `sim-async` feature is enabled, you can use `round_based::simulation::async_env::{run, run_with_setup, ...}`, but typically you don't want to use it + * `round_based::simulation::SimulationSync` has been renamed to `round_based::simulation::Simulation` Migration guidelines: * Replace `dev` feature with `sim` @@ -18,7 +19,7 @@ Migration guidelines: * Take advantage of `SimResult::{expect_ok, expect_eq}` to reduce amount of the code in your tests -Rather than simulation, there's no breaking changes in this release. +Other than simulation, there are no breaking changes in this release. [#14]: https://github.com/LFDT-Lockness/round-based/pull/14 From e8e522ee20b1b99a2de2983a5d4d3079f3d9669d Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 27 Nov 2024 15:51:45 +0100 Subject: [PATCH 09/22] Remove redundant comment Signed-off-by: Denis Varlakov --- round-based/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index f6589c2..ac9af58 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -40,7 +40,6 @@ rand_dev = "0.1" [features] default = ["std"] state-machine = [] -# dev = ["std", "tokio/sync", "tokio-stream", "futures-util/alloc"] sim = ["std", "state-machine"] sim-async = ["std", "tokio/sync", "tokio-stream", "futures-util/alloc"] derive = ["round-based-derive"] From 1d12c78406eaa6661eda794e7edd3f45c6157e97 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 27 Nov 2024 16:19:06 +0100 Subject: [PATCH 10/22] Improve UX of `expect_eq` Signed-off-by: Denis Varlakov --- .github/workflows/rust.yml | 10 +++ round-based/src/simulation/mod.rs | 106 +++++++++++++++++++++++++++--- 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7dab06e..66ed82f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,6 +37,16 @@ jobs: - name: cargo check run: cargo check -p round-based --all-features + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: cargo test + run: cargo test --all-features + check-fmt: runs-on: ubuntu-latest steps: diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index 0d44001..befbb15 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -31,8 +31,6 @@ //! //! ## Example //! ```rust,no_run -//! # #[tokio::main(flavor = "current_thread")] -//! # async fn main() { //! use round_based::{Mpc, PartyIndex}; //! //! # type Result = std::result::Result; @@ -57,17 +55,16 @@ //! n, //! |i, party| protocol_of_random_generation(party, i, n), //! ) -//! .await +//! .unwrap() //! // unwrap `Result`s //! .expect_ok() //! // check that all parties produced the same response //! .expect_eq(); //! //! println!("Output randomness: {}", hex::encode(output)); -//! # } //! ``` -use alloc::{boxed::Box, collections::VecDeque, vec::Vec}; +use alloc::{boxed::Box, collections::VecDeque, string::ToString, vec::Vec}; use core::future::Future; use crate::{state_machine::ProceedResult, Incoming, MessageDestination, MessageType, Outgoing}; @@ -142,8 +139,36 @@ where Outputs :\n", ); - for (i, res) in self.0.iter().enumerate() { - msg += &alloc::format!("- Party {i}: {res:?}"); + let mut clusters: Vec<(&T, Vec)> = Vec::new(); + for (i, value) in self.0.iter().enumerate() { + match clusters + .iter_mut() + .find(|(cluster_value, _)| *cluster_value == value) + .map(|(_, indexes)| indexes) + { + Some(indexes) => indexes.push(i), + None => clusters.push((value, alloc::vec![i])), + } + } + + for (value, parties) in &clusters { + if parties.len() == 1 { + msg += "- Party "; + } else { + msg += "- Parties " + } + + for (i, is_first) in parties + .iter() + .zip(core::iter::once(true).chain(core::iter::repeat(false))) + { + if !is_first { + msg += ", " + } + msg += &i.to_string(); + } + + msg += &alloc::format!(": {value:?}\n"); } panic!("{msg}") @@ -426,11 +451,11 @@ impl MessagesQueue { /// /// let n = 3; /// -/// let output = round_based::simulation::async_env::run( +/// let output = round_based::simulation::run( /// n, /// |i, party| protocol_of_random_generation(party, i, n), /// ) -/// .await +/// .unwrap() /// // unwrap `Result`s /// .expect_ok() /// // check that all parties produced the same response @@ -510,3 +535,66 @@ where sim.run() } + +#[cfg(test)] +mod tests { + mod expect_eq { + use crate::simulation::SimResult; + + #[test] + fn all_eq() { + let res = SimResult::from(alloc::vec!["same string", "same string", "same string"]) + .expect_eq(); + assert_eq!(res, "same string") + } + + #[test] + #[should_panic] + fn empty_res() { + SimResult::from(alloc::vec![]).expect_eq() + } + + #[test] + #[should_panic] + fn not_eq() { + SimResult::from(alloc::vec![ + "one result", + "one result", + "another result", + "one result", + "and something else", + ]) + .expect_eq(); + } + } + + mod expect_ok { + use crate::simulation::SimResult; + + #[test] + fn all_ok() { + let res = SimResult::>::from(alloc::vec![ + Ok(0), + Ok(1), + Ok(2) + ]) + .expect_ok() + .into_vec(); + + assert_eq!(res, [0, 1, 2]); + } + + #[test] + #[should_panic] + fn not_ok() { + SimResult::from(alloc::vec![ + Ok(0), + Err("i couldn't do what you asked :("), + Ok(2), + Ok(3), + Err("sorry I was pooping, what did you want?") + ]) + .expect_ok(); + } + } +} From 687132f4e29f51b0d29405174c6e7485c303451f Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 27 Nov 2024 16:22:38 +0100 Subject: [PATCH 11/22] Clarify docs on `Simulation::from_async_fn` Signed-off-by: Denis Varlakov --- round-based/src/simulation/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index befbb15..4a8cc3e 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -235,6 +235,10 @@ where /// Constructs a simulation with `n` parties from async function that defines the protocol /// /// Each party has index `0 <= i < n` and instantiated via provided `init` function + /// + /// Async function will be converted into a [state machine](crate::state_machine). Because of that, + /// it cannot await on any futures that aren't provided by `MpcParty` (that is given as an argument + /// to this function). pub fn from_async_fn( n: u16, mut init: impl FnMut(u16, crate::state_machine::MpcParty) -> F, From 599cc6988e7e353da1b35dde085e544a5cc70402 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 28 Nov 2024 12:02:51 +0100 Subject: [PATCH 12/22] Add Simulation::add_async_party Signed-off-by: Denis Varlakov --- round-based/src/simulation/mod.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/round-based/src/simulation/mod.rs b/round-based/src/simulation/mod.rs index 4a8cc3e..735c623 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/simulation/mod.rs @@ -248,8 +248,7 @@ where { let mut sim = Self::with_capacity(n); for i in 0..n { - let party = crate::state_machine::wrap_protocol(|party| init(i, party)); - sim.add_party(party) + sim.add_async_party(|party| init(i, party)) } sim } @@ -282,6 +281,24 @@ where }) } + /// Adds new party, defined as an async function, into the protocol + /// + /// New party will be assigned index `i = n - 1` where `n` is amount of parties in the + /// simulation after this party was added. + /// + /// Async function will be converted into a [state machine](crate::state_machine). Because of that, + /// it cannot await on any futures that aren't provided by `MpcParty` (that is given as an argument + /// to this function). + pub fn add_async_party(&mut self, party: impl FnOnce(crate::state_machine::MpcParty) -> F) + where + F: core::future::Future + 'a, + { + self.parties.push(Party::Active { + party: Box::new(crate::state_machine::wrap_protocol(party)), + wants_one_more_msg: false, + }) + } + /// Returns amount of parties in the simulation pub fn parties_amount(&self) -> usize { self.parties.len() From f3b576a636d25447761d51908d88b997f96343df Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 29 Nov 2024 10:11:54 +0100 Subject: [PATCH 13/22] Rename simulation mod to `sim`, improve SimResult Signed-off-by: Denis Varlakov --- .../random-generation-protocol/src/lib.rs | 4 +-- round-based/CHANGELOG.md | 15 +++++------ round-based/src/lib.rs | 2 +- .../src/{simulation => sim}/async_env.rs | 6 ++--- round-based/src/{simulation => sim}/mod.rs | 25 +++++++++++++++---- .../src/{simulation => sim}/sim_sync.rs | 0 6 files changed, 34 insertions(+), 18 deletions(-) rename round-based/src/{simulation => sim}/async_env.rs (98%) rename round-based/src/{simulation => sim}/mod.rs (97%) rename round-based/src/{simulation => sim}/sim_sync.rs (100%) diff --git a/examples/random-generation-protocol/src/lib.rs b/examples/random-generation-protocol/src/lib.rs index 7f1b125..2fa9166 100644 --- a/examples/random-generation-protocol/src/lib.rs +++ b/examples/random-generation-protocol/src/lib.rs @@ -184,7 +184,7 @@ mod tests { let n: u16 = 5; - let randomness = round_based::simulation::run_with_setup( + let randomness = round_based::sim::run_with_setup( core::iter::repeat_with(|| rng.fork()).take(n.into()), |i, party, rng| protocol_of_random_generation(party, i, n, rng), ) @@ -201,7 +201,7 @@ mod tests { let n: u16 = 5; - let randomness = round_based::simulation::async_env::run_with_setup( + let randomness = round_based::sim::async_env::run_with_setup( core::iter::repeat_with(|| rng.fork()).take(n.into()), |i, party, rng| protocol_of_random_generation(party, i, n, rng), ) diff --git a/round-based/CHANGELOG.md b/round-based/CHANGELOG.md index c7c69fc..6948e07 100644 --- a/round-based/CHANGELOG.md +++ b/round-based/CHANGELOG.md @@ -1,16 +1,17 @@ ## v0.4.0 * Improve ergonomics of protocol simulation, which is used for writing tests [#14] * Remove `dev` feature, it's replaced with `sim` and `sim-async` - * `round_based::Simulation` is renamed and moved to `round_based::simulation::async_env::Network` - * Other async simulated network related types are moved to `round_based::simulation::async_env` - * Added convenient `round_based::simulation::{run, run_with_setup}` which make simulation very ergonomic - * Simulation outputs `round_based::simulation::SimResult`, which has convenient most-common methods: + * `round_based::simulation` module is renamed into `round_based::sim` + * `round_based::simulation::Simulation` is renamed and moved to `round_based::sim::async_env::Network` + * Other async simulated network related types are moved to `round_based::sim::async_env` + * Added convenient `round_based::sim::{run, run_with_setup}` which make simulation very ergonomic + * Simulation outputs `round_based::sim::SimResult`, which has convenient most-common methods: * `.expect_ok()` that unwraps all results, and if any party returned an error, panics with a verbose error message * `.expect_eq()` that checks that all outputs are equally the same - * When `sim-async` feature is enabled, you can use `round_based::simulation::async_env::{run, run_with_setup, ...}`, - but typically you don't want to use it - * `round_based::simulation::SimulationSync` has been renamed to `round_based::simulation::Simulation` + * When `sim-async` feature is enabled, you can use `round_based::sim::async_env::{run, run_with_setup, ...}`, + but typically you don't want to use them + * `round_based::simulation::SimulationSync` has been renamed to `round_based::sim::Simulation` Migration guidelines: * Replace `dev` feature with `sim` diff --git a/round-based/src/lib.rs b/round-based/src/lib.rs index 06c9b34..01fc783 100644 --- a/round-based/src/lib.rs +++ b/round-based/src/lib.rs @@ -70,7 +70,7 @@ pub mod runtime; pub mod state_machine; #[cfg(feature = "sim")] -pub mod simulation; +pub mod sim; pub use self::delivery::*; #[doc(no_inline)] diff --git a/round-based/src/simulation/async_env.rs b/round-based/src/sim/async_env.rs similarity index 98% rename from round-based/src/simulation/async_env.rs rename to round-based/src/sim/async_env.rs index 6f28282..8bb43df 100644 --- a/round-based/src/simulation/async_env.rs +++ b/round-based/src/sim/async_env.rs @@ -49,7 +49,7 @@ //! //! let n = 3; //! -//! let output = round_based::simulation::async_env::run( +//! let output = round_based::sim::async_env::run( //! n, //! |i, party| protocol_of_random_generation(party, i, n), //! ) @@ -276,7 +276,7 @@ impl NextMessageId { /// /// let n = 3; /// -/// let output = round_based::simulation::async_env::run( +/// let output = round_based::sim::async_env::run( /// n, /// |i, party| protocol_of_random_generation(party, i, n), /// ) @@ -354,7 +354,7 @@ where /// /// let mut rng = rand_dev::DevRng::new(); /// let n = 3; -/// let output = round_based::simulation::async_env::run_with_setup( +/// let output = round_based::sim::async_env::run_with_setup( /// core::iter::repeat_with(|| rng.fork()).take(n.into()), /// |i, party, rng| protocol_of_random_generation(rng, party, i, n), /// ) diff --git a/round-based/src/simulation/mod.rs b/round-based/src/sim/mod.rs similarity index 97% rename from round-based/src/simulation/mod.rs rename to round-based/src/sim/mod.rs index 735c623..56aa794 100644 --- a/round-based/src/simulation/mod.rs +++ b/round-based/src/sim/mod.rs @@ -51,7 +51,7 @@ //! //! let n = 3; //! -//! let output = round_based::simulation::run( +//! let output = round_based::sim::run( //! n, //! |i, party| protocol_of_random_generation(party, i, n), //! ) @@ -187,6 +187,21 @@ impl SimResult { } } +impl IntoIterator for SimResult { + type Item = T; + type IntoIter = alloc::vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl core::ops::Deref for SimResult { + type Target = [T]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl From> for SimResult { fn from(list: Vec) -> Self { Self(list) @@ -472,7 +487,7 @@ impl MessagesQueue { /// /// let n = 3; /// -/// let output = round_based::simulation::run( +/// let output = round_based::sim::run( /// n, /// |i, party| protocol_of_random_generation(party, i, n), /// ) @@ -527,7 +542,7 @@ where /// /// let mut rng = rand_dev::DevRng::new(); /// let n = 3; -/// let output = round_based::simulation::run_with_setup( +/// let output = round_based::sim::run_with_setup( /// core::iter::repeat_with(|| rng.fork()).take(n.into()), /// |i, party, rng| protocol_of_random_generation(rng, party, i, n), /// ) @@ -560,7 +575,7 @@ where #[cfg(test)] mod tests { mod expect_eq { - use crate::simulation::SimResult; + use crate::sim::SimResult; #[test] fn all_eq() { @@ -590,7 +605,7 @@ mod tests { } mod expect_ok { - use crate::simulation::SimResult; + use crate::sim::SimResult; #[test] fn all_ok() { diff --git a/round-based/src/simulation/sim_sync.rs b/round-based/src/sim/sim_sync.rs similarity index 100% rename from round-based/src/simulation/sim_sync.rs rename to round-based/src/sim/sim_sync.rs From 5213b1077f6c563d193d901bf9275a81017ec362 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 29 Nov 2024 10:13:56 +0100 Subject: [PATCH 14/22] Fix docs Signed-off-by: Denis Varlakov --- round-based/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/round-based/src/lib.rs b/round-based/src/lib.rs index 01fc783..d765bb1 100644 --- a/round-based/src/lib.rs +++ b/round-based/src/lib.rs @@ -35,7 +35,7 @@ //! //! ## Features //! -//! * `dev` enables development tools such as [protocol simulation](simulation) +//! * `dev` enables development tools such as [protocol simulation](sim) //! * `runtime-tokio` enables [tokio]-specific implementation of [async runtime](runtime) //! //! ## Join us in Discord! From cf466c770bad992bf288b3564f254de726a60a94 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 29 Nov 2024 10:26:16 +0100 Subject: [PATCH 15/22] Update docs Signed-off-by: Denis Varlakov --- README.md | 7 ++++++- round-based/Cargo.toml | 2 +- round-based/src/lib.rs | 7 ++++++- round-based/src/state_machine/mod.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8de592a..5308be4 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,12 @@ the documentation of the protocol you're using), but usually they are: ## Features -* `dev` enables development tools such as protocol simulation +* `sim` enables protocol execution simulation, see `sim` module + * `sim-async` enables protocol execution simulation with tokio runtime, see `sim::async_env` + module +* `state-machine` provides ability to carry out the protocol, defined as async function, via Sync + API, see `state_machine` module +* `derive` is needed to use `ProtocolMessage` proc macro * `runtime-tokio` enables tokio-specific implementation of async runtime ## Join us in Discord! diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index ac9af58..5abf394 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -41,7 +41,7 @@ rand_dev = "0.1" default = ["std"] state-machine = [] sim = ["std", "state-machine"] -sim-async = ["std", "tokio/sync", "tokio-stream", "futures-util/alloc"] +sim-async = ["sim", "tokio/sync", "tokio-stream", "futures-util/alloc"] derive = ["round-based-derive"] runtime-tokio = ["tokio"] std = ["thiserror"] diff --git a/round-based/src/lib.rs b/round-based/src/lib.rs index d765bb1..a035069 100644 --- a/round-based/src/lib.rs +++ b/round-based/src/lib.rs @@ -35,7 +35,12 @@ //! //! ## Features //! -//! * `dev` enables development tools such as [protocol simulation](sim) +//! * `sim` enables protocol execution simulation, see [`sim`] module +//! * `sim-async` enables protocol execution simulation with tokio runtime, see [`sim::async_env`] +//! module +//! * `state-machine` provides ability to carry out the protocol, defined as async function, via Sync +//! API, see [`state_machine`] module +//! * `derive` is needed to use [`ProtocolMessage`](macro@ProtocolMessage) proc macro //! * `runtime-tokio` enables [tokio]-specific implementation of [async runtime](runtime) //! //! ## Join us in Discord! diff --git a/round-based/src/state_machine/mod.rs b/round-based/src/state_machine/mod.rs index 2d6e683..7b12c84 100644 --- a/round-based/src/state_machine/mod.rs +++ b/round-based/src/state_machine/mod.rs @@ -4,6 +4,33 @@ //! may not be possible/desirable to have async runtime which drives the futures until completion. //! For such use-cases, we provide [`wrap_protocol`] function that wraps an MPC protocol defined as //! async function and returns the [`StateMachine`] that exposes sync API to carry out the protocol. +//! +//! ## Example +//! ```rust,no_run +//! use round_based::{Mpc, PartyIndex}; +//! +//! # type Result = std::result::Result; +//! # type Randomness = [u8; 32]; +//! # type Msg = (); +//! // Any MPC protocol +//! pub async fn protocol_of_random_generation( +//! party: M, +//! i: PartyIndex, +//! n: u16 +//! ) -> Result +//! where +//! M: Mpc +//! { +//! // ... +//! # todo!() +//! } +//! +//! let state_machine = round_based::state_machine::wrap_protocol( +//! |party| protocol_of_random_generation(party, 0, 3) +//! ); +//! // `state_machine` implements `round_based::state_machine::StateMachine` trait. +//! // Its methods can be used to advance protocol until completion. +//! ``` mod delivery; mod noop_waker; From 57375b00850e7d88bf45031f1788721061b4bf9d Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 3 Dec 2024 13:13:54 +0100 Subject: [PATCH 16/22] Add example in state machine docs Signed-off-by: Denis Varlakov --- Cargo.lock | 7 +++++ round-based/Cargo.toml | 2 ++ round-based/src/lib.rs | 1 + round-based/src/state_machine/mod.rs | 40 +++++++++++++++++++++++++--- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06c4627..014e8b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + [[package]] name = "autocfg" version = "1.2.0" @@ -471,6 +477,7 @@ dependencies = [ name = "round-based" version = "0.4.0" dependencies = [ + "anyhow", "displaydoc", "futures", "futures-util", diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index 5abf394..0637bcc 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -37,6 +37,8 @@ hex = "0.4" rand = "0.8" rand_dev = "0.1" +anyhow = "1" + [features] default = ["std"] state-machine = [] diff --git a/round-based/src/lib.rs b/round-based/src/lib.rs index a035069..d93ff63 100644 --- a/round-based/src/lib.rs +++ b/round-based/src/lib.rs @@ -61,6 +61,7 @@ pub use futures_util::{Sink, SinkExt, Stream, StreamExt}; /// Fixes false-positive of `unused_crate_dependencies` lint that only occur in the tests #[cfg(test)] mod false_positives { + use anyhow as _; use futures as _; use trybuild as _; diff --git a/round-based/src/state_machine/mod.rs b/round-based/src/state_machine/mod.rs index 7b12c84..21fd370 100644 --- a/round-based/src/state_machine/mod.rs +++ b/round-based/src/state_machine/mod.rs @@ -7,9 +7,11 @@ //! //! ## Example //! ```rust,no_run +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() -> anyhow::Result<()> { //! use round_based::{Mpc, PartyIndex}; +//! use anyhow::{Result, Error, Context as _}; //! -//! # type Result = std::result::Result; //! # type Randomness = [u8; 32]; //! # type Msg = (); //! // Any MPC protocol @@ -25,11 +27,41 @@ //! # todo!() //! } //! -//! let state_machine = round_based::state_machine::wrap_protocol( +//! // `state` implements `round_based::state_machine::StateMachine` trait. +//! // Its methods can be used to advance protocol until completion. +//! let mut state = round_based::state_machine::wrap_protocol( //! |party| protocol_of_random_generation(party, 0, 3) //! ); -//! // `state_machine` implements `round_based::state_machine::StateMachine` trait. -//! // Its methods can be used to advance protocol until completion. +//! +//! // Note: this is just an example. If you have stream/sink, you don't probably need to +//! // use the sync API +//! use futures::{Sink, Stream, SinkExt, StreamExt}; +//! async fn connect() -> Result<( +//! impl Stream>>, +//! impl Sink, Error = Error> +//! )> { +//! // ... +//! # Ok((futures_util::stream::pending(), futures_util::sink::drain().sink_map_err(|err| match err {}))) +//! } +//! let (mut incomings, mut outgoings) = connect().await?; +//! +//! use round_based::state_machine::{StateMachine as _, ProceedResult}; +//! let output = loop { +//! match state.proceed() { +//! ProceedResult::SendMsg(msg) => { +//! outgoings.send(msg).await? +//! } +//! ProceedResult::NeedsOneMoreMessage => { +//! let msg = incomings.next().await.context("unexpected eof")??; +//! state.received_msg(msg) +//! .map_err(|_| anyhow::format_err!("state machine rejected received message"))?; +//! } +//! ProceedResult::Yielded => {}, +//! ProceedResult::Output(out) => break Ok(out), +//! ProceedResult::Error(err) => break Err(err), +//! } +//! }; +//! # Ok(()) } //! ``` mod delivery; From 5d53357eed0fea4780e5049b80f95fa8b7724a78 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 3 Dec 2024 13:42:50 +0100 Subject: [PATCH 17/22] Use `core::error::Error` trait Signed-off-by: Denis Varlakov --- Cargo.lock | 41 ++++----- .../random-generation-protocol/Cargo.toml | 5 +- .../random-generation-protocol/src/lib.rs | 29 +++---- round-based/CHANGELOG.md | 5 +- round-based/Cargo.toml | 9 +- round-based/src/delivery.rs | 10 +-- round-based/src/lib.rs | 12 --- round-based/src/party.rs | 9 +- round-based/src/rounds_router/mod.rs | 84 ++++++++----------- round-based/src/rounds_router/simple_store.rs | 9 +- round-based/src/rounds_router/store.rs | 2 +- round-based/src/state_machine/delivery.rs | 11 +-- round-based/src/state_machine/mod.rs | 17 ++-- 13 files changed, 94 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 014e8b6..a86357a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -100,17 +100,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] - [[package]] name = "educe" version = "0.4.23" @@ -133,7 +122,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.90", ] [[package]] @@ -198,7 +187,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.90", ] [[package]] @@ -399,9 +388,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -460,7 +449,6 @@ dependencies = [ name = "random-generation-protocol" version = "0.1.0" dependencies = [ - "displaydoc", "generic-array", "hex", "rand", @@ -478,7 +466,6 @@ name = "round-based" version = "0.4.0" dependencies = [ "anyhow", - "displaydoc", "futures", "futures-util", "hex", @@ -545,7 +532,7 @@ checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.90", ] [[package]] @@ -601,9 +588,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -621,22 +608,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.90", ] [[package]] @@ -658,7 +645,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.90", ] [[package]] diff --git a/examples/random-generation-protocol/Cargo.toml b/examples/random-generation-protocol/Cargo.toml index b8e2e3c..ddcec5e 100644 --- a/examples/random-generation-protocol/Cargo.toml +++ b/examples/random-generation-protocol/Cargo.toml @@ -13,8 +13,7 @@ rand_core = { version = "0.6", default-features = false } sha2 = { version = "0.10", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } -displaydoc = { version = "0.2", default-features = false } -thiserror = { version = "1", optional = true } +thiserror = { version = "2", default-features = false } # We don't use it directy, but we need to enable `serde` feature generic-array = { version = "0.14", features = ["serde"] } @@ -26,5 +25,3 @@ hex = "0.4" rand_dev = "0.1" rand = "0.8" -[features] -std = ["thiserror"] diff --git a/examples/random-generation-protocol/src/lib.rs b/examples/random-generation-protocol/src/lib.rs index 2fa9166..4f30ded 100644 --- a/examples/random-generation-protocol/src/lib.rs +++ b/examples/random-generation-protocol/src/lib.rs @@ -3,13 +3,13 @@ #![no_std] #![forbid(unused_crate_dependencies, missing_docs)] -#[cfg(any(feature = "std", test))] +#[cfg(test)] extern crate std; extern crate alloc; mod _unused_deps { - // We don't use it directy, but we need to enable `serde` feature + // We don't use it directly, but we need to enable `serde` feature use generic_array as _; } @@ -132,28 +132,23 @@ where } /// Protocol error -#[derive(Debug, displaydoc::Display)] -#[cfg_attr(feature = "std", derive(thiserror::Error))] +#[derive(Debug, thiserror::Error)] pub enum Error { /// Couldn't send a message in the first round - #[displaydoc("send a message at round 1")] - Round1Send(#[cfg_attr(feature = "std", source)] SendErr), + #[error("send a message at round 1")] + Round1Send(#[source] SendErr), /// Couldn't receive a message in the first round - #[displaydoc("receive messages at round 1")] - Round1Receive( - #[cfg_attr(feature = "std", source)] CompleteRoundError, - ), + #[error("receive messages at round 1")] + Round1Receive(#[source] CompleteRoundError), /// Couldn't send a message in the second round - #[displaydoc("send a message at round 2")] - Round2Send(#[cfg_attr(feature = "std", source)] SendErr), + #[error("send a message at round 2")] + Round2Send(#[source] SendErr), /// Couldn't receive a message in the second round - #[displaydoc("receive messages at round 2")] - Round2Receive( - #[cfg_attr(feature = "std", source)] CompleteRoundError, - ), + #[error("receive messages at round 2")] + Round2Receive(#[source] CompleteRoundError), /// Some of the parties cheated - #[displaydoc("malicious parties: {guilty_parties:?}")] + #[error("malicious parties: {guilty_parties:?}")] PartiesOpenedRandomnessDoesntMatchCommitment { /// List of cheated parties guilty_parties: Vec, diff --git a/round-based/CHANGELOG.md b/round-based/CHANGELOG.md index 6948e07..28d3d36 100644 --- a/round-based/CHANGELOG.md +++ b/round-based/CHANGELOG.md @@ -1,5 +1,5 @@ ## v0.4.0 -* Improve ergonomics of protocol simulation, which is used for writing tests [#14] +* BREAKING: Improve ergonomics of protocol simulation, which is used for writing tests [#14] * Remove `dev` feature, it's replaced with `sim` and `sim-async` * `round_based::simulation` module is renamed into `round_based::sim` * `round_based::simulation::Simulation` is renamed and moved to `round_based::sim::async_env::Network` @@ -12,6 +12,9 @@ * When `sim-async` feature is enabled, you can use `round_based::sim::async_env::{run, run_with_setup, ...}`, but typically you don't want to use them * `round_based::simulation::SimulationSync` has been renamed to `round_based::sim::Simulation` +* Use `core::error::Error` trait which is now always implemented for all errors regardless whether `std` feature + is enabled or not [#14] + * Update `thiserror` dependency to v2 Migration guidelines: * Replace `dev` feature with `sim` diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index 0637bcc..93aaa87 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -19,8 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"] futures-util = { version = "0.3", default-features = false, features = ["sink"] } phantom-type = { version = "0.3", default-features = false } tracing = { version = "0.1", default-features = false } -thiserror = { version = "1", optional = true } -displaydoc = { version = "0.2", default-features = false } +thiserror = { version = "2", default-features = false } round-based-derive = { version = "0.2", optional = true, path = "../round-based-derive" } @@ -40,13 +39,13 @@ rand_dev = "0.1" anyhow = "1" [features] -default = ["std"] +default = [] state-machine = [] -sim = ["std", "state-machine"] +sim = ["state-machine"] sim-async = ["sim", "tokio/sync", "tokio-stream", "futures-util/alloc"] derive = ["round-based-derive"] runtime-tokio = ["tokio"] -std = ["thiserror"] +std = [] [[test]] name = "derive" diff --git a/round-based/src/delivery.rs b/round-based/src/delivery.rs index ac31af6..25550d6 100644 --- a/round-based/src/delivery.rs +++ b/round-based/src/delivery.rs @@ -1,7 +1,5 @@ use futures_util::{Sink, Stream}; -use crate::StdError; - /// Networking abstraction /// /// Basically, it's pair of channels: [`Stream`] for receiving messages, and [`Sink`] for sending @@ -12,9 +10,9 @@ pub trait Delivery { /// Incoming delivery channel type Receive: Stream, Self::ReceiveError>> + Unpin; /// Error of outgoing delivery channel - type SendError: StdError + Send + Sync + 'static; + type SendError: core::error::Error + Send + Sync + 'static; /// Error of incoming delivery channel - type ReceiveError: StdError + Send + Sync + 'static; + type ReceiveError: core::error::Error + Send + Sync + 'static; /// Returns a pair of incoming and outgoing delivery channels fn split(self) -> (Self::Receive, Self::Send); } @@ -23,8 +21,8 @@ impl Delivery for (I, O) where I: Stream, IErr>> + Unpin, O: Sink, Error = OErr> + Unpin, - IErr: StdError + Send + Sync + 'static, - OErr: StdError + Send + Sync + 'static, + IErr: core::error::Error + Send + Sync + 'static, + OErr: core::error::Error + Send + Sync + 'static, { type Send = O; type Receive = I; diff --git a/round-based/src/lib.rs b/round-based/src/lib.rs index d93ff63..f434dc4 100644 --- a/round-based/src/lib.rs +++ b/round-based/src/lib.rs @@ -93,15 +93,3 @@ pub mod _docs; /// See [`ProtocolMessage`] docs for more details #[cfg(feature = "derive")] pub use round_based_derive::ProtocolMessage; - -mod std_error { - #[cfg(feature = "std")] - pub use std::error::Error as StdError; - - #[cfg(not(feature = "std"))] - pub trait StdError: core::fmt::Display + core::fmt::Debug {} - #[cfg(not(feature = "std"))] - impl StdError for E {} -} - -use std_error::StdError; diff --git a/round-based/src/party.rs b/round-based/src/party.rs index 77bba94..0fafcd4 100644 --- a/round-based/src/party.rs +++ b/round-based/src/party.rs @@ -34,7 +34,6 @@ use phantom_type::PhantomType; use crate::delivery::Delivery; use crate::runtime::{self, AsyncRuntime}; -use crate::StdError; /// Party of MPC protocol (trait) /// @@ -82,9 +81,9 @@ pub trait Mpc: internal::Sealed { type Runtime: AsyncRuntime; /// Sending message error - type SendError: StdError + Send + Sync + 'static; + type SendError: core::error::Error + Send + Sync + 'static; /// Receiving message error - type ReceiveError: StdError + Send + Sync + 'static; + type ReceiveError: core::error::Error + Send + Sync + 'static; /// Converts into [`MpcParty`] fn into_party(self) -> MpcParty; @@ -142,8 +141,8 @@ impl internal::Sealed for MpcParty {} impl Mpc for MpcParty where D: Delivery, - D::SendError: StdError + Send + Sync + 'static, - D::ReceiveError: StdError + Send + Sync + 'static, + D::SendError: core::error::Error + Send + Sync + 'static, + D::ReceiveError: core::error::Error + Send + Sync + 'static, R: AsyncRuntime, { type ProtocolMessage = M; diff --git a/round-based/src/rounds_router/mod.rs b/round-based/src/rounds_router/mod.rs index 7b99387..44a6b2e 100644 --- a/round-based/src/rounds_router/mod.rs +++ b/round-based/src/rounds_router/mod.rs @@ -82,7 +82,7 @@ impl RoundsRouter where M: ProtocolMessage, S: Stream, E>> + Unpin, - E: crate::StdError, + E: core::error::Error, { /// Completes specified round /// @@ -326,12 +326,11 @@ trait ProcessRoundMessage { fn take_output(&mut self) -> Result, Box>, TakeOutputError>; } -#[derive(Debug, displaydoc::Display)] -#[cfg_attr(feature = "std", derive(thiserror::Error))] +#[derive(Debug, thiserror::Error)] enum TakeOutputError { - #[displaydoc("output is already taken")] + #[error("output is already taken")] AlreadyTaken, - #[displaydoc("output is not ready yet, more messages are needed")] + #[error("output is not ready yet, more messages are needed")] NotReady, } @@ -470,38 +469,30 @@ pub mod errors { use super::TakeOutputError; /// Error indicating that `Rounds` failed to complete certain round - #[derive(Debug, displaydoc::Display)] - #[cfg_attr(feature = "std", derive(thiserror::Error))] + #[derive(Debug, thiserror::Error)] pub enum CompleteRoundError { /// [`MessagesStore`](super::MessagesStore) failed to process this message - #[displaydoc("failed to process the message")] - ProcessMessage(#[cfg_attr(feature = "std", source)] ProcessErr), + #[error("failed to process the message")] + ProcessMessage(#[source] ProcessErr), /// Receiving next message resulted into i/o error - #[displaydoc("receive next message")] - Io(#[cfg_attr(feature = "std", source)] IoError), + #[error("receive next message")] + Io(#[from] IoError), /// Some implementation specific error /// /// Error may be result of improper `MessagesStore` implementation, API misuse, or bug /// in `Rounds` implementation - #[displaydoc("implementation error")] - Other(#[cfg_attr(feature = "std", source)] OtherError), - } - - impl From> for CompleteRoundError { - fn from(err: IoError) -> Self { - Self::Io(err) - } + #[error("implementation error")] + Other(#[source] OtherError), } /// Error indicating that receiving next message resulted into i/o error - #[derive(Debug, displaydoc::Display)] - #[cfg_attr(feature = "std", derive(thiserror::Error))] + #[derive(Debug, thiserror::Error)] pub enum IoError { /// I/O error - #[displaydoc("i/o error")] - Io(#[cfg_attr(feature = "std", source)] E), + #[error("i/o error")] + Io(#[source] E), /// Encountered unexpected EOF - #[displaydoc("unexpected eof")] + #[error("unexpected eof")] UnexpectedEof, } @@ -509,45 +500,40 @@ pub mod errors { /// /// Error may be result of improper `MessagesStore` implementation, API misuse, or bug /// in `Rounds` implementation - #[derive(Debug)] - #[cfg_attr(feature = "std", derive(thiserror::Error), error(transparent))] - #[cfg_attr(not(feature = "std"), derive(displaydoc::Display), displaydoc("{0}"))] + #[derive(Debug, thiserror::Error)] + #[error(transparent)] pub struct OtherError(OtherReason); - #[derive(Debug, displaydoc::Display)] - #[cfg_attr(feature = "std", derive(thiserror::Error))] + #[derive(Debug, thiserror::Error)] pub(super) enum OtherReason { - #[displaydoc("improper `MessagesStore` implementation")] - ImproperStoreImpl(#[cfg_attr(feature = "std", source)] ImproperStoreImpl), - #[displaydoc("`Rounds` API misuse")] - RoundsMisuse(#[cfg_attr(feature = "std", source)] RoundsMisuse), - #[displaydoc("bug in `Rounds` (please, open a issue)")] - Bug(#[cfg_attr(feature = "std", source)] Bug), + #[error("improper `MessagesStore` implementation")] + ImproperStoreImpl(#[source] ImproperStoreImpl), + #[error("`Rounds` API misuse")] + RoundsMisuse(#[source] RoundsMisuse), + #[error("bug in `Rounds` (please, open a issue)")] + Bug(#[source] Bug), } - #[derive(Debug, displaydoc::Display)] - #[cfg_attr(feature = "std", derive(thiserror::Error))] + #[derive(Debug, thiserror::Error)] pub(super) enum ImproperStoreImpl { /// Store indicated that it received enough messages but didn't output /// /// I.e. [`store.wants_more()`] returned `false`, but `store.output()` returned `Err(_)`. - #[displaydoc("store didn't output")] + #[error("store didn't output")] StoreDidntOutput, } - #[derive(Debug, displaydoc::Display)] - #[cfg_attr(feature = "std", derive(thiserror::Error))] + #[derive(Debug, thiserror::Error)] pub(super) enum RoundsMisuse { - #[displaydoc("round is already completed")] + #[error("round is already completed")] RoundAlreadyCompleted, - #[displaydoc("round {n} is not registered")] + #[error("round {n} is not registered")] UnregisteredRound { n: u16 }, } - #[derive(Debug, displaydoc::Display)] - #[cfg_attr(feature = "std", derive(thiserror::Error))] + #[derive(Debug, thiserror::Error)] pub(super) enum Bug { - #[displaydoc( + #[error( "message originates from another round: we process messages from round \ {expected_round}, got message from round {actual_number}" )] @@ -555,16 +541,16 @@ pub mod errors { expected_round: u16, actual_number: u16, }, - #[displaydoc("state is incoherent, it's expected to be {expected}: {justification}")] + #[error("state is incoherent, it's expected to be {expected}: {justification}")] IncoherentState { expected: &'static str, justification: &'static str, }, - #[displaydoc("mismatched output type")] + #[error("mismatched output type")] MismatchedOutputType, - #[displaydoc("mismatched error type")] + #[error("mismatched error type")] MismatchedErrorType, - #[displaydoc("take round result")] + #[error("take round result")] TakeRoundResult(#[cfg_attr(feature = "std", source)] TakeOutputError), } diff --git a/round-based/src/rounds_router/simple_store.rs b/round-based/src/rounds_router/simple_store.rs index 6d90cb1..265dd80 100644 --- a/round-based/src/rounds_router/simple_store.rs +++ b/round-based/src/rounds_router/simple_store.rs @@ -252,13 +252,12 @@ impl RoundMsgs { } /// Error explaining why `RoundInput` wasn't able to process a message -#[derive(Debug, displaydoc::Display)] -#[cfg_attr(feature = "std", derive(thiserror::Error))] +#[derive(Debug, thiserror::Error)] pub enum RoundInputError { /// Party sent two messages in one round /// /// `msgs_ids` are ids of conflicting messages - #[displaydoc("party {sender} tried to overwrite message")] + #[error("party {sender} tried to overwrite message")] AttemptToOverwriteReceivedMsg { /// IDs of conflicting messages msgs_ids: [MsgId; 2], @@ -269,7 +268,7 @@ pub enum RoundInputError { /// /// This error is thrown when index of sender is not in `[0; n)` where `n` is number of /// parties involved in the protocol (provided in [`RoundInput::new`]) - #[displaydoc("sender index is out of range: sender={sender}, n={n}")] + #[error("sender index is out of range: sender={sender}, n={n}")] SenderIndexOutOfRange { /// Message ID msg_id: MsgId, @@ -282,7 +281,7 @@ pub enum RoundInputError { /// /// For instance, this error is returned when it's expected to receive broadcast message, /// but party sent p2p message instead (which is rough protocol violation). - #[displaydoc("expected message {expected:?}, got {actual:?}")] + #[error("expected message {expected:?}, got {actual:?}")] MismatchedMessageType { /// Message ID msg_id: MsgId, diff --git a/round-based/src/rounds_router/store.rs b/round-based/src/rounds_router/store.rs index 127abe8..a7758df 100644 --- a/round-based/src/rounds_router/store.rs +++ b/round-based/src/rounds_router/store.rs @@ -23,7 +23,7 @@ pub trait MessagesStore: Sized + 'static { /// Store output (e.g. `Vec<_>` of received messages) type Output; /// Store error - type Error: crate::StdError; + type Error: core::error::Error; /// Adds received message to the store /// diff --git a/round-based/src/state_machine/delivery.rs b/round-based/src/state_machine/delivery.rs index bfa1970..34b5936 100644 --- a/round-based/src/state_machine/delivery.rs +++ b/round-based/src/state_machine/delivery.rs @@ -74,15 +74,12 @@ impl crate::Sink> for Outgoings { } /// Error returned by [`Outgoings`] sink -#[derive(Debug, displaydoc::Display)] -#[displaydoc("{0}")] +#[derive(Debug, thiserror::Error)] +#[error(transparent)] pub struct SendErr(SendErrReason); -#[derive(Debug, displaydoc::Display)] +#[derive(Debug, thiserror::Error)] enum SendErrReason { - #[displaydoc("sink is not ready")] + #[error("sink is not ready")] NotReady, } - -#[cfg(feature = "std")] -impl std::error::Error for SendErr {} diff --git a/round-based/src/state_machine/mod.rs b/round-based/src/state_machine/mod.rs index 21fd370..13d9133 100644 --- a/round-based/src/state_machine/mod.rs +++ b/round-based/src/state_machine/mod.rs @@ -108,7 +108,7 @@ pub trait StateMachine { } /// Tells why protocol execution stopped -#[must_use = "ProceedResult must be used to correcty carry out the state machine"] +#[must_use = "ProceedResult must be used to correctly carry out the state machine"] pub enum ProceedResult { /// Protocol needs provided message to be sent SendMsg(crate::Outgoing), @@ -117,7 +117,7 @@ pub enum ProceedResult { /// After the state machine requested one more message, the next call to the state machine must /// be [`StateMachine::received_msg`]. NeedsOneMoreMessage, - /// Protocol is finised + /// Protocol is finished Output(O), /// Protocol yielded the execution /// @@ -149,15 +149,15 @@ impl core::fmt::Debug for ProceedResult { } /// Error type which indicates that state machine failed to carry out the protocol -#[derive(Debug, displaydoc::Display)] -#[displaydoc("{0}")] +#[derive(Debug, thiserror::Error)] +#[error(transparent)] pub struct ExecutionError(Reason); -#[derive(Debug, displaydoc::Display)] +#[derive(Debug, thiserror::Error)] enum Reason { - #[displaydoc("resuming state machine when protocol is already finished")] + #[error("resuming state machine when protocol is already finished")] Exhausted, - #[displaydoc("protocol polls unknown (unsupported) future")] + #[error("protocol polls unknown (unsupported) future")] PollingUnknownFuture, } @@ -172,9 +172,6 @@ impl From for ExecutionError { } } -#[cfg(feature = "std")] -impl std::error::Error for ExecutionError {} - struct StateMachineImpl> { shared_state: shared_state::SharedStateRef, exhausted: bool, From 0cb79cf0841bad0ba7ab61532073046f9fbd9901 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 3 Dec 2024 13:48:03 +0100 Subject: [PATCH 18/22] Remove `std` feature Signed-off-by: Denis Varlakov --- round-based/CHANGELOG.md | 2 ++ round-based/Cargo.toml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/round-based/CHANGELOG.md b/round-based/CHANGELOG.md index 28d3d36..a42cfc7 100644 --- a/round-based/CHANGELOG.md +++ b/round-based/CHANGELOG.md @@ -15,6 +15,7 @@ * Use `core::error::Error` trait which is now always implemented for all errors regardless whether `std` feature is enabled or not [#14] * Update `thiserror` dependency to v2 + * BREAKING: remove `std` feature, as the crate is fully no_std now Migration guidelines: * Replace `dev` feature with `sim` @@ -22,6 +23,7 @@ Migration guidelines: `round_based::simulation::{run, run_with_setup}` * Take advantage of `SimResult::{expect_ok, expect_eq}` to reduce amount of the code in your tests +* Remove `std` feature, if it was explicitly enabled Other than simulation, there are no breaking changes in this release. diff --git a/round-based/Cargo.toml b/round-based/Cargo.toml index 93aaa87..ec8b5e7 100644 --- a/round-based/Cargo.toml +++ b/round-based/Cargo.toml @@ -45,7 +45,6 @@ sim = ["state-machine"] sim-async = ["sim", "tokio/sync", "tokio-stream", "futures-util/alloc"] derive = ["round-based-derive"] runtime-tokio = ["tokio"] -std = [] [[test]] name = "derive" From 7c16331a53689ba3cc5864e6a5adee6c1d6b5253 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 3 Dec 2024 13:51:59 +0100 Subject: [PATCH 19/22] Fix broken things Signed-off-by: Denis Varlakov --- round-based/src/lib.rs | 3 --- round-based/src/rounds_router/mod.rs | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/round-based/src/lib.rs b/round-based/src/lib.rs index f434dc4..a653be1 100644 --- a/round-based/src/lib.rs +++ b/round-based/src/lib.rs @@ -50,9 +50,6 @@ #![forbid(unused_crate_dependencies, missing_docs)] #![no_std] -#[cfg(feature = "std")] -extern crate std; - extern crate alloc; #[doc(no_inline)] diff --git a/round-based/src/rounds_router/mod.rs b/round-based/src/rounds_router/mod.rs index 44a6b2e..ad52299 100644 --- a/round-based/src/rounds_router/mod.rs +++ b/round-based/src/rounds_router/mod.rs @@ -551,7 +551,7 @@ pub mod errors { #[error("mismatched error type")] MismatchedErrorType, #[error("take round result")] - TakeRoundResult(#[cfg_attr(feature = "std", source)] TakeOutputError), + TakeRoundResult(#[source] TakeOutputError), } impl CompleteRoundError { @@ -625,8 +625,9 @@ mod tests { #[tokio::test] async fn complete_round_that_expects_no_messages() { - let incomings = - futures::stream::pending::, std::io::Error>>(); + let incomings = futures::stream::pending::< + Result, core::convert::Infallible>, + >(); let mut rounds = super::RoundsRouter::builder(); let round1 = rounds.add_round(Store); From 69e752680ff7b9942e74c44f5c6f280b994ce011 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 3 Dec 2024 13:53:55 +0100 Subject: [PATCH 20/22] Require that `sim` is no_std compatible Signed-off-by: Denis Varlakov --- wasm/no_std/Cargo.lock | 53 ++++++++++++++++++++++++------------------ wasm/no_std/Cargo.toml | 13 ++++++++--- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/wasm/no_std/Cargo.lock b/wasm/no_std/Cargo.lock index 5b0de09..041bd89 100644 --- a/wasm/no_std/Cargo.lock +++ b/wasm/no_std/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "autocfg" @@ -52,17 +52,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.63", -] - [[package]] name = "educe" version = "0.4.23" @@ -85,7 +74,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.90", ] [[package]] @@ -187,9 +176,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -213,28 +202,28 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" name = "random-generation-protocol" version = "0.1.0" dependencies = [ - "displaydoc", "generic-array", "rand_core", "round-based", "serde", "sha2", + "thiserror", ] [[package]] name = "round-based" -version = "0.3.0" +version = "0.4.0" dependencies = [ - "displaydoc", "futures-util", "phantom-type", "round-based-derive", + "thiserror", "tracing", ] [[package]] name = "round-based-derive" -version = "0.2.1" +version = "0.2.2" dependencies = [ "proc-macro2", "quote", @@ -258,7 +247,7 @@ checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.90", ] [[package]] @@ -285,15 +274,35 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/wasm/no_std/Cargo.toml b/wasm/no_std/Cargo.toml index 5ac444f..09e591c 100644 --- a/wasm/no_std/Cargo.toml +++ b/wasm/no_std/Cargo.toml @@ -5,6 +5,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -random-generation-protocol = { path = "../../examples/random-generation-protocol" } -round-based = { path = "../../round-based", default-features = false, features = ["state-machine", "derive"] } +[dependencies.random-generation-protocol] +path = "../../examples/random-generation-protocol" + +[dependencies.round-based] +path = "../../round-based" +features = [ + "state-machine", + "sim", + "derive", +] From 6b564fcf859a0c0d3f45ab7a007335d4487db55c Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 3 Dec 2024 16:01:07 +0100 Subject: [PATCH 21/22] Use sync functions instead of async in state_machine examples Signed-off-by: Denis Varlakov --- round-based/src/state_machine/mod.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/round-based/src/state_machine/mod.rs b/round-based/src/state_machine/mod.rs index 13d9133..d674122 100644 --- a/round-based/src/state_machine/mod.rs +++ b/round-based/src/state_machine/mod.rs @@ -7,8 +7,7 @@ //! //! ## Example //! ```rust,no_run -//! # #[tokio::main(flavor = "current_thread")] -//! # async fn main() -> anyhow::Result<()> { +//! # fn main() -> anyhow::Result<()> { //! use round_based::{Mpc, PartyIndex}; //! use anyhow::{Result, Error, Context as _}; //! @@ -33,26 +32,23 @@ //! |party| protocol_of_random_generation(party, 0, 3) //! ); //! -//! // Note: this is just an example. If you have stream/sink, you don't probably need to -//! // use the sync API -//! use futures::{Sink, Stream, SinkExt, StreamExt}; -//! async fn connect() -> Result<( -//! impl Stream>>, -//! impl Sink, Error = Error> -//! )> { -//! // ... -//! # Ok((futures_util::stream::pending(), futures_util::sink::drain().sink_map_err(|err| match err {}))) +//! fn send(msg: round_based::Outgoing) -> Result<()> { +//! // sends outgoing message... +//! # unimplemented!() +//! } +//! fn recv() -> Result> { +//! // receives incoming message... +//! # unimplemented!() //! } -//! let (mut incomings, mut outgoings) = connect().await?; //! //! use round_based::state_machine::{StateMachine as _, ProceedResult}; //! let output = loop { //! match state.proceed() { //! ProceedResult::SendMsg(msg) => { -//! outgoings.send(msg).await? +//! send(msg)? //! } //! ProceedResult::NeedsOneMoreMessage => { -//! let msg = incomings.next().await.context("unexpected eof")??; +//! let msg = recv()?; //! state.received_msg(msg) //! .map_err(|_| anyhow::format_err!("state machine rejected received message"))?; //! } From f6f44d313263f486b70054784c239a0df32b2a5e Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 3 Dec 2024 16:06:23 +0100 Subject: [PATCH 22/22] Make clippy happy Signed-off-by: Denis Varlakov --- round-based/src/state_machine/shared_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/round-based/src/state_machine/shared_state.rs b/round-based/src/state_machine/shared_state.rs index e73cc47..cd8ddf8 100644 --- a/round-based/src/state_machine/shared_state.rs +++ b/round-based/src/state_machine/shared_state.rs @@ -132,7 +132,7 @@ impl core::fmt::Debug for SharedStateRef { /// [`SharedStateRef::can_schedule`]. pub struct CanSchedule(T); -impl<'a, M> CanSchedule<&'a SharedStateRef> { +impl CanSchedule<&SharedStateRef> { fn borrow_mut(&self) -> core::cell::RefMut> { self.0 .0.borrow_mut() }