Skip to main content

coven_replication/sync/store/commit_verification/merge_history/
membership_control.rs

1use super::membership;
2use super::*;
3
4pub(crate) fn verify_merge_membership_state_ref(
5    state: &StoreMembershipStateRef,
6    membership: &MembershipChain,
7    device_state: &ResolvedStoreDeviceState,
8) -> Result<(), StorePullError> {
9    let expected = merge_membership_state_ref(membership, device_state)?;
10    if &expected != state {
11        return Err(StorePullError::InvalidState(
12            "Store history membership reference differs from its exact resolved state".to_string(),
13        ));
14    }
15    Ok(())
16}
17
18pub(crate) fn merge_membership_state_ref(
19    membership: &MembershipChain,
20    device_state: &ResolvedStoreDeviceState,
21) -> Result<StoreMembershipStateRef, StorePullError> {
22    StoreMembershipStateRef::from_membership(membership, device_state.recovery.clone())
23        .map_err(StorePullError::Protocol)
24}
25
26#[derive(Clone, PartialEq, Eq)]
27pub(crate) struct VerifiedMergeMembershipHeadActivation {
28    pub(super) commit: StoreBatchCommitRef,
29    pub(super) transition: protocol_membership::MergeMembershipHeadTransition,
30}
31
32impl VerifiedMergeMembershipHeadActivation {
33    pub(crate) fn verifies(
34        &self,
35        reference: &protocol_membership::MembershipHeadRef,
36        head: &protocol_membership::AuthorHead,
37        commit: &StoreBatchCommitRef,
38    ) -> bool {
39        &self.commit == commit && self.transition.matches_head(head, reference)
40    }
41}
42
43#[derive(Clone)]
44pub(crate) struct VerifiedMergeMembershipControl {
45    pub(crate) activations: VerifiedCircleActivations,
46    pub(crate) head_activation: VerifiedMergeMembershipHeadActivation,
47}
48
49#[derive(Clone, Default)]
50pub struct VerifiedMergeMembershipPrefix {
51    commits: BTreeSet<StoreBatchCommitRef>,
52    predecessor_memberships: Vec<MembershipChain>,
53    head_activations: BTreeMap<StoreBatchCommitRef, VerifiedMergeMembershipHeadActivation>,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub(crate) enum VerifiedMergePrefixHeadStatus {
58    Included,
59    OutsidePrefix,
60}
61
62impl VerifiedMergeMembershipPrefix {
63    pub(super) fn extends(&self, verified: &Self) -> bool {
64        verified.commits.is_subset(&self.commits)
65    }
66
67    pub(crate) fn from_retained(
68        checkpoints: &[coven_database::RetainedMergeHistoryCheckpoint],
69    ) -> Result<Self, StorePullError> {
70        let mut prefix = Self::default();
71        for checkpoint in checkpoints {
72            match checkpoint {
73                coven_database::RetainedMergeHistoryCheckpoint::Snapshot(checkpoint) => {
74                    prefix.insert_snapshot_summary(
75                        &checkpoint.summary,
76                        checkpoint.summary.post_state.frontier(),
77                    )?;
78                }
79                coven_database::RetainedMergeHistoryCheckpoint::Commit(materialization) => {
80                    prefix.commits.insert(materialization.commit_ref().clone());
81                    if let Some(proof) = &materialization.history_evidence().membership_proof {
82                        prefix.insert_retained_proof(proof)?;
83                    }
84                }
85            }
86        }
87        Ok(prefix)
88    }
89
90    pub(super) fn insert_snapshot_summary(
91        &mut self,
92        checkpoint: &RetainedVerifiedMergeHistorySummary,
93        frontier: &CommitFrontier,
94    ) -> Result<(), StorePullError> {
95        self.commits.extend(
96            checkpoint
97                .causal_cut
98                .values()
99                .filter(|reference| frontier.covers_commit(reference))
100                .cloned(),
101        );
102        for proof in checkpoint.membership_proofs.values() {
103            if frontier.covers_commit(&proof.commit) {
104                self.insert_retained_proof(proof)?;
105            }
106        }
107        Ok(())
108    }
109
110    fn insert_retained_proof(
111        &mut self,
112        proof: &store_commit::RetainedMergeMembershipProof,
113    ) -> Result<(), StorePullError> {
114        let Some(store_commit::StoreControl { transition }) = proof.commit_value.control() else {
115            return Err(StorePullError::InvalidState(
116                "retained Merge membership proof has no membership control".to_string(),
117            ));
118        };
119        if transition.body.author_registration != proof.commit_value.author_registration {
120            return Err(StorePullError::InvalidState(
121                "retained membership transition has another Store commit author".into(),
122            ));
123        }
124        let activation = VerifiedMergeMembershipHeadActivation {
125            commit: proof.commit.clone(),
126            transition: transition.clone(),
127        };
128        match self.head_activations.entry(proof.commit.clone()) {
129            std::collections::btree_map::Entry::Vacant(entry) => {
130                entry.insert(activation);
131            }
132            std::collections::btree_map::Entry::Occupied(entry) if entry.get() == &activation => {}
133            std::collections::btree_map::Entry::Occupied(_) => {
134                return Err(StorePullError::InvalidState(
135                    "retained checkpoints disagree on a membership activation".to_string(),
136                ));
137            }
138        }
139        Ok(())
140    }
141
142    pub(crate) fn head_activation(
143        &self,
144        commit: &StoreBatchCommitRef,
145    ) -> Option<&VerifiedMergeMembershipHeadActivation> {
146        self.head_activations.get(commit)
147    }
148
149    pub(crate) fn classify_head(
150        &self,
151        reference: &protocol_membership::MembershipHeadRef,
152        head: &protocol_membership::AuthorHead,
153        commit: &StoreBatchCommitRef,
154    ) -> Result<VerifiedMergePrefixHeadStatus, StorePullError> {
155        if !self.commits.contains(commit) {
156            return Ok(VerifiedMergePrefixHeadStatus::OutsidePrefix);
157        }
158        let proof = self.head_activations.get(commit).ok_or_else(|| {
159            StorePullError::InvalidState(
160                "in-prefix membership activation is absent from its verified Store control"
161                    .to_string(),
162            )
163        })?;
164        if !proof.verifies(reference, head, commit) {
165            return Err(StorePullError::InvalidState(
166                "membership head differs from its in-prefix verified Store control".to_string(),
167            ));
168        }
169        Ok(VerifiedMergePrefixHeadStatus::Included)
170    }
171
172    pub(crate) fn validate_complete_membership(
173        &self,
174        membership: &MembershipChain,
175    ) -> Result<(), StorePullError> {
176        if self
177            .predecessor_memberships
178            .iter()
179            .any(|predecessor| !membership.causally_includes(predecessor))
180        {
181            return Err(StorePullError::InvalidState(
182                "membership state regresses below an exact Store predecessor membership"
183                    .to_string(),
184            ));
185        }
186        if self
187            .head_activations
188            .values()
189            .any(|proof| !membership.contains_coord(&proof.transition.body.entry.coord))
190        {
191            return Err(StorePullError::InvalidState(
192                "membership state omits an accepted Store membership control".to_string(),
193            ));
194        }
195        Ok(())
196    }
197}
198
199/// The membership authority a commit's predecessors establish, down to the
200/// installed baseline.
201///
202/// The snapshot's retained controls contribute only within the requested
203/// predecessor cut. An earlier retained commit must not inherit controls
204/// accepted between its predecessors and the snapshot.
205pub(crate) fn verified_merge_membership_prefix(
206    history: &VerifiedMergeHistory,
207    tips: impl IntoIterator<Item = StoreBatchCommitRef>,
208) -> Result<VerifiedMergeMembershipPrefix, StorePullError> {
209    let closure = verified_merge_commit_closure(history, tips)?;
210    let mut prefix = VerifiedMergeMembershipPrefix {
211        commits: closure.clone(),
212        ..VerifiedMergeMembershipPrefix::default()
213    };
214    if closure
215        .iter()
216        .any(|reference| history.superseded(reference))
217    {
218        let snapshot = history.baseline.snapshot().ok_or_else(|| {
219            StorePullError::InvalidState(
220                "snapshot-covered membership prefix has no retained history summary".to_string(),
221            )
222        })?;
223        let summary = &snapshot.meta.history_summary;
224        let mut frontier = CommitFrontier(BTreeMap::new());
225        for reference in &closure {
226            if history.superseded(reference)
227                && summary.causal_cut.get(&reference.coord) != Some(reference)
228            {
229                return Err(StorePullError::InvalidState(
230                    "snapshot-covered membership predecessor has no exact accepted reference"
231                        .into(),
232                ));
233            }
234            frontier = frontier.join(CommitFrontier(BTreeMap::from([(
235                reference.coord.stream_id,
236                reference.clone(),
237            )])))?;
238        }
239        prefix.insert_snapshot_summary(summary, &frontier)?;
240    }
241    for reference in closure {
242        let Some(verified) = history.commits.get(&reference) else {
243            continue;
244        };
245        prefix
246            .predecessor_memberships
247            .push(verified.predecessor_membership.clone());
248        if let Some(control) = &verified.membership_control {
249            prefix
250                .head_activations
251                .insert(reference, control.head_activation.clone());
252        }
253    }
254    Ok(prefix)
255}
256
257impl<'a> MergeHistoryVerifier<'a> {
258    pub(crate) async fn verify_membership_control_with_retained_history(
259        &mut self,
260        commit_ref: &StoreBatchCommitRef,
261        commit: &StoreBatchCommit,
262        predecessor_membership: &MembershipChain,
263        predecessor_state: &ResolvedStoreDeviceState,
264    ) -> Result<VerifiedCircleActivations, StorePullError> {
265        let Some(store_commit::StoreControl { transition }) = commit.control() else {
266            return Err(StorePullError::InvalidState(
267                "Merge membership verifier received another Store control".to_string(),
268            ));
269        };
270        let root = self.root.reference().clone();
271        let state = &commit.membership_state;
272        let commit_author = self
273            .commit_verifier
274            .load_registration(&commit.author_registration)
275            .await?;
276        if transition.body.author_registration != commit.author_registration
277            || transition.body.entry.coord.author_pubkey != commit_author.value.author_pubkey
278            || transition.body.successor.predecessor
279                != transition
280                    .body
281                    .predecessor_head()
282                    .map(|reference| reference.object.clone())
283        {
284            return Err(StorePullError::InvalidState(
285                "Merge membership transition differs from its Store authority".to_string(),
286            ));
287        }
288        match transition.body.predecessor_head() {
289            Some(predecessor) if state.heads.binary_search(predecessor).is_err() => {
290                return Err(StorePullError::InvalidState(
291                    "Merge membership transition predecessor is absent from its signed state"
292                        .to_string(),
293                ));
294            }
295            None if state.heads.iter().any(|head| {
296                head.coord.stream_key() == transition.body.entry.coord.stream_key()
297            }) =>
298            {
299                return Err(StorePullError::InvalidState(
300                    "first Merge membership transition has an existing signed predecessor"
301                        .to_string(),
302                ));
303            }
304            _ => {}
305        }
306        let opened_entry = self
307            .commit_verifier
308            .membership_objects()
309            .load_entry(&transition.body.entry)
310            .await?;
311        if opened_entry.value.coord() != transition.body.entry.coord
312            || opened_entry.value.dependencies != predecessor_membership.effective_frontier()
313        {
314            return Err(StorePullError::InvalidState(
315                "Merge membership transition differs from its exact entry".to_string(),
316            ));
317        }
318        let device_control_matches = match &opened_entry.value.change {
319            protocol_membership::StoreAuthorityChange::DeviceRegistrationActivation {
320                registration,
321            } => Some(
322                commit.device_registrations() == std::slice::from_ref(registration)
323                    && commit.device_exclusion_proposals().is_empty()
324                    && commit.device_exclusion_outcomes().is_empty(),
325            ),
326            protocol_membership::StoreAuthorityChange::DeviceExclusionProposal { proposal } => {
327                Some(
328                    commit.device_exclusion_proposals() == std::slice::from_ref(proposal)
329                        && commit.device_exclusion_outcomes().is_empty()
330                        && commit.device_registrations().is_empty(),
331                )
332            }
333            protocol_membership::StoreAuthorityChange::DeviceExclusionOutcome { outcome } => Some(
334                commit.device_exclusion_outcomes() == std::slice::from_ref(outcome)
335                    && commit.device_exclusion_proposals().is_empty()
336                    && commit.device_registrations().is_empty(),
337            ),
338            _ => None,
339        };
340        if let Some(matches) = device_control_matches {
341            if !matches || !commit.stream_activations().is_empty() {
342                return Err(StorePullError::InvalidState(
343                    "Store device control differs from its exact authority entry".into(),
344                ));
345            }
346            let mut successor_membership = predecessor_membership.clone();
347            successor_membership.add_entry(opened_entry.value)?;
348            return VerifiedCircleActivations::membership_control(commit, commit_ref)
349                .map_err(StorePullError::from);
350        }
351        if let protocol_membership::StoreAuthorityChange::RemoveMember {
352            user_pubkey,
353            removes,
354            retirement_device_state,
355            ..
356        } = &opened_entry.value.change
357        {
358            let removes_exact_member =
359                removes == &predecessor_membership.active_grant_ids(user_pubkey);
360            let retires_owner = removes.iter().any(|grant| {
361                predecessor_membership
362                    .active_grant(grant)
363                    .is_some_and(|record| {
364                        matches!(
365                            record.role,
366                            protocol_membership::StoreMembershipRoleGrant::Owner { .. }
367                        )
368                    })
369            });
370            if !removes_exact_member
371                || retires_owner != retirement_device_state.is_some()
372                || retirement_device_state
373                    .as_ref()
374                    .is_some_and(|state| state != &commit.device_state)
375                || !commit.stream_activations().is_empty()
376            {
377                return Err(StorePullError::InvalidState(
378                    "Merge removal control differs from its exact membership entry".to_string(),
379                ));
380            }
381            let mut successor_membership = predecessor_membership.clone();
382            successor_membership.add_entry(opened_entry.value)?;
383            return VerifiedCircleActivations::membership_control(commit, commit_ref)
384                .map_err(StorePullError::from);
385        }
386        if let protocol_membership::StoreAuthorityChange::SetMember {
387            user_pubkey,
388            role:
389                protocol_membership::StoreMembershipRoleGrant::Member
390                | protocol_membership::StoreMembershipRoleGrant::Follower,
391            replaces,
392            retirement_device_state,
393            ..
394        } = &opened_entry.value.change
395        {
396            if replaces != &predecessor_membership.active_grant_ids(user_pubkey)
397                || retirement_device_state
398                    .as_ref()
399                    .is_some_and(|state| state != &commit.device_state)
400                || !commit.stream_activations().is_empty()
401            {
402                return Err(StorePullError::InvalidState(
403                    "Merge member assignment differs from its exact membership entry".into(),
404                ));
405            }
406            let mut successor_membership = predecessor_membership.clone();
407            successor_membership.add_entry(opened_entry.value)?;
408            return VerifiedCircleActivations::membership_control(commit, commit_ref)
409                .map_err(StorePullError::from);
410        }
411        let protocol_membership::StoreAuthorityChange::SetMember {
412            user_pubkey,
413            role:
414                protocol_membership::StoreMembershipRoleGrant::Owner {
415                    recovery: protocol_membership::OwnerRecoveryAnchorRef::Promotion { acceptance },
416                },
417            grant_id,
418            membership: Some(membership_anchor),
419            replaces,
420            retirement_device_state,
421            ..
422        } = &opened_entry.value.change
423        else {
424            return Err(StorePullError::InvalidState(
425                "Merge membership control does not activate one Owner promotion".to_string(),
426            ));
427        };
428        if retirement_device_state.is_some()
429            || user_pubkey != &acceptance.request.member_pubkey
430            || grant_id != &acceptance.request.intended_owner_grant
431            || replaces != &BTreeSet::from([acceptance.request.member_grant.clone()])
432            || acceptance.request.promoter_registration != commit.author_registration
433        {
434            return Err(StorePullError::InvalidState(
435                "Merge Owner-promotion control differs from its exact membership entry".to_string(),
436            ));
437        }
438        let request_membership = self
439            .verify_owner_promotion_acceptance_in_loaded_history(acceptance)
440            .await?;
441        let request_activation = acceptance.activation.commit();
442        let predecessor_cut = commit.order.predecessor_cut()?;
443        let predecessor_frontier = predecessor_cut.commits();
444        let request_stream = request_activation.coord.stream_id;
445        let activation_is_covered = predecessor_frontier
446            .get(&request_stream)
447            .is_some_and(|head| head.coord.sequence() >= request_activation.coord.sequence());
448        let promoter_is_active = device_state_has_active_registration(
449            predecessor_state,
450            &acceptance.request.promoter_registration,
451        );
452        let candidate_is_active = device_state_has_active_registration(
453            predecessor_state,
454            &acceptance.request.member_registration,
455        );
456        let promoter_grant_is_active = predecessor_membership
457            .active_owner_grant(&commit_author.value.author_pubkey)
458            .as_ref()
459            == Some(&acceptance.request.promoter_owner_grant);
460        let candidate_grant_is_active = predecessor_membership
461            .active_grant(&acceptance.request.member_grant)
462            .is_some_and(|record| {
463                record.member_pubkey == acceptance.request.member_pubkey
464                    && record.role == protocol_membership::StoreMembershipRoleGrant::Member
465            });
466        if !predecessor_membership.causally_includes(&request_membership)
467            || !activation_is_covered
468            || !promoter_is_active
469            || !candidate_is_active
470            || !promoter_grant_is_active
471            || !candidate_grant_is_active
472        {
473            return Err(StorePullError::InvalidState(
474                "Merge Owner-promotion transition does not include its accepted authority"
475                    .to_string(),
476            ));
477        }
478        let store_commit::OwnerPromotionAnchors {
479            membership,
480            recovery,
481        } = &acceptance.anchors;
482        if membership != membership_anchor {
483            return Err(StorePullError::InvalidState(
484                "Merge Owner-promotion entry carries another membership anchor".to_string(),
485            ));
486        }
487        let mut expected = vec![
488            store_commit::StreamActivation::grant_authorized(
489                root.store_root_hash,
490                acceptance.request.member_registration.clone(),
491                acceptance.request.intended_owner_grant.clone(),
492                membership.clone(),
493            ),
494            store_commit::StreamActivation::grant_authorized(
495                root.store_root_hash,
496                acceptance.request.member_registration.clone(),
497                acceptance.request.intended_owner_grant.clone(),
498                recovery.clone(),
499            ),
500        ];
501        expected.sort();
502        if commit.stream_activations() != expected {
503            return Err(StorePullError::InvalidState(
504                "Merge Owner-promotion control carries different stream activations".to_string(),
505            ));
506        }
507        VerifiedCircleActivations::membership_control(commit, commit_ref)
508            .map_err(StorePullError::from)
509    }
510
511    pub(crate) async fn verified_membership_objects(
512        &self,
513        commit_ref: &StoreBatchCommitRef,
514        commit: &StoreBatchCommit,
515    ) -> Result<Option<VerifiedMergeMembershipClosure>, StorePullError> {
516        if commit.control().is_none() {
517            return Ok(None);
518        }
519        let verified = self.history.commits.get(commit_ref).ok_or_else(|| {
520            StorePullError::InvalidState(
521                "membership objects require an operation-verified Store commit".into(),
522            )
523        })?;
524        if verified.verified.value() != commit {
525            return Err(StorePullError::InvalidState(
526                "membership objects differ from their operation-verified Store commit".into(),
527            ));
528        }
529        let proof = verified
530            .history_evidence
531            .membership_proof
532            .as_deref()
533            .ok_or_else(|| {
534                StorePullError::InvalidState(
535                    "operation-verified Store control has no retained membership proof".into(),
536                )
537            })?;
538        VerifiedMergeMembershipClosure::from_verified_proof(proof.clone()).map(Some)
539    }
540
541    pub(crate) async fn verify_accepted_provider_access_activation(
542        &mut self,
543        access: &coven_protocol::provider::ActivatedStoreMemberProviderAccessGrant,
544        provider_admin: &coven_protocol::provider::ProviderAdminGrantRecord,
545        administrator: &StoreDeviceRegistration,
546    ) -> Result<(), StorePullError> {
547        let grant = self
548            .load_provider_access_grant(&access.grant_ref, administrator)
549            .await?;
550        if grant.value != access.grant {
551            return Err(StorePullError::InvalidState(
552                "device provider approval embeds a different access grant than its exact reference"
553                    .to_string(),
554            ));
555        }
556        let activation = self.load_ref(&access.activation).await?;
557        if activation.value().provider_access_grants() != std::slice::from_ref(&access.grant_ref)
558            || activation.value().author_registration != access.grant.administrator
559            || activation.author() != administrator
560        {
561            return Err(StorePullError::InvalidState(
562                "device provider approval activation is not the administrator's exact sole access grant"
563                    .to_string(),
564            ));
565        }
566        if !self.current_history_contains(&access.activation).await? {
567            return Err(StorePullError::InvalidState(
568                "device provider approval activation is absent from current accepted Store history"
569                    .to_string(),
570            ));
571        }
572        let membership = self
573            .load_predecessor_membership(&activation.value().membership_state)
574            .await
575            .map_err(StorePullError::from)?;
576        if !predecessor_verifies_provider_administrator(
577            &membership,
578            &access.grant.administrator_grant,
579            &activation.value().author_registration,
580            provider_admin,
581        ) {
582            return Err(StorePullError::InvalidState(
583                "device provider approval activation lacks exact predecessor provider-administrator authority"
584                    .to_string(),
585            ));
586        }
587        Ok(())
588    }
589
590    async fn current_history_contains(
591        &mut self,
592        expected: &StoreBatchCommitRef,
593    ) -> Result<bool, StorePullError> {
594        let publication = self.load_current_accepted_publication().await?;
595        Ok(publication.commits.contains_key(expected)
596            || publication
597                .accepted_snapshots
598                .last()
599                .is_some_and(|selected| {
600                    selected
601                        .snapshot
602                        .meta
603                        .history_summary
604                        .causal_cut
605                        .values()
606                        .any(|accepted| accepted == expected)
607                }))
608    }
609
610    /// Verify admission before opening the Store keyring. Portable membership
611    /// acceptance results establish authority without reading encrypted history.
612    pub async fn load_accepted_anchored_membership(
613        &self,
614        heads: &[protocol_membership::MembershipHeadRef],
615        owner: Option<&str>,
616    ) -> Result<MembershipChain, crate::sync::store::membership::AnchoredChainError> {
617        membership::AcceptedMembershipActivation::new(&self.root, &self.commit_verifier)
618            .load_exact_anchored_chain(heads, owner)
619            .await
620    }
621
622    pub async fn load_exact_anchored_membership(
623        &mut self,
624        heads: &[protocol_membership::MembershipHeadRef],
625        owner: Option<&str>,
626    ) -> Result<MembershipChain, crate::sync::store::membership::AnchoredChainError> {
627        self.load_exact_anchored_membership_traversal(heads, owner)
628            .await
629            .map(|(membership, _)| membership)
630    }
631
632    /// The anchored walk, and every membership object it read on the way.
633    ///
634    /// A snapshot publisher needs the second half: what it publishes as the
635    /// membership rollup is exactly the set of objects a reader of this same
636    /// frontier would otherwise fetch one at a time.
637    pub(crate) async fn load_exact_anchored_membership_traversal(
638        &mut self,
639        heads: &[protocol_membership::MembershipHeadRef],
640        owner: Option<&str>,
641    ) -> Result<
642        (MembershipChain, Vec<membership::TraversedMembershipStream>),
643        crate::sync::store::membership::AnchoredChainError,
644    > {
645        let (membership, traversed) = membership::HistoryMembershipActivation::new(self)
646            .load_exact_anchored_chain(heads, owner)
647            .await?;
648        if self.history.commits.is_empty() {
649            let authority = VerifiedMergeMembershipPrefix::default();
650            authority
651                .validate_complete_membership(&membership)
652                .map_err(crate::sync::store::membership::AnchoredChainError::from)?;
653            self.remember_verified_membership(authority, membership.clone());
654        }
655        Ok((membership, traversed))
656    }
657
658    pub(crate) async fn load_membership_at_exact_heads(
659        &mut self,
660        heads: &[protocol_membership::MembershipHeadRef],
661    ) -> Result<MembershipChain, crate::sync::store::membership::AnchoredChainError> {
662        membership::HistoryMembershipActivation::new(self)
663            .load_at_exact_heads(heads)
664            .await
665    }
666
667    pub(crate) async fn load_membership_at_verified_prefix(
668        &self,
669        heads: &[protocol_membership::MembershipHeadRef],
670        verified_activations: &VerifiedMergeMembershipPrefix,
671    ) -> Result<MembershipChain, crate::sync::store::membership::AnchoredChainError> {
672        VerifiedPrefixMembershipActivation::new(
673            &self.root,
674            &self.commit_verifier,
675            verified_activations,
676        )
677        .load_at_exact_heads(heads)
678        .await
679    }
680
681    pub(crate) async fn load_predecessor_membership(
682        &mut self,
683        state: &StoreMembershipStateRef,
684    ) -> Result<MembershipChain, RegistrationLoadError> {
685        self.load_membership_at_exact_heads(&state.heads)
686            .await
687            .map_err(RegistrationLoadError::from)
688    }
689
690    pub(crate) async fn load_predecessor_membership_at_verified_prefix(
691        &self,
692        state: &StoreMembershipStateRef,
693        verified_activations: &VerifiedMergeMembershipPrefix,
694    ) -> Result<MembershipChain, RegistrationLoadError> {
695        self.load_membership_at_verified_prefix(&state.heads, verified_activations)
696            .await
697            .map_err(RegistrationLoadError::from)
698    }
699
700    pub(crate) async fn project_membership_to_verified_prefix(
701        &self,
702        candidate_heads: &[protocol_membership::MembershipHeadRef],
703        prefix: &VerifiedMergeMembershipPrefix,
704    ) -> Result<MembershipChain, crate::sync::store::membership::AnchoredChainError> {
705        VerifiedPrefixMembershipActivation::new(&self.root, &self.commit_verifier, prefix)
706            .project(candidate_heads)
707            .await
708    }
709
710    #[cfg(any(test, feature = "test-utils"))]
711    pub(crate) async fn load_exact_membership_head(
712        &mut self,
713        reference: &protocol_membership::MembershipHeadRef,
714    ) -> Result<protocol_membership::AuthorHead, crate::sync::store::membership::AnchoredChainError>
715    {
716        self.commit_verifier
717            .membership_objects()
718            .load_head(reference)
719            .await
720            .map(|loaded| loaded.value)
721            .map_err(membership::map_membership_object_error)
722    }
723
724    #[cfg(any(test, feature = "test-utils"))]
725    pub(crate) async fn assert_deep_membership_projection(
726        &mut self,
727        heads: &[protocol_membership::MembershipHeadRef],
728    ) {
729        membership::HistoryMembershipActivation::new(self)
730            .assert_deep_valid_predecessor_path_is_iterative(heads)
731            .await;
732    }
733}