1use super::reduction::*;
2use super::*;
3
4fn causal_owner_barriers(
5 owner_barriers: &BTreeMap<MembershipGrantId, CircleOwnerGrantBarrier>,
6) -> BTreeMap<MembershipGrantId, OwnerGrantBarrier<CircleRosterCoord>> {
7 owner_barriers
8 .iter()
9 .map(|(grant, barrier)| {
10 (
11 grant.clone(),
12 OwnerGrantBarrier::from_observed(barrier.observed_streams.iter().cloned()),
13 )
14 })
15 .collect()
16}
17
18#[derive(Debug, Clone)]
19pub struct CircleRosterChain {
20 pub(super) entries: Vec<CircleRosterEntry>,
21 pub(super) reduced: Option<causal_grants::ReducedGrants<CircleRosterCoord, CircleRole>>,
22 pub(super) status: CircleRosterStatus,
23 pub(super) head_refs: Vec<CircleRosterHeadRef>,
24 pub(super) resolution_checkpoint: Option<CircleRosterResolutionCheckpoint>,
25}
26
27#[derive(Debug, Clone)]
28pub(super) struct CircleRosterResolutionCheckpoint {
29 raw_heads: Vec<CircleRosterCoord>,
30 effective_frontier: Vec<CircleRosterCoord>,
31 grants: BTreeMap<MembershipGrantId, GrantState<CircleGrantRecord, CircleGrantRetirement>>,
32 included: BTreeSet<CircleRosterCoord>,
33 resolutions: Vec<CircleRosterConflictResolutionRef>,
34}
35
36impl CircleRosterChain {
37 pub fn from_entries(entries: Vec<CircleRosterEntry>) -> Result<Self, CircleRosterError> {
38 Self::from_entries_and_head_refs(entries, Vec::new())
39 }
40
41 pub fn from_entries_with_heads(
42 entries: Vec<CircleRosterEntry>,
43 heads: Vec<ExactCircleRosterHead>,
44 ) -> Result<Self, CircleRosterError> {
45 let head_refs = Self::validate_exact_heads(&entries, &heads)?;
46 Self::from_entries_and_head_refs(entries, head_refs)
47 }
48
49 pub fn with_exact_successor(
50 &self,
51 entry: CircleRosterEntry,
52 head: ExactCircleRosterHead,
53 ) -> Result<Self, CircleRosterError> {
54 if head.head().entry_coord() != entry.coord() {
55 return Err(CircleRosterError::MissingConflictHeads);
56 }
57 let stream = entry.coord().stream_key();
58 let mut entries = self.entries.clone();
59 entries.push(entry);
60 let mut head_refs = self.head_refs.clone();
61 head_refs.retain(|reference| reference.coord.stream_key() != stream);
62 head_refs.push(head.reference().clone());
63 head_refs.sort_by_key(|reference| reference.coord.stream_key());
64 Self::from_entries_head_refs_and_checkpoint(
65 entries,
66 head_refs,
67 self.resolution_checkpoint.clone(),
68 )
69 }
70
71 pub fn resolved_with_successor(
72 &self,
73 entry: CircleRosterEntry,
74 ) -> Result<ResolvedCircleRoster, CircleRosterError> {
75 let mut entries = self.entries.clone();
76 entries.push(entry);
77 Self::from_entries_head_refs_and_checkpoint(
78 entries,
79 self.head_refs.clone(),
80 self.resolution_checkpoint.clone(),
81 )?
82 .try_resolved()
83 }
84
85 pub fn validate_exact_heads(
86 entries: &[CircleRosterEntry],
87 heads: &[ExactCircleRosterHead],
88 ) -> Result<Vec<CircleRosterHeadRef>, CircleRosterError> {
89 let founder = entries.first().ok_or(CircleRosterError::Empty)?;
90 if heads.iter().any(|bound| {
91 let head = bound.head();
92 let reference = bound.reference();
93 head.store_root_hash != founder.store_root_hash
94 || head.circle_id != founder.circle_id
95 || head.entry_coord() != reference.coord
96 || entries
97 .iter()
98 .find(|entry| entry.coord() == reference.coord)
99 .is_none_or(|entry| head.resolutions != entry.resolution_dependencies)
100 }) {
101 return Err(CircleRosterError::MissingConflictHeads);
102 }
103 Ok(heads.iter().map(|head| head.reference().clone()).collect())
104 }
105
106 fn from_entries_and_head_refs(
107 entries: Vec<CircleRosterEntry>,
108 head_refs: Vec<CircleRosterHeadRef>,
109 ) -> Result<Self, CircleRosterError> {
110 Self::from_entries_head_refs_and_checkpoint(entries, head_refs, None)
111 }
112
113 fn from_entries_head_refs_and_checkpoint(
114 entries: Vec<CircleRosterEntry>,
115 head_refs: Vec<CircleRosterHeadRef>,
116 resolution_checkpoint: Option<CircleRosterResolutionCheckpoint>,
117 ) -> Result<Self, CircleRosterError> {
118 let founder = entries.first().ok_or(CircleRosterError::Empty)?;
119 let expected_store = founder.store_root_hash;
120 let expected_circle = founder.circle_id;
121 for (index, entry) in entries.iter().enumerate() {
122 if !entry.verify() {
123 return Err(CircleRosterError::InvalidEntry(index));
124 }
125 if entry.store_root_hash != expected_store || entry.circle_id != expected_circle {
126 return Err(CircleRosterError::ContextMismatch { index });
127 }
128 if matches!(
129 entry.change,
130 CircleRosterChange::ResolutionActivation { .. }
131 ) && resolution_checkpoint.as_ref().is_none_or(|checkpoint| {
132 let already_checkpointed = checkpoint.included.contains(&entry.coord())
133 || checkpoint.raw_heads.contains(&entry.coord());
134 !already_checkpointed
135 && (entry.dependencies != checkpoint.effective_frontier
136 || entry.resolution_dependencies != checkpoint.resolutions)
137 }) {
138 return Err(CircleRosterError::InvalidEntry(index));
139 }
140 }
141 let checkpoint_heads = resolution_checkpoint
142 .as_ref()
143 .map_or_else(Vec::new, |checkpoint| checkpoint.raw_heads.clone());
144 let normalized = causal_grants::entries_beyond_checkpoint(&entries, &checkpoint_heads)
145 .map(|entry| CausalEntry {
146 coord: entry.coord(),
147 previous_hash: entry.previous_hash,
148 dependencies: entry
149 .dependencies
150 .iter()
151 .cloned()
152 .map(|coord| (coord.stream_key(), coord))
153 .collect(),
154 change: match &entry.change {
155 CircleRosterChange::Founder {
156 member_pubkey,
157 grant_id,
158 } => CausalChange::Founder {
159 member_pubkey: member_pubkey.clone(),
160 grant_id: grant_id.clone(),
161 assignment: CircleRole::Owner,
162 },
163 CircleRosterChange::SetMember {
164 member_pubkey,
165 role,
166 grant_id,
167 replaces,
168 owner_barriers,
169 } => CausalChange::SetMember {
170 member_pubkey: member_pubkey.clone(),
171 assignment: *role,
172 grant_id: grant_id.clone(),
173 replaces: replaces.clone(),
174 owner_barriers: causal_owner_barriers(owner_barriers),
175 },
176 CircleRosterChange::RemoveMember {
177 member_pubkey,
178 removes,
179 owner_barriers,
180 } => CausalChange::RemoveMember {
181 member_pubkey: member_pubkey.clone(),
182 removes: removes.clone(),
183 owner_barriers: causal_owner_barriers(owner_barriers),
184 },
185 CircleRosterChange::ResolutionActivation { .. } => {
186 CausalChange::ResolutionActivation
187 }
188 },
189 })
190 .collect::<Vec<_>>();
191 let reduction = match &resolution_checkpoint {
192 Some(checkpoint) => {
193 let seeds = causal_grants::map_checkpoint_grants(
194 &checkpoint.grants,
195 |record| causal_grants::CausalSeedGrant {
196 member_pubkey: record.member_pubkey.clone(),
197 assignment: record.role,
198 },
199 || (),
200 );
201 causal_grants::reduce_from_checkpoint(
202 &normalized,
203 &checkpoint.raw_heads,
204 &checkpoint.effective_frontier,
205 &seeds,
206 &checkpoint.included,
207 )?
208 }
209 None => causal_grants::reduce(&normalized)?,
210 };
211 let founder_entry = entries
212 .iter()
213 .find(|entry| matches!(entry.change, CircleRosterChange::Founder { .. }))
214 .expect("shared reducer requires one founder");
215 if founder_entry.circle_id
216 != CircleId::founder(
217 founder_entry.store_root_hash,
218 &founder_entry.author_pubkey,
219 &founder_entry.author_owner_grant,
220 )
221 {
222 return Err(CircleRosterError::InvalidFounderIdentity);
223 }
224 let (reduced, status) = match reduction {
225 CausalGrantStatus::Resolved(reduced) => {
226 let resolved = resolved_circle_roster(
227 &reduced,
228 resolution_checkpoint
229 .as_ref()
230 .map(|checkpoint| &checkpoint.grants),
231 )?;
232 (Some(reduced), CircleRosterStatus::Resolved(resolved))
233 }
234 CausalGrantStatus::Conflict(CausalGrantConflict::ConcurrentMemberAssignments {
235 raw_heads,
236 effective_frontier,
237 member_pubkey,
238 conflicting_grants,
239 uncontested_grants,
240 reduced,
241 }) => {
242 let heads = exact_circle_head_refs(&head_refs, &raw_heads)?;
243 let conflict_hash = circle_assignment_conflict_hash(
244 expected_store,
245 expected_circle,
246 &heads,
247 &member_pubkey,
248 &conflicting_grants,
249 );
250 (
251 Some(reduced),
252 CircleRosterStatus::Conflict(
253 CircleRosterConflict::ConcurrentMemberAssignments {
254 conflict_hash,
255 heads,
256 effective_frontier,
257 member_pubkey,
258 conflicting_grants: map_circle_grants(
259 conflicting_grants,
260 resolution_checkpoint
261 .as_ref()
262 .map(|checkpoint| &checkpoint.grants),
263 )?,
264 uncontested_grants: map_circle_grants(
265 uncontested_grants,
266 resolution_checkpoint
267 .as_ref()
268 .map(|checkpoint| &checkpoint.grants),
269 )?,
270 },
271 ),
272 )
273 }
274 CausalGrantStatus::Conflict(CausalGrantConflict::RevocationCycle {
275 raw_heads,
276 cyclic_sources,
277 involved_owner_grants,
278 maximal_valid_branches,
279 }) => {
280 let heads = exact_circle_head_refs(&head_refs, &raw_heads)?;
281 let branches = maximal_valid_branches
282 .into_iter()
283 .map(|branch| -> Result<CircleRosterBranch, CircleRosterError> {
284 let resolved = resolved_circle_roster(
285 &branch.reduced,
286 resolution_checkpoint
287 .as_ref()
288 .map(|checkpoint| &checkpoint.grants),
289 )?;
290 Ok(CircleRosterBranch {
291 heads: exact_circle_head_refs(&head_refs, &branch.raw_heads)?,
292 effective_frontier: branch.effective_frontier,
293 grants: resolved.grants,
294 state_hash: resolved.state_hash,
295 })
296 })
297 .collect::<Result<Vec<_>, _>>()?;
298 let conflict_hash = circle_revocation_conflict_hash(
299 expected_store,
300 expected_circle,
301 &heads,
302 &cyclic_sources,
303 &involved_owner_grants,
304 );
305 (
306 None,
307 CircleRosterStatus::Conflict(CircleRosterConflict::RevocationCycle {
308 conflict_hash,
309 heads,
310 cyclic_sources,
311 involved_owner_grants,
312 maximal_valid_branches: branches,
313 }),
314 )
315 }
316 };
317 Ok(Self {
318 entries,
319 reduced,
320 status,
321 head_refs,
322 resolution_checkpoint,
323 })
324 }
325
326 pub fn entries(&self) -> &[CircleRosterEntry] {
327 &self.entries
328 }
329
330 pub fn status(&self) -> &CircleRosterStatus {
331 &self.status
332 }
333
334 pub fn resolution_refs(&self) -> &[CircleRosterConflictResolutionRef] {
335 self.resolution_checkpoint
336 .as_ref()
337 .map_or(&[], |checkpoint| checkpoint.resolutions.as_slice())
338 }
339
340 pub fn resolution_checkpoint_covers(&self, coord: &CircleRosterCoord) -> bool {
341 self.resolution_checkpoint
342 .as_ref()
343 .is_some_and(|checkpoint| {
344 checkpoint.included.contains(coord) || checkpoint.raw_heads.contains(coord)
345 })
346 }
347
348 pub fn replay_resolved_history_to_heads(
349 &self,
350 entries: Vec<CircleRosterEntry>,
351 heads: Vec<CircleRosterHeadRef>,
352 ) -> Result<Self, CircleRosterError> {
353 let checkpoint = self
354 .resolution_checkpoint
355 .clone()
356 .ok_or(CircleRosterError::InvalidConflictResolution)?;
357 if heads.iter().any(|head| {
358 entries
359 .iter()
360 .find(|entry| entry.coord() == head.coord)
361 .is_none()
362 }) {
363 return Err(CircleRosterError::MissingConflictHeads);
364 }
365 Self::from_entries_head_refs_and_checkpoint(entries, heads, Some(checkpoint))
366 }
367
368 pub fn replay_merged_resolved_histories_to_heads(
369 chains: &[&CircleRosterChain],
370 entries: Vec<CircleRosterEntry>,
371 heads: Vec<CircleRosterHeadRef>,
372 ) -> Result<Self, CircleRosterError> {
373 let mut raw_by_stream = BTreeMap::new();
374 let mut effective_by_stream = BTreeMap::new();
375 let mut grants = BTreeMap::new();
376 let mut included = BTreeSet::new();
377 let mut resolutions = BTreeSet::new();
378 for chain in chains {
379 let checkpoint = chain
380 .resolution_checkpoint
381 .as_ref()
382 .ok_or(CircleRosterError::InvalidConflictResolution)?;
383 if !causal_grants::merge_checkpoint_frontier(&mut raw_by_stream, &checkpoint.raw_heads)
384 || !causal_grants::merge_checkpoint_frontier(
385 &mut effective_by_stream,
386 &checkpoint.effective_frontier,
387 )
388 || !causal_grants::merge_checkpoint_evidence(
389 &mut grants,
390 &mut included,
391 &checkpoint.grants,
392 &checkpoint.included,
393 )
394 {
395 return Err(CircleRosterError::InvalidConflictResolution);
396 }
397 resolutions.extend(checkpoint.resolutions.iter().cloned());
398 }
399 let checkpoint = CircleRosterResolutionCheckpoint {
400 raw_heads: raw_by_stream.into_values().collect(),
401 effective_frontier: effective_by_stream.into_values().collect(),
402 grants,
403 included,
404 resolutions: resolutions.into_iter().collect(),
405 };
406 let base = chains
407 .first()
408 .ok_or(CircleRosterError::InvalidConflictResolution)?;
409 let mut merged = (*base).clone();
410 merged.resolution_checkpoint = Some(checkpoint);
411 merged.replay_resolved_history_to_heads(entries, heads)
412 }
413
414 pub fn checkpoint_current_resolved_state(&mut self) -> Result<(), CircleRosterError> {
415 self.try_resolved()?;
416 let resolutions = self
417 .resolution_checkpoint
418 .as_ref()
419 .map_or_else(Vec::new, |checkpoint| checkpoint.resolutions.clone());
420 let checkpoint_grants = self
421 .resolution_checkpoint
422 .as_ref()
423 .map(|checkpoint| &checkpoint.grants);
424 let reduced = self
425 .reduced
426 .as_ref()
427 .ok_or(CircleRosterError::InvalidConflictResolution)?;
428 let grants = reduced
429 .grants
430 .iter()
431 .map(|(grant, state)| -> Result<_, CircleRosterError> {
432 Ok((
433 grant.clone(),
434 map_circle_grant_state(grant, state, checkpoint_grants)?,
435 ))
436 })
437 .collect::<Result<_, _>>()?;
438 self.resolution_checkpoint = Some(CircleRosterResolutionCheckpoint {
439 raw_heads: self.author_heads(),
440 effective_frontier: self.effective_frontier(),
441 grants,
442 included: reduced.included.clone(),
443 resolutions,
444 });
445 Ok(())
446 }
447
448 pub fn resolved(&self) -> ResolvedCircleRoster {
449 self.try_resolved()
450 .expect("caller must inspect Circle roster status before consuming resolved state")
451 }
452
453 pub fn try_resolved(&self) -> Result<ResolvedCircleRoster, CircleRosterError> {
454 match &self.status {
455 CircleRosterStatus::Resolved(resolved) => Ok(resolved.clone()),
456 CircleRosterStatus::Conflict(_) => Err(CircleRosterError::Conflict),
457 }
458 }
459
460 pub(crate) fn resolved_with(
461 &self,
462 resolutions: &[CircleRosterConflictResolution],
463 ) -> Result<ResolvedCircleRoster, CircleRosterError> {
464 match &self.status {
465 CircleRosterStatus::Resolved(resolved) if resolutions.is_empty() => {
466 Ok(resolved.clone())
467 }
468 CircleRosterStatus::Conflict(conflict) => resolve_circle_roster_conflict(
469 self.entries[0].store_root_hash,
470 self.entries[0].circle_id,
471 conflict,
472 resolutions,
473 ),
474 CircleRosterStatus::Resolved(_) => Err(CircleRosterError::InvalidConflictResolution),
475 }
476 }
477
478 pub fn apply_resolutions(
479 &mut self,
480 resolutions: &[CircleRosterConflictResolution],
481 ) -> Result<(), CircleRosterError> {
482 let (raw_heads, effective_frontier) = match self.status() {
483 CircleRosterStatus::Conflict(CircleRosterConflict::RevocationCycle {
484 heads,
485 maximal_valid_branches,
486 ..
487 }) => (
488 heads
489 .iter()
490 .map(|reference| reference.coord.clone())
491 .collect(),
492 causal_grants::selected_branch_frontier(resolutions, |resolution| {
493 maximal_valid_branches
494 .iter()
495 .find(|branch| branch.heads == resolution.resolver_branch_heads)
496 .map(|branch| branch.effective_frontier.as_slice())
497 .ok_or(CircleRosterError::InvalidConflictResolution)
498 })?,
499 ),
500 _ => return Err(CircleRosterError::InvalidConflictResolution),
501 };
502 let resolved = self.resolved_with(resolutions)?;
503 let grants = resolved.grants.clone();
504 let included = causal_grants::history_closure(&self.entries, &effective_frontier);
505 let checkpoint = CircleRosterResolutionCheckpoint {
506 raw_heads,
507 effective_frontier: effective_frontier.clone(),
508 grants: grants.clone(),
509 included: included.clone(),
510 resolutions: causal_grants::checkpoint_resolution_refs(
511 self.resolution_checkpoint
512 .as_ref()
513 .map(|checkpoint| checkpoint.resolutions.as_slice()),
514 resolutions
515 .iter()
516 .map(CircleRosterConflictResolution::resolution_ref),
517 ),
518 };
519 self.reduced = Some(causal_grants::ReducedGrants {
520 grants: causal_grants::map_checkpoint_grants(
521 &grants,
522 |record| causal_grants::GrantRecord {
523 member_pubkey: record.member_pubkey.clone(),
524 assignment: record.role,
525 creation: causal_grants::CausalGrantCreation::Checkpoint,
526 },
527 || causal_grants::CausalGrantRetirement::Checkpoint,
528 ),
529 included: included.clone(),
530 });
531 self.status = CircleRosterStatus::Resolved(resolved);
532 self.resolution_checkpoint = Some(checkpoint);
533 Ok(())
534 }
535
536 pub fn author_heads(&self) -> Vec<CircleRosterCoord> {
537 causal_grants::stream_frontier(self.entries.iter().map(CircleRosterEntry::coord))
538 }
539
540 pub fn effective_frontier(&self) -> Vec<CircleRosterCoord> {
541 let Some(reduced) = &self.reduced else {
542 return Vec::new();
543 };
544 causal_grants::stream_frontier(
545 self.entries
546 .iter()
547 .map(CircleRosterEntry::coord)
548 .filter(|coord| reduced.includes_coord(coord)),
549 )
550 }
551
552 fn active_grants(&self, member_pubkey: &str) -> BTreeSet<MembershipGrantId> {
553 let reduced = self
554 .reduced
555 .as_ref()
556 .expect("resolved roster has reduced grants");
557 reduced
558 .grants
559 .iter()
560 .filter_map(|(grant, state)| {
561 state
562 .active()
563 .is_some_and(|record| record.member_pubkey == member_pubkey)
564 .then_some(grant.clone())
565 })
566 .collect()
567 }
568
569 fn active_owner_grant(&self, member_pubkey: &str) -> Option<MembershipGrantId> {
570 self.active_grants(member_pubkey).into_iter().find(|grant| {
571 self.reduced
572 .as_ref()
573 .expect("resolved roster has reduced grants")
574 .active_grant(grant)
575 .is_some_and(|record| record.assignment == CircleRole::Owner)
576 })
577 }
578
579 pub fn reusable_author_streams(
580 &self,
581 author_pubkey: &str,
582 device_id: &str,
583 grant: &MembershipGrantId,
584 ) -> BTreeSet<AuthorStreamId> {
585 self.effective_frontier()
586 .into_iter()
587 .filter(|effective_tip| {
588 effective_tip.author_pubkey == author_pubkey
589 && effective_tip.device_id == device_id
590 && effective_tip.author_owner_grant == *grant
591 && self
592 .entries
593 .iter()
594 .map(CircleRosterEntry::coord)
595 .filter(|coord| coord.stream_key() == effective_tip.stream_key())
596 .max_by_key(|coord| coord.seq)
597 .as_ref()
598 == Some(effective_tip)
599 })
600 .map(|coord| coord.stream_id)
601 .collect()
602 }
603
604 fn owner_barriers(
605 &self,
606 grants: &BTreeSet<MembershipGrantId>,
607 dependencies: &[CircleRosterCoord],
608 ) -> BTreeMap<MembershipGrantId, CircleOwnerGrantBarrier> {
609 grants
610 .iter()
611 .filter(|grant| {
612 self.reduced
613 .as_ref()
614 .expect("resolved roster has reduced grants")
615 .active_grant(grant)
616 .is_some_and(|record| record.assignment == CircleRole::Owner)
617 })
618 .map(|grant| {
619 let observed_streams = dependencies
620 .iter()
621 .filter(|coord| coord.author_owner_grant == *grant)
622 .cloned()
623 .collect();
624 (grant.clone(), CircleOwnerGrantBarrier { observed_streams })
625 })
626 .collect()
627 }
628
629 pub(super) fn next_position(
630 &self,
631 stream: &CircleAuthorStreamKey,
632 ) -> Result<(u64, Option<ObjectHash>), CircleRosterError> {
633 let raw_tip = self
634 .entries
635 .iter()
636 .map(CircleRosterEntry::coord)
637 .filter(|coord| coord.stream_key() == *stream)
638 .max_by_key(|coord| coord.seq);
639 let effective_tip = self
640 .effective_frontier()
641 .into_iter()
642 .find(|coord| coord.stream_key() == *stream);
643 if raw_tip.is_some()
644 && !self
645 .reusable_author_streams(
646 &stream.author_pubkey,
647 &stream.device_id,
648 &stream.author_owner_grant,
649 )
650 .contains(&stream.stream_id)
651 {
652 return Err(CircleRosterError::PrunedAuthorStream);
653 }
654 match effective_tip {
655 Some(tip) => Ok((
656 tip.seq
657 .checked_add(1)
658 .ok_or(CircleRosterError::SequenceExhausted { current: tip.seq })?,
659 Some(tip.entry_hash),
660 )),
661 None => Ok((1, None)),
662 }
663 }
664
665 pub fn signed_set_member(
666 &self,
667 device_id: &str,
668 stream_id: AuthorStreamId,
669 member_pubkey: String,
670 role: CircleRole,
671 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
672 ) -> Result<CircleRosterEntry, CircleRosterError> {
673 self.signed_change(device_id, stream_id, member_pubkey, Some(role), signer)
674 }
675
676 pub fn signed_remove_member(
677 &self,
678 device_id: &str,
679 stream_id: AuthorStreamId,
680 member_pubkey: String,
681 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
682 ) -> Result<CircleRosterEntry, CircleRosterError> {
683 if self.active_grants(&member_pubkey).is_empty() {
684 return Err(CircleRosterError::NotAMember(member_pubkey));
685 }
686 self.signed_change(device_id, stream_id, member_pubkey, None, signer)
687 }
688
689 fn signed_change(
690 &self,
691 device_id: &str,
692 stream_id: AuthorStreamId,
693 member_pubkey: String,
694 role: Option<CircleRole>,
695 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
696 ) -> Result<CircleRosterEntry, CircleRosterError> {
697 if matches!(self.status, CircleRosterStatus::Conflict(_)) {
698 return Err(CircleRosterError::Conflict);
699 }
700 let author_pubkey = keys::public_key_hex(signer);
701 let author_owner_grant = self
702 .active_owner_grant(&author_pubkey)
703 .ok_or_else(|| CircleRosterError::SignerIsNotOwner(author_pubkey.clone()))?;
704 let stream = CircleAuthorStreamKey {
705 author_pubkey: author_pubkey.clone(),
706 device_id: device_id.to_string(),
707 stream_id,
708 author_owner_grant: author_owner_grant.clone(),
709 };
710 let (seq, previous_hash) = self.next_position(&stream)?;
711 let dependencies = self.effective_frontier();
712 let replaced = self.active_grants(&member_pubkey);
713 let owner_barriers = self.owner_barriers(&replaced, &dependencies);
714 let change = match role {
715 Some(role) => CircleRosterChange::SetMember {
716 member_pubkey: member_pubkey.clone(),
717 role,
718 grant_id: MembershipGrantId(ObjectHash::digest(
719 format!(
720 "coven.circle-roster-grant.v1\0{}\0{}\0{}\0{}\0{}\0{}\0{}",
721 self.entries[0].circle_id,
722 author_pubkey,
723 device_id,
724 stream_id,
725 author_owner_grant,
726 seq,
727 member_pubkey
728 )
729 .as_bytes(),
730 )),
731 replaces: replaced,
732 owner_barriers,
733 },
734 None => CircleRosterChange::RemoveMember {
735 member_pubkey,
736 removes: replaced,
737 owner_barriers,
738 },
739 };
740 let entry = Signed::sign(
741 CircleRosterEntryBody {
742 store_root_hash: self.entries[0].store_root_hash,
743 circle_id: self.entries[0].circle_id,
744 author_pubkey,
745 device_id: device_id.to_string(),
746 stream_id,
747 author_owner_grant,
748 seq,
749 previous_hash,
750 dependencies,
751 resolution_dependencies: self.resolution_refs().to_vec(),
752 change,
753 },
754 signer,
755 );
756 let mut candidate_history = self.entries.clone();
757 candidate_history.push(entry.clone());
758 Self::from_entries_head_refs_and_checkpoint(
759 candidate_history,
760 self.head_refs.clone(),
761 self.resolution_checkpoint.clone(),
762 )?;
763 Ok(entry)
764 }
765
766 #[cfg(any(test, feature = "test-utils"))]
767 pub fn signed_cycle_resolution(
768 &self,
769 resolver_branch_heads: Vec<CircleRosterHeadRef>,
770 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
771 ) -> Result<CircleRosterConflictResolution, CircleRosterError> {
772 let CircleRosterStatus::Conflict(CircleRosterConflict::RevocationCycle {
773 conflict_hash,
774 heads,
775 involved_owner_grants,
776 maximal_valid_branches,
777 ..
778 }) = self.status()
779 else {
780 return Err(CircleRosterError::Conflict);
781 };
782 let resolver_pubkey = keys::public_key_hex(signer);
783 let branch = maximal_valid_branches
784 .iter()
785 .find(|branch| branch.heads == resolver_branch_heads)
786 .ok_or(CircleRosterError::InvalidConflictResolution)?;
787 if !causal_grants::active_grants(&branch.grants).any(|(_, record)| {
788 record.member_pubkey == resolver_pubkey && record.role == CircleRole::Owner
789 }) {
790 return Err(CircleRosterError::SignerIsNotOwner(resolver_pubkey));
791 }
792 let replacement_grant = derive_circle_resolution_grant(conflict_hash, &resolver_pubkey);
793 let mut retired_owner_grants = involved_owner_grants.clone();
794 retired_owner_grants.extend(causal_grants::active_grants(&branch.grants).filter_map(
795 |(grant, record)| {
796 (record.member_pubkey == resolver_pubkey && record.role == CircleRole::Owner)
797 .then_some(grant.clone())
798 },
799 ));
800 Ok(Signed::sign(
801 super::CircleRosterConflictResolutionBody {
802 store_root_hash: self.entries[0].store_root_hash,
803 circle_id: self.entries[0].circle_id,
804 conflict_hash: *conflict_hash,
805 conflicting_heads: heads.clone(),
806 retired_owner_grants,
807 resolver_pubkey,
808 resolver_branch_heads,
809 replacement_grant,
810 },
811 signer,
812 ))
813 }
814}