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