modality_mutation_plane/
protocol.rs

1//! The modality mutation plane is comprised of a tree of communicating participants
2//! sharing information about mutators and mutations using this protocol.
3//!
4//! # Participants
5//!
6//! * A participant has a globally unique identifier
7//! * A participant may have zero to many child participants.
8//! * The root participant has zero parents; all other participants have one parent
9//! * A participant may manage zero to many mutators.
10//!
11//! # Mutators
12//!
13//! * A mutator has a globally unique identifier
14//! * A mutator may only be managed by a single participant
15//! * A mutator may have zero to many "staged" mutations
16//!     * These are not-yet-actuated mutations
17//!     * Staged mutations are only actuated when their triggering conditions are met
18//!     * Staged mutations may be canceled
19//! * A mutator may have zero to one active mutation
20//!     * This is the most-recently-actuated mutation
21//!     * A mutation is considered no longer active when either the mutator is reset
22//!       or a new mutation is actuated at the same mutator.
23//!
24//! # Mutations
25//!
26//! * A mutation has a globally unique identifier
27//! * A mutation has a map of zero to many parameters (key-value pairs)
28//! * A mutation optionally has an associated set of triggering conditions and
29//!   CRDT-like state tracking of those conditions
30//! * A mutator and its managing participant are responsible for ensuring that a mutation
31//!   is only ever actuated zero or one times.
32//!
33//! # Messages
34//!
35//! Messages typically travel in a single direction - either from the leaf/descendant participants
36//! towards the root, or from the root/ancestor participants towards the leaf/descendants.
37//!
38//! When a participant receives a rootwards message from its children, it is expected to propagate
39//! the message to its parent.
40//!
41//! When a participant receives a leafwards message from its parent either:
42//!  * The message specifically only has meaning for the current participant or its managed mutators
43//!    and must be handled locally and not propagated. E.G. `ClearMutationsForMutator`, `ChildAuthOutcome`
44//!  * The message may be relevant to some other participant, and must be propagated to children.
45//!
46//! The exception to these rules is `UpdateTriggerState`, which travels in both directions.
47//!
48//! When a participant receives this message, it attempts to update its internal
49//! triggering state for that mutation. If anything changed in the internal state as a result
50//! of incorporating the contents of the message, an `UpdateTriggerState` message should be sent
51//! to the participant's parent and any children.
52
53use crate::types::*;
54use minicbor::{data::Tag, decode, encode, Decode, Decoder, Encode, Encoder};
55use uuid::Uuid;
56
57pub const MUTATION_PROTOCOL_VERSION: u32 = 1;
58
59#[derive(Encode, Decode, Debug, PartialEq, Clone)]
60pub enum RootwardsMessage {
61    #[n(1001)]
62    ChildAuthAttempt {
63        /// Id of the child (or further descendant) requesting authorization
64        #[n(0)]
65        child_participant_id: ParticipantId,
66        /// Protocol version supported by the child participant
67        #[n(1)]
68        version: u32,
69        /// Proof the child is worthy
70        #[n(2)]
71        token: Vec<u8>,
72    },
73    #[n(1012)]
74    MutatorAnnouncement {
75        /// Id of the participant in charge of managing the mutator
76        #[n(0)]
77        participant_id: ParticipantId,
78        #[n(1)]
79        mutator_id: MutatorId,
80        #[n(2)]
81        mutator_attrs: AttrKvs,
82    },
83    #[n(1023)]
84    MutatorRetirement {
85        /// Id of the participant in charge of managing the mutator
86        #[n(0)]
87        participant_id: ParticipantId,
88        #[n(1)]
89        mutator_id: MutatorId,
90    },
91    #[n(1044)]
92    UpdateTriggerState {
93        #[n(0)]
94        mutator_id: MutatorId,
95        #[n(1)]
96        mutation_id: MutationId,
97        /// Interpret "None" as "clear your trigger state for this mutation, you don't need to
98        /// track it (anymore)"
99        #[n(2)]
100        maybe_trigger_crdt: Option<TriggerCRDT>,
101    },
102}
103
104impl RootwardsMessage {
105    pub fn name(&self) -> &'static str {
106        match self {
107            RootwardsMessage::ChildAuthAttempt { .. } => "ChildAuthAttempt",
108            RootwardsMessage::MutatorAnnouncement { .. } => "MutatorAnnouncement",
109            RootwardsMessage::MutatorRetirement { .. } => "MutatorRetirement",
110            RootwardsMessage::UpdateTriggerState { .. } => "UpdateTriggerState",
111        }
112    }
113}
114
115#[derive(Encode, Decode, Debug, PartialEq, Clone)]
116pub enum LeafwardsMessage {
117    #[n(2001)]
118    ChildAuthOutcome {
119        /// Id of the child (or further descendant) that requested authorization
120        #[n(0)]
121        child_participant_id: ParticipantId,
122        /// Protocol version supported by the ancestors
123        #[n(1)]
124        version: u32,
125        /// Did the authorization succeed?
126        #[n(2)]
127        ok: bool,
128
129        /// Possible explanation for outcome
130        #[n(3)]
131        message: Option<String>,
132    },
133    #[n(2002)]
134    UnauthenticatedResponse {},
135    #[n(2013)]
136    RequestForMutatorAnnouncements {},
137    #[n(2024)]
138    NewMutation {
139        #[n(0)]
140        mutator_id: MutatorId,
141        #[n(1)]
142        mutation_id: MutationId,
143        /// If Some, the mutation should not be actuated immediately,
144        /// and instead should only be actuated when accumulated TriggerCRDT
145        /// state (as updated by UpdateTriggerState messages) matches this value.
146        /// If None, actuate the mutation immediately.
147        #[n(2)]
148        maybe_trigger_mask: Option<TriggerCRDT>,
149        #[n(3)]
150        params: AttrKvs,
151    },
152    #[n(2035)]
153    ClearSingleMutation {
154        #[n(0)]
155        mutator_id: MutatorId,
156        #[n(1)]
157        mutation_id: MutationId,
158        #[n(2)]
159        reset_if_active: bool,
160    },
161    #[n(2036)]
162    ClearMutationsForMutator {
163        #[n(0)]
164        mutator_id: MutatorId,
165        #[n(2)]
166        reset_if_active: bool,
167    },
168    #[n(2037)]
169    ClearMutations {},
170    #[n(2044)]
171    UpdateTriggerState {
172        #[n(0)]
173        mutator_id: MutatorId,
174        #[n(1)]
175        mutation_id: MutationId,
176        /// Interpret "None" as "clear your trigger state for this mutation, you don't need to
177        /// track it (anymore)"
178        #[n(2)]
179        maybe_trigger_crdt: Option<TriggerCRDT>,
180    },
181}
182
183impl LeafwardsMessage {
184    pub fn name(&self) -> &'static str {
185        match self {
186            LeafwardsMessage::ChildAuthOutcome { .. } => "ChildAuthOutcome",
187            LeafwardsMessage::UnauthenticatedResponse { .. } => "UnauthenticatedResponse",
188            LeafwardsMessage::RequestForMutatorAnnouncements { .. } => {
189                "RequestForMutatorAnnouncements"
190            }
191            LeafwardsMessage::NewMutation { .. } => "NewMutation",
192            LeafwardsMessage::ClearSingleMutation { .. } => "ClearSingleMutation",
193            LeafwardsMessage::ClearMutationsForMutator { .. } => "ClearMutationsForMutator",
194            LeafwardsMessage::ClearMutations { .. } => "ClearMutations",
195            LeafwardsMessage::UpdateTriggerState { .. } => "UpdateTriggerState",
196        }
197    }
198}
199
200const TAG_PARTICIPANT_ID: Tag = Tag::Unassigned(40200);
201const TAG_MUTATOR_ID: Tag = Tag::Unassigned(40201);
202const TAG_MUTATION_ID: Tag = Tag::Unassigned(40202);
203
204impl Encode for ParticipantId {
205    fn encode<W: encode::Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
206        e.tag(TAG_PARTICIPANT_ID)?.bytes(self.as_ref().as_bytes())?;
207        Ok(())
208    }
209}
210
211impl<'b> Decode<'b> for ParticipantId {
212    fn decode(d: &mut Decoder<'b>) -> Result<Self, decode::Error> {
213        let t = d.tag()?;
214        if t != TAG_PARTICIPANT_ID {
215            return Err(decode::Error::Message("Expected TAG_PARTICIPANT_ID"));
216        }
217
218        Uuid::from_slice(d.bytes()?)
219            .map(Into::into)
220            .map_err(|_uuid_err| decode::Error::Message("Error decoding uuid for ParticipantId"))
221    }
222}
223
224impl Encode for MutatorId {
225    fn encode<W: encode::Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
226        e.tag(TAG_MUTATOR_ID)?.bytes(self.as_ref().as_bytes())?;
227        Ok(())
228    }
229}
230
231impl<'b> Decode<'b> for MutatorId {
232    fn decode(d: &mut Decoder<'b>) -> Result<Self, decode::Error> {
233        let t = d.tag()?;
234        if t != TAG_MUTATOR_ID {
235            return Err(decode::Error::Message("Expected TAG_MUTATOR_ID"));
236        }
237
238        Uuid::from_slice(d.bytes()?)
239            .map(Into::into)
240            .map_err(|_uuid_err| decode::Error::Message("Error decoding uuid for MutatorId"))
241    }
242}
243
244impl Encode for MutationId {
245    fn encode<W: encode::Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
246        e.tag(TAG_MUTATION_ID)?.bytes(self.as_ref().as_bytes())?;
247        Ok(())
248    }
249}
250
251impl<'b> Decode<'b> for MutationId {
252    fn decode(d: &mut Decoder<'b>) -> Result<Self, decode::Error> {
253        let t = d.tag()?;
254        if t != TAG_MUTATION_ID {
255            return Err(decode::Error::Message("Expected TAG_MUTATION_ID"));
256        }
257
258        Uuid::from_slice(d.bytes()?)
259            .map(Into::into)
260            .map_err(|_uuid_err| decode::Error::Message("Error decoding uuid for MutationId"))
261    }
262}
263
264impl Encode for TriggerCRDT {
265    fn encode<W: encode::Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
266        e.array(self.as_ref().len() as u64)?;
267        for byte in self.as_ref().iter() {
268            e.u8(*byte)?;
269        }
270
271        Ok(())
272    }
273}
274
275impl<'b> Decode<'b> for TriggerCRDT {
276    fn decode(d: &mut Decoder<'b>) -> Result<Self, decode::Error> {
277        let arr_len = d.array()?;
278
279        if let Some(len) = arr_len {
280            let mut bytes = Vec::with_capacity(len as usize);
281            for _ in 0..len {
282                bytes.push(d.u8()?);
283            }
284            Ok(TriggerCRDT::new(bytes))
285        } else {
286            Err(decode::Error::Message(
287                "missing array length for TriggerCRDT",
288            ))
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use proptest::prelude::*;
297
298    fn participant_id() -> impl Strategy<Value = ParticipantId> {
299        any::<[u8; 16]>().prop_map(|arr| Uuid::from_bytes(arr).into())
300    }
301    fn mutator_id() -> impl Strategy<Value = MutatorId> {
302        any::<[u8; 16]>().prop_map(|arr| Uuid::from_bytes(arr).into())
303    }
304    fn mutation_id() -> impl Strategy<Value = MutationId> {
305        any::<[u8; 16]>().prop_map(|arr| Uuid::from_bytes(arr).into())
306    }
307    fn trigger_crdt() -> impl Strategy<Value = TriggerCRDT> {
308        proptest::collection::vec(any::<u8>(), 1..=5).prop_map(|v| v.into_iter().into())
309    }
310
311    fn attr_kv() -> impl Strategy<Value = AttrKv> {
312        (
313            ".+",
314            modality_mutator_protocol::proptest_strategies::attr_val(),
315        )
316            .prop_map(|(k, v)| AttrKv { key: k, value: v })
317    }
318    fn attr_kvs() -> impl Strategy<Value = AttrKvs> {
319        proptest::collection::vec(attr_kv(), 1..=5).prop_map(AttrKvs)
320    }
321
322    fn rootwards_message() -> impl Strategy<Value = RootwardsMessage> {
323        prop_oneof![
324            (
325                any::<u32>(),
326                participant_id(),
327                proptest::collection::vec(any::<u8>(), 0..100)
328            )
329                .prop_map(|(version, participant, token)| {
330                    RootwardsMessage::ChildAuthAttempt {
331                        child_participant_id: participant,
332                        version,
333                        token,
334                    }
335                }),
336            (participant_id(), mutator_id(), attr_kvs()).prop_map(
337                |(participant_id, mutator_id, mutator_attrs)| {
338                    RootwardsMessage::MutatorAnnouncement {
339                        participant_id,
340                        mutator_id,
341                        mutator_attrs,
342                    }
343                }
344            ),
345            (participant_id(), mutator_id()).prop_map(|(participant_id, mutator_id)| {
346                RootwardsMessage::MutatorRetirement {
347                    participant_id,
348                    mutator_id,
349                }
350            }),
351            (
352                mutator_id(),
353                mutation_id(),
354                proptest::option::of(trigger_crdt())
355            )
356                .prop_map(|(mutator_id, mutation_id, maybe_trigger_crdt)| {
357                    RootwardsMessage::UpdateTriggerState {
358                        mutator_id,
359                        mutation_id,
360                        maybe_trigger_crdt,
361                    }
362                }),
363        ]
364    }
365    fn leafwards_message() -> impl Strategy<Value = LeafwardsMessage> {
366        prop_oneof![
367            (
368                any::<u32>(),
369                participant_id(),
370                any::<bool>(),
371                proptest::option::of(".+")
372            )
373                .prop_map(|(version, child_participant_id, ok, message)| {
374                    LeafwardsMessage::ChildAuthOutcome {
375                        version,
376                        child_participant_id,
377                        ok,
378                        message,
379                    }
380                }),
381            (mutator_id(), any::<bool>()).prop_map(|(mutator_id, reset_if_active)| {
382                LeafwardsMessage::ClearMutationsForMutator {
383                    mutator_id,
384                    reset_if_active,
385                }
386            }),
387            (mutator_id(), mutation_id(), any::<bool>()).prop_map(
388                |(mutator_id, mutation_id, reset_if_active)| {
389                    LeafwardsMessage::ClearSingleMutation {
390                        mutator_id,
391                        mutation_id,
392                        reset_if_active,
393                    }
394                }
395            ),
396            (
397                mutator_id(),
398                mutation_id(),
399                proptest::option::of(trigger_crdt()),
400                attr_kvs()
401            )
402                .prop_map(|(mutator_id, mutation_id, maybe_trigger_mask, params)| {
403                    LeafwardsMessage::NewMutation {
404                        mutator_id,
405                        mutation_id,
406                        maybe_trigger_mask,
407                        params,
408                    }
409                }),
410            Just(LeafwardsMessage::RequestForMutatorAnnouncements {}),
411            (
412                mutator_id(),
413                mutation_id(),
414                proptest::option::of(trigger_crdt())
415            )
416                .prop_map(|(mutator_id, mutation_id, maybe_trigger_crdt)| {
417                    LeafwardsMessage::UpdateTriggerState {
418                        mutator_id,
419                        mutation_id,
420                        maybe_trigger_crdt,
421                    }
422                }),
423        ]
424    }
425
426    #[test]
427    fn round_trip_rootwards() {
428        proptest!(|(msg in rootwards_message())| {
429            let mut buf = vec![];
430            minicbor::encode(&msg , &mut buf)?;
431
432            let msg_prime: RootwardsMessage = minicbor::decode(&buf)?;
433            prop_assert_eq!(msg, msg_prime);
434        });
435    }
436    #[test]
437    fn round_trip_leafwards() {
438        proptest!(|(msg in leafwards_message())| {
439            let mut buf = vec![];
440            minicbor::encode(&msg , &mut buf)?;
441
442            let msg_prime: LeafwardsMessage = minicbor::decode(&buf)?;
443            prop_assert_eq!(msg, msg_prime);
444        });
445    }
446
447    #[test]
448    fn round_trip_update_trigger_state_bidirectional() {
449        proptest!(|((mutator_id, mutation_id, maybe_trigger_crdt) in (mutator_id(), mutation_id(), proptest::option::of(trigger_crdt())))| {
450            let mut rootwards_buf = vec![];
451            let rootwards_msg = RootwardsMessage::UpdateTriggerState{
452                mutator_id, mutation_id, maybe_trigger_crdt
453            };
454            minicbor::encode(&rootwards_msg , &mut rootwards_buf)?;
455
456            let rootwards_msg_prime: RootwardsMessage = minicbor::decode(&rootwards_buf)?;
457            if let RootwardsMessage::UpdateTriggerState{
458                mutator_id, mutation_id, maybe_trigger_crdt
459            } = rootwards_msg_prime {
460                let mut leafwards_buf = vec![];
461                let leafwards_msg = LeafwardsMessage::UpdateTriggerState{
462                    mutator_id, mutation_id, maybe_trigger_crdt
463                };
464                minicbor::encode(&leafwards_msg, &mut leafwards_buf)?;
465                let leafwards_msg_prime: LeafwardsMessage = minicbor::decode(&leafwards_buf)?;
466                if let LeafwardsMessage::UpdateTriggerState{
467                    mutator_id, mutation_id, maybe_trigger_crdt
468                } = leafwards_msg_prime {
469                    let rootwards_via_leafwards = RootwardsMessage::UpdateTriggerState { mutator_id, mutation_id, maybe_trigger_crdt };
470                    prop_assert_eq!(rootwards_msg, rootwards_via_leafwards);
471                } else {
472                    panic!("Wrong leafwards variant");
473                }
474            } else {
475                panic!("Wrong rootwards variant");
476            }
477        });
478    }
479}