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 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 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 #[cfg(test)]
947 pub fn author_heads(&self) -> Vec<MembershipCoord> {
948 causal_grants::stream_frontier(self.coords.iter().cloned())
949 }
950}