diff --git a/examples/src/simple.rs b/examples/src/simple.rs index 59cd356..6566414 100644 --- a/examples/src/simple.rs +++ b/examples/src/simple.rs @@ -11,10 +11,13 @@ use alloc::collections::{BTreeMap, BTreeSet}; use core::fmt::Debug; -use manul::protocol::{ - BoxedRound, CommunicationInfo, EntryPoint, EvidenceError, EvidenceMessages, FinalizeOutcome, LocalError, NoMessage, - PartyId, Protocol, ProtocolError, ProtocolMessage, ReceiveError, RequiredMessageParts, RequiredMessages, Round, - RoundId, RoundInfo, TransitionInfo, +use manul::{ + protocol::{ + BoxedRound, CommunicationInfo, EntryPoint, EvidenceError, EvidenceMessages, FinalizeOutcome, LocalError, + NoMessage, PartyId, Protocol, ProtocolError, ProtocolMessage, ReceiveError, RequiredMessageParts, + RequiredMessages, Round, RoundId, RoundInfo, TransitionInfo, + }, + utils::Without, }; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; @@ -162,13 +165,10 @@ impl EntryPoint for SimpleProtocolEntryPoint { .map(|(idx, id)| (id.clone(), idx as u8)) .collect::>(); - let mut ids = self.all_ids; - ids.remove(id); - Ok(BoxedRound::new(Round1 { context: Context { id: id.clone(), - other_ids: ids, + other_ids: self.all_ids.clone().without(id), ids_to_positions, }, })) diff --git a/examples/src/simple_test.rs b/examples/src/simple_test.rs index aab58dd..2212b77 100644 --- a/examples/src/simple_test.rs +++ b/examples/src/simple_test.rs @@ -1,7 +1,7 @@ use alloc::collections::BTreeSet; use manul::{ - dev::{run_sync, BinaryFormat, ExtendableEntryPoint, RoundExtension, TestSessionParams, TestSigner}, + dev::{check_evidence_with_extension, BinaryFormat, RoundExtension, TestSessionParams, TestSigner, TestVerifier}, protocol::{LocalError, PartyId}, signature::Keypair, }; @@ -10,33 +10,11 @@ use test_log::test; use crate::simple::{Round1, Round1Message, Round2, Round2Message, SimpleProtocolEntryPoint}; -#[derive(Debug, Clone)] -struct Round1InvalidDirectMessage; +type Id = TestVerifier; +type SP = TestSessionParams; +type EP = SimpleProtocolEntryPoint; -impl RoundExtension for Round1InvalidDirectMessage -where - Id: PartyId, -{ - type Round = Round1; - - fn make_direct_message( - &self, - _rng: &mut impl CryptoRngCore, - round: &Self::Round, - _destination: &Id, - ) -> Result<(Round1Message, ()), LocalError> { - Ok(( - Round1Message { - my_position: round.context.ids_to_positions[&round.context.id], - your_position: round.context.ids_to_positions[&round.context.id], - }, - (), - )) - } -} - -#[test] -fn round1_attributable_failure() { +fn make_entry_points() -> (Vec<(TestSigner, EP)>, ()) { let signers = (0..3).map(TestSigner::new).collect::>(); let all_ids = signers .iter() @@ -44,94 +22,75 @@ fn round1_attributable_failure() { .collect::>(); let entry_points = signers - .iter() - .enumerate() - .map(|(idx, signer)| { - let entry_point = SimpleProtocolEntryPoint::new(all_ids.clone()); - let mut entry_point = ExtendableEntryPoint::new(entry_point); - if idx == 0 { - entry_point.extend(Round1InvalidDirectMessage); - } + .into_iter() + .map(|signer| (signer, SimpleProtocolEntryPoint::new(all_ids.clone()))) + .collect(); - (*signer, entry_point) - }) - .collect::>(); - - let mut reports = run_sync::<_, TestSessionParams>(&mut OsRng, entry_points) - .unwrap() - .reports; - - let v0 = signers[0].verifying_key(); - let v1 = signers[1].verifying_key(); - let v2 = signers[2].verifying_key(); - - let _report0 = reports.remove(&v0).unwrap(); - let report1 = reports.remove(&v1).unwrap(); - let report2 = reports.remove(&v2).unwrap(); - - assert!(report1.provable_errors[&v0].verify(&()).is_ok()); - assert!(report2.provable_errors[&v0].verify(&()).is_ok()); + (entry_points, ()) } -#[derive(Debug, Clone)] -struct Round2InvalidDirectMessage; - -impl RoundExtension for Round2InvalidDirectMessage -where - Id: PartyId, -{ - type Round = Round2; - - fn make_direct_message( - &self, - _rng: &mut impl CryptoRngCore, - round: &Self::Round, - _destination: &Id, - ) -> Result<(Round2Message, ()), LocalError> { - Ok(( - Round2Message { +#[test] +fn round1_attributable_failure() -> Result<(), LocalError> { + #[derive(Debug, Clone)] + struct Round1InvalidDirectMessage; + + impl RoundExtension for Round1InvalidDirectMessage + where + Id: PartyId, + { + type Round = Round1; + + fn make_direct_message( + &self, + _rng: &mut impl CryptoRngCore, + round: &Self::Round, + _destination: &Id, + ) -> Result<(Round1Message, ()), LocalError> { + let message = Round1Message { my_position: round.context.ids_to_positions[&round.context.id], your_position: round.context.ids_to_positions[&round.context.id], - }, - (), - )) + }; + Ok((message, ())) + } } + + check_evidence_with_extension::( + &mut OsRng, + make_entry_points(), + Round1InvalidDirectMessage, + "(Round 1): Invalid position", + ) } #[test] -fn round2_attributable_failure() { - let signers = (0..3).map(TestSigner::new).collect::>(); - let all_ids = signers - .iter() - .map(|signer| signer.verifying_key()) - .collect::>(); - - let entry_points = signers - .iter() - .enumerate() - .map(|(idx, signer)| { - let entry_point = SimpleProtocolEntryPoint::new(all_ids.clone()); - let mut entry_point = ExtendableEntryPoint::new(entry_point); - if idx == 0 { - entry_point.extend(Round2InvalidDirectMessage); - } - - (*signer, entry_point) - }) - .collect::>(); - - let mut reports = run_sync::<_, TestSessionParams>(&mut OsRng, entry_points) - .unwrap() - .reports; - - let v0 = signers[0].verifying_key(); - let v1 = signers[1].verifying_key(); - let v2 = signers[2].verifying_key(); - - let _report0 = reports.remove(&v0).unwrap(); - let report1 = reports.remove(&v1).unwrap(); - let report2 = reports.remove(&v2).unwrap(); +fn round2_attributable_failure() -> Result<(), LocalError> { + #[derive(Debug, Clone)] + struct Round2InvalidDirectMessage; + + impl RoundExtension for Round2InvalidDirectMessage + where + Id: PartyId, + { + type Round = Round2; + + fn make_direct_message( + &self, + _rng: &mut impl CryptoRngCore, + round: &Self::Round, + _destination: &Id, + ) -> Result<(Round2Message, ()), LocalError> { + let message = Round2Message { + my_position: round.context.ids_to_positions[&round.context.id], + your_position: round.context.ids_to_positions[&round.context.id], + }; + Ok((message, ())) + } + } - assert!(report1.provable_errors[&v0].verify(&()).is_ok()); - assert!(report2.provable_errors[&v0].verify(&()).is_ok()); + check_evidence_with_extension::( + &mut OsRng, + make_entry_points(), + Round2InvalidDirectMessage, + "(Round 2): Invalid position", + ) } diff --git a/manul/benches/async_session.rs b/manul/benches/async_session.rs index f53b8c9..18a2f1e 100644 --- a/manul/benches/async_session.rs +++ b/manul/benches/async_session.rs @@ -12,6 +12,7 @@ use manul::{ Protocol, ProtocolMessage, ReceiveError, Round, RoundId, RoundInfo, TransitionInfo, }, signature::Keypair, + utils::Without, }; use rand_core::{CryptoRngCore, OsRng}; use serde::{Deserialize, Serialize}; @@ -241,11 +242,9 @@ fn bench_async_session(c: &mut Criterion) { let entry_points = signers .into_iter() .map(|signer| { - let mut other_ids = all_ids.clone(); - other_ids.remove(&signer.verifying_key()); let entry_point = Inputs { rounds_num, - other_ids, + other_ids: all_ids.clone().without(&signer.verifying_key()), echo: false, }; (signer, entry_point) diff --git a/manul/benches/empty_rounds.rs b/manul/benches/empty_rounds.rs index 405bdee..f7ee0f8 100644 --- a/manul/benches/empty_rounds.rs +++ b/manul/benches/empty_rounds.rs @@ -11,6 +11,7 @@ use manul::{ Protocol, ProtocolMessage, ReceiveError, Round, RoundId, RoundInfo, TransitionInfo, }, signature::Keypair, + utils::Without, }; use rand_core::{CryptoRngCore, OsRng}; use serde::{Deserialize, Serialize}; @@ -218,13 +219,11 @@ fn bench_empty_rounds(c: &mut Criterion) { .iter() .cloned() .map(|signer| { - let mut other_ids = all_ids.clone(); - other_ids.remove(&signer.verifying_key()); ( signer, Inputs { rounds_num, - other_ids, + other_ids: all_ids.clone().without(&signer.verifying_key()), echo: false, }, ) @@ -246,13 +245,11 @@ fn bench_empty_rounds(c: &mut Criterion) { .iter() .cloned() .map(|signer| { - let mut other_ids = all_ids.clone(); - other_ids.remove(&signer.verifying_key()); ( signer, Inputs { rounds_num, - other_ids, + other_ids: all_ids.clone().without(&signer.verifying_key()), echo: true, }, ) diff --git a/manul/src/dev.rs b/manul/src/dev.rs index 87ced3d..88385cf 100644 --- a/manul/src/dev.rs +++ b/manul/src/dev.rs @@ -9,6 +9,7 @@ The [`run_sync()`] method is helpful to execute a protocol synchronously and col */ mod extend; +mod misbehave; mod run_sync; mod session_parameters; mod wire_format; @@ -17,6 +18,7 @@ mod wire_format; pub mod tokio; pub use extend::{ExtendableEntryPoint, RoundExtension}; +pub use misbehave::{check_evidence, check_evidence_with_extension, check_evidence_with_extensions, extend_one}; pub use run_sync::{run_sync, ExecutionResult}; pub use session_parameters::{TestHasher, TestSessionParams, TestSignature, TestSigner, TestVerifier}; pub use wire_format::{BinaryFormat, HumanReadableFormat}; diff --git a/manul/src/dev/misbehave.rs b/manul/src/dev/misbehave.rs new file mode 100644 index 0000000..ca9a14a --- /dev/null +++ b/manul/src/dev/misbehave.rs @@ -0,0 +1,158 @@ +use alloc::{format, vec::Vec}; + +use rand_core::CryptoRngCore; + +use super::{ + extend::{ExtendableEntryPoint, RoundExtension}, + run_sync::{run_sync, ExecutionResult}, +}; +use crate::{ + protocol::{EntryPoint, Protocol, Round}, + session::{LocalError, SessionParameters}, + signature::Keypair, +}; + +/// Applies the `extend` function to one of the entry points in the given list and returns the new list +/// along with the ID of the modified entry point. +#[allow(clippy::type_complexity)] +pub fn extend_one( + entry_points: Vec<(SP::Signer, EP)>, + extend: impl FnOnce(ExtendableEntryPoint) -> ExtendableEntryPoint, +) -> Result<(SP::Verifier, Vec<(SP::Signer, ExtendableEntryPoint)>), LocalError> +where + SP: SessionParameters, + EP: EntryPoint, +{ + let mut entry_points = entry_points; + let (modified_signer, entry_point) = entry_points + .pop() + .ok_or_else(|| LocalError::new("Entry points list cannot be empty"))?; + let modified_id = modified_signer.verifying_key(); + let modified_entry_point = extend(ExtendableEntryPoint::new(entry_point)); + + let mut modified_entry_points = entry_points + .into_iter() + .map(|(signer, entry_point)| (signer, ExtendableEntryPoint::new(entry_point))) + .collect::>(); + modified_entry_points.push((modified_signer, modified_entry_point)); + + Ok((modified_id, modified_entry_points)) +} + +/// Checks that the result for the node with the `target_id` is a provable error, +/// it can be verified using `shared_data`, and its description contains `expected_description`. +pub fn check_evidence( + target_id: &SP::Verifier, + execution_result: ExecutionResult, + shared_data: &P::SharedData, + expected_description: &str, +) -> Result<(), LocalError> +where + P: Protocol, + SP: SessionParameters, +{ + let mut reports = execution_result.reports; + + let misbehaving_party_report = reports + .remove(target_id) + .ok_or_else(|| LocalError::new("Misbehaving node ID is not present in the reports"))?; + assert!(misbehaving_party_report.provable_errors.is_empty()); + + for (id, report) in reports { + if report.provable_errors.is_empty() { + return Err(LocalError::new(format!( + "Node {id:?} did not report any provable errors, but it should have" + ))); + } + + if report.provable_errors.len() > 1 { + let errors = report + .provable_errors + .values() + .map(|evidence| evidence.description()) + .collect::>() + .join(", "); + return Err(LocalError::new(format!( + "Node {id:?} reported {} provable errors when one was expected. Errors: {errors}", + errors.len() + ))); + } + + let description = report + .provable_errors + .get(target_id) + .ok_or_else(|| { + LocalError::new(format!( + "Node {id:?} did not generate a provable error report \ + about the misbehaving node ({target_id:?})." + )) + })? + .description(); + if !description.contains(expected_description) { + return Err(LocalError::new(format!( + "Got '{description}', expected '{expected_description}'" + ))); + } + + let verification_result = report + .provable_errors + .get(target_id) + .ok_or_else(|| { + LocalError::new(format!( + "Node {id:?}'s report does not contain evidence for the misbehaving node {target_id:?}." + )) + })? + .verify(shared_data); + if verification_result.is_err() { + return Err(LocalError::new(format!("Failed to verify: {verification_result:?}"))); + } + } + + Ok(()) +} + +/// Applies the `extend` function to one of the entry points in the given list, +/// executes the protocol with the resulting entry points, +/// and checks the evidence for the modified node using [`check_evidence`]. +#[allow(clippy::type_complexity)] +pub fn check_evidence_with_extensions( + rng: &mut impl CryptoRngCore, + entry_points_and_shared_data: ( + Vec<(SP::Signer, EP)>, + >::SharedData, + ), + extend: impl FnOnce(ExtendableEntryPoint) -> ExtendableEntryPoint, + expected_description: &str, +) -> Result<(), LocalError> +where + SP: SessionParameters, + EP: EntryPoint, +{ + let (entry_points, shared_data) = entry_points_and_shared_data; + let (misbehaving_id, modified_entry_points) = extend_one::(entry_points, extend)?; + let execution_result = run_sync::<_, SP>(rng, modified_entry_points)?; + check_evidence(&misbehaving_id, execution_result, &shared_data, expected_description) +} + +/// Same as [`check_evidence_with_extensions`], but with one extension only. +#[allow(clippy::type_complexity)] +pub fn check_evidence_with_extension( + rng: &mut impl CryptoRngCore, + entry_points_and_shared_data: ( + Vec<(SP::Signer, EP)>, + >::SharedData, + ), + extension: impl RoundExtension>, + expected_description: &str, +) -> Result<(), LocalError> +where + SP: SessionParameters, + EP: EntryPoint, +{ + check_evidence_with_extensions::( + rng, + entry_points_and_shared_data, + |entry_point| entry_point.with_extension(extension), + expected_description, + ) +} diff --git a/manul/src/session/echo.rs b/manul/src/session/echo.rs index 16403da..a9ff8c0 100644 --- a/manul/src/session/echo.rs +++ b/manul/src/session/echo.rs @@ -22,7 +22,7 @@ use crate::{ DynRound, EchoBroadcast, EchoRoundParticipation, EvidenceError, FinalizeOutcome, NoArtifact, NoMessage, NoType, NormalBroadcast, PartyId, Payload, Protocol, ProtocolMessagePart, RemoteError, TransitionInfo, }, - utils::SerializableMap, + utils::{MapValues, SerializableMap}, }; /// An error that can occur on receiving a message during an echo round. @@ -261,9 +261,7 @@ where } let message_hashes = echo_broadcasts - .iter() - .map(|(id, echo_broadcast)| (id.clone(), echo_broadcast.to_signed_hash::())) - .collect::>() + .map_values(|echo_broadcast| echo_broadcast.to_signed_hash::()) .into(); let message = EchoRoundMessage:: { message_hashes }; diff --git a/manul/src/session/transcript.rs b/manul/src/session/transcript.rs index c8ac223..33f0187 100644 --- a/manul/src/session/transcript.rs +++ b/manul/src/session/transcript.rs @@ -7,7 +7,10 @@ use alloc::{ use core::fmt::Debug; use super::{evidence::Evidence, message::SignedMessagePart, session::SessionParameters, LocalError, RemoteError}; -use crate::protocol::{DirectMessage, EchoBroadcast, NormalBroadcast, Protocol, RoundId}; +use crate::{ + protocol::{DirectMessage, EchoBroadcast, NormalBroadcast, Protocol, RoundId}, + utils::Without, +}; #[derive(Debug)] pub(crate) struct Transcript, SP: SessionParameters> { @@ -182,12 +185,12 @@ where round_id: &RoundId, except_for: &SP::Verifier, ) -> Result>, LocalError> { - let mut other_echo_broadcasts = - self.echo_broadcasts.get(round_id).cloned().ok_or_else(|| { - LocalError::new(format!("Echo-broadcasts for {round_id:?} are not in the transcript")) - })?; - other_echo_broadcasts.remove(except_for); - Ok(other_echo_broadcasts) + Ok(self + .echo_broadcasts + .get(round_id) + .cloned() + .ok_or_else(|| LocalError::new(format!("Echo-broadcasts for {round_id:?} are not in the transcript")))? + .without(except_for)) } } diff --git a/manul/src/utils.rs b/manul/src/utils.rs index d1bdf71..893faeb 100644 --- a/manul/src/utils.rs +++ b/manul/src/utils.rs @@ -1,7 +1,10 @@ //! Assorted utilities. mod serializable_map; +mod traits; mod type_id; pub use serializable_map::SerializableMap; +pub use traits::{verify_that, GetOrInvalidEvidence, GetOrLocalError, MapValues, MapValuesRef, Without}; + pub(crate) use type_id::DynTypeId; diff --git a/manul/src/utils/traits.rs b/manul/src/utils/traits.rs new file mode 100644 index 0000000..b6c1b60 --- /dev/null +++ b/manul/src/utils/traits.rs @@ -0,0 +1,113 @@ +use alloc::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, + format, +}; + +use crate::protocol::{EvidenceError, LocalError}; + +/// Implemented by collections allowing removal of a specific item. +pub trait Without { + /// Returns `self` with `item` removed. + fn without(self, item: &T) -> Self; +} + +impl Without for BTreeSet { + fn without(self, item: &T) -> Self { + let mut set = self; + set.remove(item); + set + } +} + +impl Without for BTreeMap { + /// Returns `self` with the pair corresponding to the key `item` removed. + fn without(self, item: &K) -> Self { + let mut map = self; + map.remove(item); + map + } +} + +/// Implemented by map-like collections allowing mapping over values. +pub trait MapValues { + /// The type of the resulting map. + type Result; + + /// Map over values of `self`, consuming it and returning the modified collection. + fn map_values(self, f: F) -> Self::Result + where + F: Fn(OldV) -> NewV; +} + +/// Implemented by map-like collections allowing mapping over values. +pub trait MapValuesRef { + /// The type of the resulting map. + type Result; + + /// Map over values of `self`, returning a new collection. + fn map_values_ref(&self, f: F) -> Self::Result + where + F: Fn(&OldV) -> NewV; +} + +impl MapValues for BTreeMap { + type Result = BTreeMap; + + fn map_values(self, f: F) -> Self::Result + where + F: Fn(OldV) -> NewV, + { + self.into_iter().map(|(key, value)| (key, f(value))).collect() + } +} + +impl MapValuesRef for BTreeMap { + type Result = BTreeMap; + + fn map_values_ref(&self, f: F) -> Self::Result + where + F: Fn(&OldV) -> NewV, + { + self.iter().map(|(key, value)| (key.clone(), f(value))).collect() + } +} + +/// Returns an error if `condition` is false. +/// +/// A shortcut to use in evidence checking logic. +pub fn verify_that(condition: bool) -> Result<(), EvidenceError> { + if condition { + Ok(()) + } else { + Err(EvidenceError::InvalidEvidence( + "the reported error cannot be reproduced".into(), + )) + } +} + +/// A helper trait for map lookup in the context of protocol rounds. +pub trait GetOrLocalError { + /// Try to get the value by `key`; if not found, treat as a [`LocalError`]. + fn get_or_local_error(&self, container: &str, key: &K) -> Result<&V, LocalError>; +} + +impl GetOrLocalError for BTreeMap { + fn get_or_local_error(&self, container: &str, key: &K) -> Result<&V, LocalError> { + self.get(key) + .ok_or_else(|| LocalError::new(format!("Key {key:?} not found in {container}"))) + } +} + +/// A helper trait for map lookup in the context of evidence verification. +pub trait GetOrInvalidEvidence { + /// Try to get the value by `key`; if not found, treat as an invalid evidence. + fn get_or_invalid_evidence(&self, container: &str, key: &K) -> Result<&V, EvidenceError>; +} + +impl GetOrInvalidEvidence for BTreeMap { + fn get_or_invalid_evidence(&self, container: &str, key: &K) -> Result<&V, EvidenceError> { + self.get(key) + .ok_or_else(|| EvidenceError::InvalidEvidence(format!("Key {key:?} not found in {container}"))) + } +}