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