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 let current = CircleCurrentControl::from_verified(activation);
109 if current.control.value.state().is_deleted() {
112 let state = Self::Deleted(Box::new(current));
113 return if state.verify() {
114 Ok(state)
115 } else {
116 Err(CircleStateError::Invariant(
117 "verified Circle deletion cannot form a valid current state".to_string(),
118 ))
119 };
120 }
121 let state = match &activation.local_access {
122 None => Self::Inactive(Box::new(CircleInactiveState {
123 current,
124 access: CircleInactiveAccess::NotGranted,
125 })),
126 Some(VerifiedCircleAccess {
127 leaf, active: None, ..
128 }) => Self::Inactive(Box::new(CircleInactiveState {
129 current,
130 access: CircleInactiveAccess::Inactive {
131 candidate_family,
132 access: leaf.value.clone(),
133 },
134 })),
135 Some(VerifiedCircleAccess {
136 leaf,
137 active: Some(active),
138 ..
139 }) => {
140 let accessible = Box::new(CircleAccessibleState {
141 current,
142 candidate_family,
143 access: leaf.value.clone(),
144 roster: active.roster.clone(),
145 metadata: active.metadata.clone(),
146 });
147 match accessible.current.control.value.state() {
148 crate::circle::CircleControlState::ActiveEpoch(_) => Self::Active(accessible),
149 crate::circle::CircleControlState::EpochClose(_) => Self::Closing(accessible),
150 crate::circle::CircleControlState::Deleted(_) => {
151 return Err(CircleStateError::Invariant(
152 "verified Circle deletion cannot carry active access".to_string(),
153 ))
154 }
155 }
156 }
157 };
158 if state.verify() {
159 Ok(state)
160 } else {
161 Err(CircleStateError::Invariant(
162 "verified Circle activation cannot form a valid current state".to_string(),
163 ))
164 }
165 }
166
167 pub fn advance(self, next: Self) -> Result<Self, CircleStateError> {
168 if !self.verify() || !next.verify() {
169 return Err(CircleStateError::Invariant(
170 "Circle current-state reduction received invalid state".to_string(),
171 ));
172 }
173 if self.circle_id() != next.circle_id() {
174 return Err(CircleStateError::Invariant(
175 "Circle current-state reduction crossed Circle identities".to_string(),
176 ));
177 }
178 match self {
179 Self::Active(active) => advance_resolved_control(active.current, next),
180 Self::Closing(closing) => advance_resolved_control(closing.current, next),
181 Self::Inactive(inactive) => advance_resolved_control(inactive.current, next),
182 Self::Deleted(deleted) => {
188 let next_current = next.resolved_control().ok_or_else(|| {
189 CircleStateError::Invariant(
190 "new Circle activation is already conflicted".to_string(),
191 )
192 })?;
193 if next_current.causally_covers(&deleted) {
194 return Err(CircleStateError::Invariant(
195 "Circle deletion is terminal; a control descending from it is invalid"
196 .to_string(),
197 ));
198 }
199 let mut branches = vec![*deleted, next_current.clone()];
200 canonicalize_control_branches(&mut branches)?;
201 Ok(Self::ControlConflict { branches })
202 }
203 Self::ControlConflict { mut branches } => {
204 let next_current = next
205 .resolved_control()
206 .ok_or_else(|| {
207 CircleStateError::Invariant(
208 "new Circle activation is already conflicted".to_string(),
209 )
210 })?
211 .clone();
212 branches.retain(|branch| !next_current.causally_covers(branch));
213 if branches.is_empty() {
214 return Ok(next);
215 }
216 branches.push(next_current);
217 canonicalize_control_branches(&mut branches)?;
218 Ok(Self::ControlConflict { branches })
219 }
220 }
221 }
222
223 pub fn without_local_access(self) -> Self {
224 match self {
225 Self::Active(accessible) | Self::Closing(accessible) => {
226 Self::Inactive(Box::new(CircleInactiveState {
227 current: accessible.current,
228 access: CircleInactiveAccess::NotGranted,
229 }))
230 }
231 Self::Inactive(inactive) => Self::Inactive(Box::new(CircleInactiveState {
232 current: inactive.current,
233 access: CircleInactiveAccess::NotGranted,
234 })),
235 Self::Deleted(deleted) => Self::Deleted(deleted),
236 Self::ControlConflict { branches } => Self::ControlConflict { branches },
237 }
238 }
239
240 pub fn verify(&self) -> bool {
241 match self {
242 Self::Active(active) => {
243 matches!(
244 active.current.control.value.state(),
245 crate::circle::CircleControlState::ActiveEpoch(_)
246 ) && verify_accessible_state(active)
247 }
248 Self::Closing(closing) => {
249 matches!(
250 closing.current.control.value.state(),
251 crate::circle::CircleControlState::EpochClose(_)
252 ) && verify_accessible_state(closing)
253 }
254 Self::Inactive(inactive) => {
255 inactive.current.verify()
256 && match &inactive.access {
257 CircleInactiveAccess::NotGranted => true,
258 CircleInactiveAccess::Inactive {
259 candidate_family,
260 access,
261 } => {
262 access.verify_for_control(&inactive.current.control, *candidate_family)
263 && matches!(access.disposition, CircleAccessDisposition::Inactive)
264 }
265 }
266 }
267 Self::Deleted(deleted) => {
268 matches!(
269 deleted.control.value.state(),
270 crate::circle::CircleControlState::Deleted(_)
271 ) && deleted.verify()
272 }
273 Self::ControlConflict { branches } => {
274 branches.len() >= 2
275 && branches.iter().all(|branch| {
276 branch.verify() && branch.circle_id() == branches[0].circle_id()
277 })
278 && branches
279 .windows(2)
280 .all(|pair| pair[0].control_hash() < pair[1].control_hash())
281 }
282 }
283 }
284
285 pub fn circle_id(&self) -> CircleId {
286 match self {
287 Self::Active(active) => active.current.circle_id(),
288 Self::Closing(closing) => closing.current.circle_id(),
289 Self::Inactive(inactive) => inactive.current.circle_id(),
290 Self::Deleted(deleted) => deleted.circle_id(),
291 Self::ControlConflict { branches } => branches[0].circle_id(),
292 }
293 }
294
295 pub fn rotation_required(
299 &self,
300 active_store_members: &BTreeSet<String>,
301 ) -> Option<RotationRequired> {
302 let accessible = match self {
303 Self::Active(accessible) | Self::Closing(accessible) => accessible,
304 Self::Inactive(_) | Self::Deleted(_) | Self::ControlConflict { .. } => return None,
305 };
306 let removed_members: Vec<String> = accessible
307 .roster
308 .members()
309 .into_keys()
310 .filter(|pubkey| !active_store_members.contains(pubkey))
311 .collect();
312 if removed_members.is_empty() {
313 None
314 } else {
315 Some(RotationRequired { removed_members })
316 }
317 }
318
319 pub fn derived_state(
329 &self,
330 active_store_members: &BTreeSet<String>,
331 ) -> crate::circle::CircleState {
332 use crate::circle::CircleState;
333 match self {
334 Self::Active(_) => match self.rotation_required(active_store_members) {
335 Some(RotationRequired { removed_members }) => {
336 CircleState::RotationRequired { removed_members }
337 }
338 None => CircleState::Active,
339 },
340 Self::Closing(_) => CircleState::Closing,
341 Self::Inactive(_) => CircleState::Inactive,
342 Self::Deleted(_) => CircleState::Deleted,
343 Self::ControlConflict { branches } => CircleState::ControlConflict {
344 branches: branches
345 .iter()
346 .map(|branch| branch.coordinate().clone())
347 .collect(),
348 },
349 }
350 }
351
352 pub fn display(
357 &self,
358 identity_pubkey: &str,
359 ) -> (Option<String>, Option<crate::circle::CircleRole>) {
360 let accessible = match self {
361 Self::Active(accessible) | Self::Closing(accessible) => accessible,
362 Self::Inactive(_) | Self::Deleted(_) | Self::ControlConflict { .. } => {
363 return (None, None)
364 }
365 };
366 let role = accessible.roster.members().get(identity_pubkey).copied();
367 (Some(accessible.metadata.name.clone()), role)
368 }
369
370 pub fn active(
371 &self,
372 ) -> Option<(
373 &CircleCurrentControl,
374 &CircleAccessLeaf,
375 &CircleMaterializedRoster,
376 &CircleMetadata,
377 )> {
378 match self {
379 Self::Active(active) => Some((
380 &active.current,
381 &active.access,
382 &active.roster,
383 &active.metadata,
384 )),
385 Self::Closing(_)
386 | Self::Inactive(_)
387 | Self::Deleted(_)
388 | Self::ControlConflict { .. } => None,
389 }
390 }
391
392 pub fn active_record_count(&self) -> usize {
393 match self {
394 Self::Active(_) | Self::Closing(_) => 1,
395 Self::Inactive(_) | Self::Deleted(_) => 0,
396 Self::ControlConflict { branches } => branches.len(),
397 }
398 }
399
400 pub fn authoring_state(&self) -> Option<CircleAuthoringState> {
401 match self {
402 Self::Active(active) => Some(CircleAuthoringState {
403 candidate_family: active.candidate_family,
404 control: active.current.control.clone(),
405 access: active.access.clone(),
406 roster: active.roster.clone(),
407 metadata: active.metadata.clone(),
408 }),
409 Self::Closing(_)
410 | Self::Inactive(_)
411 | Self::Deleted(_)
412 | Self::ControlConflict { .. } => None,
413 }
414 }
415
416 pub fn closing_authoring_state(&self) -> Option<CircleAuthoringState> {
417 match self {
418 Self::Closing(closing) => Some(CircleAuthoringState {
419 candidate_family: closing.candidate_family,
420 control: closing.current.control.clone(),
421 access: closing.access.clone(),
422 roster: closing.roster.clone(),
423 metadata: closing.metadata.clone(),
424 }),
425 Self::Active(_)
426 | Self::Inactive(_)
427 | Self::Deleted(_)
428 | Self::ControlConflict { .. } => None,
429 }
430 }
431
432 pub fn deletable_authoring_state(&self) -> Option<CircleAuthoringState> {
438 match self {
439 Self::Active(accessible) | Self::Closing(accessible) => Some(CircleAuthoringState {
440 candidate_family: accessible.candidate_family,
441 control: accessible.current.control.clone(),
442 access: accessible.access.clone(),
443 roster: accessible.roster.clone(),
444 metadata: accessible.metadata.clone(),
445 }),
446 Self::Inactive(_) | Self::Deleted(_) | Self::ControlConflict { .. } => None,
447 }
448 }
449
450 pub fn epoch_access(
451 &self,
452 expected_control: &CircleControlCoord,
453 ) -> Result<Option<CircleEpochAccess>, CircleStateError> {
454 let Self::Active(active) = self else {
455 return Ok(None);
456 };
457 if active.current.coordinate() != expected_control {
458 return Ok(None);
459 }
460 if !verify_accessible_state(active) {
461 return Err(CircleStateError::Invariant(format!(
462 "Circle {} current package access is invalid",
463 active.current.circle_id()
464 )));
465 }
466 epoch_access_from(
467 active.current.circle_id(),
468 &active.current.control.value,
469 &active.access.disposition,
470 &active.roster,
471 )
472 .map(Some)
473 }
474
475 pub fn resolved_control(&self) -> Option<&CircleCurrentControl> {
476 match self {
477 Self::Active(active) => Some(&active.current),
478 Self::Closing(closing) => Some(&closing.current),
479 Self::Inactive(inactive) => Some(&inactive.current),
480 Self::Deleted(deleted) => Some(deleted),
481 Self::ControlConflict { .. } => None,
482 }
483 }
484
485 pub fn is_deleted(&self) -> bool {
487 matches!(self, Self::Deleted(_))
488 }
489
490 pub fn conflict_branches(&self) -> Option<Vec<CircleControlCoord>> {
494 match self {
495 Self::ControlConflict { branches } => Some(
496 branches
497 .iter()
498 .map(|branch| branch.coordinate().clone())
499 .collect(),
500 ),
501 Self::Active(_) | Self::Closing(_) | Self::Inactive(_) | Self::Deleted(_) => None,
502 }
503 }
504
505 pub fn closing_control(&self) -> Option<&PreparedCircleControl> {
506 match self {
507 Self::Closing(closing) => Some(&closing.current.control),
508 Self::Active(_)
509 | Self::Inactive(_)
510 | Self::Deleted(_)
511 | Self::ControlConflict { .. } => None,
512 }
513 }
514
515 #[cfg(any(test, feature = "test-utils"))]
516 pub fn active_current_mut_for_test(&mut self) -> Option<&mut CircleCurrentControl> {
517 match self {
518 Self::Active(active) => Some(&mut active.current),
519 _ => None,
520 }
521 }
522}
523
524fn verify_accessible_state(state: &CircleAccessibleState) -> bool {
525 state.current.verify()
526 && state
527 .access
528 .verify_for_control(&state.current.control, state.candidate_family)
529 && matches!(
530 state.access.disposition,
531 CircleAccessDisposition::Active { .. }
532 )
533 && state.roster.verify()
534 && state.metadata.verify()
535 && state.metadata.circle_id == state.current.circle_id()
536 && state.metadata.epoch_id == state.current.control.value.epoch_id()
537 && state.metadata.key_fingerprint == state.current.control.value.key_fingerprint()
538 && metadata_matches_control(&state.metadata, &state.current.control.value)
539 && roster_matches_control(&state.roster, &state.current.control.value)
540}
541
542fn advance_resolved_control(
543 current: CircleCurrentControl,
544 next: CircleCurrentState,
545) -> Result<CircleCurrentState, CircleStateError> {
546 let next_current = next.resolved_control().ok_or_else(|| {
547 CircleStateError::Invariant("new Circle activation is already conflicted".to_string())
548 })?;
549 if next_current.causally_covers(¤t) {
550 Ok(next)
551 } else {
552 let mut branches = vec![current, next_current.clone()];
553 canonicalize_control_branches(&mut branches)?;
554 Ok(CircleCurrentState::ControlConflict { branches })
555 }
556}
557
558fn canonicalize_control_branches(
559 branches: &mut [CircleCurrentControl],
560) -> Result<(), CircleStateError> {
561 branches.sort_by_key(CircleCurrentControl::control_hash);
562 if branches
563 .windows(2)
564 .any(|pair| pair[0].control_hash() == pair[1].control_hash())
565 {
566 return Err(CircleStateError::Invariant(
567 "Circle control conflict contains a duplicate branch".to_string(),
568 ));
569 }
570 Ok(())
571}
572
573fn roster_matches_control(roster: &CircleMaterializedRoster, control: &CircleControl) -> bool {
574 control.roster_state_ref().state_hash == roster.state_hash()
575}
576
577fn metadata_matches_control(metadata: &CircleMetadata, control: &CircleControl) -> bool {
578 let state = control.metadata_state_ref();
579 state.selected == metadata.coord() && state.state_hash == metadata.metadata_hash()
580}