Skip to main content

coven_protocol/membership/
chain.rs

1use super::entry::store_membership_anchor_stream;
2use super::*;
3
4impl MembershipChain {
5    pub fn from_entries_with_coords_and_heads_and_provider_admin(
6        entries: Vec<(MembershipCoord, MembershipEntry)>,
7        heads: Vec<(MembershipHeadRef, AuthorHead)>,
8        provider_admin: crate::provider::ProviderAdminState,
9    ) -> Result<Self, MembershipError> {
10        let expected_store = entries
11            .first()
12            .map(|(_, entry)| entry.store_id.as_str())
13            .ok_or(MembershipError::EmptyChain)?;
14        if heads.iter().any(|(reference, head)| {
15            reference.head_hash != head.head_hash()
16                || head.store_id != expected_store
17                || entries
18                    .iter()
19                    .find(|(coord, _)| *coord == head.entry_coord())
20                    .is_none_or(|(_, entry)| head.body.resolutions != entry.resolution_dependencies)
21        }) {
22            return Err(MembershipError::MissingConflictHeads);
23        }
24        Self::from_entries_with_coords_and_head_refs(
25            entries,
26            heads.into_iter().map(|(reference, _)| reference).collect(),
27            provider_admin,
28        )
29    }
30
31    fn from_entries_with_coords_and_head_refs(
32        entries: Vec<(MembershipCoord, MembershipEntry)>,
33        head_refs: Vec<MembershipHeadRef>,
34        provider_admin_genesis: crate::provider::ProviderAdminState,
35    ) -> Result<Self, MembershipError> {
36        if entries.is_empty() {
37            return Err(MembershipError::EmptyChain);
38        }
39        for (index, (_, entry)) in entries.iter().enumerate() {
40            Self::validate_entry_authenticity(index, entry)?;
41        }
42        let (coords, entries): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
43        let mut chain = Self {
44            entries,
45            coords,
46            state: CausalState::default(),
47            included: BTreeSet::new(),
48            status: None,
49            head_refs,
50            resolution_checkpoint: None,
51            provider_admin_genesis,
52        };
53        chain.rebuild()?;
54        Ok(chain)
55    }
56
57    pub fn entries(&self) -> &[MembershipEntry] {
58        &self.entries
59    }
60
61    pub fn status(&self) -> &MembershipStatus {
62        self.status
63            .as_ref()
64            .expect("a loaded membership chain always has status")
65    }
66
67    pub fn head_refs(&self) -> &[MembershipHeadRef] {
68        &self.head_refs
69    }
70
71    pub fn head_ref_for_stream(
72        &self,
73        author: &str,
74        grant: &MembershipGrantId,
75        stream_id: AuthorStreamId,
76    ) -> Option<&MembershipHeadRef> {
77        self.head_refs.iter().find(|reference| {
78            reference.coord.author_pubkey == author
79                && reference.coord.author_owner_grant == *grant
80                && reference.coord.stream_id == stream_id
81        })
82    }
83
84    /// Whether this chain reaches at least as far as `heads` on every stream
85    /// they name.
86    ///
87    /// A membership stream is a hash-linked list walked from its founder anchor
88    /// forward: a walk cannot start in the middle, so two walks of the same
89    /// stream verify the same heads up to the shorter one's end. A chain that
90    /// covers a set of cursors therefore contains everything a walk anchored at
91    /// those cursors would have verified, and can stand in for it.
92    pub fn covers_heads(&self, heads: &[MembershipHeadRef]) -> bool {
93        heads.iter().all(|covered| {
94            self.head_ref_for_stream(
95                &covered.coord.author_pubkey,
96                &covered.coord.author_owner_grant,
97                covered.coord.stream_id,
98            )
99            .is_some_and(|current| {
100                current.coord.seq > covered.coord.seq
101                    || (current.coord.seq == covered.coord.seq && current == covered)
102            })
103        })
104    }
105
106    pub fn membership_anchor(&self, grant: &MembershipGrantId) -> Option<&GrantStreamAnchor> {
107        self.entries
108            .iter()
109            .find_map(|entry| match &entry.change {
110                MembershipChange::Founder {
111                    owner_grant_id,
112                    membership,
113                    ..
114                } if owner_grant_id == grant => Some(membership),
115                MembershipChange::SetMember {
116                    grant_id,
117                    membership: Some(membership),
118                    ..
119                } if grant_id == grant => Some(membership),
120                _ => None,
121            })
122            .or_else(|| {
123                self.resolution_checkpoint
124                    .as_ref()?
125                    .grant_anchors
126                    .get(grant)
127            })
128    }
129
130    pub fn membership_stream_id(&self, grant: &MembershipGrantId) -> Option<AuthorStreamId> {
131        let record = self.state.grants.get(grant)?.record();
132        store_membership_anchor_stream(&record.member_pubkey, grant, self.membership_anchor(grant)?)
133    }
134
135    pub fn activated_membership_streams(&self) -> Vec<(MembershipStreamKey, GrantStreamAnchor)> {
136        let mut streams = self
137            .state
138            .grants
139            .iter()
140            .filter_map(|(grant, state)| {
141                let record = state.record();
142                let anchor = self.membership_anchor(grant)?.clone();
143                let stream_id = self.membership_stream_id(grant)?;
144                Some((
145                    MembershipStreamKey {
146                        author_pubkey: record.member_pubkey.clone(),
147                        author_owner_grant: grant.clone(),
148                        stream_id,
149                    },
150                    anchor,
151                ))
152            })
153            .collect::<BTreeMap<_, _>>();
154        let mut included = self.included.clone();
155        if let MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
156            maximal_valid_branches,
157            ..
158        }) = self.status()
159        {
160            for branch in maximal_valid_branches {
161                included.extend(causal_grants::history_closure(
162                    &self.entries,
163                    &branch.effective_frontier,
164                ));
165            }
166        }
167        for (coord, entry) in self.entries_with_coords() {
168            if !included.contains(coord) {
169                continue;
170            }
171            let (owner_pubkey, grant, anchor) = match &entry.change {
172                MembershipChange::SetMember {
173                    user_pubkey,
174                    role: StoreMembershipRoleGrant::Owner { .. },
175                    grant_id,
176                    membership: Some(membership),
177                    ..
178                } => (user_pubkey, grant_id, membership),
179                _ => continue,
180            };
181            let stream_id = store_membership_anchor_stream(owner_pubkey, grant, anchor)
182                .expect("validated Owner grant has a Store membership stream anchor");
183            streams.insert(
184                MembershipStreamKey {
185                    author_pubkey: owner_pubkey.clone(),
186                    author_owner_grant: grant.clone(),
187                    stream_id,
188                },
189                anchor.clone(),
190            );
191        }
192        streams.into_iter().collect()
193    }
194
195    pub fn activate_head_ref(
196        &mut self,
197        reference: MembershipHeadRef,
198    ) -> Result<(), MembershipError> {
199        if !self.coords.contains(&reference.coord) {
200            return Err(MembershipError::MissingConflictHeads);
201        }
202        let stream = reference.coord.stream_key();
203        self.head_refs
204            .retain(|current| current.coord.stream_key() != stream);
205        self.head_refs.push(reference);
206        self.head_refs.sort();
207        self.rebuild()
208    }
209
210    pub fn resolution_refs(&self) -> &[StoreMembershipConflictResolutionRef] {
211        self.resolution_checkpoint
212            .as_ref()
213            .map_or(&[], |checkpoint| checkpoint.resolutions.as_slice())
214    }
215
216    pub fn conflict(&self) -> Option<&MembershipConflict> {
217        match self.status() {
218            MembershipStatus::Resolved(_) => None,
219            MembershipStatus::Conflict(conflict) => Some(conflict),
220        }
221    }
222
223    pub fn ensure_resolved(&self) -> Result<(), MembershipError> {
224        match self.status() {
225            MembershipStatus::Resolved(_) => Ok(()),
226            MembershipStatus::Conflict(_) => Err(MembershipError::Conflict),
227        }
228    }
229
230    pub(crate) fn entries_with_coords(
231        &self,
232    ) -> impl Iterator<Item = (&MembershipCoord, &MembershipEntry)> {
233        self.coords.iter().zip(self.entries.iter())
234    }
235
236    pub fn store_id(&self) -> Option<&str> {
237        self.entries.first().map(|entry| entry.store_id.as_str())
238    }
239
240    pub fn founder_coord(&self) -> Option<&MembershipCoord> {
241        self.entries_with_coords().find_map(|(coord, entry)| {
242            matches!(entry.change, MembershipChange::Founder { .. }).then_some(coord)
243        })
244    }
245
246    pub(crate) fn founder_entry(&self) -> Option<&MembershipEntry> {
247        self.entries
248            .iter()
249            .find(|entry| matches!(entry.change, MembershipChange::Founder { .. }))
250    }
251
252    pub fn founder_pubkey(&self) -> Option<&str> {
253        self.founder_entry().and_then(|entry| match &entry.change {
254            MembershipChange::Founder { owner_pubkey, .. } => Some(owner_pubkey.as_str()),
255            MembershipChange::SetMember { .. }
256            | MembershipChange::RemoveMember { .. }
257            | MembershipChange::ProviderAdmin
258            | MembershipChange::ResolutionActivation { .. } => None,
259        })
260    }
261
262    pub fn is_founded_by(&self, owner_pubkey: &str) -> bool {
263        self.founder_pubkey() == Some(owner_pubkey)
264    }
265
266    pub fn add_entry(&mut self, entry: MembershipEntry) -> Result<(), MembershipError> {
267        self.add_entry_at(entry.coord(), entry)
268    }
269
270    pub fn add_entry_at(
271        &mut self,
272        coord: MembershipCoord,
273        entry: MembershipEntry,
274    ) -> Result<(), MembershipError> {
275        Self::validate_entry_authenticity(self.entries.len(), &entry)?;
276        self.entries.push(entry);
277        self.coords.push(coord);
278        if let Err(error) = self.rebuild() {
279            self.entries.pop();
280            self.coords.pop();
281            self.rebuild().expect("previous membership chain validated");
282            return Err(error);
283        }
284        Ok(())
285    }
286
287    pub fn with_exact_entry(&self, entry: &MembershipEntry) -> Result<Self, MembershipError> {
288        let coord = entry.coord();
289        if let Some((_, stored)) = self
290            .entries_with_coords()
291            .find(|(stored_coord, _)| **stored_coord == coord)
292        {
293            if stored != entry {
294                return Err(MembershipError::ExactEntryMismatch {
295                    coord: Box::new(coord),
296                });
297            }
298            return Ok(self.clone());
299        }
300        let mut chain = self.clone();
301        chain.add_entry_at(coord, entry.clone())?;
302        Ok(chain)
303    }
304
305    pub fn contains_coord(&self, expected: &MembershipCoord) -> bool {
306        self.coords.iter().any(|coord| coord == expected)
307    }
308
309    pub fn effectively_contains_coord(&self, expected: &MembershipCoord) -> bool {
310        self.included.contains(expected)
311    }
312
313    pub(crate) fn contains_member_history(&self, pubkey: &str) -> bool {
314        self.state
315            .grants
316            .values()
317            .any(|state| state.record().member_pubkey == pubkey)
318    }
319
320    pub fn reusable_author_streams(
321        &self,
322        author_pubkey: &str,
323        grant: &MembershipGrantId,
324    ) -> BTreeSet<AuthorStreamId> {
325        self.effective_frontier()
326            .into_iter()
327            .filter(|coord| {
328                coord.author_pubkey == author_pubkey
329                    && coord.author_owner_grant == *grant
330                    && self.raw_stream_tip(author_pubkey, grant, coord.stream_id)
331                        == Some(coord.clone())
332            })
333            .map(|coord| coord.stream_id)
334            .collect()
335    }
336
337    /// Effective authoring frontier after causal pruning.
338    pub fn effective_frontier(&self) -> Vec<MembershipCoord> {
339        causal_grants::stream_frontier(
340            self.coords
341                .iter()
342                .filter(|coord| self.included.contains(*coord))
343                .cloned(),
344        )
345    }
346
347    pub fn causally_includes(&self, predecessor: &MembershipChain) -> bool {
348        predecessor.included.is_subset(&self.included)
349            && predecessor
350                .resolution_refs()
351                .iter()
352                .all(|reference| self.resolution_refs().binary_search(reference).is_ok())
353    }
354
355    pub(crate) fn stream_tip(
356        &self,
357        author_pubkey: &str,
358        grant: &MembershipGrantId,
359        stream_id: AuthorStreamId,
360    ) -> Option<MembershipCoord> {
361        self.effective_frontier().into_iter().find(|coord| {
362            coord.author_pubkey == author_pubkey
363                && coord.author_owner_grant == *grant
364                && coord.stream_id == stream_id
365        })
366    }
367
368    pub(crate) fn raw_stream_tip(
369        &self,
370        author_pubkey: &str,
371        grant: &MembershipGrantId,
372        stream_id: AuthorStreamId,
373    ) -> Option<MembershipCoord> {
374        self.coords
375            .iter()
376            .filter(|coord| {
377                coord.author_pubkey == author_pubkey
378                    && coord.author_owner_grant == *grant
379                    && coord.stream_id == stream_id
380            })
381            .max_by_key(|coord| coord.seq)
382            .cloned()
383    }
384
385    pub(crate) fn next_member_grant_id_in_stream(
386        &self,
387        signer: &UserKeypair,
388        stream_id: AuthorStreamId,
389        user_pubkey: &str,
390    ) -> Result<MembershipGrantId, MembershipError> {
391        let author = keys::public_key_hex(signer);
392        let author_grant = self
393            .active_owner_grant(&author)
394            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
395        let (seq, _) = self.next_stream_position(&author, &author_grant, stream_id)?;
396        Ok(derive_grant_id(
397            self.store_id().expect("validated chain has a store id"),
398            &author,
399            &author_grant,
400            stream_id,
401            seq,
402            user_pubkey,
403        ))
404    }
405
406    pub fn next_stream_position(
407        &self,
408        author: &str,
409        grant: &MembershipGrantId,
410        stream_id: AuthorStreamId,
411    ) -> Result<(u64, Option<ObjectHash>), MembershipError> {
412        let raw_tip = self.raw_stream_tip(author, grant, stream_id);
413        let effective_tip = self.stream_tip(author, grant, stream_id);
414        if raw_tip != effective_tip {
415            return Err(MembershipError::PrunedAuthorStream);
416        }
417        effective_tip.map_or(Ok((1, None)), |tip| {
418            tip.seq
419                .checked_add(1)
420                .map(|seq| (seq, Some(tip.entry_hash)))
421                .ok_or(MembershipError::SequenceExhausted)
422        })
423    }
424
425    fn validate_entry_authenticity(
426        index: usize,
427        entry: &MembershipEntry,
428    ) -> Result<(), MembershipError> {
429        if verify_membership_entry(entry) {
430            Ok(())
431        } else {
432            Err(MembershipError::InvalidSignature(index))
433        }
434    }
435
436    fn rebuild(&mut self) -> Result<(), MembershipError> {
437        let expected_store = self
438            .entries
439            .first()
440            .ok_or(MembershipError::EmptyChain)?
441            .store_id
442            .clone();
443        if expected_store.is_empty() {
444            return Err(MembershipError::InvalidFounder);
445        }
446
447        for (index, (coord, entry)) in self.entries_with_coords().enumerate() {
448            if entry.require_version().is_err() {
449                return Err(MembershipError::UnsupportedVersion(index));
450            }
451            if entry.store_id != expected_store {
452                return Err(MembershipError::StoreMismatch {
453                    index,
454                    expected: expected_store.clone(),
455                    actual: entry.store_id.clone(),
456                });
457            }
458            let actual = entry.coord();
459            if *coord != actual {
460                return Err(MembershipError::CoordinateMismatch {
461                    index,
462                    expected: Box::new(coord.clone()),
463                    actual: Box::new(actual),
464                });
465            }
466            if !entry
467                .dependencies
468                .windows(2)
469                .all(|pair| pair[0].stream_key() < pair[1].stream_key())
470            {
471                return Err(MembershipError::NonCanonicalDependencyFrontier { index });
472            }
473            let (barriers, retirement_device_state) = match &entry.change {
474                MembershipChange::SetMember {
475                    user_pubkey,
476                    role,
477                    grant_id,
478                    replaces,
479                    membership,
480                    retirement_barriers,
481                    retirement_device_state,
482                    ..
483                } => {
484                    let valid_owner_origin = match role {
485                        StoreMembershipRoleGrant::Owner {
486                            recovery: OwnerRecoveryAnchorRef::Promotion { acceptance },
487                        } => {
488                            let request = &acceptance.request;
489                            let anchors_match =
490                                Some(&acceptance.anchors.membership) == membership.as_ref();
491                            let finalization_matches = request.finalization.author_stream
492                                == entry.stream_id
493                                && request.finalization.seq == entry.seq
494                                && request.finalization.previous_hash == entry.previous_hash;
495                            request.member_pubkey == *user_pubkey
496                                && replaces.len() == 1
497                                && replaces.contains(&request.member_grant)
498                                && request.intended_owner_grant == *grant_id
499                                && request.promoter_owner_grant == entry.author_owner_grant
500                                && anchors_match
501                                && finalization_matches
502                        }
503                        StoreMembershipRoleGrant::Owner { .. } => false,
504                        StoreMembershipRoleGrant::Member | StoreMembershipRoleGrant::Follower => {
505                            membership.is_none()
506                        }
507                    };
508                    if role.is_owner()
509                        != membership.as_ref().is_some_and(|anchor| {
510                            store_membership_anchor_stream(user_pubkey, grant_id, anchor).is_some()
511                        })
512                        || !valid_owner_origin
513                    {
514                        return Err(MembershipError::InvalidOwnerMembershipAnchor(index));
515                    }
516                    (retirement_barriers, retirement_device_state)
517                }
518                MembershipChange::RemoveMember {
519                    retirement_barriers,
520                    retirement_device_state,
521                    ..
522                } => (retirement_barriers, retirement_device_state),
523                MembershipChange::ResolutionActivation { resolution } => {
524                    if resolution.resolver_pubkey != entry.author_pubkey
525                        || entry.seq != 1
526                        || entry.previous_hash.is_some()
527                        || entry
528                            .dependencies
529                            .iter()
530                            .any(|dependency| dependency.stream_key() == entry.coord().stream_key())
531                        || entry.author_owner_grant
532                            != derive_store_resolution_grant(
533                                &resolution.conflict_hash,
534                                &resolution.resolver_pubkey,
535                            )
536                        || entry
537                            .resolution_dependencies
538                            .binary_search(resolution)
539                            .is_err()
540                        || self
541                            .resolution_checkpoint
542                            .as_ref()
543                            .is_none_or(|checkpoint| {
544                                let already_checkpointed =
545                                    checkpoint.included.contains(&entry.coord())
546                                        || checkpoint.raw_heads.contains(&entry.coord());
547                                !already_checkpointed
548                                    && (entry.dependencies != checkpoint.effective_frontier
549                                        || entry.resolution_dependencies != checkpoint.resolutions)
550                            })
551                    {
552                        return Err(MembershipError::InvalidResolutionActivation(index));
553                    }
554                    continue;
555                }
556                MembershipChange::ProviderAdmin => {
557                    let Some(crate::provider::ProviderAdminMembershipChange {
558                        owner_barriers, ..
559                    }) = &entry.provider_admin
560                    else {
561                        return Err(MembershipError::InvalidProviderAdminChange(index));
562                    };
563                    if !entry.resolution_dependencies.is_empty()
564                        || owner_barriers.values().any(|barrier| {
565                            !barrier
566                                .observed_streams
567                                .windows(2)
568                                .all(|pair| pair[0].stream_key() < pair[1].stream_key())
569                        })
570                    {
571                        return Err(MembershipError::InvalidProviderAdminChange(index));
572                    }
573                    continue;
574                }
575                MembershipChange::Founder { .. } => continue,
576            };
577            if entry.provider_admin.is_some() {
578                return Err(MembershipError::InvalidProviderAdminChange(index));
579            }
580            let owner_recoveries = barriers
581                .values()
582                .filter_map(|barrier| match barrier {
583                    MergeMembershipGrantRetirementBarrier::Owner { barrier } => {
584                        Some(&barrier.recovery)
585                    }
586                    MergeMembershipGrantRetirementBarrier::NonOwner { .. } => None,
587                })
588                .collect::<Vec<_>>();
589            match (owner_recoveries.is_empty(), retirement_device_state) {
590                (true, None) => {}
591                (false, Some(state))
592                    if owner_recoveries
593                        .iter()
594                        .all(|cursor| state.recovery().binary_search(cursor).is_ok()) => {}
595                (true, Some(_)) => return Err(MembershipError::UnexpectedOwnerRecoveryState),
596                (false, None | Some(_)) => return Err(MembershipError::MissingOwnerRecoveryState),
597            }
598            if let Some((grant, _)) = barriers.iter().find(|(_, barrier)| {
599                !barrier
600                    .author_streams()
601                    .observed_streams
602                    .windows(2)
603                    .all(|pair| pair[0].stream_key() < pair[1].stream_key())
604            }) {
605                return Err(MembershipError::InvalidOwnerRevocationBarrier {
606                    index,
607                    grant: grant.clone(),
608                });
609            }
610        }
611
612        let founders = self
613            .entries
614            .iter()
615            .filter_map(|entry| {
616                let MembershipChange::Founder {
617                    owner_pubkey,
618                    owner_grant_id,
619                    ..
620                } = &entry.change
621                else {
622                    return None;
623                };
624                Some((entry, owner_pubkey, owner_grant_id))
625            })
626            .collect::<Vec<_>>();
627        let [(founder, owner_pubkey, owner_grant_id)] = founders.as_slice() else {
628            return Err(MembershipError::InvalidFounder);
629        };
630        if founder.author_pubkey != **owner_pubkey
631            || founder.author_owner_grant != **owner_grant_id
632            || founder.stream_id != derive_founder_stream_id(&founder.store_id, owner_pubkey)
633            || founder.provider_admin.is_some()
634        {
635            return Err(MembershipError::InvalidFounder);
636        }
637
638        validate_provider_admin_controls(&self.entries, self.resolution_checkpoint.as_ref())?;
639        validate_membership_retirement_barriers(
640            &self.entries,
641            self.resolution_checkpoint.as_ref(),
642        )?;
643        validate_membership_wrapped_keys(&self.entries, self.resolution_checkpoint.as_ref())?;
644
645        let reduced = match &self.resolution_checkpoint {
646            Some(checkpoint) => reduce_store_membership_from_checkpoint(&self.entries, checkpoint)?,
647            None => reduce_store_membership(&self.entries)?,
648        };
649        let checkpoint_grants = self
650            .resolution_checkpoint
651            .as_ref()
652            .map(|checkpoint| &checkpoint.grants);
653        let provider_admin_seed = self
654            .resolution_checkpoint
655            .as_ref()
656            .map_or(&self.provider_admin_genesis, |checkpoint| {
657                &checkpoint.provider_admin
658            });
659        let (state_source, status) = match reduced {
660            CausalGrantStatus::Resolved(reduced) => {
661                let provider_admin = crate::provider::ProviderAdminState::reduce_merge(
662                    provider_admin_seed,
663                    &self.entries,
664                    &reduced.included,
665                )?;
666                let resolved = resolved_store_membership(
667                    &reduced,
668                    checkpoint_grants,
669                    provider_admin,
670                    &self.entries,
671                )?;
672                (Some(reduced), MembershipStatus::Resolved(resolved))
673            }
674            CausalGrantStatus::Conflict(CausalGrantConflict::ConcurrentMemberAssignments {
675                raw_heads,
676                effective_frontier,
677                member_pubkey,
678                conflicting_grants,
679                uncontested_grants,
680                reduced,
681            }) => {
682                let heads = self.exact_head_refs(&raw_heads)?;
683                let provider_admin = crate::provider::ProviderAdminState::reduce_merge(
684                    provider_admin_seed,
685                    &self.entries,
686                    &reduced.included,
687                )?;
688                let grants = reduced
689                    .grants
690                    .iter()
691                    .map(|(grant, state)| {
692                        Ok((
693                            grant.clone(),
694                            map_store_grant_state(grant, state, checkpoint_grants, &self.entries)?,
695                        ))
696                    })
697                    .collect::<Result<_, MembershipError>>()?;
698                let conflict = MembershipConflict::ConcurrentMemberAssignments {
699                    conflict_hash: membership_assignment_conflict_hash(
700                        &heads,
701                        &member_pubkey,
702                        &conflicting_grants,
703                    ),
704                    heads,
705                    effective_frontier,
706                    member_pubkey,
707                    conflicting_grants: map_store_grants(conflicting_grants, checkpoint_grants)?,
708                    uncontested_grants: map_store_grants(uncontested_grants, checkpoint_grants)?,
709                    grants,
710                    provider_admin,
711                };
712                (Some(reduced), MembershipStatus::Conflict(conflict))
713            }
714            CausalGrantStatus::Conflict(CausalGrantConflict::RevocationCycle {
715                raw_heads,
716                cyclic_sources,
717                involved_owner_grants,
718                maximal_valid_branches,
719            }) => {
720                let heads = self.exact_head_refs(&raw_heads)?;
721                let branches = maximal_valid_branches
722                    .into_iter()
723                    .map(|branch| -> Result<StoreMembershipBranch, MembershipError> {
724                        let resolved = resolved_store_membership(
725                            &branch.reduced,
726                            checkpoint_grants,
727                            crate::provider::ProviderAdminState::reduce_merge(
728                                provider_admin_seed,
729                                &self.entries,
730                                &branch.reduced.included,
731                            )?,
732                            &self.entries,
733                        )?;
734                        Ok(StoreMembershipBranch {
735                            heads: self.branch_head_refs(&branch.raw_heads)?,
736                            effective_frontier: branch.effective_frontier,
737                            grants: resolved.grants,
738                            provider_admin: resolved.provider_admin,
739                            state_hash: resolved.state_hash,
740                        })
741                    })
742                    .collect::<Result<Vec<_>, _>>()?;
743                let conflict_hash = membership_revocation_conflict_hash(
744                    &heads,
745                    &cyclic_sources,
746                    &involved_owner_grants,
747                );
748                (
749                    None,
750                    MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
751                        conflict_hash,
752                        heads,
753                        cyclic_sources,
754                        involved_owner_grants,
755                        maximal_valid_branches: branches,
756                    }),
757                )
758            }
759        };
760        if let Some(reduced) = state_source {
761            self.state = CausalState {
762                grants: reduced
763                    .grants
764                    .iter()
765                    .map(|(grant, state)| {
766                        Ok((
767                            grant.clone(),
768                            map_store_grant_state(grant, state, checkpoint_grants, &self.entries)?,
769                        ))
770                    })
771                    .collect::<Result<_, MembershipError>>()?,
772            };
773            self.included = reduced.included;
774        } else {
775            self.state = CausalState::default();
776            self.included.clear();
777        }
778        self.status = Some(status);
779        Ok(())
780    }
781
782    pub fn apply_resolutions(
783        &mut self,
784        store_root_hash: ObjectHash,
785        resolutions: &[(
786            StoreMembershipConflictResolutionRef,
787            StoreMembershipConflictResolution,
788        )],
789    ) -> Result<(), MembershipError> {
790        let (raw_heads, effective_frontier) = match self.conflict() {
791            Some(MembershipConflict::ConcurrentMemberAssignments {
792                heads,
793                effective_frontier,
794                ..
795            }) => (
796                heads
797                    .iter()
798                    .map(|reference| reference.coord.clone())
799                    .collect(),
800                effective_frontier.clone(),
801            ),
802            Some(MembershipConflict::RevocationCycle {
803                heads,
804                maximal_valid_branches,
805                ..
806            }) => (
807                heads
808                    .iter()
809                    .map(|reference| reference.coord.clone())
810                    .collect(),
811                causal_grants::selected_branch_frontier(resolutions, |(_, resolution)| {
812                    let MembershipConflictSelection::RevocationBranch {
813                        heads: selected_heads,
814                    } = &resolution.selection
815                    else {
816                        return Err(MembershipError::InvalidConflictResolution);
817                    };
818                    maximal_valid_branches
819                        .iter()
820                        .find(|branch| branch.heads == *selected_heads)
821                        .map(|branch| branch.effective_frontier.as_slice())
822                        .ok_or(MembershipError::InvalidConflictResolution)
823                })?,
824            ),
825            _ => return Err(MembershipError::InvalidConflictResolution),
826        };
827        let resolved = self.resolved_with(store_root_hash, resolutions)?;
828        let grants = resolved.grants.clone();
829        let mut grant_anchors = self
830            .resolution_checkpoint
831            .as_ref()
832            .map_or_else(BTreeMap::new, |checkpoint| checkpoint.grant_anchors.clone());
833        for entry in &self.entries {
834            match &entry.change {
835                MembershipChange::Founder {
836                    owner_grant_id,
837                    membership,
838                    ..
839                } => {
840                    grant_anchors.insert(owner_grant_id.clone(), membership.clone());
841                }
842                MembershipChange::SetMember {
843                    grant_id,
844                    membership: Some(membership),
845                    ..
846                } => {
847                    grant_anchors.insert(grant_id.clone(), membership.clone());
848                }
849                _ => {}
850            }
851        }
852        for (_, resolution) in resolutions {
853            grant_anchors.insert(
854                resolution.replacement_grant.clone(),
855                resolution.replacement_membership.clone(),
856            );
857        }
858        let included = causal_grants::history_closure(&self.entries, &effective_frontier);
859        self.resolution_checkpoint = Some(MembershipResolutionCheckpoint {
860            raw_heads,
861            effective_frontier: effective_frontier.clone(),
862            grants: grants.clone(),
863            grant_anchors,
864            included: included.clone(),
865            resolutions: causal_grants::checkpoint_resolution_refs(
866                self.resolution_checkpoint
867                    .as_ref()
868                    .map(|checkpoint| checkpoint.resolutions.as_slice()),
869                resolutions.iter().map(|(reference, _)| reference.clone()),
870            ),
871            provider_admin: resolved.provider_admin.combined_state().clone(),
872        });
873        self.state = CausalState { grants };
874        self.included = included;
875        self.status = Some(MembershipStatus::Resolved(resolved));
876        Ok(())
877    }
878
879    fn exact_head_refs(
880        &self,
881        raw_heads: &[MembershipCoord],
882    ) -> Result<Vec<MembershipHeadRef>, MembershipError> {
883        crate::causal_grants::exact_head_refs(&self.head_refs, raw_heads, |reference| {
884            &reference.coord
885        })
886        .ok_or(MembershipError::MissingConflictHeads)
887    }
888
889    fn branch_head_refs(
890        &self,
891        branch_heads: &[MembershipCoord],
892    ) -> Result<Vec<MembershipHeadRef>, MembershipError> {
893        let by_coord = self
894            .head_refs
895            .iter()
896            .map(|reference| (reference.coord.clone(), reference.clone()))
897            .collect::<BTreeMap<_, _>>();
898        let mut references = branch_heads
899            .iter()
900            .map(|coord| {
901                by_coord
902                    .get(coord)
903                    .cloned()
904                    .ok_or(MembershipError::MissingConflictHeads)
905            })
906            .collect::<Result<Vec<_>, _>>()?;
907        references.sort();
908        Ok(references)
909    }
910
911    #[cfg(any(test, feature = "test-utils"))]
912    pub fn from_entries(entries: Vec<MembershipEntry>) -> Result<Self, MembershipError> {
913        let provider_admin = test_provider_admin_genesis(&entries)?;
914        Self::from_entries_with_coords_and_provider_admin(
915            entries
916                .into_iter()
917                .map(|entry| (entry.coord(), entry))
918                .collect(),
919            provider_admin,
920        )
921    }
922
923    #[cfg(test)]
924    pub(crate) fn from_entries_with_coords_and_heads(
925        entries: Vec<(MembershipCoord, MembershipEntry)>,
926        heads: Vec<(MembershipHeadRef, AuthorHead)>,
927    ) -> Result<Self, MembershipError> {
928        let values = entries
929            .iter()
930            .map(|(_, entry)| entry.clone())
931            .collect::<Vec<_>>();
932        let provider_admin = test_provider_admin_genesis(&values)?;
933        Self::from_entries_with_coords_and_heads_and_provider_admin(entries, heads, provider_admin)
934    }
935
936    #[cfg(any(test, feature = "test-utils"))]
937    pub(crate) fn from_entries_with_coords_and_provider_admin(
938        entries: Vec<(MembershipCoord, MembershipEntry)>,
939        provider_admin: crate::provider::ProviderAdminState,
940    ) -> Result<Self, MembershipError> {
941        Self::from_entries_with_coords_and_head_refs(entries, Vec::new(), provider_admin)
942    }
943
944    /// Raw signed coverage: the greatest loaded coordinate in every stream,
945    /// including suffixes removed by causal pruning.
946    #[cfg(test)]
947    pub fn author_heads(&self) -> Vec<MembershipCoord> {
948        causal_grants::stream_frontier(self.coords.iter().cloned())
949    }
950}