From 826885d11a7753b6f0d2704ac2adf9864152b586 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Tue, 2 Sep 2025 15:58:28 -0700 Subject: [PATCH 1/6] Add helpers for writing misbehavior tests --- examples/src/simple_test.rs | 169 +++++++++++++----------------------- manul/src/dev.rs | 2 + manul/src/dev/misbehave.rs | 161 ++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 107 deletions(-) create mode 100644 manul/src/dev/misbehave.rs diff --git a/examples/src/simple_test.rs b/examples/src/simple_test.rs index aab58dd..516ceb9 100644 --- a/examples/src/simple_test.rs +++ b/examples/src/simple_test.rs @@ -1,8 +1,8 @@ use alloc::collections::BTreeSet; use manul::{ - dev::{run_sync, BinaryFormat, ExtendableEntryPoint, RoundExtension, TestSessionParams, TestSigner}, - protocol::{LocalError, PartyId}, + dev::{check_evidence_with_extension, BinaryFormat, RoundExtension, TestSessionParams, TestSigner, TestVerifier}, + protocol::{EntryPoint, LocalError, PartyId, Round}, signature::Keypair, }; use rand_core::{CryptoRngCore, OsRng}; @@ -10,128 +10,83 @@ 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() .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(Round1InvalidDirectMessage); - } - - (*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()); + signers + .into_iter() + .map(|signer| (signer, SimpleProtocolEntryPoint::new(all_ids.clone()))) + .collect() } -#[derive(Debug, Clone)] -struct Round2InvalidDirectMessage; - -impl RoundExtension for Round2InvalidDirectMessage +fn check_evidence(extension: &Ext, expected_description: &str) -> Result<(), LocalError> where - Id: PartyId, + Ext: RoundExtension, + Ext::Round: Round>::Protocol>, { - type Round = Round2; + check_evidence_with_extension::(&mut OsRng, make_entry_points(), extension, &(), expected_description) +} - 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(&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(&Round2InvalidDirectMessage, "(Round 2): Invalid position") } 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..4107fc2 --- /dev/null +++ b/manul/src/dev/misbehave.rs @@ -0,0 +1,161 @@ +use alloc::{collections::BTreeSet, 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 Fn(ExtendableEntryPoint) -> ExtendableEntryPoint, +) -> Result<(SP::Verifier, Vec<(SP::Signer, ExtendableEntryPoint)>), LocalError> +where + SP: SessionParameters, + EP: EntryPoint, +{ + let ids = entry_points + .iter() + .map(|(signer, _ep)| signer.verifying_key()) + .collect::>(); + let modified_id = ids + .first() + .ok_or_else(|| LocalError::new("Entry points list cannot be empty"))?; + let modified_entry_points = entry_points + .into_iter() + .map(|(signer, entry_point)| { + let id = signer.verifying_key(); + let mut entry_point = ExtendableEntryPoint::new(entry_point); + if &id == modified_id { + entry_point = extend(entry_point); + } + (signer, entry_point) + }) + .collect(); + Ok((modified_id.clone(), 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: Vec<(SP::Signer, EP)>, + extend: impl Fn(ExtendableEntryPoint) -> ExtendableEntryPoint, + shared_data: &>::SharedData, + expected_description: &str, +) -> Result<(), LocalError> +where + SP: SessionParameters, + EP: EntryPoint, +{ + 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: Vec<(SP::Signer, EP)>, + extension: &Ext, + shared_data: &>::SharedData, + expected_description: &str, +) -> Result<(), LocalError> +where + SP: SessionParameters, + EP: EntryPoint, + Ext: RoundExtension, + Ext::Round: Round, +{ + check_evidence_with_extensions::( + rng, + entry_points, + |entry_point| entry_point.with_extension(extension.clone()), + shared_data, + expected_description, + ) +} From 5e0add55a67b886db63fccc8b88e4ff75dbb8169 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Wed, 4 Jun 2025 15:47:19 -0700 Subject: [PATCH 2/6] Add `Without` trait and implement it for maps and sets --- examples/src/simple.rs | 16 ++++++++-------- manul/benches/async_session.rs | 5 ++--- manul/benches/empty_rounds.rs | 9 +++------ manul/src/session/transcript.rs | 17 ++++++++++------- manul/src/utils.rs | 3 +++ manul/src/utils/traits.rs | 24 ++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 manul/src/utils/traits.rs 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/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/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..9f17419 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::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..d046797 --- /dev/null +++ b/manul/src/utils/traits.rs @@ -0,0 +1,24 @@ +use alloc::collections::{BTreeMap, BTreeSet}; + +/// 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 + } +} From 55b20fd7a4dc49144ee2336aaee1836b34061ddc Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Wed, 4 Jun 2025 15:49:24 -0700 Subject: [PATCH 3/6] Add `MapValues` and `MapValuesRef` --- manul/src/session/echo.rs | 6 ++---- manul/src/utils.rs | 2 +- manul/src/utils/traits.rs | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) 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/utils.rs b/manul/src/utils.rs index 9f17419..b367e97 100644 --- a/manul/src/utils.rs +++ b/manul/src/utils.rs @@ -5,6 +5,6 @@ mod traits; mod type_id; pub use serializable_map::SerializableMap; -pub use traits::Without; +pub use traits::{MapValues, MapValuesRef, Without}; pub(crate) use type_id::DynTypeId; diff --git a/manul/src/utils/traits.rs b/manul/src/utils/traits.rs index d046797..e172fdc 100644 --- a/manul/src/utils/traits.rs +++ b/manul/src/utils/traits.rs @@ -22,3 +22,47 @@ impl Without for BTreeMap { 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() + } +} From 002811423d8f15eec814fa5d9bbfcefd68891123 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Wed, 4 Jun 2025 15:43:05 -0700 Subject: [PATCH 4/6] Add `verify_that()` --- manul/src/utils.rs | 2 +- manul/src/utils/traits.rs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/manul/src/utils.rs b/manul/src/utils.rs index b367e97..62d6676 100644 --- a/manul/src/utils.rs +++ b/manul/src/utils.rs @@ -5,6 +5,6 @@ mod traits; mod type_id; pub use serializable_map::SerializableMap; -pub use traits::{MapValues, MapValuesRef, Without}; +pub use traits::{verify_that, MapValues, MapValuesRef, Without}; pub(crate) use type_id::DynTypeId; diff --git a/manul/src/utils/traits.rs b/manul/src/utils/traits.rs index e172fdc..f7116d4 100644 --- a/manul/src/utils/traits.rs +++ b/manul/src/utils/traits.rs @@ -1,5 +1,7 @@ use alloc::collections::{BTreeMap, BTreeSet}; +use crate::protocol::EvidenceError; + /// Implemented by collections allowing removal of a specific item. pub trait Without { /// Returns `self` with `item` removed. @@ -66,3 +68,16 @@ impl MapValuesRef for BTreeMap 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(), + )) + } +} From d5eb32098b24c4859b0b7dfc7685047f45588452 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 30 Aug 2025 15:21:03 -0700 Subject: [PATCH 5/6] Add map lookup traits --- manul/src/utils.rs | 2 +- manul/src/utils/traits.rs | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/manul/src/utils.rs b/manul/src/utils.rs index 62d6676..893faeb 100644 --- a/manul/src/utils.rs +++ b/manul/src/utils.rs @@ -5,6 +5,6 @@ mod traits; mod type_id; pub use serializable_map::SerializableMap; -pub use traits::{verify_that, MapValues, MapValuesRef, Without}; +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 index f7116d4..b6c1b60 100644 --- a/manul/src/utils/traits.rs +++ b/manul/src/utils/traits.rs @@ -1,6 +1,10 @@ -use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + fmt::Debug, + format, +}; -use crate::protocol::EvidenceError; +use crate::protocol::{EvidenceError, LocalError}; /// Implemented by collections allowing removal of a specific item. pub trait Without { @@ -81,3 +85,29 @@ pub fn verify_that(condition: bool) -> Result<(), EvidenceError> { )) } } + +/// 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}"))) + } +} From 4c4e62d95ad9972a52e70f530cf1fadedf7e996a Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 6 Sep 2025 15:28:20 -0700 Subject: [PATCH 6/6] Adjust signatures in test helpers to make them immediately usable --- examples/src/simple_test.rs | 30 ++++++++++-------- manul/src/dev/misbehave.rs | 61 ++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/examples/src/simple_test.rs b/examples/src/simple_test.rs index 516ceb9..2212b77 100644 --- a/examples/src/simple_test.rs +++ b/examples/src/simple_test.rs @@ -2,7 +2,7 @@ use alloc::collections::BTreeSet; use manul::{ dev::{check_evidence_with_extension, BinaryFormat, RoundExtension, TestSessionParams, TestSigner, TestVerifier}, - protocol::{EntryPoint, LocalError, PartyId, Round}, + protocol::{LocalError, PartyId}, signature::Keypair, }; use rand_core::{CryptoRngCore, OsRng}; @@ -14,25 +14,19 @@ type Id = TestVerifier; type SP = TestSessionParams; type EP = SimpleProtocolEntryPoint; -fn make_entry_points() -> Vec<(TestSigner, EP)> { +fn make_entry_points() -> (Vec<(TestSigner, EP)>, ()) { let signers = (0..3).map(TestSigner::new).collect::>(); let all_ids = signers .iter() .map(|signer| signer.verifying_key()) .collect::>(); - signers + let entry_points = signers .into_iter() .map(|signer| (signer, SimpleProtocolEntryPoint::new(all_ids.clone()))) - .collect() -} + .collect(); -fn check_evidence(extension: &Ext, expected_description: &str) -> Result<(), LocalError> -where - Ext: RoundExtension, - Ext::Round: Round>::Protocol>, -{ - check_evidence_with_extension::(&mut OsRng, make_entry_points(), extension, &(), expected_description) + (entry_points, ()) } #[test] @@ -60,7 +54,12 @@ fn round1_attributable_failure() -> Result<(), LocalError> { } } - check_evidence(&Round1InvalidDirectMessage, "(Round 1): Invalid position") + check_evidence_with_extension::( + &mut OsRng, + make_entry_points(), + Round1InvalidDirectMessage, + "(Round 1): Invalid position", + ) } #[test] @@ -88,5 +87,10 @@ fn round2_attributable_failure() -> Result<(), LocalError> { } } - check_evidence(&Round2InvalidDirectMessage, "(Round 2): Invalid position") + check_evidence_with_extension::( + &mut OsRng, + make_entry_points(), + Round2InvalidDirectMessage, + "(Round 2): Invalid position", + ) } diff --git a/manul/src/dev/misbehave.rs b/manul/src/dev/misbehave.rs index 4107fc2..ca9a14a 100644 --- a/manul/src/dev/misbehave.rs +++ b/manul/src/dev/misbehave.rs @@ -1,4 +1,4 @@ -use alloc::{collections::BTreeSet, format, vec::Vec}; +use alloc::{format, vec::Vec}; use rand_core::CryptoRngCore; @@ -17,31 +17,26 @@ use crate::{ #[allow(clippy::type_complexity)] pub fn extend_one( entry_points: Vec<(SP::Signer, EP)>, - extend: impl Fn(ExtendableEntryPoint) -> ExtendableEntryPoint, + extend: impl FnOnce(ExtendableEntryPoint) -> ExtendableEntryPoint, ) -> Result<(SP::Verifier, Vec<(SP::Signer, ExtendableEntryPoint)>), LocalError> where SP: SessionParameters, EP: EntryPoint, { - let ids = entry_points - .iter() - .map(|(signer, _ep)| signer.verifying_key()) - .collect::>(); - let modified_id = ids - .first() + 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_entry_points = entry_points + 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)| { - let id = signer.verifying_key(); - let mut entry_point = ExtendableEntryPoint::new(entry_point); - if &id == modified_id { - entry_point = extend(entry_point); - } - (signer, entry_point) - }) - .collect(); - Ok((modified_id.clone(), modified_entry_points)) + .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, @@ -122,40 +117,42 @@ where #[allow(clippy::type_complexity)] pub fn check_evidence_with_extensions( rng: &mut impl CryptoRngCore, - entry_points: Vec<(SP::Signer, EP)>, - extend: impl Fn(ExtendableEntryPoint) -> ExtendableEntryPoint, - shared_data: &>::SharedData, + 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) + 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( +pub fn check_evidence_with_extension( rng: &mut impl CryptoRngCore, - entry_points: Vec<(SP::Signer, EP)>, - extension: &Ext, - shared_data: &>::SharedData, + entry_points_and_shared_data: ( + Vec<(SP::Signer, EP)>, + >::SharedData, + ), + extension: impl RoundExtension>, expected_description: &str, ) -> Result<(), LocalError> where SP: SessionParameters, EP: EntryPoint, - Ext: RoundExtension, - Ext::Round: Round, { check_evidence_with_extensions::( rng, - entry_points, - |entry_point| entry_point.with_extension(extension.clone()), - shared_data, + entry_points_and_shared_data, + |entry_point| entry_point.with_extension(extension), expected_description, ) }