p2panda_auth/group/
resolver.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Strong remove group resolver implementation.
4use std::collections::HashSet;
5use std::fmt::Display;
6use std::{fmt::Debug, marker::PhantomData};
7
8use petgraph::algo::toposort;
9
10use crate::Access;
11use crate::graph::{concurrent_bubbles, has_path};
12use crate::group::{
13    GroupAction, GroupControlMessage, GroupCrdt, GroupCrdtError, GroupCrdtState, StateChangeResult,
14};
15use crate::traits::{GroupStore, IdentityHandle, Operation, OperationId, Orderer, Resolver};
16
17/// An implementation of `GroupResolver` trait which follows strong remove ruleset.  
18///
19/// Concurrent operations are identified and processed, any which should be invalidated are
20/// added to the operation filter and not applied to the group state. Once an operation has
21/// been filtered, any operations which depended on any resulting state will not be applied to
22/// group state either. Ruleset for Concurrent Operations
23///
24/// The following ruleset is applied when choosing which operations to "filter" when concurrent
25/// operations are processed. It can be assumed that the behavior is equivalent for an admin
26/// member being removed, or demoted from admin to a lower access level.
27///
28/// ## Strong Remove Concurrency Rules
29///
30/// ### Removals
31///
32/// If a removal has occurred, filter any concurrent operations by the removed member, as long
33/// as it's 1) not a predecessor of the remove operation, and 2) not a mutual removal (removal
34/// of the remover by the removed member).
35///
36/// ### Mutual removals
37///
38/// Mutual removals result in both members being removed from the group, and any dependent
39/// concurrent branches are not applied to group state. We imagine further implementations
40/// taking different approaches, like resolving by seniority, hash id, quorum or some other
41/// parameter.
42///
43/// If a mutual removal has occurred, we want to retain the removal operations but filter all
44/// concurrent operations performed by the removed members (keeping predecessors of the
45/// remove).
46///
47/// ### Re-adding member concurrently
48///
49/// If Alice removes Charlie and Bob removes then adds Charlie concurrently, Charlie is still
50/// in the group. However, if Charlie performed any concurrent actions, these will be filtered
51/// along with any dependent operations.
52///
53/// ### Filtering of transitively dependent operations
54///
55/// When an operation is "explicitly" filtered it may cause dependent operations to become
56/// invalid, these operations will not be applied to the group state.
57#[derive(Clone, Debug, Default)]
58pub struct StrongRemove<ID, OP, C, ORD, GS> {
59    _phantom: PhantomData<(ID, OP, C, ORD, GS)>,
60}
61
62impl<ID, OP, C, ORD, GS> Resolver<ID, OP, C, ORD, GS> for StrongRemove<ID, OP, C, ORD, GS>
63where
64    ID: IdentityHandle + Display + Ord,
65    OP: OperationId + Display + Ord,
66    C: Clone + Debug + PartialEq + PartialOrd,
67    ORD: Orderer<ID, OP, GroupControlMessage<ID, C>> + Clone + Debug,
68    ORD::Operation: Clone,
69    ORD::State: Clone,
70    GS: GroupStore<ID, OP, C, Self, ORD> + Debug + Clone,
71{
72    /// Identify if an operation should trigger a group state rebuild.
73    fn rebuild_required(
74        y: &GroupCrdtState<ID, OP, C, Self, ORD, GS>,
75        operation: &ORD::Operation,
76    ) -> Result<bool, GroupCrdtError<ID, OP, C, Self, ORD, GS>> {
77        let control_message = operation.payload();
78        let group_id = control_message.group_id();
79
80        // Sanity check.
81        if y.group_id != group_id {
82            // The operation is not intended for this group.
83            return Err(GroupCrdtError::IncorrectGroupId(group_id, y.group_id));
84        }
85
86        let transitive_heads = y.transitive_heads().unwrap();
87
88        // We only need to rebuild the graph if this operation is concurrent.
89        Ok(transitive_heads.into_iter().collect::<Vec<_>>() != operation.dependencies())
90    }
91
92    /// Process the group operation graph, producing a new filter and re-building all state
93    /// accordingly.
94    fn process(
95        mut y: GroupCrdtState<ID, OP, C, Self, ORD, GS>,
96    ) -> Result<GroupCrdtState<ID, OP, C, Self, ORD, GS>, GroupCrdtError<ID, OP, C, Self, ORD, GS>>
97    {
98        // Start by draining the existing filter and re-building all states.
99        y.ignore.drain();
100        let mut y = GroupCrdt::rebuild(y).expect("no errors when re-building a group");
101        let mut filter: HashSet<OP> = Default::default();
102
103        // @TODO: avoid cloning operations here.
104        let operations = y.operations.clone();
105
106        // Keep track of mutual removes (which occur in one bubble) so that we can exclude these
107        // from the filter later.
108        let mut mutual_removes = HashSet::new();
109
110        // Get all bubbles of concurrency.
111        //
112        // A concurrency bubble is a set of operations from the group graph which share some
113        // concurrency. Multiple bubbles can occur in the same graph.
114        let mut bubbles = concurrent_bubbles(&y.graph);
115
116        let topo_sort =
117            toposort(&y.graph, None).expect("group operation sets can be ordered topologically");
118        let mut visited = HashSet::new();
119
120        // Traverse the graph visiting the operations in topological order.
121        for target_operation_id in topo_sort.iter() {
122            let Some(target_operation) = operations.get(target_operation_id) else {
123                return Err(GroupCrdtError::MissingOperation(*target_operation_id));
124            };
125
126            let bubble = bubbles
127                .iter()
128                .find(|bubble| bubble.contains(target_operation_id))
129                .cloned();
130
131            visited.insert(*target_operation_id);
132
133            let removed_manager = y.removed_manager(target_operation);
134
135            // If this operation removes/demotes a member with manager rights _and_ it is part of
136            // a concurrency bubble then we need to check the bubble for any operations which
137            // should be filtered.
138            if let (Some(removed_manager), Some(bubble)) = (removed_manager, &bubble) {
139                for bubble_operation_id in bubble.iter() {
140                    // If there's a path between the bubble and target operation, then it's not
141                    // concurrent, so we don't need to do anything.
142                    if has_path(&y.graph, *bubble_operation_id, *target_operation_id) {
143                        continue;
144                    }
145
146                    let Some(bubble_operation) = operations.get(bubble_operation_id) else {
147                        return Err(GroupCrdtError::MissingOperation(*bubble_operation_id));
148                    };
149
150                    // If this concurrent operation is _not_ authored by the "target author" then we
151                    // can continue to the next concurrent operation without taking any action.
152                    if bubble_operation.author() != removed_manager {
153                        continue;
154                    }
155
156                    // Add the concurrent operation to our filter.
157                    filter.insert(*bubble_operation_id);
158
159                    // Does this concurrent operation remove or demote an admin member?
160                    if let Some(concurrent_removed_admin) = y.removed_manager(bubble_operation) {
161                        // The removed member is concurrently removing the remover, this
162                        // is a "mutual remove".
163                        if concurrent_removed_admin == target_operation.author() {
164                            mutual_removes.insert(*bubble_operation_id);
165                        }
166                    }
167                }
168            }
169
170            match bubble {
171                Some(bubble) => {
172                    // If "bubble" is a sub-set of "visited" then we have visited all operations
173                    // in this bubble and we should now re-calculate group state using the newly
174                    // produced filter.
175                    //
176                    // This step is required so that as we traverse further into the graph we're
177                    // making decisions based on state computed _after_ we resolved any earlier
178                    // bubbles.
179                    if bubble.is_subset(&visited) {
180                        let mut filter_tmp = filter.clone();
181                        filter_tmp.retain(|op: &OP| !mutual_removes.contains(op));
182                        y.ignore = filter_tmp;
183                        y = GroupCrdt::rebuild(y).expect("no errors when re-building a group");
184                        // Remove the visited bubble from the bubbles set.
185                        bubbles.retain(|b| *b != bubble);
186
187                        // Drain visited.
188                        visited.drain();
189                    }
190                }
191                None => {
192                    let dependencies = HashSet::from_iter(target_operation.dependencies().clone());
193
194                    // As we weren't in a bubble we can directly apply this action.
195                    let result = GroupCrdt::apply_action(
196                        y,
197                        target_operation.id(),
198                        target_operation.author(),
199                        &dependencies,
200                        &target_operation.payload().action,
201                    )?;
202
203                    y = match result {
204                        StateChangeResult::Ok { state } => state,
205                        StateChangeResult::Noop { error, .. } => {
206                            return Err(GroupCrdtError::StateChangeError(
207                                target_operation.id(),
208                                error,
209                            ));
210                        }
211                        StateChangeResult::Filtered { state } => state,
212                    };
213                    visited.drain();
214                }
215            }
216        }
217
218        // Sanity check: all bubbles should be visited completely.
219        assert!(bubbles.is_empty(), "{:?}", bubbles);
220        Ok(y)
221    }
222}
223
224impl<ID, OP, C, RS, ORD, GS> GroupCrdtState<ID, OP, C, RS, ORD, GS>
225where
226    ID: IdentityHandle,
227    OP: OperationId + Ord,
228    C: Clone + Debug + PartialEq + PartialOrd,
229    RS: Resolver<ID, OP, C, ORD, GS> + Debug,
230    ORD: Orderer<ID, OP, GroupControlMessage<ID, C>> + Debug,
231    GS: GroupStore<ID, OP, C, RS, ORD> + Debug + Clone,
232{
233    /// If the given operation is an action which removes or demotes a manager member, return the
234    /// ID of the target member.
235    fn removed_manager(&self, operation: &ORD::Operation) -> Option<ID> {
236        let action = operation.payload().action;
237
238        let removed_or_demoted_member = match action {
239            GroupAction::Remove { member } => member,
240            GroupAction::Demote { member, .. } => member,
241            _ => return None,
242        };
243
244        // @TODO: either remove this step (and check for mutual removes on all remove/demote
245        // operations) or re-build graph state beforehand to in order to correctly handle
246        // certain edge-cases.
247        let was_manager = self
248            .transitive_members_at(&HashSet::from_iter(operation.dependencies()))
249            .expect("get transitive members")
250            .contains(&(removed_or_demoted_member.id(), Access::manage()));
251
252        if was_manager {
253            Some(removed_or_demoted_member.id())
254        } else {
255            None
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use rand::SeedableRng;
263    use rand::rngs::StdRng;
264
265    use crate::Access;
266    use crate::group::GroupMember;
267    use crate::group::crdt::tests::{
268        add_member, assert_members, create_group, from_create, remove_member, sync,
269    };
270    use crate::test_utils::Network;
271    use crate::traits::OperationId;
272
273    impl OperationId for &str {}
274
275    #[test]
276    fn mutual_removal_filter() {
277        //       A
278        //     /   \
279        //    B     C
280        //
281        // Node A: create the group
282        // Node B: Alice removes Bob
283        // Node C: Bob removes Alice
284        //
285        // We expect both removals to be processed (not filtered).
286
287        let alice = 'A';
288        let bob = 'B';
289        let claire = 'C';
290
291        let group = '1';
292
293        let rng = StdRng::from_os_rng();
294
295        let mut network = Network::new([alice, bob, claire], rng);
296
297        // Alice creates a group with Alice, Bob and Claire as managers.
298        network.create(
299            group,
300            alice,
301            vec![
302                (GroupMember::Individual(alice), Access::manage()),
303                (GroupMember::Individual(bob), Access::manage()),
304                (GroupMember::Individual(claire), Access::manage()),
305            ],
306        );
307
308        // Everyone processes the operation.
309        network.process();
310
311        // Alice and Bob remove one another concurrently.
312        network.remove(alice, GroupMember::Individual(bob), group);
313        network.remove(bob, GroupMember::Individual(alice), group);
314
315        // Everyone processes these operations.
316        network.process();
317
318        // We expect Claire to be the only remaining group member.
319        let alice_members = network.members(&alice, &group);
320        assert_eq!(
321            alice_members,
322            vec![(GroupMember::Individual(claire), Access::manage()),]
323        );
324
325        let bob_members = network.members(&bob, &group);
326        assert_eq!(
327            bob_members,
328            vec![(GroupMember::Individual(claire), Access::manage()),]
329        );
330
331        let claire_members = network.members(&claire, &group);
332        assert_eq!(
333            claire_members,
334            vec![(GroupMember::Individual(claire), Access::manage()),]
335        );
336
337        // We expect the "ignore" operation set to be empty, indicating that no operations have
338        // been marked as invalid by the resolver.
339        let alice_filter = network.get_y(&alice, &group).ignore;
340        assert!(alice_filter.is_empty());
341
342        let bob_filter = network.get_y(&bob, &group).ignore;
343        assert!(bob_filter.is_empty());
344
345        let claire_filter = network.get_y(&claire, &group).ignore;
346        assert!(claire_filter.is_empty());
347    }
348
349    #[test]
350    fn demote_remove_filter() {
351        //       A
352        //     /   \
353        //    B     C
354        //
355        // Node A: create the group
356        // Node B: Alice demotes Bob to Write
357        // Node C: Bob removes Claire
358        //
359        // We expect the removal of Claire (node C) to be filtered.
360
361        let alice = 'A';
362        let bob = 'B';
363        let claire = 'C';
364
365        let group = '1';
366
367        let rng = StdRng::from_os_rng();
368
369        let mut network = Network::new([alice, bob, claire], rng);
370
371        // Alice creates a group with Alice, Bob and Claire as managers.
372        network.create(
373            group,
374            alice,
375            vec![
376                (GroupMember::Individual(alice), Access::manage()),
377                (GroupMember::Individual(bob), Access::manage()),
378                (GroupMember::Individual(claire), Access::manage()),
379            ],
380        );
381
382        // Everyone processes the operation.
383        network.process();
384
385        // Alice demotes Bob.
386        network.demote(alice, GroupMember::Individual(bob), group, Access::write());
387
388        // Bob removes Claire concurrently.
389        network.remove(bob, GroupMember::Individual(claire), group);
390
391        // Everyone processes these operations.
392        network.process();
393
394        // The demote operation should have been applied.
395        // The remove operation should have been filtered.
396
397        // We expect Alice (Manage), Bob (Write) and Claire (Manage) to be the only group members.
398        let alice_members = network.members(&alice, &group);
399        assert_eq!(
400            alice_members,
401            vec![
402                (GroupMember::Individual(alice), Access::manage()),
403                (GroupMember::Individual(bob), Access::write()),
404                (GroupMember::Individual(claire), Access::manage()),
405            ]
406        );
407
408        let bob_members = network.members(&bob, &group);
409        assert_eq!(
410            bob_members,
411            vec![
412                (GroupMember::Individual(alice), Access::manage()),
413                (GroupMember::Individual(bob), Access::write()),
414                (GroupMember::Individual(claire), Access::manage()),
415            ]
416        );
417
418        let claire_members = network.members(&claire, &group);
419        assert_eq!(
420            claire_members,
421            vec![
422                (GroupMember::Individual(alice), Access::manage()),
423                (GroupMember::Individual(bob), Access::write()),
424                (GroupMember::Individual(claire), Access::manage()),
425            ]
426        );
427
428        // We expect the "ignore" operation set to contain a single operation ID.
429        // The ID should be the same across all members.
430        let alice_filter = network.get_y(&alice, &group).ignore;
431        let bob_filter = network.get_y(&bob, &group).ignore;
432        let claire_filter = network.get_y(&claire, &group).ignore;
433        assert_eq!(alice_filter.len(), 1);
434        assert_eq!(alice_filter, bob_filter);
435        assert_eq!(bob_filter, claire_filter);
436    }
437
438    #[test]
439    fn demote_add_filter() {
440        //       A
441        //     /   \
442        //    B     C
443        //
444        // Node A: create the group
445        // Node B: Alice demotes Bob to Write
446        // Node C: Bob adds Dave
447        //
448        // We expect the addition of Dave (node C) to be filtered.
449
450        let alice = 'A';
451        let bob = 'B';
452        let claire = 'C';
453        let dave = 'D';
454
455        let group = '1';
456
457        let rng = StdRng::from_os_rng();
458
459        let mut network = Network::new([alice, bob, claire, dave], rng);
460
461        // Alice creates a group with Alice, Bob and Claire as managers.
462        network.create(
463            group,
464            alice,
465            vec![
466                (GroupMember::Individual(alice), Access::manage()),
467                (GroupMember::Individual(bob), Access::manage()),
468                (GroupMember::Individual(claire), Access::manage()),
469            ],
470        );
471
472        // Everyone processes the operation.
473        network.process();
474
475        // Alice demotes Bob.
476        network.demote(alice, GroupMember::Individual(bob), group, Access::write());
477
478        // Bob adds Dave concurrently.
479        network.add(bob, GroupMember::Individual(dave), group, Access::read());
480
481        // Everyone processes these operations.
482        network.process();
483
484        // The demote operation should have been applied.
485        // The add operation should have been filtered.
486
487        // We expect Alice (Manage), Bob (Write) and Claire (Manage) to be the only group members.
488        let expected_members = vec![
489            (GroupMember::Individual(alice), Access::manage()),
490            (GroupMember::Individual(bob), Access::write()),
491            (GroupMember::Individual(claire), Access::manage()),
492        ];
493
494        let alice_members = network.members(&alice, &group);
495        assert_eq!(alice_members, expected_members);
496
497        let bob_members = network.members(&bob, &group);
498        assert_eq!(bob_members, expected_members);
499
500        let claire_members = network.members(&claire, &group);
501        assert_eq!(claire_members, expected_members);
502
503        // We expect each filter to contain a single operation: the addition of Dave.
504        let alice_filter = network.get_y(&alice, &group).ignore;
505        assert_eq!(alice_filter.len(), 1);
506
507        let bob_filter = network.get_y(&bob, &group).ignore;
508        assert_eq!(bob_filter.len(), 1);
509
510        let claire_filter = network.get_y(&claire, &group).ignore;
511        assert_eq!(claire_filter.len(), 1);
512    }
513
514    #[test]
515    fn remove_dependencies_filter() {
516        // Tree structure:
517        //       1
518        //     /   \
519        //    2     3
520        //           \
521        //            4
522
523        let group_id = '1';
524        let alice = 'A';
525        let bob = 'B';
526        let claire = 'C';
527        let dave = 'D';
528
529        let mut rng = StdRng::from_os_rng();
530
531        // Create initial group with Alice and Bob
532        let (alice_group, op_create) = create_group(
533            alice,
534            group_id,
535            vec![(alice, Access::manage()), (bob, Access::manage())],
536            &mut rng,
537        );
538
539        let bob_group = from_create(bob, group_id, &op_create, &mut rng);
540        let claire_group = from_create(claire, group_id, &op_create, &mut rng);
541
542        assert_members(
543            &alice_group,
544            &[
545                (GroupMember::Individual(alice), Access::manage()),
546                (GroupMember::Individual(bob), Access::manage()),
547            ],
548        );
549
550        // Alice removes Bob
551        let (alice_group, op_remove_bob) = remove_member(alice_group, group_id, bob);
552
553        assert_members(
554            &alice_group,
555            &[(GroupMember::Individual(alice), Access::manage())],
556        );
557
558        // Bob (in his own branch) adds Claire
559        let (bob_group, op_add_claire) = add_member(bob_group, group_id, claire, Access::manage());
560        let claire_group = sync(claire_group, &[op_add_claire.clone()]);
561
562        assert_members(
563            &bob_group,
564            &[
565                (GroupMember::Individual(alice), Access::manage()),
566                (GroupMember::Individual(bob), Access::manage()),
567                (GroupMember::Individual(claire), Access::manage()),
568            ],
569        );
570
571        // Claire adds Dave
572        let (claire_group, op_add_dave) = add_member(claire_group, group_id, dave, Access::read());
573        let bob_group = sync(bob_group, &[op_add_dave.clone()]);
574
575        assert_members(
576            &bob_group,
577            &[
578                (GroupMember::Individual(alice), Access::manage()),
579                (GroupMember::Individual(bob), Access::manage()),
580                (GroupMember::Individual(claire), Access::manage()),
581                (GroupMember::Individual(dave), Access::read()),
582            ],
583        );
584
585        // Merge branches: everyone sees each other's operations
586        let alice_group = sync(alice_group, &[op_add_claire.clone(), op_add_dave.clone()]);
587        let bob_group = sync(bob_group, &[op_remove_bob.clone()]);
588        let claire_group = sync(claire_group, &[op_remove_bob.clone()]);
589
590        let expected_members = vec![(GroupMember::Individual(alice), Access::manage())];
591
592        assert_members(&alice_group, &expected_members);
593        assert_members(&bob_group, &expected_members);
594        assert_members(&claire_group, &expected_members);
595    }
596
597    #[test]
598    fn remove_readd_dependencies_filter() {
599        //       1
600        //     /   \
601        //    2     4
602        //    |     |
603        //    3     |
604        //     \   /
605        //       5
606        //
607        // Node 1: create the group with Alice, Bob and Claire as managers
608        // Node 2: Alice removes Bob
609        // Node 3: Alice re-adds Bob
610        // Node 4: Bob adds Dave
611        // Node 5: Bob adds Eve
612        //
613        // Filtered: [4]
614        // Final members: [Alice, Bob, Claire, Eve]
615
616        let group_id = 'G';
617        let alice = 'A';
618        let bob = 'B';
619        let claire = 'C';
620        let dave = 'D';
621        let eve = 'E';
622
623        let mut rng = StdRng::from_os_rng();
624
625        // 1: Alice creates group with Alice, Bob, Claire
626        let (alice_group, op_create) = create_group(
627            alice,
628            group_id,
629            vec![
630                (alice, Access::manage()),
631                (bob, Access::manage()),
632                (claire, Access::manage()),
633            ],
634            &mut rng,
635        );
636
637        let bob_group = from_create(bob, group_id, &op_create, &mut rng);
638
639        assert_members(
640            &alice_group,
641            &[
642                (GroupMember::Individual(alice), Access::manage()),
643                (GroupMember::Individual(bob), Access::manage()),
644                (GroupMember::Individual(claire), Access::manage()),
645            ],
646        );
647
648        // 2: Alice removes Bob
649        let (alice_group, op_remove_bob) = remove_member(alice_group, group_id, bob);
650
651        assert_members(
652            &alice_group,
653            &[
654                (GroupMember::Individual(alice), Access::manage()),
655                (GroupMember::Individual(claire), Access::manage()),
656            ],
657        );
658
659        // 3: Alice re-adds Bob
660        let (alice_group, op_readd_bob) = add_member(alice_group, group_id, bob, Access::manage());
661
662        assert_members(
663            &alice_group,
664            &[
665                (GroupMember::Individual(alice), Access::manage()),
666                (GroupMember::Individual(bob), Access::manage()),
667                (GroupMember::Individual(claire), Access::manage()),
668            ],
669        );
670
671        // 4: Bob adds Dave
672        let (bob_group, op_add_dave) = add_member(bob_group, group_id, dave, Access::read());
673
674        assert_members(
675            &bob_group,
676            &[
677                (GroupMember::Individual(alice), Access::manage()),
678                (GroupMember::Individual(bob), Access::manage()),
679                (GroupMember::Individual(claire), Access::manage()),
680                (GroupMember::Individual(dave), Access::read()),
681            ],
682        );
683
684        // Everyone processes the concurrent operations
685        let alice_group = sync(alice_group, &[op_add_dave.clone()]);
686        let bob_group = sync(bob_group, &[op_remove_bob.clone(), op_readd_bob.clone()]);
687
688        // Bob adds Eve
689        let (bob_group, op_add_eve) = add_member(bob_group, group_id, eve, Access::read());
690        let alice_group = sync(alice_group, &[op_add_eve.clone()]);
691
692        // Final assertions: All 4 members should be present
693        let expected = vec![
694            (GroupMember::Individual(alice), Access::manage()),
695            (GroupMember::Individual(bob), Access::manage()),
696            (GroupMember::Individual(claire), Access::manage()),
697            (GroupMember::Individual(eve), Access::read()),
698        ];
699
700        assert_members(&alice_group, &expected);
701        assert_members(&bob_group, &expected);
702    }
703
704    #[test]
705    fn two_bubbles() {
706        // Graph structure:
707        //
708        //       1
709        //     /   \
710        //    2     3
711        //     \   /
712        //       4
713        //       |
714        //       5
715        //     /   \
716        //    6     8
717        //    |
718        //    7
719        //
720        // 1: Alice creates group with initial members Alice (admin) & Bob (admin)
721        // 2: Alice removes Bob
722        // 3: Bob adds Claire
723        // 4: Alice adds Dave (admin)
724        // 5: Dave adds Eve
725        // 6: Alice adds Frank
726        // 7: Frank adds Grace
727        // 8: Dave removes Alice
728        //
729        // Filtered (due to concurrent removal): [3, 6, 7]
730
731        let group_id = '0';
732        let alice = 'A';
733        let bob = 'B';
734        let claire = 'C';
735        let dave = 'D';
736        let eve = 'E';
737        let frank = 'F';
738        let grace = 'G';
739
740        let mut rng = StdRng::from_os_rng();
741
742        // 1: Create initial group with Alice and Bob
743        let (alice_group, op_create) = create_group(
744            alice,
745            group_id,
746            vec![(alice, Access::manage()), (bob, Access::manage())],
747            &mut rng,
748        );
749
750        // Initialize all member groups from the create operation
751        let bob_group = from_create(bob, group_id, &op_create, &mut rng);
752        let dave_group = from_create(dave, group_id, &op_create, &mut rng);
753        let frank_group = from_create(frank, group_id, &op_create, &mut rng);
754
755        assert_members(
756            &alice_group,
757            &[
758                (GroupMember::Individual(alice), Access::manage()),
759                (GroupMember::Individual(bob), Access::manage()),
760            ],
761        );
762
763        // 2: Alice removes Bob
764        let (alice_group, op_remove_bob) = remove_member(alice_group, group_id, bob);
765
766        // 3: Bob adds Claire (concurrent with 2)
767        let (_bob_group, op_add_claire) = add_member(bob_group, group_id, claire, Access::read());
768
769        // Alice processes Bob's operation
770        let alice_group = sync(alice_group, &[op_add_claire.clone()]);
771
772        // 4: Alice adds Dave (merges states 2 & 3)
773        let (alice_group, op_add_dave) = add_member(alice_group, group_id, dave, Access::manage());
774
775        // New member Dave syncs state
776        let dave_group = sync(
777            dave_group,
778            &[
779                op_remove_bob.clone(),
780                op_add_claire.clone(),
781                op_add_dave.clone(),
782            ],
783        );
784
785        // 5: Dave adds Eve (depends on 4)
786        let (dave_group, op_add_eve) = add_member(dave_group, group_id, eve, Access::read());
787
788        let alice_group = sync(alice_group, &[op_add_eve.clone()]);
789
790        // 6: Alice adds Frank (concurrent with 8)
791        let (_alice_group, op_add_frank) =
792            add_member(alice_group, group_id, frank, Access::manage());
793
794        let frank_group = sync(
795            frank_group,
796            &[
797                op_remove_bob.clone(),
798                op_add_claire.clone(),
799                op_add_dave.clone(),
800                op_add_eve.clone(),
801                op_add_frank.clone(),
802            ],
803        );
804
805        // 7: Frank adds Grace (concurrent with 8)
806        let (_, op_add_grace) = add_member(frank_group, group_id, grace, Access::read());
807
808        // 8: Dave removes Alice (concurrently with 6 & 7)
809        let (dave_group, _op_remove_alice) = remove_member(dave_group, group_id, alice);
810
811        let dave_group = sync(dave_group, &[op_add_frank.clone(), op_add_grace.clone()]);
812
813        let expected_members = vec![
814            (GroupMember::Individual(dave), Access::manage()),
815            (GroupMember::Individual(eve), Access::read()),
816        ];
817
818        let mut dave_members = dave_group.members();
819        dave_members.sort();
820        assert_eq!(expected_members, dave_members);
821    }
822}