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
199pub(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 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 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}