Skip to main content

p2panda_encryption/message_scheme/test_utils/
dcgka.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::collections::{HashMap, HashSet};
4
5use crate::Rng;
6use crate::crypto::x25519::SecretKey;
7use crate::key_bundle::Lifetime;
8use crate::key_manager::KeyManager;
9use crate::key_registry::KeyRegistry;
10use crate::message_scheme::dcgka::{
11    ControlMessage, Dcgka, DcgkaState, DirectMessage, DirectMessageType, OperationOutput,
12    ProcessOutput, UpdateSecret,
13};
14use crate::message_scheme::test_utils::dgm::AckedTestDgm;
15use crate::test_utils::{MemberId, MessageId};
16use crate::traits::{AckedGroupMembership, PreKeyManager};
17
18pub type TestDcgkaState = DcgkaState<
19    MemberId,
20    MessageId,
21    KeyRegistry<MemberId>,
22    AckedTestDgm<MemberId, MessageId>,
23    KeyManager,
24>;
25
26/// Helper method returning initialised DCGKA state for each member of a test group.
27///
28/// The method will automatically generate all required one-time pre-key bundles from each member
29/// and register them for each other.
30pub fn init_dcgka_state<const N: usize>(
31    member_ids: [MemberId; N],
32    rng: &Rng,
33) -> [TestDcgkaState; N] {
34    let mut key_bundles = HashMap::new();
35    let mut key_managers = HashMap::new();
36
37    // Generate a pre-key bundle for each other member of the group.
38    for id in member_ids {
39        let identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
40        let mut manager =
41            KeyManager::init_and_generate_prekey(&identity_secret, Lifetime::default(), rng)
42                .unwrap();
43
44        let mut bundle_list = Vec::with_capacity(member_ids.len());
45        for _ in member_ids {
46            let (manager_i, key_bundle) =
47                KeyManager::generate_onetime_bundle(manager, rng).unwrap();
48            bundle_list.push(key_bundle);
49            manager = manager_i;
50        }
51
52        key_bundles.insert(id, bundle_list);
53        key_managers.insert(id, manager);
54    }
55
56    // Register each other's pre-key bundles and initialise DCGKA state.
57    let mut result = Vec::with_capacity(member_ids.len());
58    for id in member_ids {
59        let dgm = AckedTestDgm::init(id);
60        let registry = {
61            let mut state = KeyRegistry::init();
62            for bundle_id in member_ids {
63                let bundle = key_bundles.get_mut(&bundle_id).unwrap().pop().unwrap();
64                let state_i = KeyRegistry::add_onetime_bundle(state, bundle_id, bundle).unwrap();
65                state = state_i;
66            }
67            state
68        };
69        let manager = key_managers.remove(&id).unwrap();
70        let dcgka: TestDcgkaState = Dcgka::init(id, manager, registry, dgm);
71        result.push(dcgka);
72    }
73
74    result.try_into().unwrap()
75}
76
77fn members_without(members: &[MemberId], without: &[MemberId]) -> Vec<MemberId> {
78    members
79        .iter()
80        .filter(|id| !without.contains(id))
81        .cloned()
82        .collect()
83}
84
85pub fn assert_direct_message(
86    direct_messages: &[DirectMessage<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>],
87    recipient: MemberId,
88) -> DirectMessage<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>> {
89    direct_messages
90        .iter()
91        .find(|message| message.recipient == recipient)
92        .cloned()
93        .unwrap_or_else(|| panic!("could not find direct message for {recipient:?}"))
94        .clone()
95}
96
97pub struct ExpectedMembers<'a> {
98    pub viewer: &'a [MemberId],
99    pub expected: &'a [MemberId],
100}
101
102pub fn assert_members_view(dcgka: &TestDcgkaState, assertions: &[ExpectedMembers]) {
103    for assertion in assertions {
104        for viewer in assertion.viewer {
105            assert_eq!(
106                AckedTestDgm::members_view(&dcgka.dgm, viewer).unwrap(),
107                HashSet::from_iter(assertion.expected.iter().cloned()),
108                "{} should have had members view {:?}",
109                viewer,
110                assertion.expected
111            );
112        }
113    }
114}
115
116/// Testing helper to verify DCGKA group operations and states.
117pub struct AssertableDcgka {
118    /// Update secrets the DCGKA exported for "local member -> remote member".
119    update_secrets: HashMap<(MemberId, MemberId), UpdateSecret>,
120}
121
122impl Default for AssertableDcgka {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128impl AssertableDcgka {
129    pub fn new() -> Self {
130        Self {
131            update_secrets: HashMap::new(),
132        }
133    }
134
135    /// Expected local state after a member created a group.
136    pub fn assert_create(
137        &mut self,
138        dcgka: &TestDcgkaState,
139        output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
140        creator_id: MemberId,          // Group "creator"
141        expected_members: &[MemberId], // List of expected initial group members
142        seq: MessageId,                // Id of "create" control message
143    ) {
144        // This is a local group operation and the group "creator" manages that state.
145        assert_eq!(dcgka.my_id, creator_id);
146
147        // Control messages
148        // ~~~~~~~~~~~~~~~~
149
150        // Group "creator" broadcasts a "create" control message to everyone.
151        let ControlMessage::Create {
152            ref initial_members,
153        } = output.control_message
154        else {
155            panic!("expected \"create\" control message");
156        };
157        assert_eq!(initial_members, expected_members);
158
159        // Direct messages
160        // ~~~~~~~~~~~~~~~
161
162        // Group "creator" sends direct 2SM messages to each other member of the group.
163        assert_eq!(output.direct_messages.len(), expected_members.len() - 1);
164        for (index, expected_member) in members_without(expected_members, &[creator_id])
165            .iter()
166            .enumerate()
167        {
168            assert_eq!(
169                output.direct_messages.get(index).unwrap().message_type(),
170                DirectMessageType::TwoParty,
171            );
172            assert_eq!(
173                output.direct_messages.get(index).unwrap().recipient,
174                *expected_member,
175            );
176        }
177
178        // Members view
179        // ~~~~~~~~~~~~
180
181        // Group "creator" considers that all members are part of the group now and every member
182        // has processed the "create" control message.
183        assert_members_view(
184            dcgka,
185            &[ExpectedMembers {
186                viewer: expected_members,
187                expected: expected_members,
188            }],
189        );
190
191        // Update Secrets
192        // ~~~~~~~~~~~~~~
193
194        // Group "creator" establishes the update secret for their own message ratchet.
195        assert!(output.me_update_secret.is_some());
196
197        // Remember group "creator's" update secret for later assertions.
198        self.update_secrets.insert(
199            (creator_id, creator_id),
200            output.me_update_secret.as_ref().unwrap().clone(),
201        );
202
203        // Key Material
204        // ~~~~~~~~~~~~
205
206        // Seed secret has been dropped after group got created (FS).
207        assert!(dcgka.next_seed.is_none());
208
209        // Group "creator" established member secrets for all expected members of the group.
210        assert_eq!(dcgka.member_secrets.len(), expected_members.len() - 1);
211        for member_id in members_without(expected_members, &[creator_id]) {
212            assert!(
213                dcgka
214                    .member_secrets
215                    .contains_key(&(creator_id, seq, member_id))
216            );
217        }
218
219        // Outer-Ratchet holds only the secret for the group "creator" so far.
220        assert_eq!(dcgka.ratchet.len(), 1);
221        assert!(dcgka.ratchet.contains_key(&creator_id));
222    }
223
224    /// Expected local state after an invited member processed a "create" control message.
225    pub fn assert_process_create(
226        &mut self,
227        dcgka: &TestDcgkaState,
228        output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
229        processor_id: MemberId, // "Processor" who handles "create" control message
230        creator_id: MemberId,   // Group "creator"
231        expected_members: &[MemberId], // List of expected members after processing "create"
232        seq: MessageId,         // Id of "create" control message
233    ) {
234        // We're looking at the state of the "processor".
235        assert_eq!(dcgka.my_id, processor_id);
236        assert_ne!(creator_id, processor_id);
237
238        // Control messages
239        // ~~~~~~~~~~~~~~~~
240
241        // "Processing" member of "create" message broadcasts an "ack" control message to everyone.
242        let Some(ControlMessage::Ack {
243            ack_sender,
244            ack_seq,
245        }) = output.control_message
246        else {
247            panic!("expected \"ack\" control message");
248        };
249
250        // "Processing" member acknowledges the "create" message of the creator.
251        assert_eq!(ack_sender, creator_id);
252        assert_eq!(ack_seq, seq);
253
254        // Direct messages
255        // ~~~~~~~~~~~~~~~
256
257        // No direct messages.
258        assert!(output.direct_messages.is_empty());
259
260        // Members view
261        // ~~~~~~~~~~~~
262
263        // "Processor" of "create" considers all members part of the group now and every member has
264        // processed the "create" control message.
265        assert_members_view(
266            dcgka,
267            &[ExpectedMembers {
268                viewer: expected_members,
269                expected: expected_members,
270            }],
271        );
272
273        // Update Secrets
274        // ~~~~~~~~~~~~~~
275
276        // "Processor" establishes the update secret for their own message ratchet.
277        assert!(output.me_update_secret.is_some());
278
279        // Processor establishes the update secret for creator's message ratchet.
280        assert!(output.sender_update_secret.is_some());
281
282        // Remember "processor's" update secret for later assertions.
283        self.update_secrets.insert(
284            (processor_id, processor_id),
285            output.me_update_secret.as_ref().unwrap().clone(),
286        );
287
288        // Remember "creator's" update secret for later assertions.
289        self.update_secrets.insert(
290            (processor_id, creator_id),
291            output.sender_update_secret.as_ref().unwrap().clone(),
292        );
293
294        // Processor should be aware now of creator's update secret.
295        self.assert_update_secrets(processor_id, creator_id);
296
297        // Key Material
298        // ~~~~~~~~~~~~
299
300        // Seed was never used and should be none.
301        assert!(dcgka.next_seed.is_none());
302
303        // When joining a group freshly we have member secrets for every member who is not the
304        // "creator".
305        assert_eq!(
306            dcgka.member_secrets.len(),
307            members_without(expected_members, &[creator_id, processor_id]).len()
308        );
309
310        // Outer-Ratchet holds only the secret for the group "creator" and ourselves ("processor") so far.
311        assert_eq!(dcgka.ratchet.len(), 2);
312        assert!(dcgka.ratchet.contains_key(&creator_id));
313        assert!(dcgka.ratchet.contains_key(&processor_id));
314    }
315
316    /// Expected local state after a member processed an "ack" control message.
317    pub fn assert_process_ack(
318        &mut self,
319        dcgka: &TestDcgkaState,
320        output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
321        processor_id: MemberId, // Member who "processes" the "ack" control message
322        acker_id: MemberId,     // Author of the "ack" control message,
323        seq: MessageId,         // Id of the "ack" message
324    ) {
325        // We're looking at the state of the "processor".
326        assert_eq!(dcgka.my_id, processor_id);
327        assert_ne!(acker_id, processor_id);
328
329        // Control messages
330        // ~~~~~~~~~~~~~~~~
331
332        // No control messages.
333        assert!(output.control_message.is_none());
334
335        // Direct messages
336        // ~~~~~~~~~~~~~~~
337
338        // No direct messages.
339        assert!(output.direct_messages.is_empty());
340
341        // Update Secrets
342        // ~~~~~~~~~~~~~~
343
344        // No new update secret for acking member.
345        assert!(output.me_update_secret.is_none());
346
347        // Processor establishes the update secret for acking member's message ratchet.
348        assert!(output.sender_update_secret.is_some());
349
350        // Remember ackers's update secret for later assertions.
351        self.update_secrets.insert(
352            (processor_id, acker_id),
353            output.sender_update_secret.as_ref().unwrap().clone(),
354        );
355
356        // Processor should be aware now of acker's update secret.
357        self.assert_update_secrets(processor_id, acker_id);
358
359        // Key Material
360        // ~~~~~~~~~~~~
361
362        // Seed was never used and should be none.
363        assert!(dcgka.next_seed.is_none());
364
365        // Member secrets for "acker" has to be removed (FS).
366        assert!(
367            !dcgka
368                .member_secrets
369                .contains_key(&(acker_id, seq, processor_id))
370        );
371
372        // Outer-Ratchet holds secrets for at least the "acker" and "processor" of the "ack".
373        assert!(dcgka.ratchet.contains_key(&acker_id));
374        assert!(dcgka.ratchet.contains_key(&processor_id));
375    }
376
377    /// Expected local state after an member was added to the group.
378    pub fn assert_add(
379        &mut self,
380        dcgka: &TestDcgkaState,
381        output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
382        adder_id: MemberId, // "Adder" who adds someone to the group
383        added_id: MemberId, // "Added" who will join the group
384        seq: MessageId,     // Id of the "add" control message
385    ) {
386        // This is a local group operation, so we expect this to be the "adder".
387        assert_eq!(dcgka.my_id, adder_id);
388        assert_ne!(adder_id, added_id);
389
390        // Control messages
391        // ~~~~~~~~~~~~~~~~
392
393        // "Adder" broadcasts an "add" control message to everyone.
394        let ControlMessage::Add { added } = output.control_message else {
395            panic!("expected \"add\" control message");
396        };
397        assert_eq!(added, added_id);
398
399        // Direct messages
400        // ~~~~~~~~~~~~~~~
401
402        // One direct "welcome" message to "added" was generated.
403        assert_eq!(output.direct_messages.len(), 1);
404        assert_eq!(
405            output.direct_messages.first().unwrap().message_type(),
406            DirectMessageType::Welcome
407        );
408        assert_eq!(output.direct_messages.first().unwrap().recipient, added_id);
409
410        // Update Secrets
411        // ~~~~~~~~~~~~~~
412
413        // "Adder" establishes a new update secret for their own message ratchet.
414        assert!(output.me_update_secret.is_some());
415
416        // Remember "adders's" update secret for later assertions.
417        let previous = self.update_secrets.insert(
418            (adder_id, adder_id),
419            output.me_update_secret.as_ref().unwrap().clone(),
420        );
421
422        // The new update secret does not match the previous one.
423        assert_ne!(
424            previous.unwrap(),
425            output.me_update_secret.as_ref().unwrap().clone(),
426        );
427
428        // Key Material
429        // ~~~~~~~~~~~~
430
431        // Seed was never used and should be none.
432        assert!(dcgka.next_seed.is_none());
433
434        // Member secret for the "added" was established.
435        assert!(
436            dcgka
437                .member_secrets
438                .contains_key(&(adder_id, seq, added_id))
439        );
440
441        // Outer-Ratchet holds secrets for at least the "adder".
442        assert!(dcgka.ratchet.contains_key(&adder_id));
443
444        // The added doesn't have a ratchet secret yet.
445        assert!(!dcgka.ratchet.contains_key(&added_id));
446    }
447
448    /// Expected local state after an invited member "added" processes the "add" message with a
449    /// direct "welcome" message addressing them.
450    pub fn assert_process_welcome(
451        &mut self,
452        dcgka: &TestDcgkaState,
453        output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
454        adder_id: MemberId,            // Member who invited to the group
455        added_id: MemberId,            // Member who was added to the group
456        expected_members: &[MemberId], // List of expected members after processing "add"
457        seq: MessageId,                // Id of the "add" control message
458    ) {
459        // This control message is processed by the member who was added.
460        assert_eq!(dcgka.my_id, added_id);
461        assert_ne!(adder_id, added_id);
462
463        // Control messages
464        // ~~~~~~~~~~~~~~~~
465
466        // Added broadcasts an "ack" control message to everyone, no direct messages.
467        let Some(ControlMessage::Ack {
468            ack_sender,
469            ack_seq,
470        }) = output.control_message
471        else {
472            panic!("expected \"ack\" control message");
473        };
474
475        // "Added" acknowledges the "add" message of "adder".
476        assert_eq!(ack_sender, adder_id);
477        assert_eq!(ack_seq, seq);
478
479        // Direct messages
480        // ~~~~~~~~~~~~~~~
481
482        // No direct messages.
483        assert!(output.direct_messages.is_empty());
484
485        // Members view
486        // ~~~~~~~~~~~~
487
488        // "Added" considers all members as part of the group now and that "adder" has the same
489        // view as them.
490        assert_members_view(
491            dcgka,
492            &[ExpectedMembers {
493                viewer: &[added_id, adder_id],
494                expected: expected_members,
495            }],
496        );
497
498        // Update Secrets
499        // ~~~~~~~~~~~~~~
500
501        // Remember "added's" update secret for later assertions.
502        self.update_secrets.insert(
503            (added_id, added_id),
504            output.me_update_secret.as_ref().unwrap().clone(),
505        );
506
507        // Remember "adder's" update secret for later assertions.
508        self.update_secrets.insert(
509            (added_id, adder_id),
510            output.sender_update_secret.as_ref().unwrap().clone(),
511        );
512
513        // "Added" should be aware now of "adder's" update secret.
514        self.assert_update_secrets(added_id, adder_id);
515
516        // Key Material
517        // ~~~~~~~~~~~~
518
519        // Seed was never used and should be none.
520        assert!(dcgka.next_seed.is_none());
521
522        // Member secret for the "added" was dropped (FS).
523        assert!(
524            !dcgka
525                .member_secrets
526                .contains_key(&(adder_id, seq, added_id))
527        );
528
529        // Outer-Ratchet holds secrets for at least the "adder" and "added".
530        assert!(dcgka.ratchet.contains_key(&adder_id));
531        assert!(dcgka.ratchet.contains_key(&added_id));
532    }
533
534    /// Expected local state after a member who is _not_ invited processes an "add" control
535    /// message.
536    #[allow(clippy::too_many_arguments)]
537    pub fn assert_process_add(
538        &mut self,
539        dcgka: &TestDcgkaState,
540        output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
541        processor_id: MemberId, // "Processor" of the "add" control message
542        adder_id: MemberId,     // Id of the member who invited the new member
543        added_id: MemberId,     // Id of the member which got added
544        seq: MessageId,         // Id of the "add" message which is processed
545    ) {
546        // This control message is processed by every member who is _not_ the "added" and _not_ the
547        // adder.
548        assert_eq!(dcgka.my_id, processor_id);
549        assert_ne!(adder_id, processor_id);
550        assert_ne!(added_id, processor_id);
551
552        // Control messages
553        // ~~~~~~~~~~~~~~~~
554
555        // Processor broadcasts an "add-ack" control message to everyone.
556        let Some(ControlMessage::AddAck {
557            ack_sender,
558            ack_seq,
559        }) = output.control_message
560        else {
561            panic!("expected \"add-ack\" control message");
562        };
563
564        // "Processor" acknowledges the "add" message of the "adder".
565        assert_eq!(ack_sender, adder_id);
566        assert_eq!(ack_seq, seq);
567
568        // Direct messages
569        // ~~~~~~~~~~~~~~~
570
571        // "Processor" forwards a direct message to "added". It is required so the "added" member
572        // can decrypt subsequent messages of the "processing" member.
573        assert_eq!(output.direct_messages.len(), 1);
574        assert_eq!(output.direct_messages.first().unwrap().recipient, added_id);
575        assert_eq!(
576            output.direct_messages.first().unwrap().message_type(),
577            DirectMessageType::Forward
578        );
579
580        // Update Secrets
581        // ~~~~~~~~~~~~~~
582
583        // Remember "processor's" update secret for later assertions.
584        self.update_secrets.insert(
585            (processor_id, processor_id),
586            output.me_update_secret.as_ref().unwrap().clone(),
587        );
588
589        // Remember "adder's" update secret for later assertions.
590        self.update_secrets.insert(
591            (processor_id, adder_id),
592            output.sender_update_secret.as_ref().unwrap().clone(),
593        );
594
595        // "Processor" should be aware now of "adder's" update secret.
596        self.assert_update_secrets(processor_id, adder_id);
597
598        // Key Material
599        // ~~~~~~~~~~~~
600
601        // Seed was never used and should be none.
602        assert!(dcgka.next_seed.is_none());
603
604        // Member secret for the "added" was established.
605        assert!(
606            dcgka
607                .member_secrets
608                .contains_key(&(adder_id, seq, added_id))
609        );
610
611        // No ratchet secret exists yet for "added".
612        assert!(!dcgka.ratchet.contains_key(&added_id));
613    }
614
615    /// Expected local state after processing an "add-ack" control message.
616    pub fn assert_process_add_ack(
617        &mut self,
618        dcgka: &TestDcgkaState,
619        output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
620        processor_id: MemberId, // "Processor" of the "add-ack" control message
621        add_acker_id: MemberId, // Sender of the "add-ack" control message
622    ) {
623        // The given state is from the "processor" and not the sender of the "add-ack" message.
624        assert_eq!(dcgka.my_id, processor_id);
625        assert_ne!(add_acker_id, processor_id);
626
627        // Control messages
628        // ~~~~~~~~~~~~~~~~
629
630        // No control messages.
631        assert!(output.control_message.is_none());
632
633        // Direct messages
634        // ~~~~~~~~~~~~~~~
635
636        // No direct messages.
637        assert!(output.direct_messages.is_empty());
638
639        // Update Secrets
640        // ~~~~~~~~~~~~~~
641
642        // No new update secret for "processor's" own message ratchet.
643        assert!(output.me_update_secret.is_none());
644
645        // "Processor" establishes the update secret for "add-acking" member's message ratchet.
646        assert!(output.sender_update_secret.is_some());
647
648        // Remember "add-ackers's" update secret for later assertions.
649        self.update_secrets.insert(
650            (processor_id, add_acker_id),
651            output.sender_update_secret.as_ref().unwrap().clone(),
652        );
653
654        // "Processor" should be aware now of "add-acker's" update secret.
655        self.assert_update_secrets(processor_id, add_acker_id);
656
657        // Key Material
658        // ~~~~~~~~~~~~
659
660        // Seed was never used and should be none.
661        assert!(dcgka.next_seed.is_none());
662
663        // Ratchet secret exists for "add-ack".
664        assert!(dcgka.ratchet.contains_key(&add_acker_id));
665    }
666
667    /// Expected local state after removing a member from the group.
668    pub fn assert_remove(
669        &mut self,
670        dcgka: &TestDcgkaState,
671        output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
672        remover_id: MemberId,          // Author of the "remove" control message
673        removed_id: MemberId,          // Member which gets "removed"
674        expected_members: &[MemberId], // List of expected members after removal
675        seq: MessageId,                // Id of "remove" control message
676    ) {
677        // This is a local group operation, so we expect the state to be from the "remover".
678        assert_eq!(dcgka.my_id, remover_id);
679        assert_ne!(removed_id, remover_id);
680
681        // Control messages
682        // ~~~~~~~~~~~~~~~~
683
684        // "Remover" broadcasts a "remove" control message to everyone.
685        let ControlMessage::Remove { removed } = output.control_message else {
686            panic!("expected \"remove\" control message");
687        };
688        assert_eq!(removed, removed_id);
689
690        // Direct messages
691        // ~~~~~~~~~~~~~~~
692
693        // "Remover" sends a direct 2SM message to each other member of the group who is left.
694        assert_eq!(
695            output.direct_messages.len(),
696            members_without(expected_members, &[remover_id, removed_id]).len(),
697        );
698        for (index, expected_member) in members_without(expected_members, &[remover_id, removed_id])
699            .iter()
700            .enumerate()
701        {
702            assert_eq!(
703                output.direct_messages.get(index).unwrap().message_type(),
704                DirectMessageType::TwoParty,
705                "remove operation should yield a 2SM direct message"
706            );
707            assert_eq!(
708                output.direct_messages.get(index).unwrap().recipient,
709                *expected_member,
710                "direct message should address expected member",
711            );
712        }
713
714        // Update Secrets
715        // ~~~~~~~~~~~~~~
716
717        // "Remover" establishes a new update secret for their own message ratchet.
718        assert!(output.me_update_secret.is_some());
719
720        // Remember "remover's" update secret for later assertions.
721        self.update_secrets.insert(
722            (remover_id, remover_id),
723            output.me_update_secret.as_ref().unwrap().clone(),
724        );
725
726        // Key Material
727        // ~~~~~~~~~~~~
728
729        // Seed secret has been dropped after removal (FS).
730        assert!(dcgka.next_seed.is_none());
731
732        // "Remover" established member secrets for all expected members of the group.
733        assert_eq!(
734            dcgka.member_secrets.len(),
735            members_without(expected_members, &[remover_id, removed_id]).len()
736        );
737        for member_id in members_without(expected_members, &[remover_id, removed_id]) {
738            assert!(
739                dcgka
740                    .member_secrets
741                    .contains_key(&(remover_id, seq, member_id))
742            );
743        }
744    }
745
746    /// Expected local state after processing a "remove" control message.
747    pub fn assert_process_remove(
748        &mut self,
749        dcgka: &TestDcgkaState,
750        output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
751        processor_id: MemberId,
752        remover_id: MemberId,
753        seq: MessageId,
754    ) {
755        // We're looking at the state of the processor.
756        assert_eq!(dcgka.my_id, processor_id);
757        assert_ne!(remover_id, processor_id);
758
759        // Control messages
760        // ~~~~~~~~~~~~~~~~
761
762        // "Processor" of "remove" message broadcasts an "ack" control message to everyone.
763        let Some(ControlMessage::Ack {
764            ack_sender,
765            ack_seq,
766        }) = output.control_message
767        else {
768            panic!("expected \"ack\" control message");
769        };
770
771        // "Processor" acknowledges the "remove" message of the "remover".
772        assert_eq!(ack_sender, remover_id);
773        assert_eq!(ack_seq, seq);
774
775        // Direct messages
776        // ~~~~~~~~~~~~~~~
777
778        // No direct messages.
779        assert!(output.direct_messages.is_empty());
780
781        // Update Secrets
782        // ~~~~~~~~~~~~~~
783
784        // "Processor" establishes the update secret for their own message ratchet.
785        assert!(output.me_update_secret.is_some());
786
787        // "Processor" establishes the update secret for "remover's" message ratchet.
788        assert!(output.sender_update_secret.is_some());
789
790        // Remember "processor's" update secret for later assertions.
791        self.update_secrets.insert(
792            (processor_id, processor_id),
793            output.me_update_secret.as_ref().unwrap().clone(),
794        );
795
796        // Remember "remover's" update secret for later assertions.
797        self.update_secrets.insert(
798            (processor_id, remover_id),
799            output.sender_update_secret.as_ref().unwrap().clone(),
800        );
801
802        // "Processor" should be aware now of "remover's" update secret.
803        self.assert_update_secrets(processor_id, remover_id);
804
805        // Key Material
806        // ~~~~~~~~~~~~
807
808        // Seed was never used and should be none.
809        assert!(dcgka.next_seed.is_none());
810
811        // Update secret of "remover" was dropped after use. (FS)
812        assert!(
813            !dcgka
814                .member_secrets
815                .contains_key(&(remover_id, seq, processor_id))
816        );
817
818        // Outer-Ratchet holds secrets for both "processor" and "remover".
819        assert!(dcgka.ratchet.contains_key(&processor_id));
820        assert!(dcgka.ratchet.contains_key(&remover_id));
821    }
822
823    /// Expected local state after a member updated the group.
824    pub fn assert_update(
825        &mut self,
826        dcgka: &TestDcgkaState,
827        output: &OperationOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
828        updater_id: MemberId,          // Member updating the group
829        expected_members: &[MemberId], // List of expected members during update
830        seq: MessageId,                // Id of the "update" control message
831    ) {
832        // This is a local group operation, so we expect the state to be from the "updater".
833        assert_eq!(dcgka.my_id, updater_id);
834
835        // Control messages
836        // ~~~~~~~~~~~~~~~~
837
838        // "Updater" broadcasts an "update" control message to everyone.
839        let ControlMessage::Update = output.control_message else {
840            panic!("expected \"update\" control message");
841        };
842
843        // "Updater" sends a direct 2SM message to each other member of the group.
844        assert_eq!(output.direct_messages.len(), expected_members.len() - 1);
845        for (index, expected_member) in members_without(expected_members, &[updater_id])
846            .iter()
847            .enumerate()
848        {
849            assert_eq!(
850                output.direct_messages.get(index).unwrap().message_type(),
851                DirectMessageType::TwoParty,
852            );
853            assert_eq!(
854                output.direct_messages.get(index).unwrap().recipient,
855                *expected_member,
856            );
857        }
858
859        // Update Secrets
860        // ~~~~~~~~~~~~~~
861
862        // "Updater" establishes a new update secret for their own message ratchet.
863        assert!(output.me_update_secret.is_some());
864
865        // Remember "updater's" update secret for later assertions.
866        self.update_secrets.insert(
867            (updater_id, updater_id),
868            output.me_update_secret.as_ref().unwrap().clone(),
869        );
870
871        // Key Material
872        // ~~~~~~~~~~~~
873
874        // Seed secret has been dropped after update (FS).
875        assert!(dcgka.next_seed.is_none());
876
877        // "Updater" established member secrets for all expected members of the group.
878        assert_eq!(
879            dcgka.member_secrets.len(),
880            members_without(expected_members, &[updater_id]).len()
881        );
882        for member_id in members_without(expected_members, &[updater_id]) {
883            assert!(
884                dcgka
885                    .member_secrets
886                    .contains_key(&(updater_id, seq, member_id))
887            );
888        }
889
890        // Outer-Ratchet contains secret from "updater".
891        assert!(dcgka.ratchet.contains_key(&updater_id));
892    }
893
894    /// Expected state after processing an "update" control message.
895    pub fn assert_process_update(
896        &mut self,
897        dcgka: &TestDcgkaState,
898        output: &ProcessOutput<MemberId, MessageId, AckedTestDgm<MemberId, MessageId>>,
899        processor_id: MemberId, // Member processing the "update" control message
900        updater_id: MemberId,   // Member who updated the group
901        seq: MessageId,         // Id of the "update" control message
902    ) {
903        // We're looking at the state of the processor.
904        assert_eq!(dcgka.my_id, processor_id);
905        assert_ne!(updater_id, processor_id);
906
907        // Control messages
908        // ~~~~~~~~~~~~~~~~
909
910        // Processor of "update" message broadcasts an "ack" control message to everyone.
911        let Some(ControlMessage::Ack {
912            ack_sender,
913            ack_seq,
914        }) = output.control_message
915        else {
916            panic!("expected \"ack\" control message");
917        };
918
919        // "Processor" acknowledges the "update" message of the "updater".
920        assert_eq!(ack_sender, updater_id);
921        assert_eq!(ack_seq, seq);
922
923        // Direct messages
924        // ~~~~~~~~~~~~~~~
925
926        // No direct messages.
927        assert!(output.direct_messages.is_empty());
928
929        // Update Secrets
930        // ~~~~~~~~~~~~~~
931
932        // "Processor" establishes the update secret for their own message ratchet.
933        assert!(output.me_update_secret.is_some());
934
935        // "Processor" establishes the update secret for "remover's" message ratchet.
936        assert!(output.sender_update_secret.is_some());
937
938        // Remember "processor's" update secret for later assertions.
939        self.update_secrets.insert(
940            (processor_id, processor_id),
941            output.me_update_secret.as_ref().unwrap().clone(),
942        );
943
944        // Remember "updater's" update secret for later assertions.
945        self.update_secrets.insert(
946            (processor_id, updater_id),
947            output.sender_update_secret.as_ref().unwrap().clone(),
948        );
949
950        // "Processor" should be aware now of "updater's" update secret.
951        self.assert_update_secrets(processor_id, updater_id);
952
953        // Key Material
954        // ~~~~~~~~~~~~
955
956        // Seed was never used and should be none.
957        assert!(dcgka.next_seed.is_none());
958
959        // Update secret of "updater" was dropped after use. (FS)
960        assert!(
961            !dcgka
962                .member_secrets
963                .contains_key(&(updater_id, seq, processor_id))
964        );
965
966        // Outer-Ratchet holds secrets for both "processor" and "updater".
967        assert!(dcgka.ratchet.contains_key(&processor_id));
968        assert!(dcgka.ratchet.contains_key(&updater_id));
969    }
970
971    /// Compare if member learned about the update secret from another member.
972    fn assert_update_secrets(&self, from: MemberId, to: MemberId) {
973        assert_eq!(
974            self.update_secrets.get(&(from, to)).unwrap().as_bytes(),
975            self.update_secrets.get(&(to, to)).unwrap().as_bytes(),
976        );
977    }
978}