Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ jobs:
files: lcov.info
fail_on_error: true

examples:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@v1
with:
profile: minimal
toolchain: stable
target: x86_64-unknown-linux-gnu
override: true
- name: Run `dining_cryptographers`
run: cargo run dining_cryptographers

# This is supposed to factor out possible bugs introduced by Rust compiler changes
# and dependency changes, making the results more reproducible.
stable-test:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- `session::tokio::run_session()` and `par_run_session()` take an additional `cancellation` argument to support external loop cancellation. ([#100])
- `Round` now uses associated types for messages, payloads, and artifacts instead of boxed types. ([#117])
- Protocol errors and evidence verification are now defined for each round separately. `ProtocolError` is an associated type of `Round`. ([#117])
- `misbehave` combinator is reworked ino `extend`. It now works by defining typed extensions for a specific `Round` type. ([#117])


### Fixed
Expand All @@ -17,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0


[#100]: https://github.com/entropyxyz/manul/pull/100
[#117]: https://github.com/entropyxyz/manul/pull/117
[#119]: https://github.com/entropyxyz/manul/pull/119


Expand Down
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 52 additions & 76 deletions examples/dining_cryptographers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ use std::collections::{BTreeMap, BTreeSet};
use manul::{
dev::{run_sync, BinaryFormat, TestHasher, TestSignature, TestSigner, TestVerifier},
protocol::{
Artifact, BoxedFormat, BoxedRound, CommunicationInfo, DirectMessage, EchoBroadcast, EchoRoundParticipation,
EntryPoint, FinalizeOutcome, LocalError, MessageValidationError, NoProtocolErrors, NormalBroadcast, Payload,
Protocol, ProtocolMessage, ProtocolMessagePart, ReceiveError, Round, RoundId, TransitionInfo,
BoxedRound, CommunicationInfo, EchoRoundParticipation, EntryPoint, FinalizeOutcome, LocalError, NoArtifact,
NoMessage, NoProtocolErrors, Protocol, ProtocolMessage, ReceiveError, Round, RoundId, RoundInfo,
TransitionInfo,
},
session::SessionParameters,
};
Expand All @@ -73,35 +73,18 @@ use tracing::{debug, info, trace};
#[derive(Debug)]
pub struct DiningCryptographersProtocol;

impl<Id> Protocol<Id> for DiningCryptographersProtocol {
impl Protocol<DinerId> for DiningCryptographersProtocol {
// XOR/¬XOR of the two bits of each of the three diners (one is their own cointoss, the other shared with their
// neighbour).
type Result = (bool, bool, bool);
type SharedData = ();

type ProtocolError = NoProtocolErrors;

fn verify_direct_message_is_invalid(
_format: &BoxedFormat,
_round_id: &RoundId,
_message: &DirectMessage,
) -> Result<(), MessageValidationError> {
Ok(())
}

fn verify_echo_broadcast_is_invalid(
_format: &BoxedFormat,
_round_id: &RoundId,
_message: &EchoBroadcast,
) -> Result<(), MessageValidationError> {
Ok(())
}

fn verify_normal_broadcast_is_invalid(
_format: &BoxedFormat,
_round_id: &RoundId,
_message: &NormalBroadcast,
) -> Result<(), MessageValidationError> {
Ok(())
fn round_info(round_id: &RoundId) -> Option<RoundInfo<DinerId, Self>> {
match round_id {
_ if round_id == 1 => Some(RoundInfo::new::<Round1>()),
_ if round_id == 2 => Some(RoundInfo::new::<Round2>()),
_ => None,
}
}
}

Expand All @@ -125,6 +108,14 @@ pub struct Round2 {

impl Round<DinerId> for Round1 {
type Protocol = DiningCryptographersProtocol;
type ProtocolError = NoProtocolErrors<Self>;

type DirectMessage = Round1Message;
type EchoBroadcast = NoMessage;
type NormalBroadcast = NoMessage;

type Payload = bool;
type Artifact = ();

// Used to define the possible paths to and from this round. This protocol is very simple, it's simply Round 1 ->
// Round 2, so we can use the "linear" utility method to set this up.
Expand Down Expand Up @@ -157,44 +148,37 @@ impl Round<DinerId> for Round1 {
// This is called when this diner prepares to share a random bit with their neighbour.
fn make_direct_message(
&self,
_rng: &mut dyn CryptoRngCore,
format: &BoxedFormat,
_rng: &mut impl CryptoRngCore,
destination: &DinerId,
) -> Result<(DirectMessage, Option<Artifact>), LocalError> {
) -> Result<(Self::DirectMessage, Self::Artifact), LocalError> {
info!(
"[Round1, make_direct_message] from {:?} to {destination:?}",
self.diner_id
);
let msg = Round1Message { toss: self.own_toss };
let dm = DirectMessage::new(format, msg)?;

Ok((dm, None))
Ok((Round1Message { toss: self.own_toss }, ()))
}

// This is called when this diner receives a bit from their neighbour.
fn receive_message(
&self,
format: &BoxedFormat,
from: &DinerId,
message: ProtocolMessage,
) -> Result<Payload, ReceiveError<DinerId, Self::Protocol>> {
let dm = message.direct_message.deserialize::<Round1Message>(format)?;
message: ProtocolMessage<DinerId, Self>,
) -> Result<Self::Payload, ReceiveError<DinerId, Self>> {
let dm = message.direct_message;
debug!(
"[Round1, receive_message] {:?} was dm'd by {from:?}: {dm:?}",
self.diner_id
);
let payload = Payload::new(dm.toss);
Ok(payload)
Ok(dm.toss)
}

// At the end of round 1 we construct the next one, Round 2, and return a [`FinalizeOutcome::AnotherRound`].
fn finalize(
self: Box<Self>,
_rng: &mut dyn CryptoRngCore,
payloads: BTreeMap<DinerId, Payload>,
_artifacts: BTreeMap<DinerId, Artifact>,
self,
_rng: &mut impl CryptoRngCore,
payloads: BTreeMap<DinerId, Self::Payload>,
_artifacts: BTreeMap<DinerId, Self::Artifact>,
) -> Result<FinalizeOutcome<DinerId, Self::Protocol>, LocalError> {
let payloads = downcast_payloads::<bool>(payloads)?;
debug!("[Round1, finalize] {:?} sees payloads: {payloads:?}", self.diner_id);

let neighbour_toss = *payloads
Expand All @@ -206,7 +190,7 @@ impl Round<DinerId> for Round1 {
"[Round1, finalize] {:?} is finalizing to Round 2. Own cointoss: {}, neighbour cointoss: {neighbour_toss}",
self.diner_id, self.own_toss
);
Ok(FinalizeOutcome::AnotherRound(BoxedRound::new_dynamic(Round2 {
Ok(FinalizeOutcome::AnotherRound(BoxedRound::new(Round2 {
diner_id: self.diner_id,
own_toss: self.own_toss,
neighbour_toss,
Expand All @@ -217,6 +201,14 @@ impl Round<DinerId> for Round1 {

impl Round<DinerId> for Round2 {
type Protocol = DiningCryptographersProtocol;
type ProtocolError = NoProtocolErrors<Self>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nitpick: the associated type is in the singular, so it's a bit odd to assign a value whose name is plural. OTOH calling it "NoProtocolError" isn't great either. Not sure there is a great solution to be found here (idiomatically it should really be () but that isn't possible as we saw above).

Maybe DummyProtocolError?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A random thought I had:
I wonder how far we'd be able to get if we changed the Round trait to make the type ProtocolError take a impl core::error::Error. Maybe then we could impl Round with () as the error type when there are no errors, but then we'd need some clever trick to transform or cast the impl core::error::Error into an actual ProtocolError with all the methods and trait bounds we need.

I read through rust-lang/rust#99301 which seems to be about ways to access data from nested errors in a generic way. Seems a bit stuck though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can instead rename the associated type to ProtocolErrors?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But wouldn't the plural on a type name imply that it is a collection of types of protocol errors?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to impl ProtocolError for core::convert::Infallible (described as "The error type for errors that can never happen." in the docs) and it kinda works if it wasn't for the ser/deser bounds on ProtocolError. That's sort of what I was trying to hint at by saying "it'd be so nice if we could use a impl core::error::Error and then – handwaves – transfom/cast to concrete error types": we could have less bounds.

Anyway, all of this is nitpicking. The code is fine as-is, modulo perhaps the name.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can instead rename the associated type to ProtocolErrors?

This is better.

I still think it's awkward that there isn't a better way to do this but that has nothing to do with this PR.


type DirectMessage = NoMessage;
type EchoBroadcast = NoMessage;
type NormalBroadcast = Round2Message;

type Payload = bool;
type Artifact = NoArtifact;

// This round is the last in the protocol so we can terminate here.
fn transition_info(&self) -> TransitionInfo {
Expand Down Expand Up @@ -247,11 +239,7 @@ impl Round<DinerId> for Round2 {
}

// Implementing this method means that Round 2 will make a broadcast (without echoes).
fn make_normal_broadcast(
&self,
_rng: &mut dyn CryptoRngCore,
format: &BoxedFormat,
) -> Result<NormalBroadcast, LocalError> {
fn make_normal_broadcast(&self, _rng: &mut impl CryptoRngCore) -> Result<Self::NormalBroadcast, LocalError> {
debug!(
"[Round2, make_normal_broadcast] {:?} broadcasts to everyone else",
self.diner_id
Expand All @@ -262,37 +250,33 @@ impl Round<DinerId> for Round2 {
} else {
self.own_toss ^ self.neighbour_toss
};
let msg = Round2Message { reveal };
let bcast = NormalBroadcast::new(format, msg)?;
Ok(bcast)
Ok(Round2Message { reveal })
}

// Called once for each diner as messages are delivered to it. Here we deserialize the message using the configured
// [`SessionParameters::WireFormat`] and construct the [`Payload`] that we want to make available to the `finalize`
// method below.
fn receive_message(
&self,
format: &BoxedFormat,
from: &DinerId,
message: ProtocolMessage,
) -> Result<Payload, ReceiveError<DinerId, Self::Protocol>> {
message: ProtocolMessage<DinerId, Self>,
) -> Result<Self::Payload, ReceiveError<DinerId, Self>> {
debug!("[Round2, receive_message] from {from:?} to {:?}", self.diner_id);
let bcast = message.normal_broadcast.deserialize::<Round2Message>(format)?;
let bcast = message.normal_broadcast;
trace!("[Round2, receive_message] message (deserialized bcast): {:?}", bcast);
// The payload is kept and delivered in the `finalize` method.
let payload = Payload::new(bcast.reveal);
Ok(payload)
Ok(bcast.reveal)
}

// The `finalize` method has access to all the [`Payload`]s that were sent to this diner. This protocol does not use
// [`Artifact`]s, but when used, they are also available here.
// This is the last round in the protocol, so we return a [`FinalizeOutcome::Result`] with the result of the
// protocol from this participant's point of view.
fn finalize(
self: Box<Self>,
_rng: &mut dyn CryptoRngCore,
payloads: BTreeMap<DinerId, Payload>,
_artifacts: BTreeMap<DinerId, Artifact>,
self,
_rng: &mut impl CryptoRngCore,
payloads: BTreeMap<DinerId, Self::Payload>,
_artifacts: BTreeMap<DinerId, Self::Artifact>,
) -> Result<FinalizeOutcome<DinerId, Self::Protocol>, LocalError> {
// XOR/¬XOR the two bits of this diner, depending on whether they paid or not.
let mut own_reveal = self.own_toss ^ self.neighbour_toss;
Expand All @@ -301,8 +285,7 @@ impl Round<DinerId> for Round2 {
}
// Extract the payloads from the other participants so we can produce a [`Protocol::Result`]. In this case it is
// a tuple of 3 booleans.
let payloads_d = downcast_payloads::<bool>(payloads)?;
let bits = payloads_d.values().cloned().collect::<Vec<_>>();
let bits = payloads.into_values().collect::<Vec<_>>();
Ok(FinalizeOutcome::Result((bits[0], bits[1], own_reveal)))
}
}
Expand Down Expand Up @@ -337,7 +320,7 @@ impl EntryPoint<DinerId> for DiningEntryPoint {
// Each `EntryPoint` creates one `Session`.
fn make_round(
self,
rng: &mut dyn CryptoRngCore,
rng: &mut impl CryptoRngCore,
_shared_randomness: &[u8],
id: &DinerId,
) -> Result<BoxedRound<DinerId, Self::Protocol>, LocalError> {
Expand All @@ -351,7 +334,7 @@ impl EntryPoint<DinerId> for DiningEntryPoint {
"[DiningEntryPoint, make_round] diner {id:?} tossed: {:?} (paid? {paid})",
round.own_toss
);
let round = BoxedRound::new_dynamic(round);
let round = BoxedRound::new(round);
Ok(round)
}
}
Expand All @@ -376,13 +359,6 @@ impl SessionParameters for DiningSessionParams {
type WireFormat = BinaryFormat;
}

// Just a utility method to help us convert a [`Payload`] to, for example, a `bool`.
fn downcast_payloads<T: 'static>(map: BTreeMap<DinerId, Payload>) -> Result<BTreeMap<DinerId, T>, LocalError> {
map.into_iter()
.map(|(id, payload)| payload.downcast::<T>().map(|p| (id, p)))
.collect()
}

fn main() {
tracing_subscriber::fmt::init();
info!("Dining Cryptographers Protocol Example");
Expand Down
2 changes: 1 addition & 1 deletion examples/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ pub mod simple;
pub mod simple_chain;

#[cfg(test)]
mod simple_malicious;
mod simple_test;
Loading
Loading