1use super::access::*;
2use super::*;
3
4#[derive(Debug, Clone)]
5pub struct CircleAuthoringState {
6 pub candidate_family: CandidateFamilyId,
7 pub control: PreparedCircleControl,
8 pub access: CircleAccessLeaf,
9 pub roster: CircleMaterializedRoster,
10 pub metadata: CircleMetadata,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct CircleCurrentControl {
16 pub(super) control: PreparedCircleControl,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case", deny_unknown_fields)]
21pub(crate) enum CircleInactiveAccess {
22 NotGranted,
23 Inactive {
24 candidate_family: CandidateFamilyId,
25 access: CircleAccessLeaf,
26 },
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case", deny_unknown_fields)]
31pub struct CircleAccessibleState {
32 pub(super) current: CircleCurrentControl,
33 candidate_family: CandidateFamilyId,
34 access: CircleAccessLeaf,
35 roster: CircleMaterializedRoster,
36 metadata: CircleMetadata,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct CircleInactiveState {
42 current: CircleCurrentControl,
43 access: CircleInactiveAccess,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case", deny_unknown_fields)]
48pub enum CircleCurrentState {
49 Active(Box<CircleAccessibleState>),
50 Closing(Box<CircleAccessibleState>),
51 Inactive(Box<CircleInactiveState>),
52 Deleted(Box<CircleCurrentControl>),
53 ControlConflict { branches: Vec<CircleCurrentControl> },
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct RotationRequired {
62 pub removed_members: Vec<String>,
63}
64
65impl CircleCurrentControl {
66 fn from_verified(activation: &VerifiedCircleReference) -> Self {
67 Self {
68 control: activation.control.clone(),
69 }
70 }
71
72 pub fn circle_id(&self) -> CircleId {
73 self.control.value.circle_id
74 }
75
76 pub fn coordinate(&self) -> &CircleControlCoord {
77 &self.control.coord
78 }
79
80 pub(super) fn control_hash(&self) -> ObjectHash {
81 self.control.coord.control_hash()
82 }
83
84 fn causally_covers(&self, prior: &Self) -> bool {
85 self.control.value.causally_covers(&prior.control.value)
86 }
87
88 fn verify(&self) -> bool {
89 self.control.verify()
90 }
91
92 #[cfg(any(test, feature = "test-utils"))]
93 pub fn control_mut_for_test(&mut self) -> &mut PreparedCircleControl {
94 &mut self.control
95 }
96
97 #[cfg(any(test, feature = "test-utils"))]
98 pub fn control_hash_for_test(&self) -> ObjectHash {
99 self.control_hash()
100 }
101}
102
103impl CircleCurrentState {
104 pub fn from_verified(
105 candidate_family: CandidateFamilyId,
106 activation: &VerifiedCircleReference,
107 ) -> Result<Self, CircleStateError> {
108 if activation
109 .local_access
110 .as_ref()
111 .is_some_and(|access| access.leaf.value.candidate_family != candidate_family)
112 {
113 return Err(CircleStateError::Invariant(
114 "Circle access belongs to another candidate family".to_string(),
115 ));
116 }
117 Self::from_verified_reference(activation)
118 }
119
120 pub fn from_verified_reference(
124 activation: &VerifiedCircleReference,
125 ) -> Result<Self, CircleStateError> {
126 let current = CircleCurrentControl::from_verified(activation);
127 if current.control.value.state().is_deleted() {
130 let state = Self::Deleted(Box::new(current));
131 return if state.verify() {
132 Ok(state)
133 } else {
134 Err(CircleStateError::Invariant(
135 "verified Circle deletion cannot form a valid current state".to_string(),
136 ))
137 };
138 }
139 let state = match &activation.local_access {
140 None => Self::Inactive(Box::new(CircleInactiveState {
141 current,
142 access: CircleInactiveAccess::NotGranted,
143 })),
144 Some(VerifiedCircleAccess {
145 leaf, active: None, ..
146 }) => Self::Inactive(Box::new(CircleInactiveState {
147 current,
148 access: CircleInactiveAccess::Inactive {
149 candidate_family: leaf.value.candidate_family,
150 access: leaf.value.clone(),
151 },
152 })),
153 Some(VerifiedCircleAccess {
154 leaf,
155 active: Some(active),
156 ..
157 }) => {
158 let accessible = Box::new(CircleAccessibleState {
159 current,
160 candidate_family: leaf.value.candidate_family,
161 access: leaf.value.clone(),
162 roster: active.roster.clone(),
163 metadata: active.metadata.clone(),
164 });
165 match accessible.current.control.value.state() {
166 crate::circle::CircleControlState::ActiveEpoch(_) => Self::Active(accessible),
167 crate::circle::CircleControlState::EpochClose(_) => Self::Closing(accessible),
168 crate::circle::CircleControlState::Deleted(_) => {
169 return Err(CircleStateError::Invariant(
170 "verified Circle deletion cannot carry active access".to_string(),
171 ))
172 }
173 }
174 }
175 };
176 if state.verify() {
177 Ok(state)
178 } else {
179 Err(CircleStateError::Invariant(
180 "verified Circle activation cannot form a valid current state".to_string(),
181 ))
182 }
183 }
184
185 pub fn advance(self, next: Self) -> Result<Self, CircleStateError> {
186 if !self.verify() || !next.verify() {
187 return Err(CircleStateError::Invariant(
188 "Circle current-state reduction received invalid state".to_string(),
189 ));
190 }
191 if self.circle_id() != next.circle_id() {
192 return Err(CircleStateError::Invariant(
193 "Circle current-state reduction crossed Circle identities".to_string(),
194 ));
195 }
196 match self {
197 Self::Active(active) => advance_resolved_control(active.current, next),
198 Self::Closing(closing) => advance_resolved_control(closing.current, next),
199 Self::Inactive(inactive) => advance_resolved_control(inactive.current, next),
200 Self::Deleted(deleted) => {
206 let next_current = next.resolved_control().ok_or_else(|| {
207 CircleStateError::Invariant(
208 "new Circle activation is already conflicted".to_string(),
209 )
210 })?;
211 if next_current.causally_covers(&deleted) {
212 return Err(CircleStateError::Invariant(
213 "Circle deletion is terminal; a control descending from it is invalid"
214 .to_string(),
215 ));
216 }
217 let mut branches = vec![*deleted, next_current.clone()];
218 canonicalize_control_branches(&mut branches)?;
219 Ok(Self::ControlConflict { branches })
220 }
221 Self::ControlConflict { mut branches } => {
222 let next_current = next
223 .resolved_control()
224 .ok_or_else(|| {
225 CircleStateError::Invariant(
226 "new Circle activation is already conflicted".to_string(),
227 )
228 })?
229 .clone();
230 branches.retain(|branch| !next_current.causally_covers(branch));
231 if branches.is_empty() {
232 return Ok(next);
233 }
234 branches.push(next_current);
235 canonicalize_control_branches(&mut branches)?;
236 Ok(Self::ControlConflict { branches })
237 }
238 }
239 }
240
241 pub fn without_local_access(self) -> Self {
242 match self {
243 Self::Active(accessible) | Self::Closing(accessible) => {
244 Self::Inactive(Box::new(CircleInactiveState {
245 current: accessible.current,
246 access: CircleInactiveAccess::NotGranted,
247 }))
248 }
249 Self::Inactive(inactive) => Self::Inactive(Box::new(CircleInactiveState {
250 current: inactive.current,
251 access: CircleInactiveAccess::NotGranted,
252 })),
253 Self::Deleted(deleted) => Self::Deleted(deleted),
254 Self::ControlConflict { branches } => Self::ControlConflict { branches },
255 }
256 }
257
258 pub fn verify(&self) -> bool {
259 match self {
260 Self::Active(active) => {
261 matches!(
262 active.current.control.value.state(),
263 crate::circle::CircleControlState::ActiveEpoch(_)
264 ) && verify_accessible_state(active)
265 }
266 Self::Closing(closing) => {
267 matches!(
268 closing.current.control.value.state(),
269 crate::circle::CircleControlState::EpochClose(_)
270 ) && verify_accessible_state(closing)
271 }
272 Self::Inactive(inactive) => {
273 inactive.current.verify()
274 && match &inactive.access {
275 CircleInactiveAccess::NotGranted => true,
276 CircleInactiveAccess::Inactive {
277 candidate_family,
278 access,
279 } => {
280 access.verify_for_control(&inactive.current.control, *candidate_family)
281 && matches!(access.disposition, CircleAccessDisposition::Inactive)
282 }
283 }
284 }
285 Self::Deleted(deleted) => {
286 matches!(
287 deleted.control.value.state(),
288 crate::circle::CircleControlState::Deleted(_)
289 ) && deleted.verify()
290 }
291 Self::ControlConflict { branches } => {
292 branches.len() >= 2
293 && branches.iter().all(|branch| {
294 branch.verify() && branch.circle_id() == branches[0].circle_id()
295 })
296 && branches
297 .windows(2)
298 .all(|pair| pair[0].control_hash() < pair[1].control_hash())
299 }
300 }
301 }
302
303 pub fn circle_id(&self) -> CircleId {
304 match self {
305 Self::Active(active) => active.current.circle_id(),
306 Self::Closing(closing) => closing.current.circle_id(),
307 Self::Inactive(inactive) => inactive.current.circle_id(),
308 Self::Deleted(deleted) => deleted.circle_id(),
309 Self::ControlConflict { branches } => branches[0].circle_id(),
310 }
311 }
312
313 pub fn rotation_required(
317 &self,
318 active_store_members: &BTreeSet<String>,
319 ) -> Option<RotationRequired> {
320 let accessible = match self {
321 Self::Active(accessible) | Self::Closing(accessible) => accessible,
322 Self::Inactive(_) | Self::Deleted(_) | Self::ControlConflict { .. } => return None,
323 };
324 let removed_members: Vec<String> = accessible
325 .roster
326 .members()
327 .into_keys()
328 .filter(|pubkey| !active_store_members.contains(pubkey))
329 .collect();
330 if removed_members.is_empty() {
331 None
332 } else {
333 Some(RotationRequired { removed_members })
334 }
335 }
336
337 pub fn derived_state(
347 &self,
348 active_store_members: &BTreeSet<String>,
349 ) -> crate::circle::CircleState {
350 use crate::circle::CircleState;
351 match self {
352 Self::Active(_) => match self.rotation_required(active_store_members) {
353 Some(RotationRequired { removed_members }) => {
354 CircleState::RotationRequired { removed_members }
355 }
356 None => CircleState::Active,
357 },
358 Self::Closing(_) => CircleState::Closing,
359 Self::Inactive(_) => CircleState::Inactive,
360 Self::Deleted(_) => CircleState::Deleted,
361 Self::ControlConflict { branches } => CircleState::ControlConflict {
362 branches: branches
363 .iter()
364 .map(|branch| branch.coordinate().clone())
365 .collect(),
366 },
367 }
368 }
369
370 pub fn display(
375 &self,
376 identity_pubkey: &str,
377 ) -> (Option<String>, Option<crate::circle::CircleRole>) {
378 let accessible = match self {
379 Self::Active(accessible) | Self::Closing(accessible) => accessible,
380 Self::Inactive(_) | Self::Deleted(_) | Self::ControlConflict { .. } => {
381 return (None, None)
382 }
383 };
384 let role = accessible.roster.members().get(identity_pubkey).copied();
385 (Some(accessible.metadata.name.clone()), role)
386 }
387
388 pub fn active(
389 &self,
390 ) -> Option<(
391 &CircleCurrentControl,
392 &CircleAccessLeaf,
393 &CircleMaterializedRoster,
394 &CircleMetadata,
395 )> {
396 match self {
397 Self::Active(active) => Some((
398 &active.current,
399 &active.access,
400 &active.roster,
401 &active.metadata,
402 )),
403 Self::Closing(_)
404 | Self::Inactive(_)
405 | Self::Deleted(_)
406 | Self::ControlConflict { .. } => None,
407 }
408 }
409
410 pub fn active_record_count(&self) -> usize {
411 match self {
412 Self::Active(_) | Self::Closing(_) => 1,
413 Self::Inactive(_) | Self::Deleted(_) => 0,
414 Self::ControlConflict { branches } => branches.len(),
415 }
416 }
417
418 pub fn authoring_state(&self) -> Option<CircleAuthoringState> {
419 match self {
420 Self::Active(active) => Some(CircleAuthoringState {
421 candidate_family: active.candidate_family,
422 control: active.current.control.clone(),
423 access: active.access.clone(),
424 roster: active.roster.clone(),
425 metadata: active.metadata.clone(),
426 }),
427 Self::Closing(_)
428 | Self::Inactive(_)
429 | Self::Deleted(_)
430 | Self::ControlConflict { .. } => None,
431 }
432 }
433
434 pub fn closing_authoring_state(&self) -> Option<CircleAuthoringState> {
435 match self {
436 Self::Closing(closing) => Some(CircleAuthoringState {
437 candidate_family: closing.candidate_family,
438 control: closing.current.control.clone(),
439 access: closing.access.clone(),
440 roster: closing.roster.clone(),
441 metadata: closing.metadata.clone(),
442 }),
443 Self::Active(_)
444 | Self::Inactive(_)
445 | Self::Deleted(_)
446 | Self::ControlConflict { .. } => None,
447 }
448 }
449
450 pub fn deletable_authoring_state(&self) -> Option<CircleAuthoringState> {
456 match self {
457 Self::Active(accessible) | Self::Closing(accessible) => Some(CircleAuthoringState {
458 candidate_family: accessible.candidate_family,
459 control: accessible.current.control.clone(),
460 access: accessible.access.clone(),
461 roster: accessible.roster.clone(),
462 metadata: accessible.metadata.clone(),
463 }),
464 Self::Inactive(_) | Self::Deleted(_) | Self::ControlConflict { .. } => None,
465 }
466 }
467
468 pub fn epoch_access(
469 &self,
470 expected_control: &CircleControlCoord,
471 ) -> Result<Option<CircleEpochAccess>, CircleStateError> {
472 let Self::Active(active) = self else {
473 return Ok(None);
474 };
475 if active.current.coordinate() != expected_control {
476 return Ok(None);
477 }
478 if !verify_accessible_state(active) {
479 return Err(CircleStateError::Invariant(format!(
480 "Circle {} current package access is invalid",
481 active.current.circle_id()
482 )));
483 }
484 epoch_access_from(
485 active.current.circle_id(),
486 &active.current.control.value,
487 &active.access.disposition,
488 &active.roster,
489 )
490 .map(Some)
491 }
492
493 pub fn resolved_control(&self) -> Option<&CircleCurrentControl> {
494 match self {
495 Self::Active(active) => Some(&active.current),
496 Self::Closing(closing) => Some(&closing.current),
497 Self::Inactive(inactive) => Some(&inactive.current),
498 Self::Deleted(deleted) => Some(deleted),
499 Self::ControlConflict { .. } => None,
500 }
501 }
502
503 pub fn is_deleted(&self) -> bool {
505 matches!(self, Self::Deleted(_))
506 }
507
508 pub fn conflict_branches(&self) -> Option<Vec<CircleControlCoord>> {
512 match self {
513 Self::ControlConflict { branches } => Some(
514 branches
515 .iter()
516 .map(|branch| branch.coordinate().clone())
517 .collect(),
518 ),
519 Self::Active(_) | Self::Closing(_) | Self::Inactive(_) | Self::Deleted(_) => None,
520 }
521 }
522
523 pub fn closing_control(&self) -> Option<&PreparedCircleControl> {
524 match self {
525 Self::Closing(closing) => Some(&closing.current.control),
526 Self::Active(_)
527 | Self::Inactive(_)
528 | Self::Deleted(_)
529 | Self::ControlConflict { .. } => None,
530 }
531 }
532
533 #[cfg(any(test, feature = "test-utils"))]
534 pub fn active_current_mut_for_test(&mut self) -> Option<&mut CircleCurrentControl> {
535 match self {
536 Self::Active(active) => Some(&mut active.current),
537 _ => None,
538 }
539 }
540}
541
542fn verify_accessible_state(state: &CircleAccessibleState) -> bool {
543 state.current.verify()
544 && state
545 .access
546 .verify_for_control(&state.current.control, state.candidate_family)
547 && matches!(
548 state.access.disposition,
549 CircleAccessDisposition::Active { .. }
550 )
551 && state.roster.verify()
552 && state.metadata.verify()
553 && state.metadata.circle_id == state.current.circle_id()
554 && state.metadata.epoch_id == state.current.control.value.epoch_id()
555 && state.metadata.key_fingerprint == state.current.control.value.key_fingerprint()
556 && metadata_matches_control(&state.metadata, &state.current.control.value)
557 && roster_matches_control(&state.roster, &state.current.control.value)
558}
559
560fn advance_resolved_control(
561 current: CircleCurrentControl,
562 next: CircleCurrentState,
563) -> Result<CircleCurrentState, CircleStateError> {
564 let next_current = next.resolved_control().ok_or_else(|| {
565 CircleStateError::Invariant("new Circle activation is already conflicted".to_string())
566 })?;
567 if next_current.causally_covers(¤t) {
568 Ok(next)
569 } else {
570 let mut branches = vec![current, next_current.clone()];
571 canonicalize_control_branches(&mut branches)?;
572 Ok(CircleCurrentState::ControlConflict { branches })
573 }
574}
575
576fn canonicalize_control_branches(
577 branches: &mut [CircleCurrentControl],
578) -> Result<(), CircleStateError> {
579 branches.sort_by_key(CircleCurrentControl::control_hash);
580 if branches
581 .windows(2)
582 .any(|pair| pair[0].control_hash() == pair[1].control_hash())
583 {
584 return Err(CircleStateError::Invariant(
585 "Circle control conflict contains a duplicate branch".to_string(),
586 ));
587 }
588 Ok(())
589}
590
591fn roster_matches_control(roster: &CircleMaterializedRoster, control: &CircleControl) -> bool {
592 control.roster_state_ref().state_hash == roster.state_hash()
593}
594
595fn metadata_matches_control(metadata: &CircleMetadata, control: &CircleControl) -> bool {
596 let state = control.metadata_state_ref();
597 state.selected == metadata.coord() && state.state_hash == metadata.metadata_hash()
598}