Skip to main content

p2panda_auth/group/crdt/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3pub(crate) mod state;
4
5use std::collections::{HashMap, HashSet};
6use std::fmt::Debug;
7use std::marker::PhantomData;
8
9use p2panda_core::traits::{Author, OperationId};
10use petgraph::prelude::DiGraphMap;
11use petgraph::visit::{Bfs, DfsPostOrder, IntoNodeIdentifiers, NodeIndexable, Reversed};
12#[cfg(any(test, feature = "serde"))]
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::access::Access;
17use crate::group::{GroupAction, GroupMember, GroupMembersState, GroupMembershipError};
18use crate::traits::{Conditions, Operation, Resolver};
19
20/// Max depth of group nesting allowed.
21///
22/// Depth is checked during group state queries and if the depth is exceeded further additions are
23/// ignored. The main reason for this check is to protect against accidental group nesting cycles
24/// which may occur as a result of concurrent operations.
25const MAX_NESTED_DEPTH: u32 = 1000;
26
27/// Inner error types for GroupCrdt.
28#[derive(Debug, Error)]
29pub enum GroupCrdtInnerError<OP> {
30    #[error("states {0:?} not found")]
31    StatesNotFound(Vec<OP>),
32}
33
34/// Error types for GroupCrdt.
35#[derive(Debug, Error)]
36pub enum GroupCrdtError<ID, OP, M, C, RS>
37where
38    ID: Author,
39    OP: OperationId + Ord,
40    RS: Resolver<ID, OP, M, C>,
41{
42    #[error(transparent)]
43    Inner(#[from] GroupCrdtInnerError<OP>),
44
45    #[error("duplicate operation {0} processed in group {1}")]
46    DuplicateOperation(OP, ID),
47
48    #[error("group cycle detected adding {0} to {1} operation={2}")]
49    GroupCycle(ID, ID, OP),
50
51    #[error("state change error processing operation {0}: {1:?}")]
52    StateChangeError(OP, GroupMembershipError<GroupMember<ID>>),
53
54    #[error("attempted to add group {0} with manage access")]
55    ManagerGroupsNotAllowed(ID),
56
57    #[error("resolver error: {0}")]
58    Resolver(RS::Error),
59}
60
61pub(crate) type GroupStates<ID, C> = HashMap<ID, GroupMembersState<GroupMember<ID>, C>>;
62
63/// Inner state object for `GroupCrdt` which contains the actual groups state,
64/// including operation graph and membership snapshots.
65#[derive(Debug)]
66#[cfg_attr(any(test, feature = "test_utils"), derive(Clone))]
67#[cfg_attr(
68    any(test, feature = "serde"),
69    derive(Deserialize, Serialize),
70    serde(bound(
71        deserialize = "
72            OP: Deserialize<'de>,
73            M: Deserialize<'de>,
74            C: Deserialize<'de>,
75        ",
76        serialize = "
77            OP: Serialize,
78            M: Serialize,
79            C: Serialize,
80        "
81    ))
82)]
83pub struct GroupCrdtInnerState<ID, OP, M, C>
84where
85    ID: Author,
86    OP: OperationId + Ord,
87{
88    /// All operations processed by this group.
89    pub operations: HashMap<OP, M>,
90
91    /// All operations who's actions should be ignored.
92    pub ignore: HashSet<OP>,
93
94    /// All operations which are part of a mutual remove cycle.
95    pub mutual_removes: HashSet<OP>,
96
97    /// All resolved states.
98    pub states: HashMap<OP, GroupStates<ID, C>>,
99
100    /// Operation graph of all auth operations.
101    pub graph: DiGraphMap<OP, ()>,
102}
103
104impl<ID, OP, M, C> Default for GroupCrdtInnerState<ID, OP, M, C>
105where
106    ID: Author,
107    OP: OperationId + Ord,
108{
109    fn default() -> Self {
110        Self {
111            operations: Default::default(),
112            ignore: Default::default(),
113            mutual_removes: Default::default(),
114            states: Default::default(),
115            graph: Default::default(),
116        }
117    }
118}
119
120impl<ID, OP, M, C> GroupCrdtInnerState<ID, OP, M, C>
121where
122    ID: Author,
123    OP: OperationId + Ord,
124    M: Operation<ID, OP, C>,
125    C: Conditions,
126{
127    /// Current tips for the groups operation graph.
128    pub fn heads(&self) -> HashSet<OP> {
129        self.graph
130            // TODO: clone required here when converting the GraphMap into a Graph. We do this
131            // because the GraphMap api does not include the "externals" method, where as the
132            // Graph api does. We use GraphMap as we can then access nodes by the id we assign
133            // them rather than the internally assigned id generated when using Graph. We can use
134            // Graph and track the indexes ourselves in order to avoid this conversion, or maybe
135            // there is a way to get "externals" on GraphMap (which I didn't find yet). More
136            // investigation required.
137            .clone()
138            .into_graph::<usize>()
139            .externals(petgraph::Direction::Outgoing)
140            .map(|idx| self.graph.from_index(idx.index()))
141            .collect::<HashSet<_>>()
142    }
143
144    /// Get graph tips filtered to only those which included "create" operation for passed group
145    /// ids in their causal history.
146    pub fn heads_filtered(&self, groups: &[ID]) -> HashSet<OP> {
147        let global_heads = self.heads();
148        global_heads
149            .into_iter()
150            .filter(|id| {
151                let reversed = Reversed(&self.graph);
152                let mut bfs = Bfs::new(&reversed, *id);
153                while let Some(inner_id) = bfs.next(&reversed) {
154                    let operation = self
155                        .operations
156                        .get(&inner_id)
157                        .expect("operation is present in map");
158                    if operation.action().is_create() && groups.contains(&operation.group_id()) {
159                        return true;
160                    }
161                }
162                false
163            })
164            .collect()
165    }
166
167    /// Current group states.
168    ///
169    /// This method gets the state at all graph tips and then merges them together into one new
170    /// state which represents the current state of the groups.
171    pub fn current_state(&self) -> GroupStates<ID, C> {
172        self.merge_states(&self.heads())
173            .expect("states exist for processed operations")
174    }
175
176    /// Get the state at a certain point in history.
177    pub fn state_at(
178        &self,
179        dependencies: &HashSet<OP>,
180    ) -> Result<GroupStates<ID, C>, GroupCrdtInnerError<OP>> {
181        self.merge_states(dependencies)
182    }
183
184    /// Merge multiple states together.
185    fn merge_states(
186        &self,
187        ids: &HashSet<OP>,
188    ) -> Result<GroupStates<ID, C>, GroupCrdtInnerError<OP>> {
189        let mut current_state = HashMap::new();
190        for id in ids {
191            // Unwrap as this method is only used internally where all requested states should exist.
192            let group_states = match self.states.get(id) {
193                Some(group_states) => group_states.clone(),
194                None => {
195                    return Err(GroupCrdtInnerError::StatesNotFound(
196                        ids.iter().cloned().collect(),
197                    ));
198                }
199            };
200            for (id, state) in group_states.into_iter() {
201                current_state
202                    .entry(id)
203                    .and_modify(
204                        |current_state: &mut GroupMembersState<GroupMember<ID>, C>| {
205                            *current_state = state::merge(state.clone(), current_state.clone())
206                        },
207                    )
208                    .or_insert(state);
209            }
210        }
211        Ok(current_state)
212    }
213
214    fn members_inner(
215        &self,
216        group_id: ID,
217        members: &mut HashMap<GroupMember<ID>, Access<C>>,
218        root_access: Option<Access<C>>,
219        mut depth: u32,
220    ) {
221        // If we reached max nesting depth exit from the traversal.
222        if depth == MAX_NESTED_DEPTH {
223            return;
224        }
225        depth += 1;
226
227        let current_states = self.current_state();
228        let Some(group_state) = current_states.get(&group_id) else {
229            return;
230        };
231
232        for (member, access) in group_state.access_levels() {
233            // As we recurse into sub-groups we must assure that the newly assignable access level
234            // is never higher than the previous root access level. To do this we take whichever is
235            // less.
236            let next_access = match root_access.clone() {
237                Some(root_access) => {
238                    if access <= root_access {
239                        access.clone()
240                    } else {
241                        root_access
242                    }
243                }
244                None => access.clone(),
245            };
246
247            members
248                .entry(member)
249                .and_modify(|current_access| {
250                    // If the transitive access level this member holds (the access level
251                    // the member has in it's sub-group) is greater than it's current access
252                    // level, but not greater than the root access level (the access level
253                    // initially assigned from the parent group) then update the access
254                    // level.
255
256                    // @TODO: we need to combine access levels here, which requires adding a
257                    // trait bound to conditions which allows combining them as well. Or we
258                    // return an array of access levels for each peer.
259                    if *current_access < next_access {
260                        *current_access = next_access.clone();
261                    }
262                })
263                .or_insert_with(|| next_access.clone());
264
265            if let GroupMember::Group(id) = member {
266                self.members_inner(id, members, Some(next_access), depth)
267            }
268        }
269    }
270
271    /// Get all current individual members of a group.
272    pub fn members(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
273        self.traverse_members(group_id, 0)
274            .into_iter()
275            .filter_map(|(member, access)| {
276                if member.is_individual() {
277                    Some((member.id(), access))
278                } else {
279                    None
280                }
281            })
282            .collect()
283    }
284
285    /// Get all current transitive groups inside a group.
286    pub fn groups(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
287        self.traverse_members(group_id, 0)
288            .into_iter()
289            .filter_map(|(member, access)| {
290                if member.is_group() {
291                    Some((member.id(), access))
292                } else {
293                    None
294                }
295            })
296            .collect()
297    }
298
299    /// Traverse membership graph with a specified max. depth and return all visited members
300    /// (individuals and transitive groups) of a group.
301    ///
302    /// Set depth to 0 to traverse the full graph.
303    pub fn traverse_members(&self, group_id: ID, depth: u32) -> Vec<(GroupMember<ID>, Access<C>)> {
304        let mut members = HashMap::new();
305        self.members_inner(group_id, &mut members, None, depth);
306        members.into_iter().collect()
307    }
308
309    pub(crate) fn would_create_cycle(&self, operation: &M) -> bool {
310        let parent_group_id = operation.group_id();
311
312        if let GroupAction::Add {
313            member: GroupMember::Group(child_group_id),
314            ..
315        } = &operation.action()
316        {
317            let states = self.current_state();
318            let mut stack = vec![*child_group_id];
319            let mut visited = HashSet::new();
320
321            while let Some(child_group_id) = stack.pop() {
322                if !visited.insert(child_group_id) {
323                    continue;
324                }
325                if child_group_id == parent_group_id {
326                    // Found a path from child group to parent.
327                    return true;
328                }
329                if let Some(group_state) = states.get(&child_group_id) {
330                    for (member, _) in group_state.access_levels() {
331                        if let GroupMember::Group(id) = member {
332                            stack.push(id);
333                        }
334                    }
335                }
336            }
337        }
338
339        false
340    }
341}
342
343/// State object for `GroupCrdt` containing an orderer state and the inner
344/// state.
345#[derive(Debug)]
346#[cfg_attr(any(test, feature = "test_utils"), derive(Clone))]
347#[cfg_attr(
348    any(test, feature = "serde"),
349    derive(Deserialize, Serialize),
350    serde(bound(
351        deserialize = "
352            OP: Deserialize<'de>,
353            M: Deserialize<'de>,
354            C: Deserialize<'de>,
355        ",
356        serialize = "
357            OP: Serialize,
358            M: Serialize,
359            C: Serialize,
360        "
361    ))
362)]
363pub struct GroupCrdtState<ID, OP, M, C>
364where
365    ID: Author,
366    OP: OperationId + Ord,
367{
368    /// Inner groups state.
369    pub inner: GroupCrdtInnerState<ID, OP, M, C>,
370}
371
372impl<ID, OP, M, C> Default for GroupCrdtState<ID, OP, M, C>
373where
374    ID: Author,
375    OP: OperationId + Ord,
376    M: Operation<ID, OP, C>,
377    C: Conditions,
378{
379    fn default() -> Self {
380        Self {
381            inner: Default::default(),
382        }
383    }
384}
385
386impl<ID, OP, M, C> GroupCrdtState<ID, OP, M, C>
387where
388    ID: Author,
389    OP: OperationId + Ord,
390    M: Operation<ID, OP, C>,
391    C: Conditions,
392{
393    /// Instantiate a new state.
394    pub fn new() -> Self {
395        Self::default()
396    }
397
398    /// Get all direct members of a group.
399    ///
400    /// This method does not recurse into sub-groups, but rather returns only
401    /// the direct group members and their access levels.
402    pub fn root_members(&self, group_id: ID) -> Vec<(GroupMember<ID>, Access<C>)> {
403        match self.inner.current_state().get(&group_id) {
404            Some(group_y) => group_y.access_levels(),
405            None => vec![],
406        }
407    }
408
409    /// Get all individuals of a group.
410    ///
411    /// This method recurses into all sub-groups and returns a resolved list of individual group
412    /// members and their access levels.
413    pub fn members(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
414        self.inner.members(group_id)
415    }
416
417    /// Get all transitive groups inside a group.
418    ///
419    /// This method recurses into all sub-groups and returns a resolved list of nested group members
420    /// and their access levels.
421    pub fn groups(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
422        self.inner.groups(group_id)
423    }
424
425    /// Traverse membership graph with a specified max. depth and return all visited members
426    /// (individuals and transitive groups) of a group.
427    ///
428    /// Set depth to 0 to traverse the full graph.
429    pub fn traverse_members(&self, group_id: ID, depth: u32) -> Vec<(GroupMember<ID>, Access<C>)> {
430        self.inner.traverse_members(group_id, depth)
431    }
432
433    /// Returns `true` if the passed group exists in the current state.
434    pub fn has_group(&self, group_id: ID) -> bool {
435        self.inner.current_state().contains_key(&group_id)
436    }
437
438    /// Current tips for the groups operation graph.
439    pub fn heads(&self) -> Vec<OP> {
440        self.inner.heads().into_iter().collect()
441    }
442
443    /// Get graph tips filtered to only those which included "create" operation for passed group
444    /// ids in their causal history.
445    pub fn heads_filtered(&self, groups: &[ID]) -> Vec<OP> {
446        self.inner.heads_filtered(groups).into_iter().collect()
447    }
448}
449
450/// Core group CRDT for maintaining group membership state in a decentralized
451/// system.
452///
453/// Group members can be assigned different access levels, where only a sub-set
454/// of members can mutate the state of the group itself. Group members can be
455/// (immutable) individuals or (mutable) sub-groups.
456///
457/// The core data type is a Directed Acyclic Graph of all operations containing
458/// group management actions. Operations refer to the previous global state (set
459/// of graph tips) in their "dependencies" field, this is the local state when
460/// an actor creates a new auth action; these references make up the edges in
461/// the graph.
462///
463/// A requirement of the protocol is that all messages are processed in
464/// partial-order. When using a dependency graph structure (as is the case in
465/// this implementation) it is possible to achieve partial-ordering by only
466/// processing a message once all it's dependencies have themselves been
467/// processed.
468///
469/// Group state is maintained using the state object `GroupMembersState`. Every
470/// time an action is processed, a new state is generated and added to the map
471/// of all states. When a new operation is received, it's previous state is
472/// calculated and then the message applied, resulting in a new state.
473///
474/// Group membership rules are checked when an action is applied to the previous
475/// state, read more in the `crdt::state` module.
476///
477/// The struct has several generic parameters which allow users to specify their
478/// own core types and to customise behavior when handling concurrent changes
479/// when resolving a graph to it's final state.
480///
481/// - ID : identifier for both an individual actor and group.
482/// - OP : identifier for an operation.
483/// - C  : conditions which restrict an access level.
484/// - RS : generic resolver which contains logic for deciding when group state
485///   rebuilds are required, and how concurrent actions are handled. See the
486///   `resolver` module for different implementations.
487/// - ORD: orderer which exposes an API for creating and processing operations
488///   with meta-data which allow them to be processed in partial order.
489#[derive(Clone, Debug, Default)]
490pub struct GroupCrdt<ID, OP, M, C, RS> {
491    _phantom: PhantomData<(ID, OP, M, C, RS)>,
492}
493
494impl<ID, OP, M, C, RS> GroupCrdt<ID, OP, M, C, RS>
495where
496    ID: Author,
497    OP: OperationId + Ord,
498    M: Operation<ID, OP, C> + Clone,
499    C: Conditions,
500    RS: Resolver<ID, OP, M, C, State = GroupCrdtInnerState<ID, OP, M, C>>,
501{
502    pub fn init() -> GroupCrdtState<ID, OP, M, C> {
503        GroupCrdtState {
504            inner: GroupCrdtInnerState::default(),
505        }
506    }
507
508    /// Process an operation created locally or received from a remote peer.
509    #[allow(clippy::type_complexity)]
510    pub fn process(
511        mut y: GroupCrdtState<ID, OP, M, C>,
512        operation: &M,
513    ) -> Result<GroupCrdtState<ID, OP, M, C>, GroupCrdtError<ID, OP, M, C, RS>> {
514        let operation_id = operation.id();
515        let actor = operation.author();
516        let dependencies = HashSet::from_iter(operation.dependencies().clone());
517        let group_id = operation.group_id();
518        let rebuild_required =
519            RS::rebuild_required(&y.inner, operation).map_err(GroupCrdtError::Resolver)?;
520
521        // Validate that the author of this operation had the required access rights at the point
522        // in the auth graph which they claim as their last state (the state at "dependencies").
523        // It could be that they had access at this point but concurrent changes (which we know
524        // about) mean that they have lost that access level. This case is dealt with later, here
525        // we want to catch malicious or invalid operations which should _never_ be attached to
526        // the graph.
527        y = GroupCrdt::validate(y, operation)?;
528        y = Self::add_operation(y, operation);
529
530        if rebuild_required {
531            y.inner = RS::process(y.inner).map_err(GroupCrdtError::Resolver)?;
532            return Ok(y);
533        }
534
535        // We don't need to check the state change result as validation was already performed
536        // above.
537        let mut groups_y = y.inner.state_at(&dependencies)?;
538        groups_y = apply_action(
539            groups_y,
540            group_id,
541            operation_id,
542            actor,
543            &operation.action(),
544            &y.inner.ignore,
545        )
546        .state()
547        .to_owned();
548
549        y.inner.states.insert(operation_id, groups_y);
550
551        Ok(y)
552    }
553
554    /// Validate an action by applying it to the group state build to it's previous pointers.
555    ///
556    /// When processing a new operation we need to validate that the contained action is valid
557    /// before including it in the graph. By valid we mean that the author who composed the action
558    /// had authority to perform the claimed action, and that the action fulfils all group change
559    /// requirements. To check this we need to re-build the group state to the operations claimed
560    /// previous state. This process involves pruning any operations which are not predecessors of
561    /// the new operation resolving the group state again.
562    ///
563    /// This is a relatively expensive computation and should only be used when a re-build is
564    /// actually required.
565    #[allow(clippy::type_complexity)]
566    pub(crate) fn validate(
567        y: GroupCrdtState<ID, OP, M, C>,
568        operation: &M,
569    ) -> Result<GroupCrdtState<ID, OP, M, C>, GroupCrdtError<ID, OP, M, C, RS>> {
570        // Detect already processed operations.
571        if y.inner.operations.contains_key(&operation.id()) {
572            // The operation has already been processed.
573            return Err(GroupCrdtError::DuplicateOperation(
574                operation.id(),
575                operation.group_id(),
576            ));
577        }
578
579        // Adding a group as a manager of another group is currently not
580        // supported.
581        //
582        // @TODO: To support this behavior updates in the StrongRemove resolver
583        // so that cross-group concurrent remove cycles are detected. Related to
584        // issue: https://github.com/p2panda/p2panda/issues/779
585        match &operation.action() {
586            GroupAction::Add { member, access } | GroupAction::Promote { member, access }
587                if member.is_group() && access.is_manage() =>
588            {
589                return Err(GroupCrdtError::ManagerGroupsNotAllowed(member.id()));
590            }
591            _ => (),
592        };
593
594        let last_graph = y.inner.graph.clone();
595        let last_ignore = y.inner.ignore.clone();
596        let last_mutual_removes = y.inner.mutual_removes.clone();
597        let last_states = y.inner.states.clone();
598
599        let dependencies = HashSet::from_iter(operation.dependencies().clone());
600
601        // If this operation is concurrent to our current local state we need to rebuild the graph
602        // to the operations' claimed dependencies in order to validate it correctly.
603        let temp_y = if y.inner.heads() != dependencies {
604            let mut temp_y = y;
605
606            // Collect predecessors of the new operation.
607            let mut predecessors = HashSet::new();
608            for dependency in operation.dependencies() {
609                let reversed = Reversed(&temp_y.inner.graph);
610                let mut dfs_rev = DfsPostOrder::new(&reversed, dependency);
611                while let Some(id) = dfs_rev.next(&reversed) {
612                    predecessors.insert(id);
613                }
614            }
615
616            // Remove all other nodes from the graph.
617            let to_remove: Vec<_> = temp_y
618                .inner
619                .graph
620                .node_identifiers()
621                .filter(|n| !predecessors.contains(n))
622                .collect();
623
624            for node in &to_remove {
625                temp_y.inner.graph.remove_node(*node);
626            }
627
628            temp_y.inner = RS::process(temp_y.inner).map_err(GroupCrdtError::Resolver)?;
629            temp_y
630        } else {
631            y
632        };
633
634        // Detect if this operation would cause a nested group cycle.
635        if temp_y.inner.would_create_cycle(operation) {
636            let parent_group = operation.group_id();
637
638            // Only adds cause a cycle, we just access the member id here.
639            let GroupAction::Add {
640                member: sub_group, ..
641            } = operation.action()
642            else {
643                unreachable!()
644            };
645
646            return Err(GroupCrdtError::GroupCycle(
647                parent_group,
648                sub_group.id(),
649                operation.id(),
650            ));
651        }
652
653        // Apply the operation onto the temporary state.
654        let result = apply_action(
655            temp_y.inner.current_state(),
656            operation.group_id(),
657            operation.id(),
658            operation.author(),
659            &operation.action(),
660            &temp_y.inner.ignore,
661        );
662
663        match result {
664            StateChangeResult::Ok { state } => state,
665            StateChangeResult::Error { error, .. } => {
666                // Noop shouldn't happen when processing new operations as the
667                // rebuild logic should have occurred instead.
668                return Err(GroupCrdtError::StateChangeError(operation.id(), error));
669            }
670            StateChangeResult::Filtered { .. } => {
671                // Operations can't be filtered out before they were processed.
672                unreachable!();
673            }
674        };
675
676        let mut y = temp_y;
677        y.inner.graph = last_graph;
678        y.inner.ignore = last_ignore;
679        y.inner.mutual_removes = last_mutual_removes;
680        y.inner.states = last_states;
681
682        Ok(y)
683    }
684
685    /// Add an operation to the auth graph and operation map.
686    ///
687    /// NOTE: this method _does not_ process the operation so no new state is derived.
688    fn add_operation(
689        mut y: GroupCrdtState<ID, OP, M, C>,
690        operation: &M,
691    ) -> GroupCrdtState<ID, OP, M, C> {
692        let operation_id = operation.id();
693        let dependencies = operation.dependencies();
694
695        // Add operation to the global auth graph.
696        y.inner.graph.add_node(operation_id);
697        for dependency in &dependencies {
698            y.inner.graph.add_edge(*dependency, operation_id, ());
699        }
700
701        // Insert operation into all operations map.
702        y.inner.operations.insert(operation_id, operation.clone());
703
704        y
705    }
706}
707
708/// Apply an action to a single group state.
709pub(crate) fn apply_action<ID, OP, C>(
710    mut groups_y: GroupStates<ID, C>,
711    group_id: ID,
712    id: OP,
713    actor: ID,
714    action: &GroupAction<ID, C>,
715    filter: &HashSet<OP>,
716) -> StateChangeResult<ID, C>
717where
718    ID: Author,
719    OP: OperationId + Ord,
720    C: Conditions,
721{
722    let members_y = if action.is_create() {
723        GroupMembersState::default()
724    } else {
725        groups_y
726            .remove(&group_id)
727            .expect("group already present in states map")
728    };
729
730    if filter.contains(&id) {
731        groups_y.insert(group_id, members_y);
732        return StateChangeResult::Filtered { state: groups_y };
733    }
734
735    let result = match action.clone() {
736        GroupAction::Add { member, access, .. } => state::add(
737            members_y.clone(),
738            GroupMember::Individual(actor),
739            member,
740            access,
741        ),
742        GroupAction::Remove { member, .. } => {
743            state::remove(members_y.clone(), GroupMember::Individual(actor), member)
744        }
745        GroupAction::Promote { member, access } => state::promote(
746            members_y.clone(),
747            GroupMember::Individual(actor),
748            member,
749            access,
750        ),
751        GroupAction::Demote { member, access } => state::demote(
752            members_y.clone(),
753            GroupMember::Individual(actor),
754            member,
755            access,
756        ),
757        GroupAction::Create { initial_members } => Ok(state::create(&initial_members)),
758    };
759
760    match result {
761        Ok(members_y_i) => {
762            groups_y.insert(group_id, members_y_i);
763            StateChangeResult::Ok { state: groups_y }
764        }
765        Err(err) => {
766            // Errors occur here because the member attempting to perform an action
767            // doesn't have a suitable access level, or that the action itself is invalid
768            // (eg. promoting a non-existent member).
769            //
770            // 1) We expect some errors to occur when when intentionally filtered out
771            //    actions cause later operations to become invalid.
772            //
773            // 2) Operations which other peers accepted into their graph _before_
774            //    receiving some concurrent operation which caused them to be invalid.
775            //
776            // In both cases it's critical that the action does not cause any state
777            // change, however we do want to accept them into our graph so as to ensure
778            // consistency consistency across peers.
779            groups_y.insert(group_id, members_y);
780            StateChangeResult::Error {
781                state: groups_y,
782                error: err,
783            }
784        }
785    }
786}
787
788/// Apply a remove operation without validating it against state change rules. This is required
789/// when retaining mutual-remove operations which may have lost their delegated access rights.
790pub(crate) fn apply_remove_unsafe<ID, C>(
791    mut groups_y: GroupStates<ID, C>,
792    group_id: ID,
793    removed: GroupMember<ID>,
794) -> GroupStates<ID, C>
795where
796    ID: Author,
797    C: Conditions,
798{
799    let mut members_y = groups_y
800        .remove(&group_id)
801        .expect("group already present in states map");
802
803    members_y.members.entry(removed).and_modify(|state| {
804        if state.member_counter % 2 != 0 {
805            state.member_counter += 1
806        }
807    });
808    groups_y.insert(group_id, members_y);
809    groups_y
810}
811
812/// Return types expected from applying an action to group state.
813pub enum StateChangeResult<ID, C>
814where
815    ID: Author,
816    C: Conditions,
817{
818    /// Action was applied and no error occurred.
819    Ok { state: GroupStates<ID, C> },
820
821    /// Action was not applied because it failed internal validation.
822    Error {
823        state: GroupStates<ID, C>,
824        #[allow(unused)]
825        error: GroupMembershipError<GroupMember<ID>>,
826    },
827
828    /// Action was not applied because it has been filtered out.
829    Filtered { state: GroupStates<ID, C> },
830}
831
832impl<ID, C> StateChangeResult<ID, C>
833where
834    ID: Author,
835    C: Conditions,
836{
837    pub fn state(&self) -> &GroupStates<ID, C> {
838        match self {
839            StateChangeResult::Ok { state }
840            | StateChangeResult::Error { state, .. }
841            | StateChangeResult::Filtered { state } => state,
842        }
843    }
844}
845
846#[cfg(test)]
847pub(crate) mod tests {
848    pub use p2panda_core::cbor::{decode_cbor, encode_cbor};
849
850    use crate::Access;
851    use crate::group::{GroupCrdtError, GroupMember, GroupMembershipError};
852    use crate::test_utils::{
853        TestGroup, TestGroupState, add_member, create_group, demote_member, promote_member,
854        remove_member,
855    };
856    use crate::traits::Operation;
857
858    const G1: char = '1';
859    const G2: char = '2';
860    const G3: char = '3';
861    const G4: char = '4';
862
863    const ALICE: char = 'A';
864    const BOB: char = 'B';
865    const CLAIRE: char = 'C';
866    const DAN: char = 'D';
867    const EVE: char = 'E';
868
869    #[test]
870    fn group_operations() {
871        let y = TestGroupState::new();
872
873        let op1 = create_group(
874            ALICE,
875            0,
876            G1,
877            vec![(GroupMember::Individual(ALICE), Access::manage())],
878            vec![],
879        );
880
881        let y_i = TestGroup::process(y, &op1).unwrap();
882        let mut members = y_i.members(G1);
883        members.sort();
884        assert_eq!(members, vec![(ALICE, Access::manage())]);
885
886        let op2 = add_member(
887            ALICE,
888            1,
889            G1,
890            GroupMember::Individual(BOB),
891            Access::read(),
892            vec![op1.id()],
893        );
894
895        let y_ii = TestGroup::process(y_i, &op2).unwrap();
896        let mut members = y_ii.members(G1);
897        members.sort();
898        assert_eq!(
899            members,
900            vec![(ALICE, Access::manage()), (BOB, Access::read())]
901        );
902
903        let op3 = add_member(
904            ALICE,
905            2,
906            G1,
907            GroupMember::Individual(CLAIRE),
908            Access::write(),
909            vec![op2.id()],
910        );
911
912        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
913        let mut members = y_iii.members(G1);
914        members.sort();
915        assert_eq!(
916            members,
917            vec![
918                (ALICE, Access::manage()),
919                (BOB, Access::read()),
920                (CLAIRE, Access::write())
921            ]
922        );
923
924        let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op3.id()]);
925
926        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
927        let mut members = y_iv.members(G1);
928        members.sort();
929        assert_eq!(
930            members,
931            vec![(ALICE, Access::manage()), (CLAIRE, Access::write())]
932        );
933    }
934
935    #[test]
936    fn self_remove() {
937        let y = TestGroupState::new();
938
939        let op1 = create_group(
940            ALICE,
941            0,
942            G1,
943            vec![(GroupMember::Individual(ALICE), Access::manage())],
944            vec![],
945        );
946
947        let y_i = TestGroup::process(y, &op1).unwrap();
948        let mut members = y_i.members(G1);
949        members.sort();
950        assert_eq!(members, vec![(ALICE, Access::manage())]);
951
952        let op2 = remove_member(ALICE, 1, G1, GroupMember::Individual(ALICE), vec![op1.id()]);
953
954        let y_i = TestGroup::process(y_i, &op2).unwrap();
955        let mut members = y_i.members(G1);
956        members.sort();
957        assert_eq!(members, vec![]);
958    }
959
960    #[test]
961    fn concurrent_removal() {
962        let y = TestGroupState::new();
963
964        let op1 = create_group(
965            ALICE,
966            0,
967            G1,
968            vec![(GroupMember::Individual(ALICE), Access::manage())],
969            vec![],
970        );
971
972        let y_i = TestGroup::process(y, &op1).unwrap();
973        let mut members = y_i.members(G1);
974        members.sort();
975        assert_eq!(members, vec![(ALICE, Access::manage())]);
976
977        let op2 = add_member(
978            ALICE,
979            1,
980            G1,
981            GroupMember::Individual(BOB),
982            Access::manage(),
983            vec![op1.id()],
984        );
985
986        let y_ii = TestGroup::process(y_i, &op2).unwrap();
987        let mut members = y_ii.members(G1);
988        members.sort();
989        assert_eq!(
990            members,
991            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
992        );
993
994        let op3 = add_member(
995            BOB,
996            2,
997            G1,
998            GroupMember::Individual(CLAIRE),
999            Access::write(),
1000            vec![op2.id()],
1001        );
1002
1003        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1004        let mut members = y_iii.members(G1);
1005        members.sort();
1006        assert_eq!(
1007            members,
1008            vec![
1009                (ALICE, Access::manage()),
1010                (BOB, Access::manage()),
1011                (CLAIRE, Access::write())
1012            ]
1013        );
1014
1015        let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op2.id()]);
1016
1017        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
1018        let mut members = y_iv.members(G1);
1019        members.sort();
1020        assert_eq!(members, vec![(ALICE, Access::manage())]);
1021    }
1022
1023    #[test]
1024    fn mutual_concurrent_removal() {
1025        let y = TestGroupState::new();
1026
1027        let op1 = create_group(
1028            ALICE,
1029            0,
1030            G1,
1031            vec![(GroupMember::Individual(ALICE), Access::manage())],
1032            vec![],
1033        );
1034
1035        let y_i = TestGroup::process(y, &op1).unwrap();
1036        let mut members = y_i.members(G1);
1037        members.sort();
1038        assert_eq!(members, vec![(ALICE, Access::manage())]);
1039
1040        let op2 = add_member(
1041            ALICE,
1042            1,
1043            G1,
1044            GroupMember::Individual(BOB),
1045            Access::manage(),
1046            vec![op1.id()],
1047        );
1048
1049        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1050        let mut members = y_ii.members(G1);
1051        members.sort();
1052        assert_eq!(
1053            members,
1054            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
1055        );
1056
1057        let op3 = add_member(
1058            BOB,
1059            2,
1060            G1,
1061            GroupMember::Individual(CLAIRE),
1062            Access::manage(),
1063            vec![op2.id()],
1064        );
1065
1066        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1067        let mut members = y_iii.members(G1);
1068        members.sort();
1069        assert_eq!(
1070            members,
1071            vec![
1072                (ALICE, Access::manage()),
1073                (BOB, Access::manage()),
1074                (CLAIRE, Access::manage())
1075            ]
1076        );
1077
1078        let op4 = remove_member(BOB, 3, G1, GroupMember::Individual(CLAIRE), vec![op3.id()]);
1079
1080        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
1081        let mut members = y_iv.members(G1);
1082        members.sort();
1083        assert_eq!(
1084            members,
1085            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
1086        );
1087
1088        let op5 = remove_member(CLAIRE, 4, G1, GroupMember::Individual(BOB), vec![op3.id()]);
1089
1090        let y_v = TestGroup::process(y_iv, &op5).unwrap();
1091        let mut members = y_v.members(G1);
1092        members.sort();
1093        assert_eq!(members, vec![(ALICE, Access::manage())]);
1094    }
1095
1096    #[test]
1097    fn nested_groups() {
1098        let y = TestGroupState::new();
1099
1100        let op1 = create_group(
1101            ALICE,
1102            0,
1103            G1,
1104            vec![(GroupMember::Individual(ALICE), Access::manage())],
1105            vec![],
1106        );
1107
1108        let y_i = TestGroup::process(y, &op1).unwrap();
1109        let mut members = y_i.members(G1);
1110        members.sort();
1111        assert_eq!(members, vec![(ALICE, Access::manage())]);
1112
1113        let op2 = create_group(
1114            BOB,
1115            1,
1116            G2,
1117            vec![(GroupMember::Individual(BOB), Access::manage())],
1118            vec![op1.id()],
1119        );
1120
1121        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1122        let mut members = y_ii.members(G2);
1123        members.sort();
1124        assert_eq!(members, vec![(BOB, Access::manage())]);
1125
1126        let op3 = add_member(
1127            ALICE,
1128            2,
1129            G1,
1130            GroupMember::Group(G2),
1131            Access::read(),
1132            vec![op2.id()],
1133        );
1134
1135        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1136        let mut individuals = y_iii.members(G1);
1137        individuals.sort();
1138        assert_eq!(
1139            individuals,
1140            vec![(ALICE, Access::manage()), (BOB, Access::read())]
1141        );
1142
1143        let mut groups = y_iii.groups(G1);
1144        groups.sort();
1145        assert_eq!(groups, vec![(G2, Access::read())]);
1146    }
1147
1148    #[test]
1149    fn error_on_unauthorized_add() {
1150        let y = TestGroupState::new();
1151
1152        let op1 = create_group(
1153            ALICE,
1154            0,
1155            G1,
1156            vec![(GroupMember::Individual(ALICE), Access::manage())],
1157            vec![],
1158        );
1159
1160        let y_i = TestGroup::process(y, &op1).unwrap();
1161
1162        let op2 = add_member(
1163            ALICE,
1164            1,
1165            G1,
1166            GroupMember::Individual(BOB),
1167            Access::read(),
1168            vec![op1.id()],
1169        );
1170
1171        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1172
1173        let op3 = add_member(
1174            BOB,
1175            2,
1176            G1,
1177            GroupMember::Individual(CLAIRE),
1178            Access::read(),
1179            vec![op2.id()],
1180        );
1181
1182        assert!(TestGroup::process(y_ii, &op3).is_err());
1183    }
1184
1185    #[test]
1186    fn error_on_remove_non_member() {
1187        let y = TestGroupState::new();
1188
1189        let op1 = create_group(
1190            ALICE,
1191            0,
1192            G1,
1193            vec![(GroupMember::Individual(ALICE), Access::manage())],
1194            vec![],
1195        );
1196
1197        let y_i = TestGroup::process(y, &op1).unwrap();
1198
1199        let op2 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op1.id()]);
1200
1201        assert!(TestGroup::process(y_i, &op2).is_err());
1202    }
1203
1204    #[test]
1205    fn error_on_promote_non_member() {
1206        let y = TestGroupState::new();
1207
1208        let op1 = create_group(
1209            ALICE,
1210            0,
1211            G1,
1212            vec![(GroupMember::Individual(ALICE), Access::manage())],
1213            vec![],
1214        );
1215
1216        let y_i = TestGroup::process(y, &op1).unwrap();
1217
1218        let op2 = promote_member(
1219            ALICE,
1220            1,
1221            G1,
1222            GroupMember::Individual(BOB),
1223            Access::manage(),
1224            vec![op1.id()],
1225        );
1226
1227        assert!(TestGroup::process(y_i, &op2).is_err());
1228    }
1229
1230    #[test]
1231    fn error_on_add_manager_group() {
1232        let y = TestGroupState::new();
1233
1234        let op1 = create_group(
1235            ALICE,
1236            0,
1237            G1,
1238            vec![(GroupMember::Individual(ALICE), Access::manage())],
1239            vec![],
1240        );
1241
1242        let y_i = TestGroup::process(y, &op1).unwrap();
1243
1244        let op2 = add_member(
1245            ALICE,
1246            1,
1247            G1,
1248            GroupMember::Group(BOB),
1249            Access::manage(),
1250            vec![op1.id()],
1251        );
1252
1253        assert!(TestGroup::process(y_i, &op2).is_err());
1254    }
1255
1256    #[test]
1257    fn error_on_demote_non_member() {
1258        let y = TestGroupState::new();
1259
1260        let op1 = create_group(
1261            ALICE,
1262            0,
1263            G1,
1264            vec![(GroupMember::Individual(ALICE), Access::manage())],
1265            vec![],
1266        );
1267
1268        let y_i = TestGroup::process(y, &op1).unwrap();
1269
1270        let op2 = demote_member(
1271            ALICE,
1272            1,
1273            G1,
1274            GroupMember::Individual(BOB),
1275            Access::read(),
1276            vec![op1.id()],
1277        );
1278
1279        assert!(TestGroup::process(y_i, &op2).is_err());
1280    }
1281
1282    #[test]
1283    fn error_on_add_existing_member() {
1284        let y = TestGroupState::new();
1285
1286        let op1 = create_group(
1287            ALICE,
1288            0,
1289            G1,
1290            vec![(GroupMember::Individual(ALICE), Access::manage())],
1291            vec![],
1292        );
1293
1294        let y_i = TestGroup::process(y, &op1).unwrap();
1295
1296        let op2 = add_member(
1297            ALICE,
1298            1,
1299            G1,
1300            GroupMember::Individual(ALICE),
1301            Access::manage(),
1302            vec![op1.id()],
1303        );
1304
1305        assert!(TestGroup::process(y_i, &op2).is_err());
1306    }
1307
1308    #[test]
1309    fn error_on_remove_nonexistent_subgroup() {
1310        let y = TestGroupState::new();
1311
1312        let op1 = create_group(
1313            ALICE,
1314            0,
1315            G1,
1316            vec![(GroupMember::Individual(ALICE), Access::manage())],
1317            vec![],
1318        );
1319        let y_i = TestGroup::process(y, &op1).unwrap();
1320
1321        // Attempt to remove a subgroup that was never added
1322        let op2 = remove_member(ALICE, 1, G1, GroupMember::Group(G2), vec![op1.id()]);
1323
1324        assert!(TestGroup::process(y_i, &op2).is_err());
1325    }
1326
1327    #[test]
1328    fn deeply_nested_groups_with_removals() {
1329        let y = TestGroupState::new();
1330
1331        // Create G1
1332        let op1 = create_group(
1333            ALICE,
1334            0,
1335            G1,
1336            vec![(GroupMember::Individual(ALICE), Access::manage())],
1337            vec![],
1338        );
1339        let y_i = TestGroup::process(y, &op1).unwrap();
1340
1341        // Create G2
1342        let op2 = create_group(
1343            BOB,
1344            1,
1345            G2,
1346            vec![(GroupMember::Individual(BOB), Access::manage())],
1347            vec![op1.id()],
1348        );
1349        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1350
1351        // Create G3
1352        let op3 = create_group(
1353            CLAIRE,
1354            2,
1355            G3,
1356            vec![(GroupMember::Individual(CLAIRE), Access::manage())],
1357            vec![op2.id()],
1358        );
1359        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1360
1361        // Create G4
1362        let op4 = create_group(
1363            DAN,
1364            3,
1365            G4,
1366            vec![(GroupMember::Individual(DAN), Access::write())],
1367            vec![op3.id()],
1368        );
1369        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
1370
1371        // Nest G4 into G3
1372        let op5 = add_member(
1373            CLAIRE,
1374            4,
1375            G3,
1376            GroupMember::Group(G4),
1377            Access::read(),
1378            vec![op4.id()],
1379        );
1380        let y_v = TestGroup::process(y_iv, &op5).unwrap();
1381
1382        // Nest G3 into G2
1383        let op6 = add_member(
1384            BOB,
1385            5,
1386            G2,
1387            GroupMember::Group(G3),
1388            Access::write(),
1389            vec![op5.id()],
1390        );
1391        let y_vi = TestGroup::process(y_v, &op6).unwrap();
1392
1393        // Nest G2 into G1
1394        let op7 = add_member(
1395            ALICE,
1396            6,
1397            G1,
1398            GroupMember::Group(G2),
1399            Access::read(),
1400            vec![op6.id()],
1401        );
1402        let y_vii = TestGroup::process(y_vi, &op7).unwrap();
1403
1404        let mut members = y_vii.members(G1);
1405        members.sort();
1406        assert_eq!(
1407            members,
1408            vec![
1409                (ALICE, Access::manage()),
1410                (BOB, Access::read()),
1411                (CLAIRE, Access::read()),
1412                (DAN, Access::read()),
1413            ]
1414        );
1415
1416        // Remove G3 from G2
1417        let op8 = remove_member(BOB, 7, G2, GroupMember::Group(G3), vec![op7.id()]);
1418        let y_viii = TestGroup::process(y_vii, &op8).unwrap();
1419
1420        let mut members_after_removal = y_viii.members(G1);
1421        members_after_removal.sort();
1422        assert_eq!(
1423            members_after_removal,
1424            vec![(ALICE, Access::manage()), (BOB, Access::read()),]
1425        );
1426    }
1427
1428    #[test]
1429    fn nested_groups_with_concurrent_removal_and_promotion() {
1430        let y = TestGroupState::new();
1431
1432        // Create G1
1433        let op1 = create_group(
1434            ALICE,
1435            0,
1436            G1,
1437            vec![(GroupMember::Individual(ALICE), Access::manage())],
1438            vec![],
1439        );
1440        let y_i = TestGroup::process(y, &op1).unwrap();
1441
1442        // Create G2
1443        let op2 = create_group(
1444            BOB,
1445            1,
1446            G2,
1447            vec![(GroupMember::Individual(BOB), Access::manage())],
1448            vec![op1.id()],
1449        );
1450        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1451
1452        // Create G3
1453        let op3 = create_group(
1454            CLAIRE,
1455            2,
1456            G3,
1457            vec![(GroupMember::Individual(CLAIRE), Access::manage())],
1458            vec![op2.id()],
1459        );
1460        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1461
1462        // G3 includes Dan
1463        let op4 = add_member(
1464            CLAIRE,
1465            3,
1466            G3,
1467            GroupMember::Individual(DAN),
1468            Access::write(),
1469            vec![op3.id()],
1470        );
1471        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
1472
1473        // G2 includes G3
1474        let op5 = add_member(
1475            BOB,
1476            4,
1477            G2,
1478            GroupMember::Group(G3),
1479            Access::write(),
1480            vec![op4.id()],
1481        );
1482        let y_v = TestGroup::process(y_iv, &op5).unwrap();
1483
1484        // G2 includes Claire
1485        let op6 = add_member(
1486            BOB,
1487            5,
1488            G2,
1489            GroupMember::Individual(CLAIRE),
1490            Access::read(),
1491            vec![op5.id()],
1492        );
1493        let y_vi = TestGroup::process(y_v, &op6).unwrap();
1494
1495        // G1 includes G2
1496        let op7 = add_member(
1497            ALICE,
1498            6,
1499            G1,
1500            GroupMember::Group(G2),
1501            Access::read(),
1502            vec![op6.id()],
1503        );
1504        let y_vii = TestGroup::process(y_vi, &op7).unwrap();
1505
1506        let mut members = y_vii.members(G1);
1507        members.sort();
1508        assert_eq!(
1509            members,
1510            vec![
1511                (ALICE, Access::manage()),
1512                (BOB, Access::read()),
1513                (CLAIRE, Access::read()),
1514                (DAN, Access::read()),
1515            ]
1516        );
1517
1518        // Concurrent ops from same parent state
1519        let op8_remove_g2 = remove_member(ALICE, 7, G1, GroupMember::Group(G2), vec![op7.id()]);
1520        let op9_promote_claire = promote_member(
1521            BOB,
1522            8,
1523            G2,
1524            GroupMember::Individual(CLAIRE),
1525            Access::manage(),
1526            vec![op7.id()],
1527        );
1528
1529        // Remove first
1530        let y_after_remove = TestGroup::process(y_vii.clone(), &op8_remove_g2).unwrap();
1531        let mut members = y_after_remove.members(G1);
1532        members.sort();
1533        assert_eq!(members, vec![(ALICE, Access::manage())]);
1534
1535        // Then promote
1536        let y_after_both = TestGroup::process(y_after_remove, &op9_promote_claire).unwrap();
1537        let mut g1_members = y_after_both.members(G1);
1538        g1_members.sort();
1539        assert_eq!(g1_members, vec![(ALICE, Access::manage())]);
1540
1541        let mut g2_members = y_after_both.members(G2);
1542        g2_members.sort();
1543        assert_eq!(
1544            g2_members,
1545            vec![
1546                (BOB, Access::manage()),
1547                (CLAIRE, Access::manage()),
1548                (DAN, Access::write()),
1549            ]
1550        );
1551    }
1552
1553    #[test]
1554    fn concurrent_removal_ooo_processing() {
1555        let y = TestGroupState::new();
1556
1557        // Alice creates group
1558        let op1 = create_group(
1559            ALICE,
1560            0,
1561            G1,
1562            vec![(GroupMember::Individual(ALICE), Access::manage())],
1563            vec![],
1564        );
1565        let y_i = TestGroup::process(y, &op1).unwrap();
1566
1567        // Alice adds Bob as manager
1568        let op2 = add_member(
1569            ALICE,
1570            1,
1571            G1,
1572            GroupMember::Individual(BOB),
1573            Access::manage(),
1574            vec![op1.id()],
1575        );
1576        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1577
1578        // Bob adds Claire (Read)
1579        let op3 = add_member(
1580            BOB,
1581            2,
1582            G1,
1583            GroupMember::Individual(CLAIRE),
1584            Access::read(),
1585            vec![op2.id()],
1586        );
1587
1588        // Alice removes Bob
1589        let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op2.id()]);
1590
1591        // Apply in Order A: Add Claire, then Remove Bob
1592        let y_iii_a = TestGroup::process(y_ii.clone(), &op3).unwrap();
1593        let y_iv_a = TestGroup::process(y_iii_a, &op4).unwrap();
1594
1595        // Apply in Order B: Remove Bob, then Add Claire
1596        let y_iii_b = TestGroup::process(y_ii.clone(), &op4).unwrap();
1597        let y_iv_b = TestGroup::process(y_iii_b, &op3).unwrap();
1598
1599        for (_, y) in [y_iv_a, y_iv_b].into_iter().enumerate() {
1600            let mut members = y.members(G1);
1601            members.sort();
1602            assert_eq!(members, vec![(ALICE, Access::manage())],);
1603        }
1604    }
1605
1606    #[test]
1607    fn concurrent_add_with_insufficient_access() {
1608        let y0 = TestGroupState::new();
1609
1610        // Alice creates the group
1611        let op1 = create_group(
1612            ALICE,
1613            0,
1614            G1,
1615            vec![(GroupMember::Individual(ALICE), Access::manage())],
1616            vec![],
1617        );
1618        let y1 = TestGroup::process(y0, &op1).unwrap();
1619
1620        // Alice adds Bob as manager
1621        let op2 = add_member(
1622            ALICE,
1623            1,
1624            G1,
1625            GroupMember::Individual(BOB),
1626            Access::manage(),
1627            vec![op1.id()],
1628        );
1629
1630        // Bob concurrently tries to add Eve
1631        let op3 = add_member(
1632            BOB,
1633            2,
1634            G1,
1635            GroupMember::Individual(EVE),
1636            Access::read(),
1637            vec![op1.id()],
1638        );
1639
1640        // Case 1: Apply Bob's operation first - should fail
1641        let result = TestGroup::process(y1.clone(), &op3);
1642        std::assert_matches!(
1643            result,
1644            Err(GroupCrdtError::StateChangeError(
1645                _,
1646                GroupMembershipError::UnrecognisedActor(_)
1647            ))
1648        );
1649
1650        // Case 2: Apply Alice’s op first, then Bob's - still must fail
1651        let y1_alt = TestGroup::process(y1, &op2).unwrap();
1652        let result = TestGroup::process(y1_alt.clone(), &op3);
1653        std::assert_matches!(
1654            result,
1655            Err(GroupCrdtError::StateChangeError(
1656                _,
1657                GroupMembershipError::UnrecognisedActor(_)
1658            ))
1659        );
1660
1661        // Confirm final state: Bob is a member, Eve is not
1662        let mut members = y1_alt.members(G1);
1663        members.sort();
1664        assert_eq!(
1665            members,
1666            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
1667        );
1668    }
1669
1670    #[test]
1671    fn add_group_with_concurrent_change() {
1672        let y = TestGroupState::new();
1673
1674        // Create Group 1 with Alice as manager
1675        let op1 = create_group(
1676            ALICE,
1677            0,
1678            G1,
1679            vec![(GroupMember::Individual(ALICE), Access::manage())],
1680            vec![],
1681        );
1682        let y_i = TestGroup::process(y, &op1).unwrap();
1683
1684        // Create Group 2 with Bob as manager
1685        let op2 = create_group(
1686            BOB,
1687            1,
1688            G2,
1689            vec![(GroupMember::Individual(BOB), Access::manage())],
1690            vec![op1.id()],
1691        );
1692        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1693
1694        // Alice adds Group 2 to Group 1
1695        let op3a = add_member(
1696            ALICE,
1697            2,
1698            G1,
1699            GroupMember::Group(G2),
1700            Access::read(),
1701            vec![op2.id()],
1702        );
1703
1704        // Concurrently, Bob adds Claire to Group 2
1705        let op3b = add_member(
1706            BOB,
1707            3,
1708            G2,
1709            GroupMember::Individual(CLAIRE),
1710            Access::write(),
1711            vec![op2.id()],
1712        );
1713
1714        // Order 1: Add group, then add member
1715        let y_iii = TestGroup::process(y_ii.clone(), &op3a).unwrap();
1716        let y_iv = TestGroup::process(y_iii, &op3b).unwrap();
1717
1718        let mut members_1 = y_iv.members(G1);
1719        members_1.sort();
1720        assert_eq!(
1721            members_1,
1722            vec![
1723                (ALICE, Access::manage()),
1724                (BOB, Access::read()),
1725                (CLAIRE, Access::read())
1726            ]
1727        );
1728
1729        // Order 2: Add member, then add group
1730        let y_iii_alt = TestGroup::process(y_ii.clone(), &op3b).unwrap();
1731        let y_iv_alt = TestGroup::process(y_iii_alt, &op3a).unwrap();
1732
1733        let mut members_1 = y_iv_alt.members(G1);
1734        members_1.sort();
1735        assert_eq!(
1736            members_1,
1737            vec![
1738                (ALICE, Access::manage()),
1739                (BOB, Access::read()),
1740                (CLAIRE, Access::read())
1741            ]
1742        );
1743    }
1744
1745    #[test]
1746    fn nested_group_cycle_error() {
1747        let y = TestGroupState::new();
1748
1749        // Create group G1 with ALICE as manager
1750        let op1 = create_group(
1751            ALICE,
1752            0,
1753            G1,
1754            vec![(GroupMember::Individual(ALICE), Access::manage())],
1755            vec![],
1756        );
1757        let y_i = TestGroup::process(y, &op1).unwrap();
1758
1759        // Create group G2 with BOB as manager, with G1 as a member
1760        let op2 = create_group(
1761            BOB,
1762            1,
1763            G2,
1764            vec![
1765                (GroupMember::Individual(BOB), Access::manage()),
1766                (GroupMember::Group(G1), Access::read()),
1767            ],
1768            vec![op1.id()],
1769        );
1770        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1771
1772        // Attempt to add G2 as a member of G1, which creates a cycle (G1 -> G2 -> G1)
1773        let op3 = add_member(
1774            ALICE,
1775            2,
1776            G1,
1777            GroupMember::Group(G2),
1778            Access::read(),
1779            vec![op2.id()],
1780        );
1781
1782        // This should fail due to cycle detection
1783        let result = TestGroup::process(y_ii, &op3);
1784        assert!(
1785            result.is_err(),
1786            "Creating a group cycle should cause an error"
1787        );
1788    }
1789
1790    #[test]
1791    fn serde_to_from_bytes() {
1792        let y = TestGroupState::new();
1793        let op1 = create_group(
1794            ALICE,
1795            0,
1796            G1,
1797            vec![(GroupMember::Individual(ALICE), Access::manage())],
1798            vec![],
1799        );
1800        let y_i = TestGroup::process(y, &op1).unwrap();
1801        let members = y_i.members(G1);
1802        assert_eq!(members, vec![(ALICE, Access::manage())]);
1803
1804        // Serialize auth state to cbor bytes.
1805        let bytes = encode_cbor(&y_i).unwrap();
1806
1807        // Deserialize auth state from cbor bytes.
1808        let y_i_de: TestGroupState = decode_cbor(&bytes[..]).unwrap();
1809
1810        // Assert members are the same.
1811        let members = y_i_de.members(G1);
1812        assert_eq!(members, vec![(ALICE, Access::manage())]);
1813    }
1814}